事情的起因大概是這樣……
在很久很久以前,我最早用的是MASM(Win32ASM)寫(xiě)程序,從平臺(tái)兼容性、開(kāi)發(fā)效率和規(guī)范等方面考慮,后來(lái)我義無(wú)反顧地轉(zhuǎn)成了C、C++和C++++……
主要是為了保持隊(duì)形,但那真的是4個(gè)+
當(dāng)然也很順手,不用像匯編那樣,x86是一份代碼,x64又大相徑庭,再換成ARM那就沒(méi)法玩了。至于運(yùn)行效率,更多的可以考慮開(kāi)發(fā)效率、可維護(hù)性等等,至少我是沒(méi)有蜜汁自信來(lái)認(rèn)為自己所寫(xiě)的匯編源碼全文會(huì)比當(dāng)今C編譯器所產(chǎn)生的更高效。如果MASM問(wèn)我,我會(huì)說(shuō)愛(ài)過(guò)。
很快,根據(jù)王境澤大師的真香定理,C語(yǔ)言在代碼注入上讓我一度考慮重操舊業(yè)(與MASM混編)。接下來(lái)我們先一起教科書(shū)式地復(fù)習(xí)Windows下傳統(tǒng)遠(yuǎn)程代碼注入的套路,如果學(xué)不會(huì)也沒(méi)關(guān)系,你只需要記住這一套連招,1433223、1433223、1433223、1433223。
1. 打開(kāi)遠(yuǎn)端進(jìn)程(OpenProcess/NtOpenProcess),權(quán)限至少包含以下步驟所需。
2. 以可讀可寫(xiě)可執(zhí)行的頁(yè)面保護(hù)屬性(PAGE_EXECUTE_READWRITE)為遠(yuǎn)端進(jìn)程分配新的內(nèi)存區(qū)域(VirtualAllocEx/NtAllocateVirtualMemory),需要PROCESS_VM_OPERATION權(quán)限。
3. 將代碼寫(xiě)入遠(yuǎn)端進(jìn)程(WriteProcessMemory/NtWriteVirtualMemory),需要PROCESS_VM_WRITE權(quán)限。
4. 刷新指令緩存(FlushInstructionCache/NtFlushInstructionCache),嚴(yán)謹(jǐn)起見(jiàn),根據(jù)MSDN描述,即使是剛申請(qǐng)下來(lái)用于執(zhí)行代碼的內(nèi)存,也需要刷新。
5.以剛申請(qǐng)用于執(zhí)行代碼的內(nèi)存為入口點(diǎn),創(chuàng)建遠(yuǎn)端線程(CreateRemoteThread/RtlCreateUserThread/NtCreateThreadEx)并執(zhí)行。后續(xù)可以自行發(fā)揮,比如同步和獲取退出碼
6. 最后記得收尾。有借有還,再借不難。
期間我們會(huì)遇到兩個(gè)問(wèn)題。
第一,遠(yuǎn)程注入的代碼中,如果調(diào)用了外部函數(shù),很可能導(dǎo)致違規(guī)訪問(wèn)、任意代碼執(zhí)行等問(wèn)題,因?yàn)樵谶h(yuǎn)端進(jìn)程中調(diào)用的外部函數(shù)很可能無(wú)法被正確地尋址,甚至都不存在于遠(yuǎn)端進(jìn)程中。所以我們應(yīng)該要保證所注入的代碼沒(méi)有直接的外部函數(shù)調(diào)用,而是自己尋址。
嚴(yán)謹(jǐn)?shù)姆绞绞菑腜EB中的模塊鏈和這些模塊的導(dǎo)出表出發(fā),去一步步找需要的東西,這個(gè)不是我們現(xiàn)在要討論的。只要對(duì)PEB、導(dǎo)出表結(jié)構(gòu)理解到位便不復(fù)雜,順帶一提,DLL有按序號(hào)和名稱(chēng)兩種導(dǎo)出方式,導(dǎo)出為重定向(Forwarder Name)的情況最好也納入考慮,可以參考ReactOS的實(shí)現(xiàn)(GetProcAddress -> LdrGetProcedureAddress -> LdrpGetProcedureAddress -> LdrpSnapThunk)。
第二,在第3步,如果注入本地函數(shù),我們需要知道本地函數(shù)的實(shí)際地址與大小,才能正確地寫(xiě)入到遠(yuǎn)端進(jìn)程中。MASM中我們可以放飛自我地定義標(biāo)簽:
ExampleProc_Start: ExampleProc PROC a, b MOV EAX, a ADD EAX, b RET ExampleProc ENDP ExampleProc_End:
"offset ExampleProc_Start"是過(guò)程"ExampleProc"的起始地址,"offset ExampleProc_End"是其結(jié)束地址,二者之差則是其大小。
在C語(yǔ)言中,我們還能如此順風(fēng)順?biāo)孬@得自身定義函數(shù)的實(shí)際地址和大小嗎?
我們先看地址。C語(yǔ)言無(wú)法定義函數(shù)外標(biāo)簽,函數(shù)內(nèi)標(biāo)簽從使用到訪問(wèn)處處受限,我們好像只剩函數(shù)名可以用。但函數(shù)名表達(dá)式未必等同于函數(shù)的實(shí)際地址,它可能會(huì)指向JMP stub,再由該JMP stub跳轉(zhuǎn)到函數(shù)實(shí)際地址:
有的甚至經(jīng)由JMP stub跳轉(zhuǎn)兩次才到實(shí)際地址。這樣的JMP stub自有用處,比如增量鏈接,或者兼容沒(méi)有"__declspec(dllimport)"修飾的外部函數(shù)聲明等等。關(guān)閉增量鏈接后,本地函數(shù)的函數(shù)名作表達(dá)式,應(yīng)該就是正確的內(nèi)存地址了。
至于函數(shù)體大小,"sizeof"操作符是用不了的。我看到網(wǎng)上有如下的寫(xiě)法:
int ExampleProc() { return 0; } void ExampleProcEnd() {}
然后用"ExampleProcEnd"減去"ExampleProc"。我用的是VS2019,關(guān)閉了MSVC編譯器和鏈接器的各種優(yōu)化選項(xiàng)、SDL和增量鏈接等操作,結(jié)果是從來(lái)沒(méi)對(duì)過(guò)。
話(huà)說(shuō),編譯器本身好像也沒(méi)有責(zé)任去安排函數(shù)體的內(nèi)存順序,倒是恨不得給它們折疊一下(COMDAT)或者內(nèi)聯(lián)一下。
綜上,關(guān)閉增量鏈接后,函數(shù)體實(shí)際地址有解,雖然算不上理想的解決方案;至于函數(shù)體大小,仍然是C語(yǔ)言本身不可及的地方。當(dāng)然也可以硬編碼將大小寫(xiě)大一些,足夠覆蓋該函數(shù)體,只要訪問(wèn)沒(méi)越界應(yīng)該還是可以正常工作的,我想尋求更為嚴(yán)謹(jǐn)?shù)姆绞健?/p>
似乎此時(shí)我們不得不借助匯編語(yǔ)言。MSVC中,x86支持內(nèi)聯(lián)匯編,參考MSDN: Inline assembly in MSVC;x64不支持內(nèi)聯(lián),但可以外置匯編源碼在工程中,獨(dú)立生成目標(biāo)文件與其它源文件生成的目標(biāo)文件鏈接,參考MSDN: MASM for x64 (ml64.exe)一文中"Add an assembler-language file to a Visual Studio C++ project"章節(jié)。用匯編來(lái)寫(xiě)要注入的函數(shù)(過(guò)程),此時(shí)可知其實(shí)際地址與大小,再供C語(yǔ)言中引用。
可是,這樣x86寫(xiě)一份,x64寫(xiě)一份,說(shuō)不準(zhǔn)ARM也可以來(lái)湊個(gè)熱鬧,這不又回到了以前嘛,說(shuō)好的兔子不吃……哦不,好馬不吃回頭草!是的,此時(shí)我們需要借助匯編,但未必非得以這樣的方式。
我記得MSVC編譯器可以產(chǎn)生相應(yīng)的匯編輸出,如果我們能利用它,那么或許可以保持注入函數(shù)一樣使用C來(lái)編寫(xiě)了。下面舉個(gè)栗子:
我們有C語(yǔ)言函數(shù)"ExampleProc",是我們要拿來(lái)注入的函數(shù):
int __stdcall ExampleProc(int a, int b) { return a + b; }
我們先只考慮Release構(gòu)建,對(duì)應(yīng)的x64匯編輸出大概是這個(gè)亞子,x86在PROC的定義上大同小異:
ExampleProc PROC lea eax, DWORD PTR [rcx+rdx] ret 0 ExampleProc ENDP
然后讓我們朵蜜一下它,給它頭上戴個(gè)帽子,還送一雙鞋:
它就長(zhǎng)這樣了:
E4C_Start_ExampleProc: ExampleProc PROC lea eax, DWORD PTR [rcx+rdx] ret 0 ExampleProc ENDP E4C_End_ExampleProc:
當(dāng)然,"E4C_Start"之類(lèi)的前綴自擬,后面用的時(shí)候?qū)Φ蒙咸?hào)就行。最后把我們需要的定義為常量,并且公開(kāi)給其它模塊使用:
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,對(duì)應(yīng)到C語(yǔ)言中的size_t。匯編輸出改好了,我們調(diào)用ml.exe或者ml64.exe把它重新匯編,生成新的目標(biāo)文件并替換之前MSVC編譯器生成的,此時(shí)它多了"E4C_Addr_ExampleProc"和
"E4C_Size_ExampleProc"兩個(gè)導(dǎo)出符號(hào),分別是"ExampleProc"函數(shù)(過(guò)程)的實(shí)際地址和計(jì)以字節(jié)的大小。
在同一工程的其它C語(yǔ)言源文件中,添加以下外部符號(hào)定義,即可引用它們了:
typedef int(__stdcall* PEXAMPLEPROC)(int a, int b);
extern PEXAMPLEPROC E4C_Addr_ExampleProc; extern size_t E4C_Size_ExampleProc;
地址的定義可以直接void*,像上面這樣聲明成相同的proto就可以調(diào)用它,當(dāng)然是多此一舉(同一工程下的直接用函數(shù)名調(diào)用就好了)。大小這里用的是size_t,總之和之前在匯編輸出里定義的一致就行。
但整個(gè)實(shí)現(xiàn)過(guò)程并不順利,因?yàn)镸SVC編譯器似乎管匯編輸出稱(chēng)為"Assembler Listing"(匯編列表),與源文件有不小差距。實(shí)際上我們之所以爭(zhēng)取保持使用C語(yǔ)言寫(xiě)注入的函數(shù)就是因?yàn)樾枰鼘?shí)現(xiàn)的邏輯相對(duì)復(fù)雜,而不像上述例子那樣僅僅實(shí)現(xiàn)a+b這樣的小兒科,從而生成的匯編輸出也復(fù)雜。
這時(shí),把MSVC生成的匯編輸出直接丟給MASM匯編那可就涼了,會(huì)產(chǎn)生很多錯(cuò)誤,尤其是語(yǔ)法錯(cuò)誤。比如x86匯編輸出缺少"assume fs:nothing",導(dǎo)致fs訪問(wèn)出錯(cuò);x64輸出了"FLAT:"這樣只在x86中可用的標(biāo)識(shí);"$LN??"這樣的標(biāo)簽被后向引用、重定義等;用到的浮點(diǎn)數(shù)被定義成以"__real@"開(kāi)頭的公開(kāi)符號(hào),與其它模塊產(chǎn)生沖突等等。
最后,我將這套流程寫(xiě)成了PowerShell腳本(Export4C),可集成在VS生成過(guò)程中。關(guān)于之前提到MSVC匯編輸出中的錯(cuò)誤,已有一些相應(yīng)修復(fù)措施,但我們?nèi)詰?yīng)保持注入函數(shù)盡可能簡(jiǎn)單,沒(méi)有外部函數(shù)調(diào)用,最好自己在一個(gè)獨(dú)立的C源文件中涼快。 ? 下面看一下效果: ? 在工程中,將要注入的函數(shù)獨(dú)立放在一個(gè)源文件"InjectProc.c"中,這個(gè)函數(shù)定義為"LPTHREAD_START_ROUTINE",會(huì)給調(diào)用它的老鐵返回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? 打開(kāi)"InjectProc.c"文件屬性,【C/C++】設(shè)置里關(guān)閉JMC (Just My Code) 、Security Cookie、SDL和RTC,它們會(huì)在prologue和epilogue部分產(chǎn)生外部函數(shù)調(diào)用,注入到遠(yuǎn)程那就涼了。 ? 在"Source.c"中我們把它注入指定進(jìn)程里,例子中用的是當(dāng)前進(jìn)程PID: ?DWORD WINAPI InjectProc(LPVOID lpThreadParameter) { UNREFERENCED_PARAMETER(lpThreadParameter); return 666; }
/* Example3: Inject and execute code in a process. */ #include? 打開(kāi)項(xiàng)目屬性,【C/C++】 - 【Output Files】,設(shè)置“Assembler Output”為"Assembly Only"(/FA)或者"Assembly With Source Code"(/FAs)。切換到【Advanced】,關(guān)閉“Whole Program Optimization”,至此,在默認(rèn)情況下,匯編輸出會(huì)生成于中間目錄$(IntDir)。 ? 切換到【Build Events】 - 【Pre-Link Event】,命令行輸入“PowerShell -ExecutionPolicy RemoteSigned -File $(SolutionDir)Export4CExport4C.ps1 -IntDir $(IntDir) -Source InjectProc.c -NoLogo”以在相應(yīng)的時(shí)候調(diào)用Export4C。 ? 注意Export4C路徑、IntDir(包含了匯編輸出和原目標(biāo)文件輸出的中間目錄)、Source(要Export4C公開(kāi)其中函數(shù)實(shí)際地址和大小的源文件)要配置正確。#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; }
編輯:黃飛
?
?
?
評(píng)論
查看更多