Win32系统中进程主线程初始化时,ebx指向PEB的问题

我相信大家在用OD的过程中也早已发现,如果不使用Create_SUSPENDED,初始化线程在跑到入口点后,其ebx同样指向PEB结构。

显然,我们必须进入Win32系统新进程创建的过程来找这个真相。由于我们知道初始化线程被创建后(尚未Resume)ebx就已经指向PEB了,所以要不然就是在NtCreateThread过程中,要不然就是在它前面。同时也应该在NtCreatePeb之后。因此我就在这个范围内找寻。

应该说这里我犯了个错误,就是没有详细看CreateProcess的流程,而是使劲往NtCreateThread的内核代码看去,N多函数调用关系看得我眼花,虽然这些内核函数对线程的Context有诸多操作,但是均并不涉及其Ebx。

最后终于回过头来看CreateProcessInternalW在NtCreateThread之前的过程。《Windows Internals 4th》在这里一句话带过了:

Before the thread can be created, it needs a stack and a context in which to run, so these are set up now.

原来是恰恰在NtCreateThread之前,在用户态初始化了新线程的Context,再结合网上其他的文章,终于找到关键函数:kernel32!BaseInitializeContext。

BaseInitializeContext(PCONTEXT Context, // 0x200 bytes
PPEB Peb,
PVOID EntryPoint,
DWORD StackTop,
int Type // union (Process, Thread, Fiber)
);

IDA中看一下这个函数:

.text:7C810443
.text:7C810443 __stdcall BaseInitializeContext(x, x, x, x, x) proc near
.text:7C810443 ; CODE XREF: CreateRemoteThread(x,x,x,x,x,x,x)+84 p
.text:7C810443 ; CreateProcessInternalW(x,x,x,x,x,x,x,x,x,x,x,x)+690 p
.text:7C810443 ; CreateFiberEx(x,x,x,x,x)+82 p
.text:7C810443
.text:7C810443 Context = dword ptr 8
.text:7C810443 PPeb = dword ptr 0Ch
.text:7C810443 EntryPoint = dword ptr 10h
.text:7C810443 StackTop = dword ptr 14h
.text:7C810443 Type = dword ptr 18h
.text:7C810443
.text:7C810443 ; FUNCTION CHUNK AT .text:7C81508E SIZE 00000019 BYTES
.text:7C810443 ; FUNCTION CHUNK AT .text:7C82FF86 SIZE 0000000F BYTES
.text:7C810443
.text:7C810443 mov edi, edi
.text:7C810445 push ebp
.text:7C810446 mov ebp, esp
.text:7C810448 mov eax, [ebp+Context]
.text:7C81044B mov ecx, [ebp+EntryPoint]
.text:7C81044E and [eax+CONTEXT.SegGs], 0
.text:7C810455 cmp [ebp+Type], 1
.text:7C810459 mov [eax+CONTEXT.Eax], ecx
.text:7C81045F mov ecx, [ebp+PPeb]
.text:7C810462 mov [eax+CONTEXT.Ebx], ecx
.text:7C810468 push 20h
.text:7C81046A pop ecx
.text:7C81046B mov [eax+CONTEXT.SegEs], ecx
.text:7C810471 mov [eax+CONTEXT.SegDs], ecx
.text:7C810477 mov [eax+CONTEXT.SegSs], ecx
.text:7C81047D mov ecx, [ebp+StackTop]
.text:7C810480 mov [eax+CONTEXT.SegFs], 38h
.text:7C81048A mov [eax+CONTEXT.SegCs], 18h
.text:7C810494 mov [eax+CONTEXT.EFlags], 3000h
.text:7C81049E mov [eax+CONTEXT.Esp], ecx
.text:7C8104A4 jnz loc_7C81508E
.text:7C8104A4
.text:7C8104AA mov dword ptr [eax+CONTEXT.Eip], offset BaseThreadStartThunk(x,x)
.text:7C8104AA
.text:7C8104B4
.text:7C8104B4 loc_7C8104B4: ; CODE XREF: BaseInitializeContext(x,x,x,x,x)+4C5F j
.text:7C8104B4 ; BaseInitializeContext(x,x,x,x,x)+1FB4D j
.text:7C8104B4 add ecx, 0FFFFFFFCh
.text:7C8104B7 mov [eax+CONTEXT.ContextFlags], 10007h
.text:7C8104BD mov [eax+CONTEXT.Esp], ecx
.text:7C8104C3 pop ebp
.text:7C8104C4 retn 14h
.text:7C8104C4
.text:7C8104C4 __stdcall BaseInitializeContext(x, x, x, x, x) endp
.text:7C8104C4

.text:7C81508E
.text:7C81508E loc_7C81508E: ; CODE XREF: BaseInitializeContext(x,x,x,x,x)+61 j
.text:7C81508E cmp [ebp+Type], 2
.text:7C815092 jz loc_7C82FF86
.text:7C815092
.text:7C815098 mov dword ptr [eax+CONTEXT.Eip], offset BaseProcessStartThunk(x,x)
.text:7C8150A2 jmp loc_7C8104B4
.text:7C8150A2

.text:7C82FF86
.text:7C82FF86 loc_7C82FF86: ; CODE XREF: BaseInitializeContext(x,x,x,x,x)+4C4F j
.text:7C82FF86 mov dword ptr [eax+CONTEXT.Eip], offset BaseFiberStart()
.text:7C82FF90 jmp loc_7C8104B4
.text:7C82FF90

这个函数就是新线程的线程上下文初始化的关键。下面这两行代码:

.text:7C81045F mov ecx, [ebp+PPeb]
.text:7C810462 mov [eax+CONTEXT.Ebx], ecx

将其Ebx指向了PEB。同时这个函数也对几个段选择子进行了初始化,因此诸如为什么Ring3进线程初始化时这几个段选择子总是那样的值的问题,也一并解决了。

因为BaseInitializeContext在CreateRemoteThread中也被调用(CreateThread则又调用了CreateRemoteThread),所以Ring3下用这些常规方式创建的进线程,初始化时都经过这个函数,其线程上下文的相关内容便也保持一致的特点。

Update:

之前的说法还是有点不足之处,BaseInitializeContext的第二个参数,准确地来说应该是PPebOrPParameter。当被CreateProcessInternalW调用时,此处传入的是新进程的PEB指针(由父进程对新进程调用NtQueryInformationProcess得到);当被CreateRemoteThread调用时,这里传入的是CreateRemoteThread的第五个输入参数,也就是lpParameter,也就是为新线程指定的传入参数。

BaseInitializeContext函数将相应CONTEXT结构中的Eip字段指定为BaseProcessStartThunk(新进程创建时CreateProcessInternalW调用的时候)或BaseThreadStartThunk(创建非主线程时,由CreateRemoteThread调用的时候),将Eax字段内容指定为线程入口点,将Ebx字段内容指定为新进程的PEB指针(新进程创建时CreateProcessInternalW调用的时候)或创建远程线程时传给新线程的参数(创建非主线程时,由CreateRemoteThread调用的时候)。


新进程的主线程进入BaseProcessStartThunk后:
xor ebp, ebp
push eax; 线程入口点
push 0
jmp BaseProcessStart

BaseProcessStart之后使用这个push eax时压入栈的线程入口点值来调用入口点函数,当入口点函数返回时,使用其返回值调用ExitThread退出线程:
call dword ptr [ebp+8] ; 之前压入栈的线程入口点
push eax ; 进入主线程入口点时,[esp]的值就是这行指令的地址
call ExitThread

远程线程进入BaseThreadStartThunk后的行为有点类似,但push的参数中多了一个ebx,即传入参数:
xor ebp, ebp
push ebx; 传入参数
push eax; 线程入口点
push 0
jmp BaseThreadStart

BaseThreadStart之后使用前面压入栈的线程入口点值和传入参数来调用入口点函数,当入口点函数返回时,使用其返回值调用ExitThread退出线程:
push dword ptr [ebp+0Ch] ; 之前压入栈的传入参数
call dword ptr [ebp+8] ; 之前压入栈的线程入口点
push eax ; 进入远程线程入口点时,[esp]的值就是这行指令的地址
call ExitThread

另外除了进程和线程这两种类型之外,BaseInitializeContext还会被CreateFiberEx调用,相应的对象称为纤程,我还不知道这个纤程具体是怎么回事。


文章来自: 本站原创
Tags:
评论: 0 | 查看次数: 8087