天天看點

從DllMain下斷點到LdrpCallInitRoutine

    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