C语言如何获得自身定义函数的实际地址和大小吗

事情的起因大概是这样……
在很久很久以前,我最早用的是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, bmov eax, aadd eax, bretexampleproc endpexampleproc_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 proclea eax, dword ptr [rcx+rdx]ret 0exampleproc endp 然后让我们朵蜜一下它,给它头上戴个帽子,还送一双鞋:
它就长这样了:
e4c_start_exampleproc:exampleproc proclea eax, dword ptr [rcx+rdx]ret 0exampleproc endpe4c_end_exampleproc: 当然,e4c_start之类的前缀自拟,后面用的时候对得上号就行。最后把我们需要的定义为常量,并且公开给其它模块使用:
public e4c_addr_exampleprocpublic e4c_size_exampleprocconst segmente4c_addr_exampleproc dq offset e4c_start_exampleproce4c_size_exampleproc dq offset e4c_end_exampleproc - offset e4c_start_exampleprocconst 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 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 #include // 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 cacheif (!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)export4cexport4c.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 是支持宏的。


关于电路板的小知识
汽车线束气密性测试使用快速接头进行密封密封应该如何操作
防静电电阻大好还是小好
建设数字中国 腾讯木星云打通数字化“任督二脉”
R-78S评估板是如何延长电池使用寿命的
C语言如何获得自身定义函数的实际地址和大小吗
公共网络安全,还得看华为云虚拟专用网络VPN
为什么物联网吞噬嵌入式RTOS
一种可穿戴的、能够识别并捕获癌细胞德微流体机器
Technodinamika公司已为俄罗斯卡-226T直升机研制出了抗坠毁燃油系统
新一轮动力电池技术竞赛开启 固态电池再现重量级玩家!
传iPad 3电池更轻薄 续航时间更长
为什么要选择SWYTCH代币跟踪可再生能源的生产
手机大战提前启动 台积电28奈米产能续吃紧
国能与AutoX与达成战略合作在欧洲部署大规模的机器人出租车
Lumerical光纤布拉格光栅温度传感器的仿真模拟
混合云会有哪一些影响
来了!vivoX9sPlus曝光:骁龙660加后置双摄
高压差分探头与低压探头的区别有哪些?
智能座舱HMI自动化测试之车机性能测试