1.概覽
.構造DLL
(1)僅導出函數
DLL可以導出全局變量和類,但我們不建議這么做,建議導出函數。
(2).lib
每個DLL都有與之相對應的.lib文件,該文件中列出了DLL中導出的函數和變量的符號名
(3)指定要導出的函數名
因為不同編譯器的Name mangle規則不同,這就導致DLL不能跨編譯器使用。
有以下兩種方法可以解決這個問題:
1.在.def文件中指定要導出的函數名
2.在編譯指中指定要導出的函數名:
#pragma comment(linker, "/export:MyFunc=_MyFunc@8")
.DLL加載路徑
當需要加載一個DLL時,系統會依照下面的順序去尋找所需DLL直到找到為止,然后加載,否則加載失敗。
(1)當前可執行文件路徑
(2)GetWindowsDirectory返回的Windows系統路徑
(3)16位系統的路徑 windows"system
(4)GetSystemDirectory返回的Windows系統路徑
(5)當前進程所在路徑
(6)PATH環境中所指定的路徑
.創建\使用動態鏈接庫
首先必須創建一個包含需要導出的符號的頭文件,以便其他程序鏈接到該dll上:
// dllexample.h
#ifdef DLLEXAMPLE_EXPORTS // 在編譯命令中已定義,所以實際用的是 __declspec(dllexport)
#define DLLEXAMPLE_API __declspec(dllexport)
#else
#define DLLEXAMPLE_API __declspec(dllimport)
#endif
DLLEXAMPLE_API int fnDllexample(void);
當其他應用包含該頭文件,意圖使用該dll的導出符號時,因為沒有定義DLLEXAMPLE_EXPORTS,所以使用的是__declspec(dllimport),這樣編譯器編譯時便知道這是從外部引入的函數。在鏈接時,鏈接程序將生成導入表(ImportAddressTable),該表羅列了所有調用到的函數,以及一個空白的對應地址。在程序執行時,加載器將動態的填入每個函數符號在本進程中的地址,使得程序能正確的調用到dll中的函數上。
這種通過dll提供的.h和.lib文件進行鏈接dll的使用方式,稱為隱式鏈接。用vc開發程序時,幾乎所有的系統API調用都用了隱式鏈接。
.顯式鏈接
在exe創建時不引用.lib文件中的符號,當然也不必包含.h頭文件,而是由程序調用LoadLibrary(Ex)以及GetProcAddress函數來獲取每個需要使用的函數地址,從而進行dll中的函數調用,這種dll使用方法稱為顯式鏈接。顯式鏈接時不生成對應dll的IAT.
當決定不再使用該dll時,通過調用FreeLibrary來卸載。需要注意的是,同一個進程中共計調用LoadLibrary的次數要和調用FreeLibrary的次數相等,因為系統維護了一個使用計數,當計數為0時,才會真正的卸載該dll.
如果想確認一個dll是否已經被映射到進程空間中,盡量使用GetModuleHandle,最好不要冒然使用LoadLibrary(Ex).
GetProcAddress可以傳遞函數名或者序號(通過MAKEINTRESOURCE(2)來"制作"序號).
1.1動態加載DLL文件 LoadLibraryEx
HMODULE LoadLibraryEx( //返回DLL加載到進程空間原首地址。
PCTSTR pszDLLPathName,
HANDLE hFile,
DWORD dwFlags);
dwFlags 可以有以下幾個值
(1) DONT_RESOLVE_DLL_REFERENCES
建議永遠不要使有這個值,它的存在僅僅是為了向后兼容、
(2) LOAD_LIBRARY_AS_DATAFILE
把要加載的DLL文件以數據文件的形式加載到進程中。
GetModuleHandle和GetProcAddress返回NULL
(3) LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE
與前者相同,不同的時獨占打開,禁止其它進程訪問和修改該DLL中的內容。
(4) LOAD_LIBRARY_AS_IMAGE_RESOURCE
不修改DLL中的RVA,以image的形式加載到進程中。常與LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE一起使用。
(5) LOAD_WITH_ALTERED_SEARCH_PATH
修改DLL的加載路徑
1.2 DLL的加載與卸載
(1)加載
不要在同一進程中,同時使用LoadLIbrary和LoadLibraryEx加載同一DLL文件。
DLL的引用計數是以進程為單位的。LoadLibrary會把DLL文件加載到內存,然后映射到進程空間中。
多次加載同一DLL只會增加引用計數而不會多次映射。當所有進程對DLL的引用計數都為0時,系統會在內存中釋放該DLL。
(2)卸載
FreeLibrary,FreeLibraryAndExitThread對當前進程的DLL的引用計數減1
(3) GetProcAddress
取得函數地址。它只接受ANSI字符串。
2.DLL的入口函數
2.1 DllMain
BOOL WINAPI DllMain(
HINSTANCE hInstDll, ""加載后在進程中的虛擬地址
DWORD fdwReason, ""系統因何而調用該函數
PVOID fImpLoad ""查看是隱工還是動態加載該DLL
DLLs用DllMain方法來初始化他們自已。DllMain中的代碼應盡量簡單,只做一些簡單的初始化工作。
不要在DllMain中調用LoadLibrary,FreeLibrary及Shell, ODBC, COM, RPC, 和 socket 函數,從而避免不可預期的錯誤。
2.2 fdwReason的值
(1)DLL_PROCESS_ATTACH
系統在為每個進程第一次加載該DLL時會,執行DLL_PROCESS_ATTACH后面的語句來初始化DLL,DllMain的返回值僅由它決定。
系統會忽略DLL_THREAD_ATTACH等執行后DllMain的返回值。
如果DllMain返回FALSE,系統會自動調用DLL_PROCESS_DETACH的代碼并解除DLL文件中進程中的內存映射。
(2)DLL_PROCESS_DETACH
如果DLL是因進程終止而卸載其在進程中的映射,那么負責調用ExitProcess的線程會調用DllMain中DLL_PROCESS_DETACH所對應的代碼。
如果DLL是因FreeLibrary或FreeLibraryAndExitThread,而卸載其在進程中的映射, 那么FreeLibrary或FreeLibraryAndExitThread會負責調用DllMain中DLL_PROCESS_DETACH所對應的代碼。
如果DLL是因TerminateProcess而卸載其在進程中的映射,系統不會調用DllMain中DLL_PROCESS_DETACH所對應的代碼。
(3) DLL_THREAD_ATTACH
若進程是先加載的DLL,后創建的線程
那么在進程中創建新線程時(主線程除外),系統會執行該進程已載的所有DLL的DllMain中DLL_THREAD_ATTACH對應的代碼。
若進程是先創建的線程,后加載的DLL
那么系統不會調用DLL的DllMain中的代碼。
(4) DLL_THREAD_DETACH
進程中的線程退出時,會先執行所有已加載DLL的DllMain中DLL_THREAD_DETACH所對應的代碼。若該代碼中有死循環,線程不會退出。
2.3 同步化DllMain的調用
同一時間只能有一個線程調用DllMain中的代碼,所以下面的代碼會導致死循環
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {
HANDLE hThread;
DWORD dwThreadId;
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// The DLL is being mapped into the process' address space.
// Create a thread to do some stuff.
hThread = CreateThread(NULL, 0, SomeFunction, NULL,
0, &dwThreadId);// CreateThread會DLL_THREAD_ATTACH中的代碼,但是由于當前線程并未執行完畢,
//所以DLL_THREAD_ATTACH 中的代碼不會被執行,且CreateThread永無不會返回。
// Suspend our thread until the new thread terminates.
WaitForSingleObject(hThread, INFINITE);
// We no longer need access to the new thread.
CloseHandle(hThread);
break;
case DLL_THREAD_ATTACH:
// A thread is being created.
break;
case DLL_THREAD_DETACH:
// A thread is exiting cleanly.
break;
case DLL_PROCESS_DETACH:
// The DLL is being unmapped from the process' address space.
break;
}
return(TRUE);
}
3.延時加載DLL
(1)延時加載DLL的限制
延遲加載的D L L是個隱含鏈接的D L L,它實際上要等到你的代碼試圖引用D L L中包含的一個符號時才進行加載,它與動態加載不同。
4.已知的DLL (Known DLLs)
位置:HKEY_LOCAL_MACHINE"SYSTEM"CurrentControlSet"Control"Session Manager"KnownDLLs
LoadLibrary在查找DLL會先去該位置查找有無相應的鍵值與DLL要對應,若有則根據鏈值去%SystemRoot%"System32加載鍵值對應的DLL
若無則根據默認規去尋找DLL
5.Bind and Rebase Module
它可以程序啟動的速度。ReBaseImage
DLL 注入和API鉤(DLL Injection and API Hooking)
1.概覽
每個進程都有自已獨立的地址空間,一個進程不可能創建一個指向其它進程地址空間的指針。
然而如果我們把自已的DLL注射到另一個進程的地址空間去,我們就可以在那個被注入的進程里為所欲為了。
2.用注冊表注入DLL
該方法適用于給GUI的程序注入DLL
所有的GUI應用程序在啟動時都會加載User32.dll,而在User32.dll的DLL_PROCESS_ATTACH代碼根據注冊表中的信息
來注入用戶指定的DLL
注冊表項 HKEY_LOCAL_MACHINE"Software"Microsoft"Windows NT"CurrentVersion"Windows"
中有兩個值:
LoadAppInit_Dlls:鍵值中指定要注入的DLL 如:c:"inject.dll
AppInit_Dlls:若其鍵值為1,則注入LoadAppInit_Dlls中指定的DLL,否則若為0則不注入。
注:
(1)LoadAppInit_Dlls中的值是以空格或分號分隔的,所以DLL的路徑中最好不要有空格,最后不指定路徑,直接將DLL放到windows系統目錄中。
(2) 用注冊表注入DLL的方式有很大的局限性,Kernel32.dll和Ntdll.dll中有的函數才能調用
一.注入dll
1.通過注冊表項 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs 來指定你的dll的路徑,那么當一個GUI程序啟動時就要加載User32.dll,而User32.dll將會檢查這個值,如果有的話就LoadLibrary該Dll。這個方法不好,因為大多數情況我們只需要針對性的注入,并且沒辦法注入到不使用User32.dll的進程中;
2.用SetWindowsHookEx函數,并傳遞目標線程ID、需要掛載的Dll在本進程中的映射地址(hInstance)、替換函數在本進程中的地址。這樣,當被掛載進程的這個線程要執行相應的操作時(GETMESSAGE、鍵盤消息之類的),就會發現已經安裝了WH_XX,The system checks to see whether the DLL containing the GetMsgProc function is mapped into Process B's address space,如果還未映射該Dll,則強制LoadLibrary。然后系統調用hThisInstance + (GetMsgProc - hInstance),從而實現了事件的通知。這種方法的好處是可以針對某個進程安裝Hook,缺點是容易被目標進程發現、同樣只適用于GUI進程。如果不再想使用掛鉤了,那么需要調用UnhookWindowsHookEx,卸載Hook。
3.使用遠程線程注入Dll(Injecting a DLL Using Remote Threads)
這個方法比較好。流程是這樣的:
?調用VirtualAllocEx,在目標進程保留一塊內存,并提交,其長度是你要注入Dll的全路徑長度nLen + 1,返回地址pv;
?調用WriteProcessMemory,在目標進程的pv處寫入Dll的全路徑,注意要添加\0結束符;
?獲取本進程的LoadLibrary函數的地址,方法是調用pfn = GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA")——之所以獲取本進程的地址,是因為kernel32.dll在每個進程的映射地址都相同,倘若不同,那么此方法則無效;
?調用HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfn, pv, 0, NULL)來創建遠程線程,其實這個線程函數就是LoadLibrary函數,因此將執行映射Dll到目標進程的操作;
?調用VirtuallFreeEx(hProcessRemote, pv)釋放提交的內存;
這便完成了dll注入。
缺點是不能用在windows98上。但是對于xp都要被微軟拋棄的年代,windows98地影響不大了。
4.披著羊皮的狼:使用特洛伊Dll來注入Dll(Injecting a DLL with a Trojan DLL)
其實就是替換某個目標進程要加載的a.dll,并把a.dll的所有引出函數用函數轉發器在自己的dll引出。
5.用調試函數插入Dll
ReadProcessMemory和WriteProcessMemory是windows提供的調試函數。如果在方法3中調用WriteProcessMemory寫入的不是字串而是精心編排好的機器指令,并且寫在目標進程特定的地址空間,那么這段機器指令就有機會執行——而這段機器指令恰好完成了LoadLibrary功能;
6.其他方法(略)
二.掛接API(API Hooking)
其實,這是許多注入的Dll都愿意做的事情。
所謂掛接API就是在目標進程調用windows API之前,先執行我們的仿API函數,從而控制系統API的行為,達到特殊的目的。
我們的仿造函數必須與要替換的系統API有相同的型參表以及相同的返回值類型.
1.改寫系統API代碼的前幾個字節,通過寫入jmp指令來跳轉到我們的函數。在我們的函數里執行操作,可以直接返回一個值,也可以將系統API的前幾個字節復原,調用系統API,并返回系統API的值——隨便你想怎么做。
此方法的缺點是對于搶占式多線程的系統不太管用。
2.通過改寫目標進程IAT中要調用的函數地址來達到目的。具體操作見書中示例
線程本地存儲(Thread-Local Storage)
例子C / C + +運行期庫要使用線程本地存儲器( T L S)。由于運行期庫是在多線程應用程序出現前的許多年設計的,因此運行期庫中的大多數函數是用于單線程應用程序的。函數s t r t o k就是個很好的例子。
盡可能避免使用全局變量和靜態變量
1.動態TLS
圖21-1 用于管理T L S的內部數據結構
在創建線程時,進程會為當前創建的線程分配一個void *的數組作為TLS用。它用于存儲只限當前線程可見的全局變量。
從而使進程中的每個線程都可以有自已的(不能其它線程訪問的)全局變量。
TlsAlloc在返回時會先把槽中的值置為0。每個線程至少有64個槽。
2.靜態TLS
__declspec(thread)關鍵字用于聲明,線程本地的全局變量。
要求聲明的變量必須是全局變量或靜態變量。
3.Common API:
TlsAlloc TlsFree
TlsSetValue TlsGetValue
__declspec(thread)