?
事情的起因大概是這樣……
在很久很久以前,我最早用的是MASM(Win32ASM)寫程序,從平臺兼容性、開發(fā)效率和規(guī)范等方面考慮,后來我義無反顧地轉成了C、C++和C++++……
主要是為了保持隊形,但那真的是4個+
當然也很順手,不用像匯編那樣,x86是一份代碼,x64又大相徑庭,再換成ARM那就沒法玩了。至于運行效率,更多的可以考慮開發(fā)效率、可維護性等等,至少我是沒有蜜汁自信來認為自己所寫的匯編源碼全文會比當今C編譯器所產生的更高效。如果MASM問我,我會說愛過。
很快,根據(jù)王境澤大師的真香定理,C語言在代碼注入上讓我一度考慮重操舊業(yè)(與MASM混編)。接下來我們先一起教科書式地復習Windows下傳統(tǒng)遠程代碼注入的套路,如果學不會也沒關系,你只需要記住這一套連招,1433223、1433223、1433223、1433223。
1. 打開遠端進程(OpenProcess/NtOpenProcess),權限至少包含以下步驟所需。
2. 以可讀可寫可執(zhí)行的頁面保護屬性(PAGE_EXECUTE_READWRITE)為遠端進程分配新的內存區(qū)域(VirtualAllocEx/NtAllocateVirtualMemory),需要PROCESS_VM_OPERATION權限。
3. 將代碼寫入遠端進程(WriteProcessMemory/NtWriteVirtualMemory),需要PROCESS_VM_WRITE權限。
4. 刷新指令緩存(FlushInstructionCache/NtFlushInstructionCache),嚴謹起見,根據(jù)MSDN描述,即使是剛申請下來用于執(zhí)行代碼的內存,也需要刷新。
5.以剛申請用于執(zhí)行代碼的內存為入口點,創(chuàng)建遠端線程(CreateRemoteThread/RtlCreateUserThread/NtCreateThreadEx)并執(zhí)行。后續(xù)可以自行發(fā)揮,比如同步和獲取退出碼
6. 最后記得收尾。有借有還,再借不難。
期間我們會遇到兩個問題。
第一,遠程注入的代碼中,如果調用了外部函數(shù),很可能導致違規(guī)訪問、任意代碼執(zhí)行等問題,因為在遠端進程中調用的外部函數(shù)很可能無法被正確地尋址,甚至都不存在于遠端進程中。所以我們應該要保證所注入的代碼沒有直接的外部函數(shù)調用,而是自己尋址。
嚴謹?shù)姆绞绞菑腜EB中的模塊鏈和這些模塊的導出表出發(fā),去一步步找需要的東西,這個不是我們現(xiàn)在要討論的。只要對PEB、導出表結構理解到位便不復雜,順帶一提,DLL有按序號和名稱兩種導出方式,導出為重定向(Forwarder Name)的情況最好也納入考慮,可以參考ReactOS的實現(xiàn)(GetProcAddress -> LdrGetProcedureAddress -> LdrpGetProcedureAddress -> LdrpSnapThunk)。
第二,在第3步,如果注入本地函數(shù),我們需要知道本地函數(shù)的實際地址與大小,才能正確地寫入到遠端進程中。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語言中,我們還能如此順風順水地獲得自身定義函數(shù)的實際地址和大小嗎?
我們先看地址。C語言無法定義函數(shù)外標簽,函數(shù)內標簽從使用到訪問處處受限,我們好像只剩函數(shù)名可以用。但函數(shù)名表達式未必等同于函數(shù)的實際地址,它可能會指向JMP stub,再由該JMP stub跳轉到函數(shù)實際地址:
有的甚至經(jīng)由JMP stub跳轉兩次才到實際地址。這樣的JMP stub自有用處,比如增量鏈接,或者兼容沒有"__declspec(dllimport)"修飾的外部函數(shù)聲明等等。關閉增量鏈接后,本地函數(shù)的函數(shù)名作表達式,應該就是正確的內存地址了。
至于函數(shù)體大小,"sizeof"操作符是用不了的。我看到網(wǎng)上有如下的寫法:
?
int ExampleProc() { return 0; } void ExampleProcEnd() {}
?
然后用"ExampleProcEnd"減去"ExampleProc"。我用的是VS2019,關閉了MSVC編譯器和鏈接器的各種優(yōu)化選項、SDL和增量鏈接等操作,結果是從來沒對過。
話說,編譯器本身好像也沒有責任去安排函數(shù)體的內存順序,倒是恨不得給它們折疊一下(COMDAT)或者內聯(lián)一下。
綜上,關閉增量鏈接后,函數(shù)體實際地址有解,雖然算不上理想的解決方案;至于函數(shù)體大小,仍然是C語言本身不可及的地方。當然也可以硬編碼將大小寫大一些,足夠覆蓋該函數(shù)體,只要訪問沒越界應該還是可以正常工作的,我想尋求更為嚴謹?shù)姆绞健?/p>
似乎此時我們不得不借助匯編語言。MSVC中,x86支持內聯(lián)匯編,參考MSDN: Inline assembly in MSVC;x64不支持內聯(lián),但可以外置匯編源碼在工程中,獨立生成目標文件與其它源文件生成的目標文件鏈接,參考MSDN: MASM for x64 (ml64.exe)一文中"Add an assembler-language file to a Visual Studio C++ project"章節(jié)。用匯編來寫要注入的函數(shù)(過程),此時可知其實際地址與大小,再供C語言中引用。
可是,這樣x86寫一份,x64寫一份,說不準ARM也可以來湊個熱鬧,這不又回到了以前嘛,說好的兔子不吃……哦不,好馬不吃回頭草!是的,此時我們需要借助匯編,但未必非得以這樣的方式。
我記得MSVC編譯器可以產生相應的匯編輸出,如果我們能利用它,那么或許可以保持注入函數(shù)一樣使用C來編寫了。下面舉個栗子:
我們有C語言函數(shù)"ExampleProc",是我們要拿來注入的函數(shù):
?
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"函數(shù)(過程)的實際地址和計以字節(jié)的大小。
在同一工程的其它C語言源文件中,添加以下外部符號定義,即可引用它們了:
?
typedef int(__stdcall* PEXAMPLEPROC)(int a, int b); extern PEXAMPLEPROC E4C_Addr_ExampleProc; extern size_t E4C_Size_ExampleProc;
?
地址的定義可以直接void*,像上面這樣聲明成相同的proto就可以調用它,當然是多此一舉(同一工程下的直接用函數(shù)名調用就好了)。大小這里用的是size_t,總之和之前在匯編輸出里定義的一致就行。
但整個實現(xiàn)過程并不順利,因為MSVC編譯器似乎管匯編輸出稱為"Assembler Listing"(匯編列表),與源文件有不小差距。實際上我們之所以爭取保持使用C語言寫注入的函數(shù)就是因為需要它實現(xiàn)的邏輯相對復雜,而不像上述例子那樣僅僅實現(xiàn)a+b這樣的小兒科,從而生成的匯編輸出也復雜。
這時,把MSVC生成的匯編輸出直接丟給MASM匯編那可就涼了,會產生很多錯誤,尤其是語法錯誤。比如x86匯編輸出缺少"assume fs:nothing",導致fs訪問出錯;x64輸出了"FLAT:"這樣只在x86中可用的標識;"$LN??"這樣的標簽被后向引用、重定義等;用到的浮點數(shù)被定義成以"__real@"開頭的公開符號,與其它模塊產生沖突等等。
?
最后,我將這套流程寫成了PowerShell腳本(Export4C),可集成在VS生成過程中。關于之前提到MSVC匯編輸出中的錯誤,已有一些相應修復措施,但我們仍應保持注入函數(shù)盡可能簡單,沒有外部函數(shù)調用,最好自己在一個獨立的C源文件中涼快。 ? 下面看一下效果: ? 在工程中,將要注入的函數(shù)獨立放在一個源文件"InjectProc.c"中,這個函數(shù)定義為"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 */ #includeDWORD WINAPI InjectProc(LPVOID lpThreadParameter) { UNREFERENCED_PARAMETER(lpThreadParameter); return 666; }
? 打開"InjectProc.c"文件屬性,【C/C++】設置里關閉JMC (Just My Code) 、Security Cookie、SDL和RTC,它們會在prologue和epilogue部分產生外部函數(shù)調用,注入到遠程那就涼了。 ?
在"Source.c"中我們把它注入指定進程里,例子中用的是當前進程PID: ?
/* Example3: Inject and execute code in a process. */ #include?#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; }
打開項目屬性,【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公開其中函數(shù)實際地址和大小的源文件)要配置正確。 ?
至此,項目可以正常生成和運行了,預期會輸出“Remote thread returns: 666”。Export4C會為輸入的源文件(InjectProc.c)增加其中所有函數(shù)實際地址和大小的公開符號,函數(shù)實際地址命名為"E4C_Addr_[函數(shù)名]",可定義為LPVOID或該函數(shù)實際原型;函數(shù)體大小命名為"E4C_Size_[函數(shù)名]",數(shù)據(jù)類型為SIZE_T。如同例子中"Source.c"對"E4C_Addr_InjectProc"和"E4C_Size_InjectProc"的引用。
? 如上,我們可以在C語言中引用源碼自身中函數(shù)的實際地址與大小,全程不需要手動寫一句匯編指令,并且編譯器、鏈接器可以按原本的方式工作,支持x86和x64目標,基本不需要強行關閉什么優(yōu)化。
? 上述例子中關閉了全程序優(yōu)化,是因為它將影響匯編輸出的位置,手動處理一下也可以保持它開啟的狀態(tài),只要Export4C的IntDir參數(shù)目錄能找到它和目標文件即可。 ?
關閉"InjectProc.c"文件的JMC (Just My Code) 、Security Cookie、SDL和RTC功能是遠程代碼注入的業(yè)務需要,防止產生外部函數(shù)調用。JMC與RTC啟用時是會導致一些匯編輸出里的語法錯誤,但在Export4C腳本已對其進行處理。 ?
這個腳本已開源于GitHub:KNSoft/Export4C,包含VS解決方案和3個示例工程(Example1 ~ Example3, VS 2019),腳本本身由PowerShell所寫,在"Export4C"工程(目錄)內,可以用"Get-Help" cmdlet獲取它參數(shù)的詳細說明。 ?
有空的時候會繼續(xù)維護和更新,增加更多的功能,導出更多有用的符號供使用。最早寫這個腳本的時候,分析匯編輸出時我在正則中放飛自我,結果速度有些感人,現(xiàn)在盡量老老實實匹配字符串去了。目前只是用它滿足個人需求,進而分享,所以不算完善,暫時也只能確保在我個人的使用場景下(如VS 2019,業(yè)務需求等)符合預期。 ?
目前只是在個人所寫的小程序中使用,通過Export4C獲取線程函數(shù)"RProc_LoadProcAddr_InjectThread"的實際地址與大小,供注入遠端進程使用。而在"RProc_LoadProcAddr_InjectThread"線程函數(shù)中,可以根據(jù)傳入的DLL模塊名與函數(shù)名,得到該函數(shù)在遠端進程的地址。 ?
先遍歷PEB的已加載模塊鏈表,查找指定的DLL。如果找到,則遍歷其導出表,獲取指定的函數(shù)實際地址。若未被加載,則調用ntdll!LdrLoadDll嘗試加載進來,再進行操作。 ?
這或許是個新的思路,借助MASM的特性,來擴展C的功能。MASM中的第一個M是Macro而不是Microsoft,它對宏的支持可謂功能強大,并且現(xiàn)在ML64 是支持宏的。
?
?
審核編輯:湯梓紅
?
評論
查看更多