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.exe
Executable search path is:
ModLoad: 01000000 0101f000 calc.exe
ModLoad: 7c920000 7c9b3000 ntdll.dll
ModLoad: 7c800000 7c91e000 C:\WINDOWS\system32\kernel32.dll
ModLoad: 7d590000 7dd84000 C:\WINDOWS\system32\SHELL32.dll
ModLoad: 77da0000 77e49000 C:\WINDOWS\system32\ADVAPI32.dll
ModLoad: 77e50000 77ee2000 C:\WINDOWS\system32\RPCRT4.dll
ModLoad: 77fc0000 77fd1000 C:\WINDOWS\system32\Secur32.dll
ModLoad: 77ef0000 77f39000 C:\WINDOWS\system32\GDI32.dll
ModLoad: 77d10000 77da0000 C:\WINDOWS\system32\USER32.dll
ModLoad: 77be0000 77c38000 C:\WINDOWS\system32\msvcrt.dll
ModLoad: 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=001a1eb4
eip=7c92120e esp=0007fb20 ebp=0007fc94 iopl=0 nv up ei pl nz na po nc
cs=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事件时发生中断
;下面的输出源自windbg
CommandLine: notepad.exe
Executable search path is:
ModLoad: 01000000 01013000 notepad.exe
0:000> lm
start end module name
01000000 01013000 notepad (deferred) ;此时,只加载可执行程序本身
趁这个机会,可以启用kernel32.dll加载事件。
这样当kernel32.dll加载到进程空间后会中断到windbg,请注意我的用词,是dll加载到进程空间。
0:000> sxe ld kernel32
0:000> g
AVRF: notepad.exe: pid 0xAD8: flags 0x6: application verifier enabled
ModLoad: 7c800000 7c91e000 C:\WINDOWS\system32\KERNEL32.dll ;Modload显示 现在正在加载Kernel32
ntdll!KiFastSystemCallRet:
7c92e4f4 c3 ret
0:000> lm
start end module name
01000000 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 Msg
0:000> g
AVRF: notepad.exe: pid 0xA34: flags 0x6: application verifier enabled
ntdll!KiFastSystemCallRet:
7c92e4f4 c3 ret
0:000> .lastevent
Last 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> kb
ChildEBP RetAddr Args to Child
0007f444 7c92d50c 7c93bd03 000007a8 ffffffff ntdll!KiFastSystemCallRet
0007f448 7c93bd03 000007a8 ffffffff 0007f520 ntdll!ZwMapViewOfSection+0xc
0007f53c 7c93624a 7c99e4b0 0007f5c8 00000000 ntdll!LdrpMapDll+0x330
0007f7fc 7c9364b3 00000000 7c99e4b0 00000000 ntdll!LdrpLoadDll+0x1e9
0007faa4 7c975216 7c99e4b0 00000000 001a23b0 ntdll!LdrLoadDll+0x230
0007faf8 7c97608c 001a23a8 7ffd7000 00020000 ntdll!AVrfpLoadAndInitializeProvider+0x6d
0007fb10 7c95e3cd 00000000 00000000 00000001 ntdll!AVrfInitializeVerifier+0xbc
0007fc94 7c94108f 0007fd30 7c920000 0007fce0 ntdll!LdrpInitializeProcess+0xc73
0007fd1c 7c92e437 0007fd30 7c920000 00000000 ntdll!_LdrpInitialize+0x183
00000000 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.dll
Msg!DllMain+0x18:
10001088 cc int 3
0:000> kb
ChildEBP 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+0x14
0007faf8 7c97608c 001a23a8 7ffdb000 00020000 ntdll!AVrfpLoadAndInitializeProvider+0x116
0007fb10 7c95e3cd 00000000 00000000 00000001 ntdll!AVrfInitializeVerifier+0xbc
0007fc94 7c94108f 0007fd30 7c920000 0007fce0 ntdll!LdrpInitializeProcess+0xc73
0007fd1c 7c92e437 0007fd30 7c920000 00000000 ntdll!_LdrpInitialize+0x183
00000000 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 _pRawDllMain
Type information missing error for DllMain
Type information missing error for _DllMainCRTStartup
0:000> kb
ChildEBP 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+0x14
0:000> ln 100012a0
dllcrt0.c(211)
(100012a0) Msg!_DllMainCRTStartup | (100013a0) Msg!_amsg_exit
Exact matches:
Type information missing error for _DllMainCRTStartup
从命令ln的输出来看0x100012a0不偏不倚的砸中_DllMainCRTStartup----这个函数简单的封装并跳转到DllMain。LdrpCallInitRoutine内部正是使用这个地址作为动态库的入口点,跳入自定义模块的DllMain。所以,以后我们大可以在这个函数中搜索函数入口地址,找到后可以下断点还可以做一些其他有趣的事~
除此之外LdrpCallInitRoutine还把模块加载地址作为参数,传递给_DllMainCRTStartup,即DllMain的第一个参数:
0:000> kb
ChildEBP 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+0x14
0:000> dd hModule L1
0007fa74 10000000
LdrpCallInitRoutine还真是一个有趣而又重要的函数,值得好好发掘。本篇完~
Reference:
[系统底层] 线程初始化过程 PK 加载DLL过程(详细)
标签:LdrpCallInitRoutine,00000000,断点,ntdll,dll,000,DllMain,加载 From: https://blog.51cto.com/u_13927568/5831351