程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 《windows核心編程系列》談談使用遠程線程來注入DLL

《windows核心編程系列》談談使用遠程線程來注入DLL

編輯:關於C語言

 

windows內的各個進程有各自的地址空間。它們相互獨立互不干擾保證了系統的安全性。但是windows也為調試器或是其他工具設計了一些函數,這些函數可以讓一個進程對另一個進程進行操作。雖然他們是為調試器設計的,但是任何應用程序都可以調用它們 。接下來我們來談談使用遠程線程來注入DLL。

 

         從根本上說,DLL注入就是將某一DLL注入到某一進程的地址空間。該進程中的一個線程調用LoadLibrary來載入想要注入的DLL。由於我們不能直接控制其他進程內的線程,因此我們必須在其他進程內創建一個我們自己的線程。我們可以對新創建的線程加以控制,讓他調用LoadLibrary來載入DLL。windows提供了一個函數,可以讓我們在其他進程內創建一個線程:

 

       在其他進程內創建的線程被稱為:遠程線程,該進程被稱為遠程進程。

 

 

<span style="font-size:18px;">    HANDLE WINAPI CreateRemoteThread( 

      __in   HANDLE hProcess, 

      __in   LPSECURITY_ATTRIBUTES lpThreadAttributes, 

      __in   SIZE_T dwStackSize, 

      __in   LPTHREAD_START_ROUTINE lpStartAddress, 

      __in   LPVOID lpParameter, 

      __in   DWORD dwCreationFlags, 

      __out  LPDWORD lpThreadId 

    ); 

</span> 

 

       很容易吧。該函數除了第一個參數hProcess,標識要創建的線程所屬的進程外,其他參數與CreateThread的參數完全相同。

 

參數lpstartAddress是線程函數的地址。由於是在遠程進程創建的,所以該函數一定必須在遠程進程的地址空間內。

 

       現在知道了如何在另一個進程創建一個線程,那麼我們如何讓該線程載入我們的DLL呢?

 

       先別急著讓線程調用LoadLibrary載入DLL,現在要考慮的是如何讓線程運行起來,即為線程選擇線程函數。因為線程是在其他進程內運行的,所以該線程函數必須符合以下條件:

 

       1:該函數符合線程函數的原型,

 

       2:存在於遠程線程地址空間內。

 

       仔細分析下,遠程線程的任務只有一個。就是調用LoadLibray加載DLL。

 

       既然如此可不可以讓LoadLibrary直接作為線程函數呢?

 

         先看第一個條件:函數簽名是否相同。你還別說,除了參數類型有點不一樣外,其他一摸一樣的。由於參數類型可以通過強轉實現,所以第一個條件是滿足的。

 

         再看第二個條件:該函數是否在遠程進程地址空間內。用屁股想一下我們都知道肯定在。另外他們都有相同的函數調用約定,也就是說他們的參數傳遞是從右到左壓棧的,有子程序平衡堆棧。OK,太棒了。使用LoadLibrary作為線程函數真的是太方便了 。

 

       難道是微軟故意為我們這樣設計的?無從知曉。但在這裡要謝謝發現這一技巧的牛人。

 

      查看MSDN可以發現LoadLibrary並不是一個API,它其實是一個宏。

 

     在WinBase.h可以發現這樣一句話:

 

      #ifdef UNICODE

 

      #define LoadLibrary LoadLibraryW

 

     #else

 

     #define LoadLibrary LoadLibraryA

 

     #endif

 

     明白了嗎?實際上有兩個Load*函數,他們的唯一區別就是參數類型不同。如果DLL文件名是以ANSI形式保存的,我們就必須調用LoadLibraryA,如果是UNICODE形式保存的我們就必須調用LoadLibraryW。

 

      接下來我們要做的事情就簡單了,只需要調用CreateThread函數,傳給標識線程函數的參數LoadLibraryA或是LoadLibraryW。然後將我們要遠程進程加載的DLL的路徑名的地址作為參數傳給它。哈哈,很興奮吧!一切都是那麼的順!

 

       不要高興的太早。你就沒發現哪有不對的地方嗎?傳給線程函數的參數是DLL路徑名的地址。但是該地址是在我們進城內的。如果遠程進程引用此地址的數據,很可能會導致訪問違規,遠程進程被終止。怎麼樣很嚴重吧。但這也給我們一個破壞其他進程的思路。哈哈。自己發揮吧!

 

      為了解決這個問題,我們應該將該字符串放到遠程地址的地址空間去。有沒有相應的函數呢?當然有!

 

      首先應該在遠程進程的地址空間分配一塊兒內存。如何做呢!或許你很熟悉VirtualAlloc,但是他沒有這個功能。他兄弟VirtualAllocEx可以解決這個問題。看原型:

 

 

<span style="font-size:18px;">    LPVOID WINAPI VirtualAllocEx( 

      __in      HANDLE hProcess, 

      __in_opt  LPVOID lpAddress, 

      __in      SIZE_T dwSize, 

      __in      DWORD flAllocationType, 

      __in      DWORD flProtect 

    ); 

</span> 

 

     hProcess應該知道是干嘛的吧。他就是標識你要想在那個進程的地址空間申請內存的進程句柄。其他參數跟VirtualAlloc完全相同。此處不再介紹。

 

        當然知道如何申請還有知道如何釋放!看他搭檔:VirtualFreeEx

 

 

<span style="font-size:18px;">    BOOL WINAPI VirtualFreeEx( 

      __in  HANDLE hProcess, 

      __in  LPVOID lpAddress, 

      __in  SIZE_T dwSize, 

      __in  DWORD dwFreeType 

    ); 

</span> 

 

    與VirtualFree的區別這只是多一個進程句柄。

 

    現在申請空間的任務完成了,要怎麼樣將本進程的數據復制到另外一個進程呢?可以使用ReadProcessMemory和WriteProcessMemory

 

 

<span style="font-size:18px;">    BOOL WINAPI ReadProcessMemory( 

      __in   HANDLE hProcess, 

      __in   LPCVOID lpBaseAddress, 

      __out  LPVOID lpBuffer, 

      __in   SIZE_T nSize, 

     __out  SIZE_T *lpNumberOfBytesRead 

    ); 

</span> 

 

 

 

 

<span style="font-size:18px;">    BOOL WINAPI WriteProcessMemory( 

      __in   HANDLE hProcess, 

      __in   LPVOID lpBaseAddress, 

      __in   LPCVOID lpBuffer, 

      __in   SIZE_T nSize, 

      __out  SIZE_T *lpNumberOfBytesWritten 

    ); 

</span> 

 

    由於他們簽名類似,此處放在一塊介紹。

 

    hProcess是用來標識遠程進程的。

 

    lpBaseAddress是在遠程進程地址空間的地址,是VirtualAllocEx的返回值。

 

    lpBuffer是在本進程的內存地址。此處也就是DLL路徑名的地址。

 

    nSize為要傳輸的字符串。

 

    lpNumberOfByteRead和lpNumberOfByteWrite為實際傳輸的字節數。

 

     注意:當調用WriteProcessMemory時有時會導致失敗。此時可以嘗試調用VirtualProtect來修改寫入頁面的屬性,寫入之後再改回來。

 

    到此為止,看起來沒啥東西了,但是還有一個比較隱晦的問題,如果不對PE文件格式和DLL加載的方式有所了解的話是很難發現的。

 

       我們知道導入函數的真實地址是在DLL加載的時候獲得的。加載程序從導入表取得每一個導入函數的函數名(字符串),然後在被加載到進程地址空間的DLL中查詢之後,填到導入表的相應位置(IAT)的。也就是說在運行之前我們並不知道導入函數的地址(當然模塊綁定過得除外)。那麼程序代碼中是如何表示對導入函數的調用呢?有沒有想過這個問題呢。

 

       你或許覺得應該是:CALL DWORD PTR[004020108]       (   [   ]內僅表示導入函數地址,無實際意義)。

 

       由於程序的代碼在經過編譯連接之後就已經確定,而導入表的地址如00402010是在程序運行的時候獲得的。所以程序在調用導入函數的時候並不能這樣實現。那到底是如何實現的呢?

 

      [   ]內有一個確定的地址這是毋庸置疑的,但是他的值並不是導入函數的地址,而是一個子程序的地址。該子程序被稱為轉換函數(thunk)。這些轉換函數用來跳轉到導入函數。當程序調用導入函數時,先會調用轉換函數,轉換函數從導入表的IAT獲得導入函數的真實地址時在調用相應地址。

 

      所以對導入函數的調用形如如下的形式:www.2cto.com

 

   

 

 

         CALL  00401164                ;轉換函數的地址。 

 

             。。。。。。 

 

:00401164 

 

         。。。。。 

 

             CALL DWORD PTR [00402010]    ;調用導入函數。 

 

 

 

    分析到這兒,我們也可以明白為什麼在聲明一個導出函數的時候要加上_decllpec(dllimport)前綴。

 

原因是:編譯器無法區分應用程序是對一般函數的調用還是對導入函數的調用。當我們在一個函數前加上此前綴就是告訴編譯器此函數來自導入函數,編譯器就會產生如上的指令。而不是CALL XXXXXXXX的形式。

 

所以在寫一個輸出函數的時候一定要在函數聲明前加上修飾符:_decllpec(dllimport)。

 

 

 

         言歸正傳.之所以說這麼多,就是因為我們傳給CreateRemoteThread的線程函數LoadLibrary*,會被解析成我們進程內的轉換函數的地址。如果把這個轉換函數的地址作為線程函數的起始地址很可能導致訪問違規。解決方法是:強制代碼略過轉換函數而直接調用LoadLibrary*.

 

       這可以通過GetProAddress來實現。

 

 

<span style="font-size:18px;">    FARPROC WINAPI GetProcAddress( 

      __in  HMODULE hModule, 

      __in  LPCSTR lpProcName 

     ); 

</span> 

 

     hModule是模塊句柄。標志某一模塊。

 

    lpProcName是該模塊內某一函數的函數名。

 

    它返回該函數在模塊所屬進程地址空間的地址。

 

    如GetProcAddress(GetModuleHandle("Kernel.dll","LoadLibraryW"));

 

     此語句取得LoadLibrary在Kernel.dll所在進程空間的真實地址。注意此時僅僅是取得在本進程Kernel.dll的地址和LoadLibraryW的地址。難道在遠程進程內也是一樣嗎?

 

      《windows核心編程》第五版589頁第三段中說,”從作者的經驗來看,Kernel.dll映射到每個進程的地址都是相同的。“基於此,我們可以認為,我們調用此語句是取得了Kernel.dll和LoadLibraryW在遠程地址空間的地址。

 

 

 

     到此為止,關於遠程線程就介紹完畢。

 

     參考自《windows核心編程》第五版 第二十二章 ,《加密與解密》第二版 段鋼著,第十章

 

     以上僅僅在參考各書籍的基礎之上加以總結。如有錯誤,請不吝賜教。

 

摘自 ithzhang的專欄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved