事情的起因大概是這樣……
在很久很久以前,我最早用的是MASM(Win32ASM)寫程序,從平臺兼容性、開發效率和規范等方面考慮,后來我義無反顧地轉成了C、C++和C++++……
主要是為了保持隊形,但那真的是4個+
當然也很順手,不用像匯編那樣,x86是一份代碼,x64又大相徑庭,再換成ARM那就沒法玩了。至于運行效率,更多的可以考慮開發效率、可維護性等等,至少我是沒有蜜汁自信來認為自己所寫的匯編源碼全文會比當今C編譯器所產生的更高效。如果MASM問我,我會說愛過。
很快,根據王境澤大師的真香定理,C語言在代碼注入上讓我一度考慮重操舊業(與MASM混編)。接下來我們先一起教科書式地復習Windows下傳統遠程代碼注入的套路,如果學不會也沒關系,你只需要記住這一套連招,1433223、1433223、1433223、1433223。
1. 打開遠端進程(OpenProcess/NtOpenProcess),權限至少包含以下步驟所需。
2. 以可讀可寫可執行的頁面保護屬性(PAGE_EXECUTE_READWRITE)為遠端進程分配新的內存區域(VirtualAllocEx/NtAllocateVirtualMemory),需要PROCESS_VM_OPERATION權限。
3. 將代碼寫入遠端進程(WriteProcessMemory/NtWriteVirtualMemory),需要PROCESS_VM_WRITE權限。
4. 刷新指令緩存(FlushInstructionCache/NtFlushInstructionCache),嚴謹起見,根據MSDN描述,即使是剛申請下來用于執行代碼的內存,也需要刷新。
5.以剛申請用于執行代碼的內存為入口點,創建遠端線程(CreateRemoteThread/RtlCreateUserThread/NtCreateThreadEx)并執行。后續可以自行發揮,比如同步和獲取退出碼
6. 最后記得收尾。有借有還,再借不難。
期間我們會遇到兩個問題。
第一,遠程注入的代碼中,如果調用了外部函數,很可能導致違規訪問、任意代碼執行等問題,因為在遠端進程中調用的外部函數很可能無法被正確地尋址,甚至都不存在于遠端進程中。所以我們應該要保證所注入的代碼沒有直接的外部函數調用,而是自己尋址。
嚴謹的方式是從PEB中的模塊鏈和這些模塊的導出表出發,去一步步找需要的東西,這個不是我們現在要討論的。只要對PEB、導出表結構理解到位便不復雜,順帶一提,DLL有按序號和名稱兩種導出方式,導出為重定向(Forwarder Name)的情況最好也納入考慮,可以參考ReactOS的實現(GetProcAddress -> LdrGetProcedureAddress -> LdrpGetProcedureAddress -> LdrpSnapThunk)。
第二,在第3步,如果注入本地函數,我們需要知道本地函數的實際地址與大小,才能正確地寫入到遠端進程中。MASM中我們可以放飛自我地定義標簽:
ExampleProc_Start: ExampleProc PROC a, b MOV EAX, a ADD EAX, b RET ExampleProc ENDP ExampleProc_End:
"offset ExampleProc_Start"是過程"ExampleProc"的起始地址,"offset ExampleProc_End"是其結束地址,二者之差則是其大小。
在C語言中,我們還能如此順風順水地獲得自身定義函數的實際地址和大小嗎?
我們先看地址。C語言無法定義函數外標簽,函數內標簽從使用到訪問處處受限,我們好像只剩函數名可以用。但函數名表達式未必等同于函數的實際地址,它可能會指向JMP stub,再由該JMP stub跳轉到函數實際地址:
有的甚至經由JMP stub跳轉兩次才到實際地址。這樣的JMP stub自有用處,比如增量鏈接,或者兼容沒有"__declspec(dllimport)"修飾的外部函數聲明等等。關閉增量鏈接后,本地函數的函數名作表達式,應該就是正確的內存地址了。
至于函數體大小,"sizeof"操作符是用不了的。我看到網上有如下的寫法:
int ExampleProc() { return 0; } void ExampleProcEnd() {}
然后用"ExampleProcEnd"減去"ExampleProc"。我用的是VS2019,關閉了MSVC編譯器和鏈接器的各種優化選項、SDL和增量鏈接等操作,結果是從來沒對過。
話說,編譯器本身好像也沒有責任去安排函數體的內存順序,倒是恨不得給它們折疊一下(COMDAT)或者內聯一下。
綜上,關閉增量鏈接后,函數體實際地址有解,雖然算不上理想的解決方案;至于函數體大小,仍然是C語言本身不可及的地方。當然也可以硬編碼將大小寫大一些,足夠覆蓋該函數體,只要訪問沒越界應該還是可以正常工作的,我想尋求更為嚴謹的方式。
似乎此時我們不得不借助匯編語言。MSVC中,x86支持內聯匯編,參考MSDN: Inline assembly in MSVC;x64不支持內聯,但可以外置匯編源碼在工程中,獨立生成目標文件與其它源文件生成的目標文件鏈接,參考MSDN: MASM for x64 (ml64.exe)一文中"Add an assembler-language file to a Visual Studio C++ project"章節。用匯編來寫要注入的函數(過程),此時可知其實際地址與大小,再供C語言中引用。
可是,這樣x86寫一份,x64寫一份,說不準ARM也可以來湊個熱鬧,這不又回到了以前嘛,說好的兔子不吃……哦不,好馬不吃回頭草!是的,此時我們需要借助匯編,但未必非得以這樣的方式。
我記得MSVC編譯器可以產生相應的匯編輸出,如果我們能利用它,那么或許可以保持注入函數一樣使用C來編寫了。下面舉個栗子:
我們有C語言函數"ExampleProc",是我們要拿來注入的函數:
int __stdcall ExampleProc(int a, int b) { return a + b; }
我們先只考慮Release構建,對應的x64匯編輸出大概是這個亞子,x86在PROC的定義上大同小異:
ExampleProc PROC lea eax, DWORD PTR [rcx+rdx] ret 0 ExampleProc ENDP
然后讓我們朵蜜一下它,給它頭上戴個帽子,還送一雙鞋:
它就長這樣了:
E4C_Start_ExampleProc: ExampleProc PROC lea eax, DWORD PTR [rcx+rdx] ret 0 ExampleProc ENDP E4C_End_ExampleProc:
當然,"E4C_Start"之類的前綴自擬,后面用的時候對得上號就行。最后把我們需要的定義為常量,并且公開給其它模塊使用:
PUBLIC E4C_Addr_ExampleProc PUBLIC E4C_Size_ExampleProc CONST SEGMENT E4C_Addr_ExampleProc DQ OFFSET E4C_Start_ExampleProc E4C_Size_ExampleProc DQ OFFSET E4C_End_ExampleProc - OFFSET E4C_Start_ExampleProc CONST ENDS
x86就把DQ改為DD,對應到C語言中的size_t。匯編輸出改好了,我們調用ml.exe或者ml64.exe把它重新匯編,生成新的目標文件并替換之前MSVC編譯器生成的,此時它多了"E4C_Addr_ExampleProc"和
"E4C_Size_ExampleProc"兩個導出符號,分別是"ExampleProc"函數(過程)的實際地址和計以字節的大小。
在同一工程的其它C語言源文件中,添加以下外部符號定義,即可引用它們了:
typedef int(__stdcall* PEXAMPLEPROC)(int a, int b);
extern PEXAMPLEPROC E4C_Addr_ExampleProc; extern size_t E4C_Size_ExampleProc;
地址的定義可以直接void*,像上面這樣聲明成相同的proto就可以調用它,當然是多此一舉(同一工程下的直接用函數名調用就好了)。大小這里用的是size_t,總之和之前在匯編輸出里定義的一致就行。
但整個實現過程并不順利,因為MSVC編譯器似乎管匯編輸出稱為"Assembler Listing"(匯編列表),與源文件有不小差距。實際上我們之所以爭取保持使用C語言寫注入的函數就是因為需要它實現的邏輯相對復雜,而不像上述例子那樣僅僅實現a+b這樣的小兒科,從而生成的匯編輸出也復雜。
這時,把MSVC生成的匯編輸出直接丟給MASM匯編那可就涼了,會產生很多錯誤,尤其是語法錯誤。比如x86匯編輸出缺少"assume fs:nothing",導致fs訪問出錯;x64輸出了"FLAT:"這樣只在x86中可用的標識;"$LN??"這樣的標簽被后向引用、重定義等;用到的浮點數被定義成以"__real@"開頭的公開符號,與其它模塊產生沖突等等。
最后,我將這套流程寫成了PowerShell腳本(Export4C),可集成在VS生成過程中。關于之前提到MSVC匯編輸出中的錯誤,已有一些相應修復措施,但我們仍應保持注入函數盡可能簡單,沒有外部函數調用,最好自己在一個獨立的C源文件中涼快。 ? 下面看一下效果: ? 在工程中,將要注入的函數獨立放在一個源文件"InjectProc.c"中,這個函數定義為"LPTHREAD_START_ROUTINE",會給調用它的老鐵返回666:??
/** * @warning Disable features like JMC (Just My Code) , Security Cookie, SDL and RTC to prevent external procedure calls generated. * @see See also the C/C++ settings for this file */ #include? 打開"InjectProc.c"文件屬性,【C/C++】設置里關閉JMC (Just My Code) 、Security Cookie、SDL和RTC,它們會在prologue和epilogue部分產生外部函數調用,注入到遠程那就涼了。 ? 在"Source.c"中我們把它注入指定進程里,例子中用的是當前進程PID: ?DWORD WINAPI InjectProc(LPVOID lpThreadParameter) { UNREFERENCED_PARAMETER(lpThreadParameter); return 666; }
/* Example3: Inject and execute code in a process. */ #include? 打開項目屬性,【C/C++】 - 【Output Files】,設置“Assembler Output”為"Assembly Only"(/FA)或者"Assembly With Source Code"(/FAs)。切換到【Advanced】,關閉“Whole Program Optimization”,至此,在默認情況下,匯編輸出會生成于中間目錄$(IntDir)。 ? 切換到【Build Events】 - 【Pre-Link Event】,命令行輸入“PowerShell -ExecutionPolicy RemoteSigned -File $(SolutionDir)Export4CExport4C.ps1 -IntDir $(IntDir) -Source InjectProc.c -NoLogo”以在相應的時候調用Export4C。 ? 注意Export4C路徑、IntDir(包含了匯編輸出和原目標文件輸出的中間目錄)、Source(要Export4C公開其中函數實際地址和大小的源文件)要配置正確。#include // Export4C externs EXTERN_C LPTHREAD_START_ROUTINE E4C_Addr_InjectProc; EXTERN_C SIZE_T E4C_Size_InjectProc; int main() { DWORD dwPID, dwLastError, dwResult; HANDLE hProc, hRemoteThread; LPVOID lpRemoteMem; dwLastError = ERROR_SUCCESS; // Use current process ID in this example. // Architecture (x64/x86) of target process should be the same with this example. dwPID = GetCurrentProcessId(); hProc = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | SYNCHRONIZE, FALSE, dwPID); if (hProc == INVALID_HANDLE_VALUE) { dwLastError = ERROR_INVALID_HANDLE; goto Label_3; } // Allocate memory for the process lpRemoteMem = VirtualAllocEx(hProc, NULL, E4C_Size_InjectProc, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!lpRemoteMem) { dwLastError = GetLastError(); printf_s("Allocate memory failed with error: %d", dwLastError); goto Label_2; } // Write code to the memory and flush cache if (!WriteProcessMemory(hProc, lpRemoteMem, E4C_Addr_InjectProc, E4C_Size_InjectProc, NULL)) { dwLastError = GetLastError(); printf_s("Write code failed with error: %d", dwLastError); goto Label_1; } FlushInstructionCache(hProc, lpRemoteMem, E4C_Size_InjectProc); // Create remote thread and wait for the result hRemoteThread = CreateRemoteThread(hProc, NULL, 0, lpRemoteMem, NULL, 0, NULL); if (!hRemoteThread) { dwLastError = GetLastError(); printf_s("Create remote thread failed with error: %d", dwLastError); goto Label_1; } WaitForSingleObject(hRemoteThread, INFINITE); if (!GetExitCodeThread(hRemoteThread, &dwResult)) { dwLastError = GetLastError(); printf_s("Get exit code of remote thread failed with error: %d", dwLastError); goto Label_0; } // "InjectProc" function returns "666" printf_s("Remote thread returns: %d", dwResult); // Cleanup and exit Label_0: CloseHandle(hRemoteThread); Label_1: VirtualFreeEx(hProc, lpRemoteMem, 0, MEM_RELEASE); Label_2: CloseHandle(hProc); Label_3: return dwLastError; }
編輯:黃飛
?
?
?
評論
查看更多