ExampleProc_Start:ExampleProc PROC a, b MOV EAX, a ADD EAX, b RETExampleProc ENDPExampleProc_End:
"offset ExampleProc_Start"是过程"ExampleProc"的起始地址,"offset ExampleProc_End"是其结束地址,二者之差则是其大小。 在C语言中,我们还能如此顺风顺水地获得自身定义函数的实际地址和大小吗?
我们先看地址。C语言无法定义函数外标签,函数内标签从使用到访问处处受限,我们好像只剩函数名可以用。但函数名表达式未必等同于函数的实际地址,它可能会指向JMP stub,再由该JMP stub跳转到函数实际地址:
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的定义上大同小异:
/** * @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 <Windows.h>
DWORD WINAPI InjectProc(LPVOID lpThreadParameter) { UNREFERENCED_PARAMETER(lpThreadParameter); return 666;}
打开"InjectProc.c"文件属性,【C/C++】设置里关闭JMC (Just My Code) 、Security Cookie、SDL和RTC,它们会在prologue和epilogue部分产生外部函数调用,注入到远程那就凉了。
在"Source.c"中我们把它注入指定进程里,例子中用的是当前进程PID:
/* Example3: Inject and execute code in a process.*/
#include <Windows.h>#include <stdio.h>
// Export4C externsEXTERN_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 exitLabel_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)Export4C\Export4C.ps1 -IntDir $(IntDir) -Source InjectProc.c -NoLogo”以在相应的时候调用Export4C。
注意Export4C路径、IntDir(包含了汇编输出和原目标文件输出的中间目录)、Source(要Export4C公开其中函数实际地址和大小的源文件)要配置正确。
至此,项目可以正常生成和运行了,预期会输出“Remote thread returns: 666”。Export4C会为输入的源文件(InjectProc.c)增加其中所有函数实际地址和大小的公开符号,函数实际地址命名为"E4C_Addr_[函数名]",可定义为LPVOID或该函数实际原型;函数体大小命名为"E4C_Size_[函数名]",数据类型为SIZE_T。如同例子中"Source.c"对"E4C_Addr_InjectProc"和"E4C_Size_InjectProc"的引用。
如上,我们可以在C语言中引用源码自身中函数的实际地址与大小,全程不需要手动写一句汇编指令,并且编译器、链接器可以按原本的方式工作,支持x86和x64目标,基本不需要强行关闭什么优化。
上述例子中关闭了全程序优化,是因为它将影响汇编输出的位置,手动处理一下也可以保持它开启的状态,只要Export4C的IntDir参数目录能找到它和目标文件即可。
关闭"InjectProc.c"文件的JMC (Just My Code) 、Security Cookie、SDL和RTC功能是远程代码注入的业务需要,防止产生外部函数调用。JMC与RTC启用时是会导致一些汇编输出里的语法错误,但在Export4C脚本已对其进行处理。
这个脚本已开源于GitHub:KNSoft/Export4C,包含VS解决方案和3个示例工程(Example1 ~ Example3, VS 2019),脚本本身由PowerShell所写,在"Export4C"工程(目录)内,可以用"Get-Help" cmdlet获取它参数的详细说明。
有空的时候会继续维护和更新,增加更多的功能,导出更多有用的符号供使用。最早写这个脚本的时候,分析汇编输出时我在正则中放飞自我,结果速度有些感人,现在尽量老老实实匹配字符串去了。目前只是用它满足个人需求,进而分享,所以不算完善,暂时也只能确保在我个人的使用场景下(如VS 2019,业务需求等)符合预期。
目前只是在个人所写的小程序中使用,通过Export4C获取线程函数"RProc_LoadProcAddr_InjectThread"的实际地址与大小,供注入远端进程使用。而在"RProc_LoadProcAddr_InjectThread"线程函数中,可以根据传入的DLL模块名与函数名,得到该函数在远端进程的地址。
先遍历PEB的已加载模块链表,查找指定的DLL。如果找到,则遍历其导出表,获取指定的函数实际地址。若未被加载,则调用ntdll!LdrLoadDll尝试加载进来,再进行操作。
这或许是个新的思路,借助MASM的特性,来扩展C的功能。MASM中的第一个M是Macro而不是Microsoft,它对宏的支持可谓功能强大,并且现在ML64是支持宏的。