插入DLL和挂接API——Windows核心编程学习手札之二十二
插入DLL和掛接API
——Windows核心編程學習手札之二十二
如下情況,可能要打破進程的界限,訪問另一個進程的地址空間:
1)為另一個進程創(chuàng)建的窗口建立子類時;
2)需要調(diào)試幫助時,如需要確定另一個進程正在使用那個DLL;
3)需要掛接其他進程時;
基于之上情況,下面的方法可將DLL插入到另一個進程的地址空間中,一旦DLL進入另一個進程的地址空間,就可以對另一個進程為所欲為。
使用注冊表來插入DLL
整個系統(tǒng)的配置都是在注冊表中維護的,可調(diào)整其設置改變系統(tǒng)的行為特性,下面的關鍵字:
HKEY_LOCAL_MACHINE/Software/Microsoft/WindowsNT/
CurrentVersion/Windows/AppInit_DLLS
AppInit_DLLS關鍵字包含一個DLL文件或一組DLL文件名(用空格或逗號隔開,避免使用含空格的文件名),列出第一個DLL文件名可包含其路徑,但包含路徑的其他DLL將被忽略,因此最好將DLL放入Windows的系統(tǒng)目錄中,這樣不需要設定路徑,如設置該關鍵字值為C:/MyLib.dll。當重啟計算機及Windows進行初始化時,系統(tǒng)將保存這個關鍵字的值。然后當User32.dll庫被映射到進程中時,將接收到一個DLL_PROCESS_ATTACH通知,這個通知被處理時,User32.dll便檢索保存這個關鍵字的值,并且為字符串中指定的每個DLL調(diào)用LoadLibrary函數(shù)。當每個庫被加載時,便調(diào)用和該庫相關的DllMain函數(shù),其fdwReason的值是DLL_PROCESS_ATTACH,如此,每個庫就能夠對自己進行初始化。該方法簡單,但不足有:
1)系統(tǒng)是在初始化時讀取該關鍵字的值,因此修改該值需重新啟動計算機;
2) 插入的DLL是映射到使用User32.dll進程中,所有基于GUI的應用程序都使用User32.dll,不過多數(shù)基于GUI的應用程序不使用插入的DLL,而且如將DLL插入編譯器或鏈接程序,這種方法將不起作用;
3)插入的DLL被映射到每個基于GUI應用程序中,DLL映射的進程太多,“容器”進程崩潰的可能性越大;
4)插入的DLL被映射到每個基于GUI應用程序中,應僅必要時保持DLL的插入狀態(tài)。
使用Windows掛鉤來插入DLL
進程A(類似Microsoft Spy++的一個實用程序)安裝了一個掛鉤WN_GETMESSAGE,以便查看系統(tǒng)中的各個窗口處理的消息,掛鉤是通過調(diào)用SetWindowsHookEx函數(shù)來安裝的:
?????? HHOOK hHook=SetWindowsHookEx(WH_GETMESSAGE,GetMsgProc,hinstDll,0);
參數(shù)WH_GETMESSAGE用于指明要安裝的掛鉤類型,參數(shù)GetMsgProc是窗口準備處理一個消息時系統(tǒng)調(diào)用的函數(shù)的地址(在進程地址空間內(nèi));第三個參數(shù)hinstDll是包含GetMsgProc函數(shù)的DLL。在Windows中,DLL的hinstDll的值用于標識DLL被映射到的進程的地址空間中的虛擬內(nèi)存地址,最后一個參數(shù)0表示要掛接的線程,也可以傳遞系統(tǒng)中另一個線程的ID,傳遞0告訴系統(tǒng)想要掛接系統(tǒng)中的所有GUI線程。安裝掛鉤后的情況:
1)進程B中的一個線程準備將一條消息發(fā)送到一個窗口;
2)系統(tǒng)查看該線程上是否已安裝了WH_GETMESSAGE掛鉤;
3)系統(tǒng)查看包含GetMsgProc函數(shù)的DLL是否包含被映射到進程B的地址空間中;
4)如果該DLL尚未被映射,系統(tǒng)將強制該DLL映射到進程B的地址空間,并且將進程B中的DLL映像的自動跟蹤計數(shù)遞增1;
5)當DLL的hinstDll用于進程B時,系統(tǒng)查看該函數(shù),并且檢查該DLL的hinstDll是否與其在進程A時所處的位置相同;如果兩個hinstDll在系統(tǒng)位置上,那GetMsgProc函數(shù)的內(nèi)存地址在兩個進程的地址空間中的位置也是相同的,這種情況下,系統(tǒng)只要調(diào)用進程A的地址空間中的GetMsgProc函數(shù)即可;如位置不同,則系統(tǒng)要確定進程B的地址空間中GetMsgProc函數(shù)的虛擬內(nèi)存地址,用下面公式確定:
?????? GetMsgProc B=hinstDll B+(GetMsgProc A-hinstDll A);
將GetMsgProc A的地址減去hinstDll A的地址,得到GetMsgProc函數(shù)的地址位移(以字節(jié)為計量單位),將這個位移與hinstDll B的地址相加,得到GetMsgProc函數(shù)在用于進程B的地址空間中該DLL的映像時其位置;
6)系統(tǒng)將進程B中的DLL映像的自動跟蹤計數(shù)遞增1;
7)系統(tǒng)調(diào)用進程B的地址空間中的GetMsgProc函數(shù);
8)當GetMsgProc函數(shù)返回時,系統(tǒng)將進程B中的DLL映像的自動跟蹤計數(shù)遞減1;
當系統(tǒng)插入或者映射包含掛鉤過濾器函數(shù)的DLL時,整個DLL均被映射,而不只是掛鉤過濾器函數(shù)被映射,這意味著DLL中包含的任何一個函數(shù)或所有函數(shù)都被映射,可被進程B的環(huán)境運行的線程所調(diào)用。
若要為進程B中的線程創(chuàng)建的窗口建立子類,首先可以在創(chuàng)建該窗口的掛鉤上設置一個WH_GETMESSAGE掛鉤,然后在GetMsgProc函數(shù)被調(diào)用時,調(diào)用SetWindowLongPtr函數(shù)來建立窗口的子類,子類的過程與GetMsgProc函數(shù)在同一DLL文件中。
進程B中不再需要DLL時刪除DLL映像,方法是調(diào)用:
?????? BOOL UnhookWindowsHookEx(HHOOK hhook);
當洋線程調(diào)用UnhookWindowsHookEx函數(shù)時,系統(tǒng)將遍歷將DLL插入到的各個進程的內(nèi)部列表,并對DLL的自動跟蹤計數(shù)進行遞減,當遞減到0時,DLL就從進程的地址空間中被刪除。
使用遠程線程來插入DLL
Windows的多數(shù)函數(shù)允許進程只對自己進行操作,可防止一個進程破壞另一個進程的運行,但對調(diào)試程序和一些工具而言,則需要操作其他進程。使用遠程線程來插入DLL的方法要求目標進程中的線程調(diào)用LoadLibrary函數(shù)來加載必要的DLL,需要在目標進程中創(chuàng)建一個新線程,Windows提供了這樣一個函數(shù):
?????? HANDLE CreateRomoteThread(
????????????????????????????????????????? HANDLE hProcess,
????????????????????????????????????????? PSECURITY_ATTRIBUTES psa,
????????????????????????????????????????? DWORD dwStackSize,
????????????????????????????????????????? PTHREAD_START_ROUTINE pfnStartAddr,
????????????????????????????????????????? PVOID pvParam,
????????????????????????????????????????? DWORD fdwCreate,
????????????????????????????????????????? PDWORD pdwThreadId);
參數(shù)hProcess指明擁有新創(chuàng)建線程的進程,參數(shù)pfnStartAddr指明線程函數(shù)的內(nèi)存地址,該內(nèi)存地址和遠程進程是相關的,線程函數(shù)代碼不能在自己進程的地址空間中。
如執(zhí)行下面代碼:
?????? HANDLE hThread=CreateRemoteThread(hProcessRemote,NULL,0,
????????????????????? LoadLibraryA,”C://MyLib.dll”,0,NULL);
或選用Unicode:
??? HANDLE hThread=CreateRemoteThread(hProcessRemote,NULL,0,
?????? ???????????????LoadLibraryW,L”C://MyLib.dll”,0,NULL);
當在遠程進程中創(chuàng)建新線程時,該線程立即調(diào)用LoadLibrary函數(shù),并將DLL的路徑名的地址傳遞給它。直接將LoadLibrary作為線程執(zhí)行函數(shù)(所傳遞的參數(shù)就是遠程線程的起始地址),會出先問題:
1)當編譯或鏈接一個程序時,產(chǎn)生的二進制代碼包含一個輸入節(jié),這一節(jié)是有一系列輸入函數(shù)的形式替換程序(thunk)組成。當代碼調(diào)用一個如LoadLibrary函數(shù)時,鏈接程序將生成一個模塊輸入節(jié)中的形實替換程序并調(diào)用,然后,該形實替換程序便轉移到實際的函數(shù)。在CreateRemoteThread的調(diào)用中使用一個對LoadLibrary的直接調(diào)用,則將在模塊的輸入節(jié)中轉換成LoadLibrary的形實替換程序的地址,將形實替換程序的地址作為遠程線程的起始地址來傳遞,會導致線程開始執(zhí)行莫名其妙的代碼。
解決方法是:若要強制直接調(diào)用LoadLibrary函數(shù),避開形實替換程序,必須調(diào)用GetProcAddress函數(shù),獲取LoadLibrary的準確內(nèi)存位置。對CreateRemoteThread調(diào)用的前提是,Kernel32.dll已經(jīng)被同時映射到本地和遠程進程的地址空間中,每個應用程序都需要Kernel32.dll,將Kernel32.dll映射到每個進程的同一個地址,如調(diào)用如下函數(shù):
?????? PTHREAD_START_ROUTINE pfnThreadRtn=(PTHREAD_START_ROUTINE)
???????????????? GetProcAddress(GetModuleHandle(TEXT(“Kernel32”)),”LoadLibraryA”);
?????? HANDLE hThread=CreateRemoteThread(hProcessRemote,NULL,0,
?????????????????????? pfnThreadRtn,” C://MyLib.dll”,0,NULL);
或選用Unicode:
PTHREAD_START_ROUTINE pfnThreadRtn=(PTHREAD_START_ROUTINE)
???????????????? GetProcAddress(GetModuleHandle(TEXT(“Kernel32”)),”LoadLibraryW”);
?????? HANDLE hThread=CreateRemoteThread(hProcessRemote,NULL,0,
?????????????????????? pfnThreadRtn,L” C://MyLib.dll”,0,NULL);
2)字符串” C://MyLib.dll”是在調(diào)用進程的地址空間中,該字符串的地址已經(jīng)被賦予新創(chuàng)建的遠程線程,該線程將其傳遞給LoadLibrary,但是,當LoadLibrary取消對內(nèi)存地址的引用時,DLL路徑名字符串將不再存在,遠程進程的線程就可能引發(fā)訪問違規(guī),向用戶顯示一個未處理的異常條件消息框,并終止遠程進程。
解決方法是:將DLL的路徑名字符串放入遠程進程的地址空間中,然后當CreateRemoteThread函數(shù)被調(diào)用時,必須將放置該字符串的地址(相對于遠程進程的地址)傳遞給它,Windows提供了一個函數(shù)使得一個進程能夠分配另一個進程的地址空間中的內(nèi)存:
?????? PVOID VirtualAllcoEx(
?????????????????????????????????? HANDLE hProcess,
?????????????????????????????????? PVOID pvAddress,
?????????????????????????????????? SIZE_T dwSize,
?????????????????????????????????? DWORD flAllocationType,
?????????????????????????????????? DWORD flProtect);
另一個函數(shù)則能夠釋放該內(nèi)存:
?????? BOOL VirtualFreeEx(
?????????????????????????????????? HANDLE hProcess,
?????????????????????????????????? PVOID pvAddress,
?????????????????????????????????? SIZE_T dwSize,
?????????????????????????????????? DWORD dwFreeType);
一旦為該字符串分配內(nèi)存,還需要將該字符串從進程的地址空間拷貝到遠程進程的地址空間中,Windows提供了一些函數(shù),使得一個進程能夠從另一個進程的地址空間中讀取數(shù)據(jù),并將數(shù)據(jù)寫入另一個進程的地址空間。
?????? BOOL ReadProcessMemory(
????????????????????????????????????????? HANDLE hProcess,
????????????????????????????????????????? PVOID pvAddressRemote,
????????????????????????????????????????? PVOID pvBufferlocal,
???????????????????????????????????????? DWORD dwSize,
????????????????????????????????????????? PDWORD pdwNumbytesRead);
?????? BOOL WriteProcessMemory(
????????????????????????????????????????? HANDLE hProcess,
????????????????????????????????????????? PVOID pvAddressRemote,
????????????????????????????????????????? PVOID pvBufferlocal,
???????????????????????????????????????? DWORD dwSize,
????????????????????????????????????????? PDWORD pdwNumbytesWritten);
遠程進程由hProcess參數(shù)來標識,參數(shù)pvAddressRemote用于指明遠程進程的地址,參數(shù)pvBufferlocal是本地進程中的內(nèi)存地址,參數(shù)dwSize需要傳送的字節(jié)數(shù),pdwNumbytesWritten和pdwNumbytesRead用于指明實際傳送的字節(jié)數(shù),當函數(shù)返回時,可查看這兩個參數(shù)的值。
對此,執(zhí)行步驟歸納如下:
1)? 使用VirtualAllocEx函數(shù),分配遠程進程的地址空間中的內(nèi)存;
2)? 使用WriteProcessMemory函數(shù),將DLL路徑名拷貝到第一個步驟中已經(jīng)分配的內(nèi)存中;
3)? 使用GetProcAddress函數(shù),獲取LoadLibrary函數(shù)的實地址(在kernel32.dll中);
4)? 使用CreateRemoteThread函數(shù),在遠程進程中創(chuàng)建一個線程,調(diào)用正確的LoadLibrary函數(shù),為其傳遞第一個步驟中分配的內(nèi)存地址;此時,DLL已經(jīng)被插入到遠程進程的地址空間中,同時DLL的DllMain函數(shù)接收到一個DLL_PROCESS_ATTACH通知,并且能夠執(zhí)行需要的代碼,當DllMain函數(shù)返回時,遠程線程從它對LoadLibrary的調(diào)用返回到BaseThreadStart函數(shù),然后BaseThreadStart調(diào)用ExitThread,使遠程線程終止運行;現(xiàn)在遠程進程擁有第一個步驟中分配的內(nèi)存塊,而DLL仍保留在它的地址空間中,若要將它刪除,需要在遠程線程退出后執(zhí)行下面步驟:
5)? 使用VirtualFreeEx函數(shù),釋放第一個步驟中分配的內(nèi)存;
6)? 使用GetProcAddress函數(shù),獲得FreeLibrary函數(shù)的實地址(在Kernel32.dll中);
7)? 使用CreateRemotThread函數(shù),在遠程進程中創(chuàng)建一個線程,調(diào)用FreeLibrary函數(shù),傳遞遠程DLL的HINSTANCE;
使用遠程線程調(diào)用DLL只適用Windows2000,Windows1998不支持。
使用特洛伊DLL來插入DLL
該方法是取代所知道進程將要加載的DLL,如當你知道一個進程將要加載Xyz.dll,則創(chuàng)建自己的DLL并賦予相同的文件名,而把原來的Xyz.dll改成別的名字。在自己定義的Xyz.dll中,輸出的全部符號必須與原始的Xyz.dll輸出的符號相同,使用函數(shù)轉發(fā)器可掛接某些函數(shù)。如果只想在單個應用程序中使用這個方法,那可以為自己的DLL給個獨一的名字,并改變應用程序的.exe模塊的輸入節(jié),并且只包含模塊需要的DLL的名字。可搜索文件中的這個輸入節(jié),并且將它改變,使加載程序加載自己的DLL,需要數(shù)字.exe和DLL文件的格式。
將DLL作為調(diào)試程序來插入
調(diào)試程序能夠對調(diào)試的進程執(zhí)行特殊的操作,當被調(diào)試進程加載時,在被調(diào)試進程的地址空間作好準備,但是被調(diào)試進程的主線程尚未執(zhí)行任何代碼之前,系統(tǒng)將自動將這個情況統(tǒng)治調(diào)試程序,這時,調(diào)試程序可以強制將某些代碼插入被調(diào)試進程的地址空間中(比如使用WriteProcessMemory函數(shù)來插入),然后使被調(diào)試進程的主線程執(zhí)行該代碼。這種方法要求對被調(diào)試線程的CONTEXT結構進行操作,意味著要編寫特定CPU的代碼,因此需要修改源代碼,使之能夠在不同的CPU平臺上正確地運行,且對被調(diào)試進程執(zhí)行的機器語言指令進行硬編碼。調(diào)試程序和被調(diào)試程序之間必須存在固定關系。如果調(diào)試程序終止運行,Windows將自動撤消被調(diào)試進程。
用Windows98上的內(nèi)存映射文件插入代碼
在Windows98上運行的所有32位Windows應用程序均共享同樣的最上面的2GB地址空間,如果分配這里面的某些存儲器,那該存儲器在每個進程的地址空間中均可以使用。若要分配2GB以上的存儲器,則可使用內(nèi)存映射文件。可以創(chuàng)建一個內(nèi)存映射文件,然后調(diào)用函數(shù)MapViewOfFile顯示,然后將數(shù)據(jù)寫入地址空間區(qū)域(所有進程地址空間中的相同區(qū)域),必須使用硬編碼的機器語言來進行這項操作,其結果是這種解決方案很難移植到別的CPU平臺。
用CreateProcess插入代碼
如果是父子進程關系,父進程可以得到子進程的主線程的句柄,使用該句柄,可以修改線程執(zhí)行的代碼。控制子進程的主線程執(zhí)行:
1)使進程生成暫停運行的子進程;
2)從.exe模塊的頭文件中檢索主線程的起始內(nèi)存地址;
3)將機器指令保存在該內(nèi)存地址中;
4)將某些硬編碼的機器指令強制放入該地址中,這些指令應該調(diào)用LoadLibrary函數(shù)來加載DLL;
5)繼續(xù)運行子進程的主線程,使該代碼得以執(zhí)行;
6)將原始指令重新放入起始地址;
7)讓進程繼續(xù)從起始地址開始執(zhí)行,如同沒有任何事一樣。
掛接API
將DLL插入進程的地址空間是確定進程運行狀況的方法,但僅僅插入DLL無法提供足夠信息,有時需要知道進程中的線程是如何調(diào)用各個函數(shù)的,也可能需要修改Windows函數(shù)的功能。
例子:某DLL是由一個數(shù)據(jù)庫產(chǎn)品加載的,該DLL作用是增強和擴展數(shù)據(jù)庫產(chǎn)品的功能。當數(shù)據(jù)庫產(chǎn)品終止運行時,該DLL就會收到DLL_PROCESS_DETACH通知,并且只有在這時才執(zhí)行它的所有清除代碼,清除工作包括調(diào)用其他DLL中的函數(shù),以關閉套接字連接、文件和其他資源,但是在該DLL收到DLL_PROCESS_DETACH通知時,進程地址空間中的其他DLL已經(jīng)收到它們的DLL_PROCESS_DETACH通知。因此,當該DLL視圖清除時,其所調(diào)用其他DLL中的函數(shù)都失敗,因為其他DLL已經(jīng)撤消了初始化信息。
解決建議是掛接函數(shù)ExitProcess,調(diào)用ExitProcess將導致系統(tǒng)向該DLL發(fā)送DLL_PROCESS_DETACH通知,這個通知將在任何DLL得到DLL_PROCESS_DETACH通知之前進來,因此進程中的所有DLL仍然處于初始狀態(tài),并且能夠正常運行,此時,該DLL知道進程將要終止,從而能夠成功執(zhí)行它的全部清除操作。然后,操作系統(tǒng)的ExitProcess函數(shù)被調(diào)用,使所有DLL收到它們的DLL_PROCESS_DETACH通知并進行清除操作,當該公司的DLL收到這個通知時,它將不執(zhí)行專門清除操作,因為之前已經(jīng)執(zhí)行了。
插入DLL是隨意進行,當該DLL被加載時,必須掃描所有已經(jīng)加載的可執(zhí)行模塊和DLL模塊,以便找出對ExitProcess的調(diào)用。該DLL必須修改調(diào)用ExitProcess的模塊,該模塊能調(diào)用該DLL中的函數(shù),而不是調(diào)用操作系統(tǒng)的ExitProcess函數(shù),一旦該DLL中的ExitProcess替換函數(shù)(掛鉤函數(shù))執(zhí)行它的清除代碼,操作系統(tǒng)的ExitProcess函數(shù)(在Kernel32.dll文件中)就被調(diào)用。下面是通過改寫代碼掛接API的具體操作方法:
1)找到要掛接的函數(shù)在內(nèi)存中的地址(如Kernel32.dll中的ExitProcess);
2)將該函數(shù)的頭幾個字節(jié)保存在你自己的內(nèi)存中;
3) 用一個JUMP CPU指令改寫該函數(shù)的頭幾個字節(jié),該指令會轉移到替換函數(shù)的內(nèi)存地址。當然,替換函數(shù)的標記必須與掛接的函數(shù)標記完全相同,即所有參數(shù)必須一樣,返回值一樣,調(diào)用規(guī)則一樣;
4)當一個線程調(diào)用已經(jīng)掛接的函數(shù)時,JUMP指令實際上將轉移到替換函數(shù),這樣就能夠執(zhí)行任何代碼;
5)取消函數(shù)的掛接狀態(tài),取出第二步保存的字節(jié),將它們放會掛接函數(shù)的開頭;
6)調(diào)用掛接函數(shù)(已不再掛接),該函數(shù)將執(zhí)行其通常的處理操作;
7)但原始函數(shù)返回時,再次執(zhí)行第二步和第三步,如此,替換函數(shù)就可以被調(diào)用;
該方法對CPU依賴性很大,需使用手工編碼的機器指令才能使這個方法生效,且在搶占式多線程環(huán)境中根本不起作用。
另一種掛接方法是通過操作模塊的輸入節(jié)來實現(xiàn)。模塊的輸入節(jié)包含一組該模塊運行時需要的DLL,另外,還包含該模塊從每個DLL輸入的符號的列表,當模塊調(diào)用一個輸入函數(shù)時,線程實際上要從模塊的輸入節(jié)中捕獲需要的輸入函數(shù)的地址,然后轉移到該地址。要掛接一個特定函數(shù),只要改變模塊輸入節(jié)中的地址。
總結
以上是生活随笔為你收集整理的插入DLL和挂接API——Windows核心编程学习手札之二十二的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 股票投资--中线篇(转)
- 下一篇: 结束处理程序——Windows核心编程学