在NT環境下隱藏進程,也就是說在用戶不知情的條件下,執行自己的代碼的 方法有很多種,比如說使用注冊表插入DLL,使用Windows掛鉤等等。其中比較有 代表性的是Jeffrey Richer在《Windows核心編程》中介紹的LoadLibrary方法和 羅雲彬在《windows環境下32位匯編語言程序設計》中介紹的方法。兩種方法的 共同特點是:都采用遠程線程,讓自己的代碼作為宿主進程的線程在宿主進程的 地址空間中執行,從而達到隱藏的目的。相比較而言,Richer的方法由於可以使 用c/c++等高級語言完成,理解和實現都比較容易,但他讓宿主進程使用 LoadLibrary來裝入新的DLL,所以難免留下蛛絲馬跡,隱藏效果並不十分完美。 羅雲彬的方法在隱藏效果上絕對一流,不過,由於他使用的是匯編語言,實現起 來比較難(起碼我寫不了匯編程序:))。筆者下面介紹的方法可以說是對上述兩 種方法的綜合:采用c/c++編碼,實現完全隱藏。並且,筆者的方法極大的簡化 了遠程線程代碼的編寫,使其編寫難度與普通程序基本一致。
基礎知識
讓自己的代碼作為宿主進程的線程,在宿主進程的地址空間中執行確實是個 不錯的主意。但是要自己把程序放到其他進程的地址空間中去運行,將面臨一個 嚴峻的問題:如何實現代碼重定位。關於重定位問題,請看下面的程序:
…
int func()//函數func的定義
…
int a = func();//對func的調用
…
這段程序經過編譯鏈接後,可能會變成下面的樣子:
…
0x00401800: push ebp//這是函數func的入口
0x00401801: mov ebp, esp
…
0x00402000: call 00401800//對函數func的調用
0x00402005: mov dword ptr [ebp-08], eax
…
請注意“0x00402000”處的直接尋址指令“call 00401800”。上面的程序在正常執行(由windows裝入並執行)時,因為PE 文件的文件頭中含有足夠的信息,所以系統能夠將代碼裝入到合適的位置從而保 證地址“00401800”處就是函數func的入口。但是當我們自己把程序 裝入到其他進程的地址空間中時,我們無法保證這一點,最終的結果可能會象下 面這樣:
…
0x00801800: push ebp//這是函數func的入口
0x00801801: mov ebp, esp
…
0x00802000: call 00401800//00401800處是什麼
0x00802005: mov dword ptr [ebp-08], eax
…
顯然,運行上面的代碼將產生不可預料的結果(最大的可能就是執行我們費 盡千辛萬苦才裝入的代碼的線程連同宿主進程一起被系統殺死)。 不知大家注 意過系統中動態鏈接庫(dll)的裝入沒有:一個dll被裝入不同進程時,裝入的 地址可能不同,所以系統在這種情況下也必須解決dll中直接尋址指令的重定位 問題。原來,絕大多數dll中都包含一些由編譯器插入的用於重定位的數據,這 些數據就構成了重定位表。系統根據重定位表中的數據,修改dll的代碼,完成 重定位操作。Richer使用的LoadLibrary也是借用了這一點。所以我們的重定位 方法就是:替系統來完成工作,自己根據重定位表中的數據進行重定位。既然如 此,那就讓我們來了解一下重定位表吧。
先來分析一下重定位表中需要保存哪些信息。還以上面的代碼為例,要讓它 能正確執行,就必須把指令“call 00401800”改為“call 00801800”。進行這一改動需要兩個數據,第一是改哪,也就是哪個內存 地址中的數據需要修改,這裡是“0x00802001”(不是 “0x00802000”);第二是怎麼改,也就是應該給該位置的數據加上 多少,這裡是“0x00400000”。這第二個數據可以從dll的實際裝入 地址和建議裝入地址計算而來,只要讓前者減後者就行了。其中實際裝入地址裝 入的時候就會知道,而建議裝入地址記錄在文件頭的ImageBase字段中。所以, 綜上所述,重定位表中需要保存的信息是:有待修正的數據的地址。
位置 數據 描述 0000h 00001000h 頁起始地址(RVA) 0004h 00000010h 重定位塊長度 0008h 3006h 第一個重定位項,32位都須修正 000ah 300dh 第二個重定位項,32位都須修正 000ch 3015h 第三個重定位項,32位都須修正 000eh 0000h 第四個重定位項,用於對齊 0010h 00003000h 頁起始地址(RVA) 0014h 0000000ch 重定位塊長度 0018h 3008h 第一個重定位項,32位都須修正 001ah 302ah 第二個重定位項,32位都須修正 … … 其他重定位塊 0100h 0000h 重定位表結束標志
知道了重定位表要保存哪些信息,我們再來看看PE文件的重定位表是如何保 存這些信息的。重定位表的位置和大小可以從PE文件頭的數據目錄中的第六個 IMAGE_DATA_DIRECTORY結構中獲取。由於記錄一個需要修正的代碼地址需要一個 雙字(32位)的存儲空間,而且程序中直接尋址指令也比較多,所以為了節省存 儲空間,windows把重定位表壓縮了一下,以頁(4k)為單位分塊存儲。在一個 頁面中尋址只需要12位的數據,把這12位數據再加上4位其它數據湊齊16位就構 成一個重定位項。在每一頁的所有重定位項前面附加一個雙字表示頁的起始地址 ,另一個雙字表示本重定位塊的長度,就可以記錄一個頁面中所有需要重定位的 地址了。所有重定位塊依次排列,最後以一個頁起始地址為0的重定位塊結束重 定位表。上表是一個重定位表的例子(表中每種顏色代表一個重定位塊)。
上面提到每個重定位項還包括4位其他信息,這4位是重定位項的高4位,雖然 有4位,但我們實際上能看到的值只有兩個:0和3。0表示此項僅用作對齊,無其 他意義;3表示重定位地址指向的雙字的32位都需要修正。還要注意一點的是頁 起始地址是一個相對虛擬地址(RVA),必須加上裝入地址才能得到實際頁地址 。例如上表中的第一個重定位項表示需要重定位的數據位於地址(假設裝入地址 是00400000h):裝入地址(00400000h)+頁地址(1000h)+頁內地址(0006h) =00401006h。
至此,已經解決了重定位問題。應該說,現在我們已經能夠開始編碼了。但 是,不知你是否讀過其它有關進程隱藏的文章(使用類似Jeffrey Richer的方法 的例外)並且注意到它們總是以顯式鏈接的方式調用Windows API,例如下面對 MessageBox的調用:
//fnLoadLibrary和fnGetProcAddress分別指向Windows API函數 LoadLibraryW和GetProcAddress
typedef int (WINAPI *FxMsgBox)(HWND, LPCWSTR, LPCTSTR, UINT);
…
HMODULE hUser32 = fnLoadLibrary(L"User32.dll");
FxMsgBox fnMsgBox = (FxMsgBox)(fnGetProcAddress(hUser32, "MessageBoxW"));
fnMsgBox(…);
…
那它們為什麼不使用更簡便的隱式鏈接呢?原來,要隱式鏈接dll並調用其中 的輸出函數,首先必須保證程序運行時dll已經被裝入,否則就會出錯。其次, 調用API函數的指令格式一般是:call dword ptr [xxxxxxxx],要讓程序正常運 行,就必須在調用前在地址“xxxxxxxx”處填入目標函數的入口地址 。程序正常裝入時,系統會保證這兩點。但是要自己裝入程序,保證這兩點就有 一些麻煩,所以它們一般使用顯式鏈接來繞過這兩個問題。
如果你不在乎為每一個API使用一個typedef和一個GetProcAddress的話(也 許還有一個LoadLibrary),使用顯式鏈接就已經足夠好了。但是設想一下實際 情況吧:你的代碼中調用幾十乃至數百個API的情況是很常見的,為每一個API寫 這些重復性的代碼將使編程毫無樂趣可言,所以,我們一定要解決那兩個問題, 從而使用隱式鏈接。我們處理隱式鏈接問題的思路和前面處理重定位問題時是一 樣的,即:替系統來完成工作,在遠程線程代碼調用第一個API之前,裝入dll並 填好相關入口地址。
//摘自WINNT.H
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
還是先來學習一下基礎知識—PE文件的輸入表。輸入表記錄了一個 Win32程序隱式加載的所有dll的文件名及從中引入的API的函數名,通過PE文件 頭的數據目錄中的第二個IMAGE_DATA_DIRECTORY,我們可以獲得輸入表的位置和 大小。實際上,輸入表是一個由IMAGE_IMPORT_DESCRIPTOR結構組成的數組,每 個結構對應一個需要隱式加載的dll文件,整個輸入表以一個Characteristics字 段為0的IMAGE_IMPORT_DESCRIPTOR結束。上面就是IMAGE_IMPORT_DESCRIPTOR結 構的定義。
其中的Name字段是一個RVA,指向此結構所對應的dll的文件名,文件名是以 NULL結束的字符串。在PE文件中,OriginalFirstThunk和FirstThunk都是RVA, 分別指向兩個內容完全相同的IMAGE_THUNK_DATA結構的數組,每個結構對應一個 引入的函數,整個數組以一個內容為0的IMAGE_THUNK_DATA結構作為結束標志。 IMAGE_THUNK_DATA結構定義如下:
//摘自WINNT.H
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 IMAGE_THUNK_DATA;
從上面的定義可以看出,完全能夠把IMAGE_THUNK_DATA結構當作一個DWORD使 用。當這個DWORD的最高為是1時,表示函數是以序號的形式引入的;否則函數是 以函數名的形式引入的,且此DWORD是一個RVA,指向一個IMAGE_IMPORT_BY_NAME 結構。我們可以使用在WINNT.H中預定義的常量IMAGE_ORDINAL_FLAG來測試最高 位是否為1。IMAGE_IMPORT_BY_NAME結構定義如下:
//摘自WINNT.H
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME;
其中Hint字段的內容是可選的,如果它不是0,則它也表示函數的序號,我們 編程是不必考慮它。雖然上面的定義中Name數組只包含一個元素,但其實它是一 個變長數組,保存的是一個以NULL結尾的字符串,也就是函數名。
也許上面的解釋已經把你弄得頭暈腦漲了,來看看下面的導入表的實際結構 吧,希望下圖能幫你清醒一下:
光看前面的講解中,你也許會有一個疑問:既然OriginalFirstThunk和 FirstThunk指向的內容完全一樣,只用一個不就行了嗎?好了,不要再懷疑 Windows的設計者了,在PE文件中它們確實是一樣的,但是當文件被裝入內存後 ,差別就出現了:OriginalFirstThunk的內容不會變,但FirstThunk裡數據卻會 變成與其相對應的函數的入口地址。內存中的輸入表結構如下圖所示:
事實上,前面提到的call dword ptr [xxxxxxxx]指令中的 “xxxxxxxx”就是FirstThunk中的一個IMAGE_THUNK_DATA的地址,而 這個IMAGE_THUNK_DATA在裝入完成之後保存的就是與其對應的函數的入口地址。 知道動態鏈接是怎麼回事了吧!
編程實現
到現在為止,有關進程隱藏的基礎知識就都說完了,下面我們就開始動手編 程,其他問題我將結合代碼進行說明。
我們要編寫兩個程序,一個是dll,它裡面包含要插入到宿主進程中去的代碼 和數據;另一個是裝載器程序,它將把dll裝入宿主進程並通過創建遠程線程來 運行這些代碼。為了更好的隱藏,我把編譯好的dll作為資源加入到了裝載器之 中。至於宿主進程,我選擇的是“explorer.exe”,因為每一個 windows系統中都有它的身影。裝載器程序運行之後,遠程線程將彈出如下一個 消息框,證明代碼插入成功。
兩個程序有一個公用的頭文件“ThreadParam.h”,我在它裡面定 義了要傳遞給遠程線程的參數的結構,這個結構包括兩個函數指針,使用時,它 們將分別指向windows API“LoadLibrary”和 “GetProcAddress”,還有一個指針指向遠程線程在目標進程中的映 像基址,後面將對這三個指針進行具體說明,下面是 “ThreadParam.h”的內容:
typedef HMODULE (WINAPI *FxLoadLibrary)(LPCSTR lpFileName);
typedef FARPROC (WINAPI *FxGetProcAddr)(HMODULE hModule, LPCSTR lpProcName);
typedef struct tagTHREADPARAM
{
FxLoadLibrary fnLoadLibrary;
FxGetProcAddr fnGetProcAddr;
LPBYTE pImageBase;
}THREADPARAM, *PTHREADPARAM;
我們先來看裝載器程序。這裡面還會涉及到其他一些PE文件格式方面的內容 ,限於篇幅,我將不再詳細介紹,請讀者參考相關資料。同時,為了使程序更加 短小,我假設它從不出錯,去掉了所有用於錯誤處理的代碼。
首先介紹一下程序中用到的全局變量和常數。其中“_pinh”指向 嵌入裝載器的dll的PE文件頭,供需要的地方使用。之後的四個宏是為了以後程 序書寫方便而定義,“IMAGE_SIZE”表示dll的映像大小,也就是需 要在宿主進程中開辟多大的內存空間;“RVA_EXPORT_TABEL”表示 dll輸出表的RVA地址;“RVA_RELOC_TABEL”表示dll重定位表的RVA 地址;“PROCESS_OPEN_MODE”表示打開宿主進程的方式,只有按這 種方式打開,我們才能完成所有必需的工作。
static PIMAGE_NT_HEADERS _pinh = NULL;
#define IMAGE_SIZE (_pinh->OptionalHeader.SizeOfImage)
#define RVA_EXPORT_TABEL (_pinh->OptionalHeader.DataDirectory [0].VirtualAddress)
#define RVA_RELOC_TABEL (_pinh->OptionalHeader.DataDirectory [5].VirtualAddress)
#define PROCESS_OPEN_MODE (PROCESS_CREATE_THREAD|PROCESS_VM_WRITE|PROCESS_VM_OPERATION)
下面是主函數的定義,從中我們可以看到大致的工作步驟,注釋中的序號標 明了每一步的開始位置。
int APIENTRY _tWinMain(HINSTANCE hInst, HINSTANCE, LPTSTR lpCmdLine, int nCmdShow)
{
LPTHREAD_START_ROUTINE pEntry = NULL;
PTHREADPARAM pParam = NULL;
LPBYTE pImage = (LPBYTE)MapRsrcToImage(); //①
DWORD dwProcessId = GetTargetProcessId(); //②
HANDLE hProcess = OpenProcess(PROCESS_OPEN_MODE, FALSE, dwProcessId);
LPBYTE pInjectPos = (LPBYTE)VirtualAllocEx(hProcess, NULL, IMAGE_SIZE,
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
PrepareData(pImage, pInjectPos, (PVOID*)&pEntry, (PVOID*) &pParam); //③
WriteProcessMemory(hProcess, pInjectPos, pImage, IMAGE_SIZE, NULL); //④
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pEntry, pParam, 0, NULL);
CloseHandle(hThread); //⑤
CloseHandle(hProcess);
VirtualFree(pImage, 0, MEM_RELEASE);
return 0;
}
第①步:將資源中的dll文件映射到內存,形成映像。這一步由函數 “MapRsrcToImage”完成。它首先將打開資源中的dll,找到dll的PE 文件頭並讓全局變量_pinh指向它。然後,它再根據文件頭中的 “SizeOfImage”字段在裝載器進程(為求方便,我們的數據准備工 作都在裝載器進程中實現,只是到最後,才把准備好的數據一次性寫入宿主進程 )中開辟足夠的內存空間用於存放dll的內存映像。把dll映射到內存的操作是以 節為單位來進行的,PE文件中的節表(IMAGE_SECTION_HEADER)提供了每個節的 大小、在文件中的位置和要放到內存中的位置(RVA)等信息。文件頭不屬於任 何節,我們把它的數據放到內存區的起始位置(這樣做是有原因的,將在介紹 dll程序時說明)。
static LPBYTE MapRsrcToImage() //將資源中的DLL映射到內存
{
HRSRC hRsrc = FindResource(NULL, _T("rtdll"), _T ("RT_DLL"));
HGLOBAL hGlobal = LoadResource(NULL, hRsrc);
LPBYTE pRsrc = (LPBYTE)LockResource(hGlobal);
_pinh = (PIMAGE_NT_HEADERS)(pRsrc + ((PIMAGE_DOS_HEADER)pRsrc)- >e_lfanew);
LPBYTE pImage = (LPBYTE)VirtualAlloc(NULL, IMAGE_SIZE, MEM_COMMIT, PAGE_READWRITE);
DWORD dwSections = _pinh->FileHeader.NumberOfSections;
DWORD dwBytes2Copy = (((LPBYTE)_pinh) - pRsrc) + sizeof (IMAGE_NT_HEADERS);
PIMAGE_SECTION_HEADER pish = (PIMAGE_SECTION_HEADER)(pRsrc + dwBytes2Copy);
dwBytes2Copy += dwSections * sizeof(IMAGE_SECTION_HEADER);
memcpy(pImage, pRsrc, dwBytes2Copy);
for(DWORD i=0; i>dwSections; i++, pish++)
{
LPBYTE pSrc = pRsrc + pish->PointerToRawData;
LPBYTE pDest = pImage + pish->VirtualAddress;
dwBytes2Copy = pish->SizeOfRawData;
memcpy(pDest, pSrc, dwBytes2Copy);
}
_pinh = (PIMAGE_NT_HEADERS)(pImage + ((PIMAGE_DOS_HEADER)pImage)- >e_lfanew);
return pImage;
}
第②步:打開宿主進程,並在其中開辟用於寫入數據的內存空間。這一步比 較簡單,其中函數“GetTargetProcessId”用於獲取 “explorer.exe”的進程ID。
static DWORD GetTargetProcessId() //取得explorer進程的pid
{
DWORD dwProcessId = 0;
HWND hWnd = FindWindow(_T("Progman"), _T("Program Manager"));
GetWindowThreadProcessId(hWnd, &dwProcessId);
return dwProcessId;
}
第③步:准備好要寫入宿主進程的數據。這一步要把①中建立的dll映像根據 ②中開辟的存儲空間的基址進行重定位,為線程准備參數,並計算線程的入口地 址。
static void PrepareData(LPBYTE pImage, LPBYTE pInjectPos, PVOID* ppEntry, PVOID* ppParam)
{
LPBYTE pRelocTbl = pImage + RVA_RELOC_TABEL;
DWORD dwRelocOffset = (DWORD)pInjectPos - _inh.OptionalHeader.ImageBase;
RelocImage(pImage, pRelocTbl, dwRelocOffset);
PTHREADPARAM param = (PTHREADPARAM)pRelocTbl;
HMODULE hKernel32 = GetModuleHandle(_T ("kernel32.dll"));
param->fnGetProcAddress=(FxGetProcAddress)GetProcAddress (hKernel32,"GetProcAddress");
param->fnLoadLibrary= (FxLoadLibrary)GetProcAddress(hKernel32, "LoadLibraryA");
param->pImageBase = pInjectPos;
*ppParam = pInjectPos + RVA_RELOC_TABEL;
*ppEntry = pInjectPos + GetEntryPoint(pImage);
}
首先,它根據實際裝入地址和建議地址計算出要加到重定位數據上去的數值 ,然後調用函數“RelocImage”進行重定位操作。 “RelocImage”主要是根據我們前面介紹的重定位表的結構來對dll 映像進行重定位。看了“RelocImage”的代碼,你是不是感到有些驚 訝?我們費了那麼多氣力來說明重定位問題,但實現它卻只需要這麼幾行程序! 其實這說明了一點:PE文件格式設計得非常簡潔,我們完全沒必要對它有恐懼感 。後面處理隱式鏈接的代碼將再次證明這一點。
static void RelocImage(PBYTE pImage, PBYTE pRelocTbl, DWORD dwRelocOffset)
{
PIMAGE_BASE_RELOCATION pibr = (PIMAGE_BASE_RELOCATION) pRelocTbl;
while(pibr->VirtualAddress != NULL)
{
WORD* arrOffset = (WORD*)(pRelocTbl + sizeof (IMAGE_BASE_RELOCATION));
DWORD dwRvaCount = (pibr->SizeOfBlock - sizeof (IMAGE_BASE_RELOCATION)) / 2;
for(DWORD i=0; i<dwRvaCount; i++ )
{
DWORD dwRva = arrOffset[i];
if((dwRva & 0xf000) != 0x3000)
continue;
dwRva &= 0x0fff;
dwRva += pibr->VirtualAddress + (DWORD)pImage;
*(DWORD*)dwRva += dwRelocOffset;
}
pRelocTbl += pibr->SizeOfBlock;
pibr = (PIMAGE_BASE_RELOCATION)pRelocTbl;
}
}
由於我們在宿主進程中分配的內存只有IMAGE_SIZE那麼大,所以必須在重定 位操作完成之後,才能把線程參數寫進去,這是因為重定位表在完成重定位之後 ,就沒用了,我們正好可以借用它的空間來存放線程參數,而且一般情況下,空 間足夠使用,除非你要傳遞特別多的參數。這樣,參數的地址自然就是實際裝入 地址加上重定位表的RVA地址了。
最後的工作是獲取線程的入口地址,由函數“GetEntryPoint”來 完成。我們的dll程序輸出一個名為“ThreadEntry”的函數,其原型 兼容windows的線程入口函數,我們把它作為遠程線程的執行體。GetEntryPoint 根據dll的輸出表信息從映像中找到ThreadEntry的入口地址並將其返回。不過, “GetEntryPoint”返回的地址是一個RVA,必須加上裝入地址 “pInjectPos”才是實際入口地址。
static DWORD GetEntryPoint(LPBYTE pImage)
{
DWORD dwEntry = 0, index = 0;
IMAGE_EXPORT_DIRECTORY* pied = (IMAGE_EXPORT_DIRECTORY*)(pImage + RVA_EXPORT_TABEL);
DWORD* pNameTbl = (DWORD*)(pImage + pied->AddressOfNames);
for(index=0; index<pied->NumberOfNames; index++, pNameTbl++)
if(strcmp("ThreadEntry", (char*)(pImage + (*pNameTbl))) == 0)
{
index = ((WORD*)(pImage + pied->AddressOfNameOrdinals))[index];
dwEntry = ((DWORD*)(pImage + pied->AddressOfFunctions))[index];
break;
}
return dwEntry;
}
第④步:把准備好的數據寫入宿主進程,並創建遠程線程來運行寫入的代碼 。
第⑤步:進行裝載器程序結束前的清理工作。
以上是裝載器程序的全部內容,接下來介紹dll程序。前面已經說過,dll要 輸出一個名為“ThreadEntry”的函數作為遠程線程的入口,所以我 們從“ThreadEntry”開始。
extern DWORD ThreadMain(HINSTANCE hInst);
DWORD WINAPI ThreadEntry(PTHREADPARAM pParam)
{
DWORD dwResult = -1;
__try{
if(LoadImportFx(pParam->pImageBase, pParam- >fnLoadLibrary, pParam->fnGetProcAddr))
dwResult = ThreadMain((HINSTANCE)pParam->pImageBase);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
dwResult = -2;
}
return dwResult;
}
整個ThreadEntry的代碼被包含在一個SEH(結構化異常處理)之中,這可以 避免部分由於寄生代碼出錯而導致宿主被系統殺死的情況。ThreadEntry首先調 用LoadImportFx函數完成隱式鏈接dll的處理。
LoadImportFx的工作原理就是按照前面介紹的輸入表的結構,使用 LoadLibrary加載dll文件,然後用GetProcAddress獲得輸入函數的入口地址並寫 入相應的IMAGE_THUNK_DATA中。我在這裡要說明的是:為什麼遠程線程能使用裝 載器進程中LoadLibrary和GetProcAddress的入口地址來實現對這兩個函數的調 用?因為按照前面的說法,我們無法保證包含這兩個函數的dll已被裝入,更無 法保證它們的指向的正確性。其實,這裡我利用了windows系統中的兩個事實: 一是基本上所有的windows進程都會裝入“kernel32.dll”(在我的 機器上,只有smss.exe例外),而這兩個函數就位於 “kernel32.dll”中;另一個是所有裝入 “kernel32.dll”的進程都會把它裝入同一個內存地址,這是因為它 是windows系統中最基本的dll之一。所以,我這樣使用在絕大多數情況下不會有 任何問題。
BOOL LoadImportFx(LPBYTE pBase, FxLoadLibrary fnLoadLibrary, FxGetProcAddr fnGetProcAddr)
{
PIMAGE_DOS_HEADER pidh = (PIMAGE_DOS_HEADER)pBase;
PIMAGE_NT_HEADERS pinh = (PIMAGE_NT_HEADERS)(pBase + pidh- >e_lfanew);
PIMAGE_IMPORT_DESCRIPTOR piid = (PIMAGE_IMPORT_DESCRIPTOR)
(pBase + pinh->OptionalHeader.DataDirectory [1].VirtualAddress);
for(; piid->OriginalFirstThunk != 0; piid++)
{
HMODULE hDll = fnLoadLibrary((LPCSTR)(pBase + piid->Name));
PIMAGE_THUNK_DATA pOrigin = (PIMAGE_THUNK_DATA)(pBase + piid- >OriginalFirstThunk);
PIMAGE_THUNK_DATA pFirst = (PIMAGE_THUNK_DATA)(pBase + piid- >FirstThunk);
LPCSTR pFxName = NULL;
PIMAGE_IMPORT_BY_NAME piibn = NULL;
for(; pOrigin->u1.Ordinal != 0; pOrigin++, pFirst++)
{
if(pOrigin->u1.Ordinal & IMAGE_ORDINAL_FLAG)
pFxName = (LPCSTR)IMAGE_ORDINAL(pOrigin- >u1.Ordinal);
else
{
piibn = (PIMAGE_IMPORT_BY_NAME)(pBase + pOrigin- >u1.AddressOfData);
pFxName = (LPCSTR)piibn->Name;
}
pFirst->u1.Function = (DWORD)fnGetProcAddr(hDll, pFxName);
}
}
return TRUE;
}
處理完隱式鏈接之後,ThreadEntry調用ThreadMain來進行完成遠程線程的實 際工作。可能你已經注意到ThreadMain有一個參數是HINSTANCE類型,但從 ThreadEntry可知,它實際上是dll在宿主中的裝入地址,為什麼可以這樣做呢? 答案是:我不知道,你去問微軟吧。不過據我觀察,普通程序的任何一個模塊( module)的句柄都是其裝入地址,所以我也就照貓畫虎了。這也解釋了前面處理 重定位時把文件頭放入映像基址的原因—系統需要文件頭信息,我必須為 它准備好(雖然LoadImportFx函數也需要文件頭來定位輸入表,但不是根本原因 ,因為完全可以讓它使用其他方式)。
下面是我的ThreadMain,它彈出前面提到的消息框。看到了吧?你可以像寫 普通程序一樣寫遠程線程的代碼,沒有復雜的自定位,也沒有煩人的顯式鏈接, 這個世界真美好!
小結
本文在相當大的程度上簡化了進程隱藏技術,你甚至可以把它當作一個模板 ,僅僅實現一個ThreadMain就可以把代碼隱藏到其他進程中去為所欲為了。但這 決不是筆者寫作此文的目的,我希望讀者只把它當作一項技術,加深自己對 windows系統的理解。其實,本文對動態鏈接的處理還遠沒有達到操作系統程度 ,舉例來說:PE文件的數據目錄現在使用了15項,但本文只處理了4項:輸出表 ,輸入表,重定位表和IAT(可以看作輸入表的一部分),不把所有15項都處理 完,遠程代碼的行為就可能與正常情況不同。我希望能與各位讀者共同努力,不 斷完善這項技術,更希望大家能夠負責任的使用它,利用它更好的防治各種有害 代碼。
本文配套源碼