Sec Hotspot 首页  排行榜  收藏本站  技术博客  RSS
统计信息
已收录文章数量:13793 篇
已收录公众号数量:89 个
本站文章为爬虫采集,如有侵权请告知
已收录微信公众号
网信中国 区块链大本营 白说区块链 区块链投资家 区块链官微 区块链铅笔Blockchain HACK学习呀 二道情报贩子 合天智汇 小白帽学习之路 小米安全中心 弥天安全实验室 SAINTSEC SecPulse安全脉搏 TideSec安全团队 360安全卫士 游侠安全网 计算机与网络安全 安全祖师爷 安全学习那些事 腾讯安全联合实验室 黑客技术与网络安全 安全圈 腾讯御见威胁情报中心 Python开发者 Python之禅 编程派 Python那些事 Python程序员 安全威胁情报 吾爱破解论坛 行长叠报 安在 i春秋 嘶吼专业版 E安全 MottoIN 网信防务 网安杂谈 数说安全 互联网安全内参 漏洞战争 安全分析与研究 邑安全 ChaMd5安全团队 天融信阿尔法实验室 安全牛 SecWiki 安全学术圈 信安之路 漏洞感知 浅黑科技 Secquan圈子社区 奇安信集团 奇安信 CERT 国舜股份 雷神众测 盘古实验室 美团安全应急响应中心 瓜子安全应急响应中心 顺丰安全应急响应中心 蚂蚁金服安全响应中心 携程安全应急响应中心 滴滴安全应急响应中心 字节跳动安全中心 百度安全应急响应中心 腾讯安全应急响应中心 网易安全应急响应中心 OPPO安全应急响应中心 京东安全应急响应中心 Bypass CNNVD安全动态 安恒应急响应中心 天融信每日安全简报 奇安信威胁情报中心 看雪学院 黑白之道 水滴安全实验室 安全客 木星安全实验室 云鼎实验室 绿盟科技安全预警 白帽汇 深信服千里目安全实验室 腾讯玄武实验室 长亭安全课堂 FreeBuf 绿盟科技 nmask
让C语言源码可知自身函数的实际地址与大小
本文来自公众号:看雪学院   2020.08.05 18:17:48


本文为看雪论坛优秀文章

看雪论坛作者 ID:Ratin



事情的起因大概是这样……

在很久很久以前,我最早用的是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    RETExampleProc 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 PROC    lea eax, DWORD PTR [rcx+rdx]    ret 0ExampleProc ENDP

然后让我们朵蜜一下它,给它头上戴个帽子,还送一双鞋:


它就长这样了:

E4C_Start_ExampleProc:ExampleProc PROC    lea eax, DWORD PTR [rcx+rdx]    ret 0ExampleProc ENDPE4C_End_ExampleProc:

当然,"E4C_Start"之类的前缀自拟,后面用的时候对得上号就行。最后把我们需要的定义为常量,并且公开给其它模块使用:

PUBLIC E4C_Addr_ExampleProcPUBLIC E4C_Size_ExampleProc CONST   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 <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
是支持宏的。


第一次发帖,还请大佬们多多指教!



- End -



看雪ID:Ratin

https://bbs.pediy.com/user-853701.htm

*本文由看雪论坛 Ratin 原创 ,转载请注明来自看雪社


推荐文章++++

* Win10 1909逆向(反向计算Windows内核内存布局及代码实现)

* Chunk Extend and Overlapping笔记

* Frida 入门小练习

* 索然无味的勒索病毒

* 初试so文件解密

好书推荐









公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



ps. 觉得对你有帮助的话,别忘点 分享 点赞 在看 ,支持看雪哦~


“阅读原文 一起来充电吧!