DLL搜索路徑和DLL劫持
環境:XP SP3 VS2005
作者:magictong
為什么要把DLL搜索路徑(DLL ORDER)和DLL劫持(DLL Hajack)拿到一起講呢?呵呵,其實沒啥深意,僅僅是二者有因果關系而已。可以講正是因為Windows系統下面DLL的搜索路徑存在的漏洞才有了后來的一段時間的DLL劫持大肆流行。
最近(其實不是最近,哈,是以前分析過,斷斷續續的……)簡單分析了一個DLL劫持下載者的行為,感覺有必要寫點東西說明一下。其實DLL劫持是比較好預防的,從編程規范上我們可以進行規避(后面會專門講到),從實時防護的角度來講我們也可以想出一些辦法進行攔截。新的DLL劫持者基本都是通過當前路徑來入侵,一些老的DLL劫持者一般都是通過exe的安裝目錄來入侵的,為什么會這樣,后面還會講到。
要搞清DLL劫持的原理,首先要搞清DLL搜索路徑,到哪去搞清?當然是問微軟啦!MSDN上面有一篇專門講DLL搜索順序的文章(Dynamic-Link Library Search Order http://msdn.microsoft.com/en-us/library/ms682586%28VS.85%29.aspx),雖然是英文的但是並不復雜講得很清楚,大家如果對這塊興趣大的話可以仔細研讀下,我就不翻譯了。
Dynamic-Link Library Search Order里面主要講到一個安全DLL搜索模式的問題,大家可以通過下面的表格來看一下不同系統對安全DLL搜索模式的支持情況(下表中用SDS代表安全DLL搜索模式):
系統 |
Win2k |
Win2kSP4 |
XP |
XPSP2 |
XPSP3 |
是否支持SDS |
不支持 |
支持 |
支持 |
支持 |
支持 |
SDS是否默認開啟 |
不適用 |
否 |
否 |
是 |
是 |
SDS是否可以通過注冊表開啟 |
不適用 |
是 |
是 |
不適用 |
不適用 |
是否支持SetDllDirectory |
不支持 |
不支持 |
不支持 |
支持 |
支持 |
注:在vista和win7下沒有做過實驗,有興趣的可以自己做做實驗。
注:上面說到通過注冊表開啟是指將HKLM\System\CurrentControlSet\Control\Session Manager鍵值下的屬性SafeDllSearchMode的值設置為1(如果沒有SafeDllSearchMode就自己手動創建)。
在安全DLL搜索模式開啟的情況下,搜索順序是:
1、應用程序EXE所在的路徑。
2、系統目錄。
3、16位系統目錄
4、Windows目錄
5、當前目錄
6、PATH環境變量指定的目錄
如果安全DLL搜索模式不支持或者被禁用,那么搜索順序是:
1、應用程序EXE所在的路徑。
2、當前目錄
3、系統目錄。
4、16位系統目錄
5、Windows目錄
6、PATH環境變量指定的目錄
說了這么多?我們怎么校驗自己的系統的DLL的搜索順序呢?其實是很簡單的,我們首先構造兩個簡單的程序,一個DLL程序一個EXE程序,代碼很簡單,如下:
DLL程序:
- <span style="font-size:16px;">BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
- {
- switch(ul_reason_for_call)
- {
- case DLL_PROCESS_ATTACH:
- {
- char szDllPath[MAX_PATH] = {0};
- GetModuleFileNameA(hModule, szDllPath, MAX_PATH);
- cout << "DLL PATH: " << szDllPath << endl;
- break;
- }
- case DLL_THREAD_ATTACH:
- break;
- case DLL_THREAD_DETACH:
- break;
- case DLL_PROCESS_DETACH:
- break;
- default:
- break;
- }
- return TRUE;
- }</span>
EXE程序:
- <span style="font-size:16px;">int _tmain(int argc, _TCHAR* argv[])
- {
- HMODULE hDll = LoadLibrary(TEXT("DLLHijackSO.dll"));
- if (!hDll)
- {
- cout << "DLLHijackSO.dll Load Faild!" << endl;
- }
- return 0;
- }</span>
夠簡單了吧,思路也很明確,在DLL的DLLMAIN里面會打印出DLL所在的路徑,在EXE程序里面通過DLL的名字(不是全路徑)去加載這個DLL,如果加載失敗會打印出一條加載失敗信息。然后根據上面提到的6個地方,分別放一個DLL程序編譯出來的DLL(我起的名字是DLLHijackSO.dll),EXE編譯出來的DLLHijackApp.exe是放在H:\Prj_N\DLLHijackSO\Release里面的,然后把cmd啟動,cmd啟動之后,它的當前路徑一般都是設置的用戶目錄,譬如我的機器上面就是C:\Documents and Settings\magictong,通過CD命令對當前文件夾的切換,當前路徑也隨之改變。實驗的基本過程,因為整個系統放置了6個DLLHijackSO.dll,每運行DLLHijackApp.exe一次,如果成功加載DLL,那么就把加載的那個DLL刪除,持續進行,直到加載失敗。好,實驗開始……
我的系統的六個位置(其中最后一個PATH變量指定的路徑,你選取一個就可以了):
H:\Prj_N\DLLHijackSO\Release
C:\WINDOWS\system32
C:\WINDOWS\system
C:\WINDOWS
C:\Documents and Settings\magictong
C:\Python25
實驗結果如圖:
根據結果,我想已經很明確了,我的系統是啟用了安全DLL搜索模式的,因為我的系統是XPSP3。另外就是關於當前路徑的問題,其實當前路徑是可以由進程的父進程來設置的,大家可以去看CreateProcess里面的參數,有一項是設置當前路徑的,也就是為什么CMD啟動你的進程的時候,當前路徑會在“你想不到的地方”,explorer啟動進程則是把當前路徑設置為應用程序所在的路徑(當前路徑可以通過GetCurrentDirectory這個API來獲取)。
我想如果DLL搜索路徑搞清楚了,DLL劫持的原理就很簡單了。個人覺得一兩句話就可以說清楚:在進行DLL加載時,如果不是絕對路勁,系統會按照DLL的搜索路徑依次進行目標DLL的搜索直到找到目標DLL或者加載失敗,如果你在真實的DLL被找到之前的路徑放入你的劫持(同名)DLL,那么應用程序將先加載到你的DLL,這樣就是DLL劫持的過程。
原理雖然簡單,你的劫持DLL的選取和編寫則要有些技巧,不是所有的DLL都可以被劫持的,有些DLL是受系統保護的,譬如user32.dll等,這些是不能劫持的。在一些老的DLL劫持病毒里面一般是選取usp10.dll,lpk.dll等,原因很簡單,一般的應用程序都會加載它們,而且沒有被系統保護(好不好用,誰用誰知道,我反正沒用過)。
首先簡單總結下DLL劫持和DLL注入的區別:
|
DLL劫持 |
DLL注入 |
主動性 |
被動,等待目標運行 |
主動,目標必須已經在運行 |
是否需要跳板 |
不需要 |
需要,由第三方來幫助注入 |
是否容易攔截 |
不容易 |
容易 |
是否容易免疫 |
容易 |
不容易 |
DLL的編寫要求 |
偽造真實DLL一樣的導出函數表 |
可以按自己的流程寫導出函數 |
下面講一下新老兩種DLL劫持的攻擊原理和防御方案:
之前提到過一種老的DLL劫持的利用,劫持usp10.dll,lpk.dll等等。這些DLL的實際目錄在system32下,病毒利用DLL的搜索排名第一的是應用程序自身所在的目錄,釋放同名的劫持DLL到應用程序目錄下,這樣應用程序啟動時就會先加載了劫持DLL,達到不可告人目的。應用程序加載了劫持DLL之后又有兩種后續的攻擊方案,一種是轉發調用到正常的DLL,使應用程序毫無覺察,同時秘密在后台下載更多的下載器木馬等。另一種就是直接破壞造成程序無法運行,這種主要用於干掉殺軟等安全軟件。
通用免疫方案:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs]在此注冊表項下定義一個“已知DLL名稱”,那么凡是此項下的DLL文件就會被禁止從EXE自身目錄下調用,而只能從系統目錄,也就是system32目錄下調用。根據這個方案是不是可以寫一個簡單的DLL劫持免疫器了呢?當然對於一個安全軟件來說,這個地方也是要保護的地方之一。下圖是我機器上面該鍵值下面的一些DLL名稱:
之前看過講微點的一篇文章,講的是對這種老的DLL劫持實行的一種攔截方案:如果發現一個系統下的敏感DLL被加載,則通過堆棧回溯找到該敏感DLL的加載者的地址,如果是在一個同名的DLL里面則認為是被DLL劫持了,報告發現病毒。從對抗的角度來講,這種方案很容被過掉,只要修改棧幀指針可能就發現不了了。
新的DLL劫持的攻擊原理和防御方案:
很多應用程序都是支持插件和擴展的,尤其是一些播放器軟件,支持解碼插件,第一次安裝的時候可能只裝了常見的一些音視頻解碼插件,在遇到一些特殊的音視頻格式時就需要實時去網絡上拖取一個解碼插件下來進行解碼操作,當然軟件會首先嘗試加載這個解碼插件(通常是一個DLL),這個時候一些設計有缺陷的產品(譬如不是通過絕對路徑加載插件)在加載DLL時就會搜索上面提到過的各個路徑。一般這種情況下,惡意攻擊者會在網絡上提供一些用戶感興趣名字的視頻圖片神馬的,用戶下載壓縮包解壓后,其實壓縮文件中包含着兩個以上的文件,用戶很難發現,解壓后,劫持DLL和視頻或者圖片文件是放在同一個目錄的,當然劫持DLL文件的屬性是系統隱藏,然后用戶高高興興的去雙擊那個視頻或者圖片文件,杯具發生了……這實際上是利用的DLL搜索時會搜索當前目錄這個特點來進行DLL劫持的,為什么當前目錄是視頻圖片文件存放的目錄呢?
這個可以做個實驗,與文件關聯有關,在注冊表里面注冊一個._magic后綴的文件類型,打開這種文件的應用程序是c盤下的DLLHijackApp.exe(文件關聯這塊有興趣的自己可以查資料,因為與本文關系不大就不細講了,見下圖),DLLHijackApp.exe的作用就是彈出一個MessageBox打出當前目錄,如果直接雙擊運行DLLHijackApp.exe,當前目錄就是c盤,如果在桌面上新建一個x._magic文件再雙擊運行,打印出的當前目錄則是桌面目錄(也就是x._magic文件所在的目錄,見下面的圖)。
直接雙擊運行C盤下的DLLHijackApp.exe,當前目錄:
雙擊打開桌面上的x._magic的文件,當前目錄:
現在大家應該明白為什么當前目錄是文件所在的目錄了吧。病毒正是利用這一點,把劫持DLL和音視頻,圖片文件捆綁在一起下載,達到入侵的目的。
防御方案:
暫無通用的防御方案,因為劫持的都是一些第三方的DLL,暫時只能通過下載保護之類的途徑進行保護(這類攻擊最終還是會轉去下載更多的盜號木馬或者下載器之類的,然后進行一些盜號、破壞等等的事情)。
講了這么多,來看一個DLL劫持的實例,是簡單寫的一個說明原理的小例子:
劫持DLL要保證應用程序運行正常,不被用戶發現,除了和原來的DLL有相同的名字之外還需要導出和原DLL一樣的函數。我們現在已經有了一個名字是DLLHijackSO.dll的DLL,他導出一個Add函數,這個函數原型是int Add(int a, int b),很簡單吧。假設這個DLL是一個系統DLL,是放在system32目錄下。
原DLL的代碼如下(Add函數通過def文件導出):
- <span style="font-size:16px;">// DLLHijackSO.cpp : Defines the entry point for the DLL application.
- //
- #include "stdafx.h"
- #include <iostream>
- using namespace std;
- BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
- {
- switch(ul_reason_for_call)
- {
- case DLL_PROCESS_ATTACH:
- {
- char szDllPath[MAX_PATH] = {0};
- GetModuleFileNameA(hModule, szDllPath, MAX_PATH);
- cout << "ORC DLL - DLL PATH: " << szDllPath << endl;
- break;
- }
- case DLL_THREAD_ATTACH:
- break;
- case DLL_THREAD_DETACH:
- break;
- case DLL_PROCESS_DETACH:
- break;
- default:
- break;
- }
- return TRUE;
- }
- int Add(int a, int b)
- {
- return a + b;
- }</span>
我們的應用程序的代碼如下(名字是DLLHijackApp.exe,加載這個DLL,然后調用它的Add函數):
- <span style="font-size:16px;">// DLLHijackApp.cpp : Defines the entry point for the console application.
- //
- #include "stdafx.h"
- #include <Windows.h>
- #include <iostream>
- using namespace std;
- int _tmain(int argc, _TCHAR* argv[])
- {
- SetDllDirectoryW(TEXT(""));
- HMODULE hDll = LoadLibrary(TEXT("DLLHijackSO.dll"));
- if (!hDll)
- {
- cout << "DLLHijackSO.dll Load Faild!" << endl;
- }
- else
- {
- typedef int (*PFUNADD)(int , int );
- cout << "App - Add(int a, int b)" << endl;
- HMODULE hMod = LoadLibrary(TEXT("DLLHijackSO.dll"));
- if (hMod)
- {
- PFUNADD pfnAdd = (PFUNADD)GetProcAddress(hMod, "Add");
- cout << "App 1022 + 1022 = " << pfnAdd(1022, 1022) << endl;
- }
- }
- if (hDll)
- {
- FreeLibrary(hDll);
- hDll = NULL;
- }
- char szCurrentDir[MAX_PATH] = {0};
- GetCurrentDirectoryA(MAX_PATH, szCurrentDir);
- MessageBoxA(NULL, szCurrentDir, "當前目錄", MB_OK);
- cout << "CUR PATH: " << szCurrentDir << endl;
- return 0;
- }</span>
我們先測試一下,把DLLHijackSO.dll放入system32下,然后應用程序DLLHijackApp.exe放在任意位置,運行結果如下:嗯,是沒有問題的。
現在我們寫劫持DLL,其實也很簡單,它編譯出來的DLL名字也是DLLHijackSO.dll,也通過def文件導出了Add函數:
- <span style="font-size:16px;">// DLLHijackHijack.cpp : Defines the entry point for the DLL application.
- //
- #include "stdafx.h"
- #include <iostream>
- #include <tchar.h>
- using namespace std;
- namespace DLLHijackName
- {
- HMODULE m_hModule = NULL; //原始模塊句柄
- // 加載原始模塊
- inline BOOL WINAPI Load()
- {
- TCHAR tzPath[MAX_PATH] = {0};
- GetSystemDirectory(tzPath, MAX_PATH);
- _tcsncat_s(tzPath, MAX_PATH, TEXT("\\DLLHijackSO.dll"), _TRUNCATE);
- m_hModule = LoadLibrary(tzPath);
- if (!m_hModule)
- {
- cout << "無法加載DLLHijackSO.dll,程序無法正常運行。" << endl;
- }
- return (m_hModule != NULL);
- }
- // 釋放原始模塊
- inline VOID WINAPI Free()
- {
- if (m_hModule)
- FreeLibrary(m_hModule);
- }
- // 獲取原始函數地址
- FARPROC WINAPI GetOrgAddress(PCSTR pszProcName)
- {
- FARPROC fpAddress;
- if (m_hModule == NULL)
- {
- if (Load() == FALSE)
- ExitProcess(-1);
- }
- fpAddress = GetProcAddress(m_hModule, pszProcName);
- if (!fpAddress)
- {
- cout << "無法找到函數,程序無法正常運行。" << endl;
- ExitProcess(-2);
- }
- return fpAddress;
- }
- }
- BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
- {
- switch(ul_reason_for_call)
- {
- case DLL_PROCESS_ATTACH:
- {
- char szDllPath[MAX_PATH] = {0};
- GetModuleFileNameA(hModule, szDllPath, MAX_PATH);
- cout << "DLL Hijack - DLL PATH: " << szDllPath << endl;
- break;
- }
- case DLL_THREAD_ATTACH:
- break;
- case DLL_THREAD_DETACH:
- break;
- case DLL_PROCESS_DETACH:
- DLLHijackName::Free();
- break;
- default:
- break;
- }
- return TRUE;
- }
- typedef int (*PFUNADD)(int , int );
- int Add(int a, int b)
- {
- cout << "DLL Hijack - Add(int a, int b)" << endl;
- PFUNADD pfnAdd = (PFUNADD)DLLHijackName::GetOrgAddress("Add");
- if (pfnAdd)
- {
- return pfnAdd(a, b);
- }
- return 0;
- }</span>
好,現在我們把這個劫持的DLLHijackSO.dll放在DLLHijackApp.exe同目錄下,運行:
劫持成功!
當然,我們的重點還是要放在避免我們編寫的軟件被DLL劫持,一般有以下一些針對DLL劫持的安全編碼的規范(其實大家也應該可以從上述的DLL劫持的原理自己總結出來)::
1)調用LoadLibrary,LoadLibraryEx,CreateProcess,ShellExecute等等會進行模塊加載操作的函數時,指明模塊的完整(全)路徑,禁止使用相對路徑(這樣基本就可以防死上面所講的第二種DLL劫持了)。
2)在應用程序的開頭調用SetDllDirectory(TEXT(""));從而將當前目錄從DLL的搜索列表中刪除,也就是搜索時不搜索當前目錄。
3)打上最新的系統補丁,確保安全DLL搜索模式是開啟狀態。
4)對於安全軟件來講要確保用戶的機器上面的KnownDLLs下是完整的。
5)DLL的重定向等需要注意的問題。
from:http://blog.csdn.net/magictong/article/details/6931520