前置知識
匯入表
在一個可執行檔案需要用到其餘DLL
檔案中的函式時,就需要用到匯入表,用於記錄需要引用的函式。例如我們編寫的可執行檔案需要用到CreateProcess
函式,就需要用到kernel32.dll
檔案並且將其中的CreateProcess
函式的資訊匯入到我們的可執行檔案中,然後再呼叫。
為了管理這些匯入函式,就構建了一個匯入表進行統一的管理,簡單來說,當我們編寫的可執行檔案中使用到匯入函式就會去匯入表中去搜尋找到指定的匯入函式,獲取該匯入函式的地址並呼叫。
因此載入器再呼叫匯入函式之前需要先找到匯入表的所在處。在可執行檔案對映到記憶體空間是,都是以Dos Header
開始的,在該頭部存在elfanew
的欄位,用於記錄PE
檔案頭的偏移,在PE
檔案頭存在可選頭的結構體,該結構體中儲存資料目錄項,其中就包括了匯入表。因此在記憶體中我們需要透過Dos Header -> Nt Header -> Option Header -> Import Table
的順序獲取匯入表。
這裡使用《加密與解密》的圖來看一下匯入表的結構體,如下圖。
可以看到匯入表涉及的變數非常多,這裡重點關注OriginalFirstThunk
、FistThunk
以及Name
-
Name
:指向匯入庫的名稱。 -
OriginalFistThunk
:指向輸入名稱表,裡面儲存了匯入函式的資訊。 -
FirstThunk
:指向輸入地址表,可以看到在初始化的時候OriginalFistThunk
與FirstThunk
指向的是同一塊區域,即匯入函式的資訊。
輸入名稱表的結構體如下圖,這裡重點關注Ordinal
與AddressOfData
-
Ordinal
:記錄函式的序號,即匯入函式以序號儲存 -
AdressOfData
:以函式命的形式記錄匯入函式
那麼INT
與IAT
的區別在於,載入器會在從匯入表中獲取了匯入函式名稱後,會搜尋該函式的名稱並獲取該函式的地址並填入到IAT
中,因此在經歷了載入器後,IAT
中儲存了實際地址。如下圖。
匯入地址表鉤取技術
輸入地址表鉤取技術就是透過修改輸入地址表的地址值,因此當呼叫該匯入函式時會跳轉到被篡改的地址上。
在鉤取之前的狀態如下圖
在鉤取之後的狀態如下圖
因此總結一下輸入地址表鉤取技術的流程
-
確定需要鉤取的匯入函式
-
獲取輸入地址表的地址
-
在輸入地址表中搜尋需要鉤取的匯入函式地址並且將匯入函式地址修改為自定義的函式
-
在處理完之後需要在自定義函式中重新呼叫被鉤取的函式
確定需要鉤取的匯入函式
首先確定可執行檔案中存在什麼匯入函式,可以發現目標的可執行檔案中匯入了kernel32.dll
的系統庫,並且匯入的CreateProcessW
那麼採用輸入地址表鉤取方法鉤取CreateProcessW
函式。
【----幫助網安學習,以下所有學習資料免費領!加vx:dctintin,備註 “部落格園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
獲取匯入地址表的地址
根據DOS Header -> Nt Header -> Option Header ->Import Table
的順序進行搜尋,即可獲取匯入地址表的地址。
程式碼如下
...
//獲取當前程序的基地址
hMod = GetModuleHandle(NULL);
pBase = (PBYTE)hMod;
//程序的基地址是從DOS頭開始的
pImageDosHeader = (PIMAGE_DOS_HEADER)hMod;
//透過e_lfanew變數獲取NT頭的偏移,然後加上基地址及NT頭的位置
pImageNtHeaders = (PIMAGE_NT_HEADERS)(pBase + pImageDosHeader->e_lfanew);
//資料目錄項下標為1的項是匯入表
pImageImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pImageNtHeaders->OptionalHeader.DataDirectory[1].VirtualAddress + pBase)
...
獲取匯入函式地址並修改
在獲取匯入地址表的地址後,首先透過遍歷匯入表的結構體,提取其中的Name
欄位,判斷是否為我們需要鉤取的匯入庫名。在匹配完成後則選擇繼續遍歷IAT
中的函式地址,找到需要鉤取的函式地址,找到後則修改為自定義函式的地址。
程式碼如下
...
//遍歷匯入表項
for (; pImageImportDescriptor->Name; pImageImportDescriptor++)
{
//獲取匯入庫的名稱
szLibName = (LPCSTR)(pImageImportDescriptor->Name + pBase);
//比較匯入庫的名稱,判斷是否為kernel32.dll
if (!_stricmp(szLibName, szDllName))
{
//獲取IAT
PIMAGE_THUNK_DATA pImageThunkData = (PIMAGE_THUNK_DATA)(pImageImportDescriptor->FirstThunk + pBase);
//獲取匯入函式地址
for (; pImageThunkData->u1.Function; pImageThunkData++)
{
//判斷函式地址是否是需要鉤取的函式地址,這裡需要注意的是64位與32位地址的區別
if (pImageThunkData->u1.Function == (ULONGLONG)pfnOrg)
{
//修改IAT的許可權為可寫
VirtualProtect(&pImageThunkData->u1.Function, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//將原始的地址修改為自定義函式地址
pImageThunkData->u1.Function = (ULONGLONG)pfnNew;
//將許可權恢復
VirtualProtect(&pImageThunkData->u1.Function, 4, dwOldProtect, &dwOldProtect);
return TRUE;
}
}
}
...
在自定義函式中重新呼叫被鉤取的函式
這裡需要注意的是,我們需要構建一個自定函式,該函式的返回型別與引數需要與鉤取的函式一模一樣,這樣我們就可以獲取所有引數的資訊,然後篡改後重新傳遞給原始的匯入函式,即可完成鉤取。
程式碼如下,這裡篡改原始CreateaProcessW
函式的第一個引數,使計算器
...
LPCWSTR applicationName = L"C:\\Windows\\System32\\calc.exe";
return ((LPFN_CreateProcessW)g_pOrgFunc)(applicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);
...
完整程式碼:https://github.com/h0pe-ay/HookTechnology/blob/main/Hook-IAT/iat.cpp
除錯
剛開始使用的是xdbg
除錯,但是用的不太習慣,後面改用WinDbg
還可以原始碼除錯,這裡記錄一下需要用到的操作與指令。
符號表與原始碼載入
在設定中可以選擇原始碼預設的目錄以及符號表預設的目錄,符號檔案則是利用Visutal Studio
編譯生成的pdb
檔案。
其中srv*c:\Symbols*https://msdl.microsoft.com/download/symbols
是下載官方的符號表檔案,這裡可以選擇刪掉只除錯我們設定的檔案。不然每次都需要下載一遍影響時間。
原始碼檔案也可以在側邊欄選擇Open source file
選項開啟。
DLL載入除錯
由於鉤取時需要先使用DLL
注入技術將自定義的DLL
檔案注入進去,因此想要除錯鉤取過程則需要在DLL
附著的時候打下斷點。
利用sxe ld:xxx.dll
即可在載入xxx.dll
的時候打下斷點。
利用sxe ud:xxx.dll
即在解除安裝xxx.dll
的時候打下斷點。
關閉最佳化除錯
防止自定義函式中的變數被最佳化導致不方便單步除錯,在Visual Studio
中可以選擇關閉最佳化進行編譯。
更多網安技能的線上實操練習,請點選這裡>>