Windows x64内核中修改进程入口点

来源:互联网 发布:7英寸windows平板 编辑:程序博客网 时间:2024/05/20 14:42

  现如今Windows x64已经很普遍了。基于内核驱动,监视进程创建,在进程主线程启动之前修改其入口点以便执行一段自己的代码是十分有用的。本文以Windows 7 x64为基础,对这个问题进行了尝试。

 

0x1 修改的时机

  Windows x64提供了注册回调的方法,PsSetCreateProcessNotifyRoutine/ PsSetCreateProcessNotifyRoutineEx,以使得安全产品开发者可以在进程创建时得到通知。在回调函数中得到新建进程的主线程对象是修改入口点的第一步。

PsSetCreateProcessNotifyRoutineEx回调函数的原型是:

VOIDCreateProcessNotifyEx(__inout PEPROCESS  Process,__in HANDLE  ProcessId,__in_opt PPS_CREATE_NOTIFY_INFO  CreateInfo);

第一个参数是一个指向新创建的进程对象。通过 _EPROCESS 可以得到该进程目前唯一的一个线程:

0:kd> dt _EPROCESS ThreadListHeadntdll!_EPROCESS   +0x300 ThreadListHead : _LIST_ENTRY

  ThreadListHead的Flink指向主线程 _ETHREAD 结构中的 ThreadListEntry的地址:

0:kd> dt _ETHREAD ThreadListEntryntdll!_ETHREAD   +0x420 ThreadListEntry: _LIST_ENTRY

  得到ThreadListEntry地址后即可反推出线程对象的地址,本例中即为

ethread = poi(eprocess+0x300) - 0x420

  PsSetCreateProcessNotifyRoutine回调函数的原型是:

VOID(*PCREATE_PROCESS_NOTIFY_ROUTINE)(    IN HANDLE ParentId,    IN HANDLE ProcessId,    IN BOOLEAN Create    );

  第二个参数是新创建进程ID。可以采用PsLookupProcessByProcessId可以得到进程对象,从而再进一步采用上面的方面获得新进程的主线程对象:

NTSTATUS PsLookupProcessByProcessId(  _In_  HANDLE ProcessId,  _Out_ PEPROCESS *Process);

Windows还提供了通过PsSetCreateThreadNotifyRoutine注册线程创建回调的方法。回调函数的原型是:

VOID(*PCREATE_THREAD_NOTIFY_ROUTINE) (    IN HANDLE  ProcessId,    IN HANDLE  ThreadId,    IN BOOLEAN  Create    );

  同理也可以通过ThreadId得到线程对象,同时通过判断线程对象ThreadListEntry所链起来的线程个数,可得知当前线程是否是某个进程的主线程。当然,通过测量进程PEB中的Ldr.Initialized成员也可以判断当前是否正在创建新进程。

  通常进程创建的流程是这样的:

1.  创建Section

2.  创建Process

3.  创建挂起的Thread

4.  通知进程创建回调

5.  通知线程创建回调

6.  Resume挂起的Thread

  因此,在我们得到回调时就可以改变Thread的上下文(Context),随后线程在Resume时以新的Context运行,这样就达到了修改进程(主线程)入口点的目的。

 

0x2 64位进程入口点的修改

  当我们在回调函数中观察主线程对象时,可以看到:

2:kd> dt _KTHREAD InitialStack fffffa801929c060ntdll!_KTHREAD   +0x028 InitialStack : 0xfffff880`033e0c70 Void

  而查看其调用堆栈可见:

2:kd> !thread fffffa801929c060…………nt!KiStartUserThread……nt!KiStartUserThreadReturn(TrapFrame @ fffff880`033e0ae0)……ntdll!RtlUserThreadStart

  针对InitialStack(fffff880`033e0c70)不难计算得出:

2:kd> ?fffff880033e0c70-@@(sizeof(_KTRAP_FRAME))Evaluateexpression: -8246282810656 = fffff880`033e0ae0

  这样一来,原来线程初始内核堆栈的底部原来就是存储TrapFrame的地方,通过下面的方面可验证TrapFrame.Rip存储的就是线程用户态起始地址:

2:kd> dt _KTRAP_FRAME Rip fffff880`033e0ae0ntdll!_KTRAP_FRAME   +0x168 Rip : 0x7704c5002:kd> dt _ETHREAD StartAddress fffffa801929c060ntdll!_ETHREAD   +0x388 StartAddress : 0x00000000`7704c500 Void

  实践证明,修改TrapFrame.Rip,可以改变主线程运行的起始地址。


0x3 Wow64入口点的修改

当把以上的方法应用到Wow64进程时,发现这种修改竟然失效了!

我首先怀疑是否Wow64的线程上下文不在TrapFrame里了?苦思无果后,我在TrapFrame上下了一个内存访问断点——看看是否有人修改了它:

3:kd> ba w 8 fffff880`03bbdae0+168

  这下发现,确实有人修改了TrapFrame中的Rip。初始看到的Rip是:

2:kd> dt _KTRAP_FRAME Rip fffff880`03bbdae0ntdll!_KTRAP_FRAME   +0x168 Rip : 0x7704c500

  Rip被修改后变成了:

2:kd> dt _KTRAP_FRAME Rip fffff880`03bbdae0ntdll!_KTRAP_FRAME   +0x168 Rip : 0x7704c320

  打印函数调用堆栈:

nt!PspSetContext+0x69nt!PspGetSetContextInternal+0x40dnt!PspGetSetContextSpecialApc+0xa1nt!PspSetContextThreadInternal+0xe5nt!PspInitializeThunkContext+0x1b4(这个表达式调试器给的是错的)nt!PspUserThreadStartup+0xc3nt!KiStartUserThread+0x16nt!KiStartUserThreadReturn(TrapFrame @ fffff880`03bbdae0)ntdll!LdrInitializeThunk

  原来,系统将新线程在内核中的起始定为nt!KiStartUserThread,缺省将用户态起始定为ntdll!RtlUserThreadStart。而对于线程自从内核态开始运转后,却偷偷将用户态起始定为ntdll!LdrInitializeThunk,以便做些初始化的工作。其用户态关键函数的调用次序是:

ntdll!LdrInitializeThunkntdll!RtlUserThreadStartPE!EntryPoint

  (其中PE的入口点可以用伪寄存器@$exentry定位)

  但到此为止,64位线程也应当是这样处理的,但为什么修改Rip可以改变64位线程却改变不了Wow64线程昵?

  一个合理的推理是64位线程Rip显然被自身修改为ntdll!LdrInitializeThunk,这就意味着从ntdll!LdrInitializeThunk开始的用户态代码最终也要去执行我们修改的Rip。也就是说系统有一种机制在ntdll!LdrInitializeThunk之前记录Rip,在之后又找回了它。ntdll!LdrInitializeThunk位于64位ntdll中。这令我恍然大悟Wow64线程是32位的,也许系统找回了Rip,但不是我们修改过的,而是某个32位的代码地址!系统用户态代码在我面前耍了一回花枪。对于Wow64线程来说,用户态关键函数的调用次序是:

ntdll!LdrInitializeThunkntdll32!LdrInitializeThunkntdll32!RtlUserThreadStartPE!EntryPoint

  下一个问题——怎么验证?我们正在64虚拟机中进行内核调试,用户态代码内存当前全都无法访问,而且奇怪的是象bp /t这样的断点竟然无法达到预期(Microsoft怎么会有这样的Bug?不过WinDbg的粗制滥造不是由来以久了吗?)。好吧,现实逼迫我在虚拟机中再安装个一WinDbg用于用户态调试(够变态)!而且32位WinDbg最早也只能断到32位代码,不能停留在64位初始代码上,所以我们需要x64版本。

  虚拟机中的WinDbg中断在ntdll!LdrInitializeThunk后的某个地方,这是方便我们加载符号用的。调试发现系统从ntdll!LdrInitializeThunk向下执行并未转到ntdll!RtlUserThreadStart,而是转到了ntdll32!LdrInitializeThunk。注意这已经是32位代码了,这之前一定发生了什么。调试跟踪发现:

ntdll!LdrInitializeThunk->wow64!Wow64LdrpInitialize->wow64cpu!CpuGetContext->ntdll!ZwQueryInformationThread->wow64!RunCpuSimulation

  ntdll!ZwQueryInformationThread获得的上下文中就已经有了ntdll32!LdrInitializeThunk。而wow64!RunCpuSimulation只不过是转去执行ntdll32!LdrInitializeThunk。这个地址又是如何取得的呢?观察NtQueryInformationThread的调用代码:

and     qword ptr [rsp+20h],0mov     r9d,2CChmov     r8,r10mov     edx,1Dhcall    qword ptr [wow64cpu!_imp_NtQueryInformationThread (00000000`73931010)] 

  对照NtQueryInformationThread的原型,我们得知,它的调用是这样的:

NtQueryInformationThread(    ThreadHandle,           // -2, 即 NtCurrentThread()    ThreadWow64Context, // 1Dh    Wow64_Context,      // Wow64_Context.ContextFlags=0x1002f    sizeof(WOW64_CONTEX),   // 2CCh    0);

  其中Wow64_Context为指向一个WOW64_CONTEXT结构的指针。取得的上下文中Wow64_Context.Eip被赋为ntdll32!LdrInitializeThunk(Wow64_Context.Eax指向PE文件的入口点)。

  找到了事情的重点,剩下就是针对内核中的NtQueryInformationThread去逆向。NtQueryInformationThread函数调用的关键路径摘要如下:

NtQueryInformationThread-> PspWow64GetContextThreadOnAmd64    -> PspWow64ReadOrWriteThreadCpuArea        -> PsGetThreadTeb

  结果是,WOW64_CONTEXT地址保存在线程对象的Teb的0x1488编移处(实际上这个值要再加上4),也就是所谓的线程局部存储槽口处TlsSlots[WOW64_TLS_CPURESERVED]+4。在用户态调试器中可以通过!wow64exts.info观察到这个值。而这个值等于起始于Teb相同地址的_NT_TIB的StackBase+4。因此,我们在进程创建时,可以修改这个值,从而实现了修改Wow64进程入口点的目的。

 

0x4 总结

  本文中说到的方法需要大量逆向Windows系统,这可不是一件简单的工作,何况同时还要同易出错的WinDbg斗争。在调试Wow64时,我忘记了内核本身是64位的,而32位线程也必将起始于64位代码这个事实。Wow64线程的入口点也正如我最初怀疑的一样,不在TrapFrame当中。

原创粉丝点击