Windows編程總結之 DLL

+-- 版本 --+-- 編輯日期 --+-- 做者 -------------+
|   V1.0   |   2014.9.16  | yin_caoyuan@126.com |
+----------+--------------+---------------------+


這篇文章是對 《Windows核心編程(第五版)》 19,20,22 這三章的總結。
這篇文章共有 12 小節:
    1. Dll 和 進程的地址空間;
    2. 隱式載入時連接 和 顯式運行時連接;
    3. 構建 dll;
    4. 構建 可執行模塊;
    5. 運行 可執行模塊;
    6. 入口點函數;
    7. 函數轉發器;
    8. DLL 重定向;
    9. 模塊的基地址重定位;
    10. 模塊的綁定;
    11. DLL 注入;
    12. API 攔截;
如下是這 12 小節的概要:
    1:     介紹 dll 與 進程地址空間 之間的關係;
    2~5:   介紹 dll 載入 進程地址空間 的方法,這些方法是如何起做用的;
    6~8:   介紹 dll 中涉及到的其它知識點;
    9~10:  介紹 dll 載入速度的優化方法,基地址重定位和綁定技術;
    11~12: 介紹 dll 注入 技術,和基於 DLL注入 的 API攔截 技術; 
其中,1~8 節是基礎知識,9~12 節的知識依賴於 1~8 節;



1. DLL 和 進程的地址空間:
在 可執行模塊 可以調用一個 dll 中的函數以前,必須將該 dll 的文件映像映射到進程的 地址空間中。
注意:
在 dll 中預約地址空間或者分配內存,這段內存是從進程地址空間中分配的,所以當 dll 被卸載時,以前由 dll 分配的內存並不會被清理掉。好比在 dll 中的一個函數 new 了一塊內存,若是稍後將這個 dll 卸載,這塊內存並不會被清理。
當 dll 被卸載時,dll 中的「全局變量」也將被卸載。
多個可執行文件共享一個 dll 時,dll 中的全局變量和靜態變量並不會共享,當一個進程將一個 dll 映像文件映射到本身的地址空間時,系統會爲全局變量和靜態變量建立新的實例。
可執行文件和 dll 有可能使用不一樣的 C運行時庫,好比在 dll 中使用 malloc 分配一塊內存,在可執行文件中使用 free 去釋放內存,可能由於二者使用了不一樣的 C運行時庫,free 不能正確釋放 malloc 分配的內存。針對這種問題,當一個模塊提供一個內存分配函數時,必須同時提供另外一個用來釋放內存的函數。
void* DllAllocMem()
{
    return (malloc(100));
}
void DllFreeMem(void* pv)
{
    free(pv);
}


2. 隱式載入時連接 和 顯式運行時連接
將 dll 文件載入到 進程地址空間中,有兩種方法, 隱式載入時連接 和 顯式運行時連接。
隱式載入時連接,是指在可執行模塊載入的時候,把這個可執行模塊須要用到的 dll 載入到進程地址空間中。
顯式運行時連接,是指在可執行模塊運行的時候,動態載入指定的 dll,而後設法獲取導出內容的地址,進行調用。
使用 隱式載入時連接,須要在可執行模塊編譯的時候,傳入一個 .lib 文件,這個 .lib 文件稱爲導入庫文件。
使用 顯式運行時連接,不須要 .lib 文件,僅從 .dll 文件中就能夠解析出導出的內容。
隱式連接 dll,須要在工程的設置面板中設置 附加庫文件,或者使用 #pragma comment(lib,"xxx.lib") 命令,告知連接器去連接指定的 .lib 文件。
顯式連接 dll 須要用到的函數:
LoadLibrary(pszDllPathName);        // 載入 dll 到進程地址空間中 
LoadLibraryEx(pszDllPathName, hFile, dwFlags);      // 提供額外參數
FreeLibrary(hInstDll);              // 從進程地址空間中卸載 dll 
GetModuleHandle(pszModuleName);     // 能夠用來檢查一個 模塊 是否被載入 
FARPROC GetProcAddress(hInstDll, pszSymbolName);    // 獲得 dll 中的指定導出函數。
使用 隱式載入時連接 能夠在代碼中直接引用 dll 中的符號,很是方便, 顯式運行時連接 不能直接引用,可是能夠在程序運行時動態地去加載 dll。
隱式載入時連接 爲什麼能直接在代碼中引用 dll 的符號(編譯時並不知道 dll 的符號存在於哪裏),會在下面的構建過程當中給出解釋。


3. 構建 dll
dll 模塊的 構建過程:
       testdll.h
a.cpp    b.cpp    c.cpp
  |        |        |
  V        V        V
編譯器   編譯器   編譯器
  |        |        |
  V        V        V
a.obj    b.obj    c.obj
     \     |     /
      V    V    V
         連接器 <-- (.def)
           |
           V
       .dll, .lib
編譯期:
在編譯器編譯各個 .cpp 文件的時候,若是發現 __declspec(dllexport) 修飾符修飾的 變量、函數、或C++類 的話,就會在生成的 .obj 文件裏嵌入這些要導出的內容的信息
連接期:
連接器會檢測 .obj 中嵌入的導出信息,並利用這些信息生成一個 導出段(export section),這個導出段中列出了導出的變量、函數和類的符號名,連接器還會保存相對虛擬地址RVA(relative virtual address),表示每一個符號能夠在 dll 中的何處找到。
此外,連接器還會用導出信息 生成一個 .lib 文件,這個 .lib 文件列出了這個 dll 導出的符號。
咱們可使用 dumpbin.exe (加上-exports 選項)來查看一個 .dll 文件的導出段:
...
ordinal hint  RVA      name
   1    0     00001010 ReadBinaryFileToBuffer = ReadBinaryFileToBuffer
   2    1     00001090 WriteBinaryFileWithBuffer = WriteBinaryFileWithBuffer
...
如上,hint表示序號,也可使用這個序號來訪問 dll 中導出的內容,name 表示導出符號的名字,而 RVA 表示一個偏移量,導出的符號位於 dll 映像文件的這個位置。
注意:
    1. 爲何要有 __declspec(dllexport):
        咱們必須告訴編譯器和連接器,哪些函數、變量、C++類是須要導出的。所以須要 __declspec(dllexport) 這個修飾符來修飾那些須要導出的內容。
    2. 生成的 .dll 裏包含了哪些信息?
        一個導出段,標識了這個 dll 裏有哪些導出符號,如何尋找這些符號。
        這個導出段記錄了訪問導出內容所須要的所有信息。藉助於導出段,只須要一個 dll 文件就足以訪問 dll 中導出的全部內容。
    3. 生成的 .lib 裏包含了哪些信息?
        既然只要有 .dll 就能夠訪問到 dll 中導出的內容,爲何還須要一個 .lib 呢?
        .lib 文件是專門爲了隱式連接 dll 而建立的,其中僅包含了 dll 導出的符號。使用 .def 文件也能夠產生出 .lib 文件,可見 .lib 中的信息有多簡單。
    4. C++代碼的名稱粉碎問題:
        C++ 編譯器在編譯 C++ 代碼的時候,會對 C++ 代碼進行名稱粉碎,好比 ReadBinaryFileToBuffer 被重命名爲 ?ReadBinaryFileToBuffer@@YGKPB_WPAEI@Z, 這是爲了實現函數重載,不一樣的參數調用不一樣的函數。
        由於 C++ 會有名稱粉碎,而 C 沒有,因此使用 C++ 編寫的 dll 被 C模塊 調用的時候,C 模塊沒法使用 ReadBinaryFileToBuffer 找到正確的函數。
        C++ 爲了解決這個問題,引入了一個修飾符: extern "C" ,這要求編譯器不要對指定的符號進行 名稱粉碎。注意: extern "C" 是 C++ 的特性,C 語言中是沒有這個修飾符的。
        若是使用 C++ 編寫 dll,那麼要在聲明導出函數的時候加上 extern "C"
    5. 導出 C++類 的名稱粉碎問題:
        針對導出 C++類,對於類來講,名稱粉碎是必須的,不能經過 extern "C" 來消除,這就要求只有當導出 C++ 類的模塊使用的編譯器與導入 C++ 類的模塊使用的編譯器由同一廠商提供時,咱們才能夠導出 C++ 類,這樣才能夠保證C++類名稱粉碎以後的結果是一致的。所以,除非知道可執行模塊的開發人員與 dll 模塊的開發人員使用的是相同的工具包,不然咱們應該避免從 dll 中導出類。
    6. C 代碼的名稱粉碎問題:
        以前說過 C 編譯器不會進行名稱粉碎,可是不知道爲啥,即便根本沒有用到 C++,Microsoft 的 C 編譯器也會對 C 函數進行名稱粉碎,和 C++編譯器粉碎的結果不大同樣, ReadBinaryFileToBuffer 被粉碎爲 _ReadBinaryFileToBuffer@12
        所以若是咱們在VC上使用 C 語言編寫 dll 模塊,而後這個 dll 模塊要給別的廠商使用的話,名稱粉碎問題仍然會可執行模塊不能正確找到 ReadBinaryFileToBuffer
        解決的辦法是使用 模塊定義文件 .def 。
        當連接器連接各個 .obj 文件的時候,會從 .obj 裏找到對應的導出信息,好比 _ReadBinaryFileToBuffer@12,若是有 .def 文件,又從 .def 裏找到了 ReadBinaryFileToBuffer,這兩個函數是匹配的,連接器就會使用 .def 裏定義的名字來做爲導出的函數名。
        注意:即便在 .cpp 文件裏沒有使用 __declspec(dllexport) 修飾導出函數,在 .def 裏聲明瞭導出函數的話,也同樣是能夠的。
        
        
4. 構建可執行模塊
可執行文件的構建過程:
        testexe.h
a.cpp    b.cpp    c.cpp
  |        |        |
  V        V        V
編譯器   編譯器   編譯器
  |        |        |
a.obj    b.obj    c.obj
     \     |     /
      V    V    V
         連接器  <-- (.lib)
           |
           V
       testexe.exe
編譯期:
編譯各個 .cpp 文件產生出 .obj文件,若是使用到了 dll 中的符號(隱式載入時連接 dll),把它看成外部符號暫不處理。
連接期:
將各個 .obj 文件合併,並使用 .lib 來解析對導入的函數、變量的引用,.lib 中只是包含了 dll 中導出的符號,連接器只是想知道被引用的符號確實存在,以及符號來自於哪一個 dll。
若是連接器可以解決對全部外部符號的引用,就能連接成功生成可執行模塊。若是沒有包含 .lib 可是引用了 dll 中的符號的話,將會出現 error Link2091: 沒法解析的外部符號 xxxFunc,由於連接器沒法知道這個外部符號是否存在。
對於引用了外部符號的代碼,連接器將這段代碼編譯爲跳轉到一個地址表中,在連接期不知道導入函數的地址因此這個地址表是空的,當可執行模塊載入的時候,這個地址表將被導入函數的地址填充起來。
連接器解決導入符號的時候,會在生成的可執行模塊中嵌入一個特殊的段,稱爲 導入段(import section)。導入段中列出了須要使用的 dll 模塊,以及從每一個 dll 模塊中引用的符號。
咱們可使用 dumpbin.exe (加上 -imports 選項)來查看一個模塊的導入段:
...
TestDll.dll
    4020B4 Import Address Table
    40242C Import Name Table
        0 time date stamp
        0 Index of first forwarder reference
        1 _WriteBinaryFileWithBuffer@12
        0 _ReadBinaryFileToBuffer@12
...
如上,TestDll.dll 是該可執行模塊所依賴的 dll 的名稱,Import Address Table 是 導入內容地址表,在 TestDll.dll 被加載以後,dll 中導出函數的地址將被填充到這個表裏,此時爲空。Import Name Table 是導入內容名稱表,其中記錄了從 TestDll.dll 導入的函數名稱。
注意:
    1. 可執行文件構建完成後,只是知道依賴於哪些 dll,哪些dll中存在着外部符號。它的 Import Address Table 是空的,可執行模塊和 dll 被加載的時候, Import Address Table 將被填充起來。
    2. .lib 文件並無什麼神奇的,它只是包含了 dll 導出函數的名稱,連接器使用它只是爲了確認被引用的外部符號存在與哪一個dll中,根據這一條信息,連接器就能夠產生針對某個 dll 的導入段。
    3. 咱們可使用 pexports.exe 工具由 .dll 產生出 .def, 而後使用 VC 的 lib.exe 工具由 .def 產生出 .lib 文件。
    
    
5. 運行可執行模塊
運行過程:
    1. 爲進程建立虛擬地址空間;
    2. 把可執行模塊映射到進程地址空間中;
    3. 檢查可執行模塊的導入段,根據規則搜索程序路徑和系統路徑,找到所需的 dll 並加載;
    4. 檢查 dll 的導入段,若是這個 dll 還依賴別的 dll,那麼繼續去定位所需的 dll 並加載;
    5. 開始修復全部對導入符號的引用,此時會再次查看全部模塊的導入段。對導入段中列出的每一個符號,加載程序會檢查對應 dll 的導出段,看符號是否存在,若是符號存在,就從 dll 的導出段中取出 RVA 並加上模塊的虛擬地址,這樣就獲得了這個符號在進程地址空間中的地址。
    6. 獲得符號的地址後,加載程序會把這個虛擬地址保存到可執行模塊的導入段中,此時 Import Address Table 將被填充起來。
    7. 當代碼引用到一個導入符號的時候,會查看 Import Address Table 獲得導入符號的地址,這樣就能訪問被導入的 變量、函數、C++類了。
注意:
    1. 在第三步定位 dll 的時候,若是沒有找到所須要的 dll,則會彈出錯誤提示:「沒法啓動,由於計算機中缺失 xxx.dll」
    2. 在第五步修復導入符號引用的時候,若是在 dll 的導出段中沒有找到對應的導出符號,則會彈出錯誤提示:「程序入口點 xxxFunc 沒法定位到動態連接庫 xxx.dll 上」
    
    
6. 入口點函數
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:    // dll 被映射到地址空間時,會調用 DllMain 並傳入 DLL_PROCESS_ATTACH
        break;
    case DLL_THREAD_ATTACH:     // 進程中有線程建立時,新建立的線程會調用 DllMain 並傳入 DLL_THREAD_ATTACH, DllMain 能夠根據這個消息執行與線程相關的初始化
        break;
    case DLL_THREAD_DETACH:     // 當線程函數返回時,會調用 ExitThread 函數,即將終止的線程會在 ExitThread 中調用 DllMain 並傳入 DLL_THREAD_DETACH
        break;
    case DLL_PROCESS_DETACH:    // 當 dll 從進程地址空間中卸載時,發出卸載 dll 指令的線程會調用 DllMain 並傳入 DLL_PROCESS_DETACH
        break;
    }
    return TRUE;
}
參數 hModule 是這個 dll 實例的句柄;
參數 lpReserved 表示 dll 是如何載入的,若是是隱式載入 lpReserved 不爲零,若是是顯式載入 lpReserved 將爲零;
參數 ul_reason_for_call 是 DllMain 被調用的緣由,多是下列 4 個值之一: DLL_PROCESS_ATTACH,DLL_THREAD_ATTACH,DLL_THREAD_DETACH,DLL_PROCESS_DETACH
注意:
    1. DLL 使用 DllMain 函數來對本身初始化,當 DllMain 執行的時候,其它 DLL 的 DllMain 可能尚未被執行,此時若是咱們在 DllMain 中調用其它 DLL 中的導出函數,就可能出現問題。Platform SDK 文檔中說 DllMain 函數只應該執行簡單的初始化,好比設置線程局部存儲區,建立內核對象,打開文件等等。咱們必須避免調用 User,Shell,ODBC,COM,RPC以及套接字函數,這是由於包含這些函數的 DLL 可能還沒有初始化完畢。
    2. 若是要在 Dll 中建立全局或者靜態 C++ 對象,會存在一樣的問題,由於在 DllMain 被調用的同時,這些對象的構造函數和析構函數也會被調用。
    3. 當 DllMain 處理 DLL_PROCESS_ATTACH 的時候, DllMain 的返回值用來表示該 DLL 是否初始化成功,若是在這個時候 DllMain 返回了 FALSE ,則會彈出窗口,程序沒法啓動。
    4. 若是在 DLL_PROCESS_DETACH 中存在無限循環,有可能會致使進程沒法終止,只有全部 DLL 的 DLL_PROCESS_DETACH 消息都被處理完,進程纔會終止。除非使用 TerminateProcess 強行停止進程,這種狀況下 DllMain 不會收到 DLL_PROCESS_DETACH 消息。
    5. 若是在 DLL_THREAD_DETACH 中存在無限循環,有可能會致使線程沒法終止,除非使用 TerminateThread 強行終止線程。


7. 函數轉發器
函數轉發器(function forwarder)是 DLL 輸出段中的一個條目,用來將一個函數調用轉發到另外一個 DLL 中的另外一個函數。例如,用 dumpbin.exe(-exports) 工具查看 kernel32.dll 咱們會看到相似下面的輸出:
1486  5CD  WaitForThreadpoolIoCallbacks (forwarded to NTDLL.TpWaitForIoCompletion)
1487  5CE  WaitForThreadpoolTimerCallbacks (forwarded to NTDLL.TpWaitForTimer)
1488  5CF  WaitForThreadpoolWaitCallbacks (forwarded to NTDLL.TpWaitForWait)
1489  5D0  WaitForThreadpoolWorkCallbacks (forwarded to NTDLL.TpWaitForWork)
這個輸出顯示了4個被轉發的函數。
若是使用隱式載入時連接 kernel32.dll ,當可執行文件運行的時候,加載程序會載入 kernel32.dll 並發現被轉發的函數其實是在 NTDLL.dll 中,而後它會將 NTDLL.dll 模塊也一併載入。當可執行文件調用 WaitForThreadpoolIoCallbacks 的時候,實際上調用的是 NTDLL 的 TpWaitForIoCompletion 函數。
若是使用顯式運行時連接 kernel32.dll ,若是在可執行文件運行的時候調用 WaitForThreadpoolIoCallbacks ,那麼 GetProcAddress 會先在 kernel32.dll 的導出段中查找,並發現 WaitForThreadpoolIoCallbacks 是一個轉發器函數,因而它會遞歸調用 GetProcAddress ,在 NTDLL.dll 的導出段中查找 TpWaitForIoCompletion
使用 pragma 指示符,咱們能夠在本身的 dll 模塊中使用函數轉發器。以下所示:
#pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")
這個 pragma 告訴連接器,正在編譯的 DLL 應該輸出一個名爲 SomeFunc 的函數,但實際實現 SomeFunc 的是另外一個名爲 SomeOtherFunc 的函數,該函數被包含在另外一個名爲 DllWork.dll 的模塊中。咱們必須爲每個想要轉發的函數單首創建一行 pragma。
注意:
pragma 語句要放在 函數定義的後面:
DLLTEST_LIB int DllTestFunc()
{
    return 10;
}
#pragma comment(linker, "/export:DllTestFunc=FileSystem.fnFileSystem")
利用這種技術,能夠經過僞造 dll 的方式對目標 dll 中的函數進行攔截。使用僞造 dll 轉發目標 dll 中的大部分函數,而想要攔截的函數則不轉發。


8. DLL 重定向
DLL 重定向指的是咱們能夠經過某種手段強制系統在加載 DLL 的時候首先從應用程序的目錄中載入模塊。
這須要介紹 DLL 加載時候的搜索順序。
// TODO 介紹加載 DLL 的搜索順序,貌似這個和系統有關係。
DLL 重定向是在 Windows2000 以後添加的一項特性,在這以前,出於節約內存和磁盤的緣由,dll 儘可能被放在 系統目錄中,當多個程序使用同一個 dll 的時候,就可能出現 DLL Hell 的問題。
所以微軟提供了 DLL 重定向技術,強制先從應用程序目錄中載入 dll 模塊,也就是不一樣程序使用各自的 dll 互不影響。
// TODO 介紹如何使用 DLL 重定向,並肯定 DLL 重定向在不一樣的系統中是否默認打開。


9. 模塊的基地址重定位:
每一個可執行文件和 DLL 模塊都有一個首選基地址(preferred base address),它表示在將模塊映射到進程的地址空間中時的首選位置。當咱們在構建一個可執行文件的時候,連接器會將模塊的首選基地址設爲 0x00400000。對 DLL 模塊來講,連接器會將首選基地址設爲 0x10000000 。
咱們能夠用 dumpbin.exe(/headers) 來查看模塊的首選基地址:
OPTIONAL HEADER VALUES
         10B magic # (PE32)
        9.00 linker version
        1200 size of code
         C00 size of initialized data
           0 size of uninitialized data
        1630 entry point (00401630)
        1000 base of code
        3000 base of data
      400000 image base (00400000 to 00405FFF)    <-- 首選基地址是 0x00400000
編譯器和連接器會依據首選基地址來產生機器碼:
在可執行文件中的一段機器碼:(首選基地址爲 0x00400000)
MOV    [0x00414540],5
在 DLL 中的一段機器碼:(首選基地址爲 0x10000000)
MOV    [0X10014540],5
若是可執行文件只依賴於一個 dll,那麼不會有啥問題,dll 被加載時會正確載入到它的首選基地址 0x10000000 上;
若是可執行文件依賴於多個 dll,問題來了,默認狀況下每一個 dll 的首選基地址都是 0x10000000,第一個 dll 被正確載入到了 0x10000000 上,第二個 dll 就不可能載入到 0x10000000 上了。這種狀況下,加載程序會對第二個 dll 進行基地址重定位,把它放到別的地方。
在 dll 加載時對其進行基地址重定位是個很是痛苦的過程。假如 dll 被重定位到了 0x20000000 處,那麼這個 dll 中全部的機器碼都應該改爲 0x2xxxxxxx,這樣才能正確運行這些機器碼。
基地址重定位會損害應用程序的初始化時間。所以若是要將多個模塊載入到同一個進程地址空間中,咱們必須給每一個模塊指定不一樣的首選基地址。
指定首選基地址的方法:
1. 能夠在 VS 的配置項中修改首選基地址:配置屬性 --> 連接器 --> 高級 --> 基址
2. 在全部的 dll 編譯完成者後,使用 Rebase.exe 工具,能夠對須要載入到進程地址空間的全部模塊進行基地址重定位,使每一個 dll 都使用不一樣的基地址,彼此互不干擾。
Rebase.exe 運行的時候會模擬全部模塊被加載時進行的基地址重定位操做,重定位後各個模塊之間彼此互不干擾,而後將模擬的結果寫入到各個模塊的磁盤文件中。(0x1xxxxxxx 被修改成 0x2xxxxxxx)


10. 模塊的綁定
回想一下隱式載入時連接的原理:
可執行模塊的導入段中有一個 Import Address Table , 載入以前這個表是空的,可執行模塊載入的時候,載入程序會加載須要的 dll ,獲取 dll 的基地址,獲取導出符號的 RVA ,基地址加上 RVA 就是導出符號的真實地址,每一個導出符號的真實地址都會被加載程序填充到 Import Address Table 表中;
可執行模塊運行期間若是調用了某個dll的導出函數,那麼會跳轉到 Import Address Table 來獲得這個導出函數的地址,而後進行調用。
Import Address Table 中填充的是 dll 載入到地址空間的基地址+RVA。RVA在 dll 的導出段中已經有了,而經過基地址重定位技術能夠肯定 dll 載入的基地址是多少。也就是說若是一個 dll 已經進行太重定位,就能夠直接推算出它的 Import Address Table 應該填充哪些內容。若是一開始就把 Import Address Table 填充在 dll 文件裏的話,載入程序就不須要進行填充工做了,這能夠加快應用程序的初始化速度。
進行這種填充相似於將所需的 dll 與可執行文件綁定在一塊兒,可執行文件的 Import Address Table 與 dll 的基地址和導出符號 RVA 一一對應,因此這種技術被稱爲模塊綁定技術。
VS 提供的 Bind.exe 提供了綁定可執行文件與dll的功能。
Bind.exe 的工做原理正如上面所寫的那樣,讀取全部 dll 的基地址和RVA,將其填充到可執行文件的 Import Address Table 中。
注意:
    1. 若是要使用 Bind.exe 必須保證 dll 已經進行太重定位,dll 會被加載到首選基地址上。
    2. 什麼時候使用 Bind.exe 進行模塊綁定呢?由於不一樣的 Windows 版本系統 dll 可能會不一樣,因此針對不一樣版本的 Windows須要分別進行綁定,咱們能夠在應用程序的安裝過程當中來進行綁定。
    

11. DLL 注入
指將一個 DLL 注入到另一個進程的地址空間中,從而跨越進程地址邊界來訪問另一個進程的地址空間。
    1. 使用註冊表來注入 DLL:
    HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\ 在這個註冊表項中能夠找到兩個註冊表值: AppInit_Dlls,LoadAppInit_Dlls, 將 AppInit_Dlls 的值設定爲咱們要注入的 dll 路徑,將 LoadAppInit_Dlls 的值設定爲 1。當 User32.dll 被映射到一個新的進程的時候,會收到 DLL_PROCESS_ATTACH 通知,當 User32.dll 對這個通知進行處理的時候,會取得 AppInit_Dlls 中的值,並調用 LoadLibrary 來載入指定的 dll。
    這種方法利用 User32.dll 被加載時檢索註冊表的特性來實現 DLL 注入,這種方法有侷限性,全部基於 GUI 的程序都會被注入,沒法注入到指定的程序中。不過能夠借鑑這種思路,若是咱們知道目標程序會在加載的時候從註冊表中加載 dll,就能夠把本身的 dll 也添加到註冊表裏面。
    
    2. 使用 Windows 掛鉤來注入 DLL:
    咱們能夠爲另一個進程安裝掛鉤,監聽另一個進程的消息,當另一個進程的窗口即將處理一條消息的時候,將會引發掛鉤函數的調用。安裝掛鉤使用 SetWindowsHookEx 函數:
    HHOOK SetWindowsHookEx(
        int idHook,         // 要安裝的掛鉤類型,好比 WH_KEYBOARD 用於監聽鍵盤事件,WH_GETMESSAGE 用於監聽消息被 Post 進窗口消息隊列事件
        HOOKPROC lpfn,      // 函數地址,監聽的事件發生時,系統會調用這個函數,若是咱們要把掛鉤安裝到另一個進程中,這個函數必須被放到一個 dll 中;若是隻是安裝到本進程,則不須要放到 dll 中。
        HINSTANCE hMod,     // 標識一個 dll, 這個 dll 中包含了 lpfn 函數。
        DWORD dwThreadId    // 要給哪一個線程安裝掛鉤,若是指定爲 0 的話,系統會爲系統中全部的線程安裝掛鉤。
        )
    進程 A 調用 SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstDll, 0) 將會發生的事情:
        0. 進程 A 在調用 SetWindowsHookEx 以前,已經把 hInstDll 映射到了本身的進程地址空間中, GetMsgProc 是函數在進程 A 的地址空間中的地址;
        1. 某進程 B 中的一個線程準備向窗口 Post 一條消息;
        2. 系統檢查該線程是否安裝了 WH_GETMESSAGE 掛鉤;
        3. 系統檢查 GetMsgProc 所在的 DLL 是否被映射到進程 B 的地址空間中;
        4. 若是 DLL 沒有被映射, 系統將會強制將該 DLL 映射到進程 B 的地址空間中;
        5. 系統必須肯定 GetMsgProc 在進程 B 的地址空間中的地址,能夠用如下公式計算得來: GetMsgProc B = hInstDll B + (GetMsgProc A - hInstDll A)
        7. 系統在進程 B 的地址空間中調用 GetMsgProc 函數;
    經過安裝掛鉤的方式, hInstDll 這個 DLL 被注入到了進程 B 中,因爲整個 dll 都被加載到了進程 B 的地址空間中, dll 中的全部函數均可以被進程 B 中的任何線程調用。
    問題:
        1. hInstDll 被注入到了全部進程中嗎?
        2. GetMsgProc 是在進程 B 中被調用的,進程 A 能夠經過這種方法直接獲取到進程 B 的信息嗎?
        
    3. 使用 遠程線程 來注入 DLL
    利用遠程線程,咱們能夠在目標進程中建立一個本身的線程,這個線程是本身建立的,但在目標進程的地址空間中執行,咱們能夠設置本身的線程函數,只要在線程函數裏調用 LoadLibrary ,就能夠將 DLL 注入到目標進程中。
    Windows 提供了 CreateRemoteThread 函數:
    HANDLE CreateRemoteThread(
        HANDLE hProcess,                     // 目標進程句柄
        LPSECURITY_ATTRIBUTES psa,           // 安全屬性
        SIZE_T dwStackSize,                  // 線程棧大小
        LPTHREAD_START_ROUTINE lpStartAddr,  // 線程函數地址,這個地址應該在目標進程的地址空間中,由於線程函數的代碼不能在咱們本身進程的地址空間中執行。
        LPVOID lpParameter,                  // 傳遞給線程函數的參數
        DWORD dwCreationFlags                // 控制建立出來的線程,好比: CREATE_SUSPENDED
        LPDWORD lpThreadId                   // 線程 Id
    )
    咱們能夠把 LoadLibrary 做爲線程函數傳給 CreateRemoteThread, 讓 LoadLibrary 載入指定的 dll ,來實現注入 DLL 的目的:
    HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryW, L"C:\\MyLib.dll", 0, NULL)
    上面的代碼有幾個問題:
        1. LoadLibraryW 這個函數是 kernel32.dll 的導出函數,咱們的進程載入 kernel32.dll 的時候,會把 LoadLibraryW 的實際地址放入到進程的導入段中,咱們給 CreateRemoteThread 傳入的 LoadLibraryW ,其實是 LoadLibraryW 的導入段地址,並非 LoadLibraryW 的實際地址。而 lpStartAddr 要求這個地址是在目標進程的地址空間中,若是把導入段地址傳過去的話,遠程線程並不能執行到 LoadLibraryW。因此咱們應該設法獲取到 LoadLibraryW 在目標進程中的地址,而後再傳給 CreateRemoteThread 。
        解決的辦法使用 GetProcAddress 獲取 LoadLibraryW 在 kernel32.dll 中的地址,由於進程每次加載 dll 的時候系統都會把 kernel32.dll 映射到相同的地址上面,因此不一樣進程間, LoadLibraryW 的地址都等於 LoadLibraryW 在 kernel32.dll 中的地址。
        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. CreateRemoteThread 的參數 "C:\\MyLib.dll" 這個字符串位於當前進程而不是目標進程的地址空間中。目標進程訪問它的時候就會引發訪問違規,程序崩潰。因此咱們須要設法在目標進程中分配一塊內存,存儲這個字符串。
        解決的辦法是使用 VirtualAllocEx 函數在目標進程中分配內存,使用 ReadProcessMemory 和 WriteProcessMemory 來讀寫目標進程的內存,把 "C:\\MyLib.dll" 這個字符串寫入其中。
        LPVOID lpRemoteMemory = VirtualAllocEx(hProcessRemote, 0, sizeof(L"C:\\MyLib.dll"), MEM_COMMIT, PAGE_READWRITE)
        BOOL bRet = WriteProcessMemory(hProcessRemote, lpRemoteMemory, L"C:\\MyLib.dll", sizeof(L"C:\\MyLib.dll"), 0)
        HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, lpRemoteMemory, 0, NULL)
        在遠程線程執行結束後,咱們須要調用 VirtualFreeEx 來釋放以前在目標進城中分配的內存。
        3. 在遠程線程執行結束後,注入的 dll 仍然存在與目標進程中,咱們須要再次使用 CreateRemoteThread ,在遠程函數中執行 FreeLibrary ,將以前注入的 dll 從目標進程中卸載。
    參考文章:http://blog.csdn.net/g710710/article/details/7303081
    
    4. 使用木馬 DLL 來注入 DLL
    這種方法是說,把咱們知道的進程必然會載入的一個 DLL 給替換掉。咱們的 dll 須要導出原有 dll 中的全部導出符號,這可使用以前講過的函數轉發器來實現。
    若是隻想把這個方法應用在某個應用程序中,則能夠給咱們的 dll 起一個獨一無二的名稱,並修改應用程序 .exe 模塊的導入段。這要求咱們要很是熟悉 .exe 和 .dll 的文件格式。
    
    
12. API 攔截
DLL 注入可讓咱們訪問另一個進程的地址空間,獲取其它進程內部的各類信息。可是,咱們沒法知道其它進程中的線程具體是怎麼調用各類函數的,API 攔截指的是攔截 Windows 系統函數,並修改這些函數的行爲。
在對另外一個進程進行 API 攔截前,咱們必須先進行 DLL 注入,這樣另外的進程纔可以執行咱們的攔截代碼。
經過修改模塊的導入段來攔截 API:
以前關於dll的內容中說過, 可執行模塊的導入段中包含了一個符號表,其中列出了該模塊從各個 dll 中導入的符號,當可執行模塊調用一個導入函數的時候,會先從導入段的符號表中獲取到導入函數的地址,而後再跳轉到那個地址。
所以,爲了攔截一個特定的函數,咱們所須要作的就是修改這個函數在模塊的導入段中的地址。要達到這個目的,須要以下步驟:
    1. 獲取可執行模塊的導入段;
    2. 遍歷導入段中導入了哪些 dll, 經過比對找到 目標API 所屬 dll 在導入段中的信息;
    3. 遍歷從這個 dll 中導入的函數,經過比對找到 目標API 在導入段中的位置。在這以前咱們須要先獲取 目標API 的實際地址,而後跟導入段中記錄的導入函數地址相比對,才能確認這個導入函數是否是咱們要找的函數;
    4. 修改這個導入段,將其所指向的地址改爲 攔截API 的地址;
要實現上述步驟,須要如下的函數和數據結構:
    1. PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData(hExeMod, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);  // IMAGE_DIRECTORY_ENTRY_IMPORT 標記會讓這個函數返回導入段的地址;返回的結果是若干個  PIMAGE_IMPORT_DESCRIPTOR 結構體構成的數組;
    2. PIMAGE_IMPORT_DESCRIPTOR: 這個結構體描述了某一個 dll 的導入段信息, Name 字段標識這個 dll 的名字, FirstThunk 是一個 PIMAGE_THUNK_DATA 結構體,表明了從這個 dll 中導入的第一個函數的信息;
    3. PIMAGE_THUNK_DATA: PIMAGE_THUNK_DATA.u1.Function 就是這個導入函數的實際地址了;將這個地址與 GetProcAddress 獲取的地址相比對,就能肯定這個地址是否是咱們要找的那個函數的地址;
    4. WriteProcessMemory 能夠將這個地址替換爲咱們的 攔截函數 的地址;
經過上述函數和數據結構,咱們能夠構造以下的代碼:
    PROC pfnOri = GetProcAddress(GetModuleHandle("kernel32"), "ExitProcess");    // 獲取 ExitProcess 函數在進程中的地址;
    HMODULE hExeMod = GetModuleHandle("Database.exe");                           // 獲取可執行模塊的句柄;
    ULONG ulSize;                                                                // 獲取可執行模塊的導入段
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(hExeMod, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
    for(;pImportDesc->Name;pImportDesc++){                                       // 遍歷導入段,找 dll 模塊
        PSTR pszModName = (PSTR)((PBYTE)hExeMod + pImportDesc->Name);            // 獲取 dll 模塊的模塊名
        if(lstrcmpiA("kernel32.dll", pszModName) == 0 ){                         // 找到模塊名爲 kernel32.dll 的模塊
            PIMAGE_THUNK_DATA pTrunk = (PIMAGE_THUNK_DATA)((PBYTE)hExeMod + pImportDesc->FirstThunk);
            for(;pTrunk->u1.Function;pTrunk++) {                                 // 遍歷從 kernel32.dll 中導入的函數
                PROC* ppfn = (PROC*)&pTrunk->u1.Function;
                if(ppfn == pfnOri){                                              // 比對 pfnOri 和 ppfn,若是同樣,說明 pTrunk 中記錄了 ExitProcess 在導入段中的信息
                    WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL)    // 使用新的地址 pfnNew 替換 ppfn
                    return;
                }
            }
        }
    }
    上面的代碼攔截了 Database.exe 模塊對 ExitProcess 函數的調用,Database.exe 對 ExitProcess 的調用,將被咱們本身的函數 pfnNew 所替代;
    注意:上面的代碼運行在目標進程的地址空間中,在進行 API 注入前,必須把包含這段代碼的 dll 注入到目標進程中;
    上面的代碼沒有考慮安全性,原始代碼參考《Windows核心編程》的相關章節;
相關文章
相關標籤/搜索