網絡安全編程:DLL編程
DLL(Dynamic Link Library,動態連接庫)是一個可以被其他應用程序調用的程序模塊,其中封裝了可以被調用的資源或函數。動態連接庫的擴展名一般是DLL,不過有時也可能是其他的擴展名。DLL文件屬于可執行文件,它符合Windows系統的PE文件格式,不過它是依附于EXE文件創建的進程來執行的,不能單獨運行。一個DLL文件可以被多個進程所裝載調用。
Windows操作系統下有非常多的DLL文件,有的是操作系統的DLL文件,有的是應用程序的DLL文件。使用DLL文件有什么好處呢?DLL是動態連接庫,相對應地,有靜態連接庫。動態連接庫是在EXE文件運行時被加載執行的,而靜態連接庫是OBJ文件進行連接時同時被保存到程序中的。動態連接庫可以減少可執行文件的體積,在需要的時候進入內存;將軟件劃分為多個模塊,可以按照模塊進行開發,對于發布與升級也非常方便。在某些情況下,必須使用DLL才能完成一些工作內容。
本文通過一個簡單的DLL程序來初步了解DLL程序的編寫。
1. 編寫簡單的DLL程序
首先從一個簡單的DLL程序開始,并在DLL程序中添加一個導出函數。所謂導出函數,就是DLL提供給外部EXE或其他類型的可執行文件調用的函數。當然,DLL本身也可以自身進行調用。
DLL程序的入口函數不是main()函數,也不是WinMain()函數,而是DllMain()函數,該函數的定義如下:
- BOOL WINAPI DllMain(
- HINSTANCE hinstDLL, // handle to the DLL module
- DWORD fdwReason, // reason for calling function
- LPVOID lpvReserved // reserved
- );
參數說明如下。
hinstDLL:該參數是當前 DLL 模塊的句柄,即本動態連接庫模塊的實例句柄。
fdwReason:該參數表示 DllMain()函數被調用的原因。
該參數的取值有4種,也就是說存在4種調用DllMain()函數的情況,這4個值分別是DLL_PROCESS_ATTACH(當DLL被某進程加載時,DllMain()函數被調用)、DLL_PRO CESS_DETACH(當DLL被某進程卸載時,DllMain()函數被調用)、DLL_THREAD_ATTACH(當進程中有線程被創建時,DllMain()函數被調用)和DLL_THREAD_DETACH(當進程中有線程結束時,DllMain()函數被調用)。
lpvReserved:保留參數,即不被程序員使用的參數。
啟動VC6集成開發環境,創建一個DLL工程。創建一個“A simple DLL Project”類型的工程,VC生成代碼如下:
- BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
- {
- return TRUE;
- }
在生成的代碼中,函數定義處有一個APIENTRY的函數修飾符。該修飾符為一個宏,其定義如下:
- #define APIENTRY WINAPI
由于DllMain()函數不止一次地被調用,根據調用的情況不同,需要執行不同的代碼,比如當進程加載該DLL文件時,可能在DLL中要申請一些資源;而在卸載該DLL時,則需要將先前自身所申請的資源進行釋放。出于種種原因,在編寫DLL程序時,需要把DllMain()函數的結構寫成如下形式:
- BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
- {
- switch ( ul_reason_for_call )
- {
- case DLL_PROCESS_ATTACH:
- {
- break;
- }
- case DLL_PROCESS_DETACH:
- {
- break;
- }
- case DLL_THREAD_ATTACH:
- {
- break;
- }
- case DLL_THREAD_DETACH:
- {
- break;
- }
- }
- return TRUE;
- }
這是一個switch/case結構,這樣寫可以達到根據不同的調用原因執行不同的代碼。
2. 給DLL添加一個簡單的導出函數
上面的代碼只是一個簡單的DLL程序的開始,并沒有實際的意義。對于DLL文件來說,DllMain()并不是必需的。按照DLL文件的本質作用是為其他的可執行文件提供使用,那么DLL程序中需要編寫能夠提供其他程序使用的函數,這些公開提供給其他程序使用的函數被稱為導出函數。在上面代碼的基礎上添加一個導出函數,定義如下:
- extern "C" __declspec(dllexport) VOID MsgBox(char *szMsg);
extern "C"表示該函數以 C 方式導出。由于源代碼是.CPP 文件,因此,如果按照 C++的方式導出的話,那么在編譯后函數名會被名字粉碎,導致在動態調用該函數時就會極為不方便。__declspec(dllexport)的作用是聲明一個導出函數,將該函數從本 DLL 中開放提供給其他模塊使用。
MsgBox()函數的實現如下:
- VOID MsgBox(char *szMsg)
- {
- char szModuleName[MAX_PATH] = { 0 };
- GetModuleFileName(NULL, szModuleName, MAX_PATH);
- MessageBox(NULL, szMsg, szModuleName, MB_OK);
- }
該函數在被調用時會在MessageBox窗口的標題欄處顯示其所在進程的進程名。
這樣,第一個DLL文件的編寫就完成了。編譯連接該代碼,查看編譯和連接的輸出情況會發現VC共生成了2個文件,分別是“FirstDll.dll”和“FirstDll.lib”,前者是供其他可執行程序使用的DLL文件,其中包含了程序員編寫的代碼、導出函數,而后者是一個庫文件,其中包含一些導出函數的相關信息,供調用DLL文件中導出函數函數的程序員編譯時使用。
導出DLL中的函數有兩種方法,這是其中的一種。另外一種方式是建立一個.DEF的文件來定義導出哪些函數。函數除了可以通過函數名導出外,還可以通過序號進行導出。建立.DEF文件可以較為方便地管理DLL項目中的導出函數(總比在代碼中逐個找__declspec(dllexport)要方便很多)。由于這里的代碼比較短小,因此使用了__declspec(dllexport)這種定義方法。
3. 對DLL程序的調用方法一
DLL程序是無法單獨運行的,它需要通過編寫一個EXE程序(當然也可以在另外的DLL程序中調用)來調用這個DLL文件中的導出函數。在VC集成開發環境中添加一個測試項目,在工作區的“Workspace ‘FirstDll’:1 project(s)”上單擊右鍵,在彈出的菜單中選擇“Add New Project to Workspace”,如圖1所示。
圖1 添加對DLL進行測試的項目
添加一個控制臺的項目,然后編寫對DLL進行調用的測試代碼,具體如下:
- #include <windows.h>
- #pragma comment (lib, "FirstDll")
- extern "C" VOID MsgBox(char *szMsg);
- int main(int argc, char* argv[])
- {
- MsgBox("Hello First Dll !");
- return 0;
- }
#pragma comment (lib, "FirstDll")告訴連接器需要在FirstDll.lib文件中找到DLL中導出函數的信息。
對以上代碼進行編譯連接,VC會產生一個連接錯誤,如圖2所示。
圖2 連接出錯信息
這個錯誤是因為連接器找不到“FirstDll.lib”文件。將“FirstDll.lib”復制到測試項目的目錄下,然后添加到測試工程中,再次進行編譯連接就成功了。運行編寫好的測試程序,會彈出一個錯誤對話框,如圖3所示。
圖3 運行測試程序時的錯誤信息
根據錯誤提示可以看出是缺少要測試的DLL文件,也就是“FirstDll.dll”文件。將其復制到與可執行文件相同的目錄下,然后再次運行,程序可以順利地被執行。
一般在發布DLL文件時,需要將DLL文件、Lib文件和.h文件同時發布,當然有一個說明文檔或手冊會顯得更加專業。
4. 對DLL程序的調用方法二
前一種方法屬于靜態調用,其方式是通過連接器將DLL函數的導出函數寫進可執行文件。現在使用第二種方法來調用DLL中的函數,這種方法相對于前一種方法是動態調用。動態調用不是在連接時完成的,而是在運行時完成的。動態調用不會在可執行文件中寫入DLL的相關信息。現在來寫一個關于動態調用的測試程序,該程序的創建方法與靜態調用的方法相同,這里不再復述。
動態調用DLL函數的代碼如下:
- #include <windows.h>
- typedef VOID (*PFUNMSG)(char *);
- int main(int argc, char* argv[])
- {
- HMODULE hModule = LoadLibrary("FirstDll.dll");
- if ( hModule == NULL )
- {
- MessageBox(NULL, "FirstDll.dll 文件不存在","DLL 文件加載失敗", MB_OK);
- return -1;
- }
- PFUNMSG pFunMsg = (PFUNMSG)GetProcAddress(hModule, "MsgBox");
- pFunMsg("Hello First Dll !");
- return 0;
- }
對代碼進行編譯連接都正常通過。但是請注意,這個程序中并沒有用到#pragma comment()指令,也沒有通過lib在程序中留下相關的導入信息。運行編譯連接好的程序,程序會給出提示“FirstDll.dll文件不存在”。按照前面的方法,將FirstDll.dll文件復制到與測試程序相同的目錄下,運行測試程序,程序執行成功。
DLL的動態加載調用是非常有用的。在第一個測試程序中,如果測試系統的裝載器無法找到DLL文件,那么系統會直接報錯而退出。而在第二個測試程序中,如果測試程序無法找到DLL文件,則由程序給出一個錯誤的提示,同時程序其實可以繼續往下執行,而不會影響其他代碼的運行(當然,由于DLL無法加載可能會損失部分的功能)。明白了動態加載調用和靜態加載調用的區別,那么它們的優缺點就很清楚了。靜態加載調用使用方便,而動態加載調用靈活性較好。
在有些情況下,必須使用動態加載調用的方法來使用DLL中的導出函數。比如函數OpenThread(),該函數在VC6自帶的PSDK中沒有提供LIB文件和函數原型定義,沒有LIB文件就無法連接成功(在新版的PSDK中有該函數對應的LIB文件)。在這種情況下,只能使用LoadLibrary()和GetProcAddress()這兩個函數來動態加載調用OpenThread()函數(其實有很多情況下,在使用DLL文件中的導出函數時是找不到對應的LIB文件的,比如ntdll.dll中的很多函數雖然有導出,但是系統沒有提供與其對應的LIB文件)。
現在了解一下LoadLibrary()函數和GetProcAddress()函數的定義。LoadLibrary()函數的定義如下:
- HMODULE LoadLibrary( LPCTSTR lpFileName);
該函數只有一個參數,即要加載的DLL文件的文件名。該函數調用成功,則返回一個模塊句柄。
GetProcAddress()函數的定義如下:
- FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName);
該函數有兩個參數,分別如下。
hModule:該參數是模塊句柄,通常通過 LoadLibrary()函數或 GetModuleHandle()函數獲得;
lpProcName:該參數指定要獲得函數地址的函數名稱。
該函數調用成功,則返回lpProcName指向的函數名的函數地址。
5. 查看DLL程序導出函數的工具介紹
前面介紹DLL編程時提到了導出函數,這里介紹兩款查看DLL程序的導出函數的工具。其中一款是VC自帶的工具“Depends”,另一款工具是一個功能更加強大的可以用來查看PE結構和識別加殼信息的工具“PEID”。
首先用“Depends”來查看DLL的導出函數,該工具可以在VC6的安裝菜單下找到,具體位置為“開始”→“程序”→“Microsoft Visual Studio 6.0”→“Microsoft Visual Studio 6.0 Tools”→“Depends”。打開該程序,依次單擊菜單項“File”→“Open”,在“打開”對話框中找到所寫的FirstDll.dll文件,選中并打開(也可以直接進行拖曳),其工作窗口中顯示了FirstDll.dll的信息,如圖4所示。
圖4 Depends顯示界面
在圖4的右下角區域范圍顯示的是該DLL文件導出的函數。從圖4中可以看出,FirstDll.dll文件只導出一個MsgBox函數。
對于Depends的介紹就這么多,現在來看另外一個工具“PEID”。該工具是用來識別軟件“指紋”信息(開發環境、版本、加殼信息等)的。將FirstDll.dll文件拖曳到PEID界面上,PEID會自動解析出該DLL文件的PE結構信息,界面如圖5所示。
圖5 PEID顯示界面
從圖5可以看出,PEID最下方的只讀編輯框中顯示了FirstDll.dll文件是由VC6開發的,并且版本是Debug版本。單擊“子系統”右邊的“大于號”按鈕,會顯示PE結構的詳細信息,如圖6所示。
圖6 PE結構詳情
在圖6中的PE結構詳細信息的下半部分有個“目錄信息”,其中的第一個目錄信息就是導出表信息,單擊“導出表”最右側的“大于號”按鈕,出現“導出查看器”界面,如圖7所示。
圖7 導出查看器
從圖7中可以看出,FirstDll.dll文件只有一個導出函數MsgBox(),只存在一個導出項。導出函數的信息與Depends相同。