热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

语言列表导出xlxs_让C语言源码可知自身函数的实际地址与大小

本文为看雪论坛优秀文章看雪论坛作者ID:Ratin事情的起因大概是这样……在很久很久以前,我最早用的是MASM(Win32ASM)写程序,
873cef50f42451105ba6072f2526e117.png

本文为看雪论坛优秀文章

看雪论坛作者ID:Ratin

事情的起因大概是这样……在很久很久以前,我最早用的是MASM(Win32ASM)写程序,从平台兼容性、开发效率和规范等方面考虑,后来我义无反顾地转成了C、C++和C++++……主要是为了保持队形,但那真的是4个+699e288b479af9c04571907f62e0fc6c.png当然也很顺手,不用像汇编那样,x86是一份代码,x64又大相径庭,再换成ARM那就没法玩了。至于运行效率,更多的可以考虑开发效率、可维护性等等,至少我是没有蜜汁自信来认为自己所写的汇编源码全文会比当今C编译器所产生的更高效。如果MASM问我,我会说爱过。723aefd544be32779e5d083b7b72e747.png很快,根据王境泽大师的真香定理,C语言在代码注入上让我一度考虑重操旧业(与MASM混编)。接下来我们先一起教科书式地复习Windows下传统远程代码注入的套路,如果学不会也没关系,你只需要记住这一套连招,1433223、1433223、1433223、1433223。ecb1c2495e96e7393f1ad720780d1e91.png1. 打开远端进程(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语言中,我们还能如此顺风顺水地获得自身定义函数的实际地址和大小吗?1fb4f1c51682325c618b28ab82a0ac1c.png我们先看地址。C语言无法定义函数外标签,函数内标签从使用到访问处处受限,我们好像只剩函数名可以用。但函数名表达式未必等同于函数的实际地址,它可能会指向JMP stub,再由该JMP stub跳转到函数实际地址:ac891a1daebdd20d0342bbaeee1fb506.png有的甚至经由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然后让我们朵蜜一下它,给它头上戴个帽子,还送一双鞋:80873e99fb847e7dcf6a23043740bc31.gif它就长这样了:

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 ENDSx86就把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 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是支持宏的。c0e0e08c88ec4dcc0583f105ff86ed22.png第一次发帖,还请大佬们多多指教!fbfedb4de088d39ded91f64ee75e9ae0.gif- End -

4eec67ec79e24dd4a51ddd0197b77609.png

看雪ID:Ratin

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

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

推荐文章++++

f3a179214a8e5eebabdf4492c93945a9.png

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

* Chunk Extend and Overlapping笔记

* Frida 入门小练习

* 索然无味的勒索病毒

* 初试so文件解密

好书推荐add52b87e580ad4f6198533a2eb1dd35.png

﹀﹀﹀3236ac6f692fcf584a0f830882a1b1ba.png公众号ID:ikanxue官方微博:看雪安全商务合作:wsc@kanxue.comps. 觉得对你有帮助的话,别忘点分享,点赞和在看,支持看雪哦~1721f66d3ed1e4c3a7683d6dc2587df9.gif



推荐阅读
  • 本文介绍了JSP的基本概念、常用标签及其功能,并通过示例详细说明了如何在JSP页面中使用Java代码。 ... [详细]
  • C基本语法C程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象,方法、即时变量。对象-对象具有状态和行为 ... [详细]
  • 详解MyBatis二级缓存的启用与配置
    本文深入探讨了MyBatis二级缓存的启用方法及其配置细节,通过具体的代码实例进行说明,有助于开发者更好地理解和应用这一特性,提升应用程序的性能。 ... [详细]
  • 本文介绍了一种算法,用于在一个给定的二叉树中找到一个节点,该节点的子树包含最大数量的值小于该节点的节点。如果存在多个符合条件的节点,可以选择任意一个。 ... [详细]
  • 本文主要解决了在编译CM10.2时出现的关于Samsung Exynos 4 HDMI HAL库中SecHdmiV4L2Utils.cpp文件的编译错误。 ... [详细]
  • 本文探讨了 Boost 库中的 Program Options 组件,这是一个强大的工具,用于解析命令行参数和配置文件。文章介绍了如何正确设置和使用该组件,包括处理复杂选项和负数值的方法。 ... [详细]
  • 本文详细解析了Java中流的概念,特别是OutputStream和InputStream的区别,并通过实际案例介绍了如何实现Java对象的序列化。文章不仅解释了流的基本概念,还探讨了序列化的重要性和具体实现步骤。 ... [详细]
  • 深入解析mt_allocator内存分配器(二):多线程与单线程场景下的实现
    本文详细介绍了mt_allocator内存分配器在多线程和单线程环境下的实现机制。该分配器以2的幂次方字节为单位分配内存,支持灵活的配置和高效的性能。文章分为内存池特性描述、内存池实现、单线程内存池实现、内存池策略类实现及多线程内存池实现等部分,深入探讨了内存池的初始化、内存分配与回收的具体实现。 ... [详细]
  • 本文详细介绍了Socket在Linux内核中的实现机制,包括基本的Socket结构、协议操作集以及不同协议下的具体实现。通过这些内容,读者可以更好地理解Socket的工作原理。 ... [详细]
  • 拖拉切割直线 ... [详细]
  • 使用Objective-C实现苹果官方NSLayoutConstraint页面布局
    本文详细介绍了如何在iOS开发中使用Objective-C语言通过NSLayoutConstraint来实现页面布局。示例代码展示了如何创建和应用约束,以确保界面元素能够正确地响应不同屏幕尺寸的变化。 ... [详细]
  • 本文详细介绍如何在Spring Boot项目中集成和使用JPA,涵盖JPA的基本概念、Spring Data JPA的功能以及具体的操作步骤,帮助开发者快速掌握这一强大的持久化技术。 ... [详细]
  • 题目描述:给定 n 把雨伞和 m 个人,t 分钟后开始下雨。求在每个人只能使用一把雨伞的情况下,最多有多少人可以拿到雨伞。 ... [详细]
  • 本文详细介绍了在 Windows 7 上安装和配置 PHP 5.4 的 Memcached 分布式缓存系统的方法,旨在减少数据库的频繁访问,提高应用程序的响应速度。 ... [详细]
  • 题目概述:Sereja 拥有一个由 n 个整数组成的数组 a1, a2, ..., an。他计划执行 m 项操作,这些操作包括更新数组中的特定元素、增加数组中所有元素的值,以及查询数组中的特定元素。 ... [详细]
author-avatar
哇哈哈啦啦啦啦_729
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有