从DllMain下断点到LdrpCallInitRoutine

来源:互联网 发布:橘子vr软件下载 编辑:程序博客网 时间:2024/06/10 23:58

    windbg中有个sxe命令,用于启动某类事件上的调试中断。例如

sxe ld:kernel32.dll
可以在exe加载kernel32.dll时中断到调试器。不过,一般情况下,exe无法捕获kernel32.dll加载的事件。因为当windbg启动捕获到ibp事件(初始断点
)而中断到调试器后,exe启动时所依赖的dll都已加载完毕(包括kernel32.dll)。如下面的清单,当windbg打开calc.exe时,第一次中断到windbg时,kernel32.dll已经加载到进程地址空间:

CommandLine: C:\WINDOWS\system32\calc.exeExecutable search path is: ModLoad: 01000000 0101f000   calc.exeModLoad: 7c920000 7c9b3000   ntdll.dllModLoad: 7c800000 7c91e000   C:\WINDOWS\system32\kernel32.dllModLoad: 7d590000 7dd84000   C:\WINDOWS\system32\SHELL32.dllModLoad: 77da0000 77e49000   C:\WINDOWS\system32\ADVAPI32.dllModLoad: 77e50000 77ee2000   C:\WINDOWS\system32\RPCRT4.dllModLoad: 77fc0000 77fd1000   C:\WINDOWS\system32\Secur32.dllModLoad: 77ef0000 77f39000   C:\WINDOWS\system32\GDI32.dllModLoad: 77d10000 77da0000   C:\WINDOWS\system32\USER32.dllModLoad: 77be0000 77c38000   C:\WINDOWS\system32\msvcrt.dllModLoad: 77f40000 77fb6000   C:\WINDOWS\system32\SHLWAPI.dll <----至此,所有模块都已加载完毕(ad0.ccc): Break instruction exception - code 80000003 (first chance)eax=001a1eb4 ebx=7ffd4000 ecx=00000007 edx=00000080 esi=001a1f48 edi=001a1eb4eip=7c92120e esp=0007fb20 ebp=0007fc94 iopl=0         nv up ei pl nz na po nccs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202*** ERROR: Symbol file could not be found.  Defaulted to export symbols for ntdll.dll - ntdll!DbgBreakPoint:7c92120e cc              int     3  
    当然在程序中通过LoadLibrary加载的动态库还是可以通过这个命令捕获到的。那是不是不能捕获kernel32.dll的加载事件了?那倒也不至于。exe启动时,还有个调试事件----cpr(进程创建事件,请注意与ibp事件区分):它发生在dll加载到进程地址空间之前。不过,要捕获这个事件需要在命令行下启动windbg:

c:>windbg.exe -xe cpr notepad.exe ;命令行下启动windbg,其中 -xe cpr是使windbg在出现cpr事件时发生中断;下面的输出源自windbgCommandLine: notepad.exeExecutable search path is: ModLoad: 01000000 01013000   notepad.exe0:000> lmstart    end        module name01000000 01013000   notepad    (deferred)  ;此时,只加载可执行程序本身
    趁这个机会,可以启用kernel32.dll加载事件。这样当kernel32.dll加载到进程空间后会中断到windbg,请注意我的用词,是dll加载到进程空间

0:000> sxe ld kernel320:000> gAVRF: notepad.exe: pid 0xAD8: flags 0x6: application verifier enabledModLoad: 7c800000 7c91e000   C:\WINDOWS\system32\KERNEL32.dll ;Modload显示 现在正在加载Kernel32ntdll!KiFastSystemCallRet:7c92e4f4 c3              ret0:000> lmstart    end        module name01000000 01013000   notepad    (deferred)             10000000 10033000   Msg        (deferred)             5ad50000 5ad99000   verifier   (deferred)             7c800000 7c91e000   KERNEL32   (deferred)             7c920000 7c9b3000   ntdll      (pdb symbols)          c:\symbols\dll\ntdll.pdb
    现在,我们会借助kernel32.pdb在动态库入口处kernel32!DllMain下断点并继续后续的调试。

    不知大家注意没,前面我解释sxe ld命令的作用时用了红字标注?是的,它相当于在加载dll时下断点,并不是在DllMain处下断点。从dll加载到进入DllMain还有很长一段路需要执行。以下面这个简单代码为例,DllMain入口处加入int 3断点,一旦进入DllMain,windbg就会中断。借此,我们对比一下dll加载事件时的堆栈和进入DllMain时的堆栈:

#include <windows.h>BOOL APIENTRY DllMain( HANDLE hModule,                        DWORD  ul_reason_for_call,                        LPVOID lpReserved ){_asm int 3;    switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:break;    }    return TRUE;}
下面清单为dll加载事件时的堆栈:

0:000> sxe ld Msg0:000> gAVRF: notepad.exe: pid 0xA34: flags 0x6: application verifier enabledntdll!KiFastSystemCallRet:7c92e4f4 c3              ret0:000> .lasteventLast event: a34.4f4: Load module C:\WINDOWS\System32\Msg.dll at 10000000  debugger time: Thu Apr 27 22:49:15.909 2017 (UTC + 8:00)0:000> kbChildEBP RetAddr  Args to Child              0007f444 7c92d50c 7c93bd03 000007a8 ffffffff ntdll!KiFastSystemCallRet0007f448 7c93bd03 000007a8 ffffffff 0007f520 ntdll!ZwMapViewOfSection+0xc0007f53c 7c93624a 7c99e4b0 0007f5c8 00000000 ntdll!LdrpMapDll+0x3300007f7fc 7c9364b3 00000000 7c99e4b0 00000000 ntdll!LdrpLoadDll+0x1e90007faa4 7c975216 7c99e4b0 00000000 001a23b0 ntdll!LdrLoadDll+0x2300007faf8 7c97608c 001a23a8 7ffd7000 00020000 ntdll!AVrfpLoadAndInitializeProvider+0x6d0007fb10 7c95e3cd 00000000 00000000 00000001 ntdll!AVrfInitializeVerifier+0xbc0007fc94 7c94108f 0007fd30 7c920000 0007fce0 ntdll!LdrpInitializeProcess+0xc730007fd1c 7c92e437 0007fd30 7c920000 00000000 ntdll!_LdrpInitialize+0x18300000000 00000000 00000000 00000000 00000000 ntdll!KiUserApcDispatcher+0x7
下列清单为进入DllMain时的堆栈:

0:000> g(6a4.ad8): Break instruction exception - code 80000003 (first chance)*** WARNING: Unable to verify checksum for C:\WINDOWS\System32\Msg.dllMsg!DllMain+0x18:10001088 cc              int     30:000> kbChildEBP RetAddr  Args to Child              0007fa6c 10001320 10000000 00000004 0007fad8 Msg!DllMain+0x18 [C:\DOCUMENTS AND SETTINGS\ADMINISTRATOR\桌面\STUDIO\Msg\Msg.cpp @ 12]0007fa84 7c92118a 10000000 00000004 0007fad8 Msg!_DllMainCRTStartup+0x80 [dllcrt0.c @ 237]0007faa4 7c9752bf 100012a0 10000000 00000004 ntdll!LdrpCallInitRoutine+0x140007faf8 7c97608c 001a23a8 7ffdb000 00020000 ntdll!AVrfpLoadAndInitializeProvider+0x1160007fb10 7c95e3cd 00000000 00000000 00000001 ntdll!AVrfInitializeVerifier+0xbc0007fc94 7c94108f 0007fd30 7c920000 0007fce0 ntdll!LdrpInitializeProcess+0xc730007fd1c 7c92e437 0007fd30 7c920000 00000000 ntdll!_LdrpInitialize+0x18300000000 00000000 00000000 00000000 00000000 ntdll!KiUserApcDispatcher+0x7
对比两者的堆栈输出,只有最底部的4个堆栈帧是相同的,从侧面也说明了dll加载到进程空间和进入DllMain两者之间代码相差甚远。所以,我们不能把sxe ld断点发生的位置当做是动态库的入口点。

    当知道了这个,顿时觉得调试Dll是个麻烦事了,因为,我再也找不到Dll入口了(有时候,就算有符号文件,也找不到DllMain)。如下面的代码,明明搜索不到DllMain的符号,结果却能在调用堆栈中找到它的踪影:

0:000> x Msg!*DllMain*Type information missing error for _pRawDllMainType information missing error for DllMainType information missing error for _DllMainCRTStartup0:000> kbChildEBP RetAddr  Args to Child              0007fa6c 10001320 10000000 00000004 0007fad8 Msg!DllMain+0x18 
    那有没有什么办法可以在没有源码的情况下找到DllMain入口点?我在调用栈发现一个有趣的函数,LdrpCallInitRoutine:

0007fa84 7c92118a 10000000 00000004 0007fad8 Msg!_DllMainCRTStartup+0x80 [dllcrt0.c @ 237]0007faa4 7c9752bf 100012a0 10000000 00000004 ntdll!LdrpCallInitRoutine+0x14
    这个函数具有承上启下的作用:它本身位于ntdll模块中(windows进程的加载器),执行没多久就会进入到自定义动态库Msg中。要跳到自定义的Dll中,那么,它必然知道Dll的入口地址,前人通过大量的逆向工程告诉我们这些后人这个函数的参数可能如下:

LdrpCallInitRoutine(Ldr->EntryPoint, Ldr>DllBase, DLL_THREAD_ATTACH, NULL); //call dll oep

    结合前人的结论,让我们LdrpCallInitRoutine的第一个参数的值0x100012a0靠近哪个符号?

0:000> kb...0007faa4 7c9752bf 100012a0 10000000 00000004 ntdll!LdrpCallInitRoutine+0x140:000> ln 100012a0 dllcrt0.c(211)(100012a0)   Msg!_DllMainCRTStartup   |  (100013a0)   Msg!_amsg_exitExact matches:Type information missing error for _DllMainCRTStartup
    从命令ln的输出来看0x100012a0不偏不倚的砸中_DllMainCRTStartup----这个函数简单的封装并跳转到DllMain。LdrpCallInitRoutine内部正是使用这个地址作为动态库的入口点,跳入自定义模块的DllMain。所以,以后我们大可以在这个函数中搜索函数入口地址,找到后可以下断点还可以做一些其他有趣的事~
    除此之外LdrpCallInitRoutine还把模块加载地址作为参数,传递给_DllMainCRTStartup,即DllMain的第一个参数:

0:000> kbChildEBP RetAddr  Args to Child              0007fa6c 10001320 10000000 <----DllMain的第一个参数,来自ntdll!LdrpCallInitRoutine 00000004 0007fad8 Msg!DllMain+0x18 [...]0007fa84 7c92118a 10000000 00000004 0007fad8 Msg!_DllMainCRTStartup+0x80 [dllcrt0.c @ 237]0007faa4 7c9752bf 100012a0 10000000 <---第二个参数 00000004 ntdll!LdrpCallInitRoutine+0x140:000> dd hModule L10007fa74  10000000
    LdrpCallInitRoutine还真是一个有趣而又重要的函数,值得好好发掘。本篇完~

Reference:

[系统底层] 线程初始化过程 PK 加载DLL过程(详细)


0 0