[2025] Introduction to IDA's Internal Principles (Part 1): Overview & Function Recognition huoji ida,编译器,反编译 2025-01-24 1350 次浏览 10 次点赞 中文标题: IDA背后的原理入门(一): 简介&函数识别 # 简介 在《2024年终: 木马病毒自动化特征提取&云端机器学习的思路分享》 中我提到过 > 具体细节在明年2025年我会开一整个系列介绍实现,原理与思路,其实这种操作效果就是手写了一套IDA,但是比IDA更小,更可控。让我们回到正题,继续介绍模式匹配的具体用法 现在,2025年,让我们开始系统从头设计一个"IDA",从基本原理入手,逐步变成可"一键F5"的工具。 免责声明: 我不是专业搞编译器的,整个过程的路线是能跑就行,要是有高手发现我写的有问题,评论区跟我说一下,我们一起探讨一下 # 原理 IDA识别到能一键F5实际上可以分为几个具体步骤(略有不同,IDA是微码, 不是IL): 1. PE识别,导入表,导出表,TLS 等等 2. 基于(1),进行函数识别,标记哪些位置是函数 3. 基于(2),提升为 LLIL 也就是 low level IL,这个主要负责将 汇编 转为自己的通用翻译语言。这代表上层不需要关系汇编的架构什么的(比如,X64,MIPS,等等 转IL后都不需要关心了) 4. 基于(3) 提升为MIDDLE LEVEL IL,而这个过程我们需要根据RSP/RBP 访问标记函数,解析符号,并且识别函数内容 5. 基于(4) 做进一步优化,总而成为HIGHT LLIL。这一步后,就已经是IDA F5后的形状了 需要注意的是,这个过程并不是严格线性的。比如在LLIL提升过程中,可能会发现新的函数,这时需要回到第2步重新进行函数识别。整个过程是迭代式的,每一步都可能影响其他步骤。 我们以BN为例子: 1. bn的汇编模式:  你可以看到,在这个阶段,bn只是显示纯汇编(虽然可能有一些标注) 2. BN的llil模式:  这个时候,原来的汇编已经变成了BN的一种中间码,这样BN就能很愉快的处理各种乱七八糟的平台的汇编,比如ARM,mips等等 3.MLIL模式:  这个模式下的BN,在LLIL的基础上已经标识了 **函数调用**/**函数参数**/**符号**及代码做了**优化 ** 4. HLIL模式与C语言模式:  这个时候,已经跟本来的C语言没什么区别了.可能没有C语言能导出去运行的功能而已,这个是C语言的  # 函数识别 ## 初级函数识别 我们整个旅程的第一步,就是识别函数.如何做到在没符号识别函数是一个学问,我们不能太宽泛,就先假定目标是windows x64。 识别winx64的 函数 最基本的办法是遍历text段找call,我们可以遍历出 所有的 call imm指令以及JMP 指令,这些指令通常是一个函数. 基本代码如下:  其中call后就是我们识别的函数。 此外,msvc编译器编译出来的程序,会带cfg保护区段 > CFG(Control Flow Guard)是微软推出的漏洞利用缓解机制,从Windows 8.1开始引入,并且需要编译器的支持,编译器的版本为Virtual Studio 2015 Updated 2版本以上。 在开启了CFG支持以后,编译生成exe程序中,所有间接调用前面都会插入一个_guard_check_icall的检查函数,如果系统不支持CFG机制,则该函数不会生效。 如下就是一个经典的例子: ```cpp eax,[esi] ecx, eax call _guard_check_icall // CFG检查函数 call eax ``` _guard_check_icall函数的地址,在PE程序被系统加载的时候会被替换成nt!LdrpValidateUserCallTarget函数的地址 nt!LdrpValidateUserCallTarget函数的参数就是上面ecx的值,也就是间接调用的函数地址 校验逻辑如下: 1)间接调用函数地址共4字节,取高3字节的值加上CFGBitmap表的基址,得到CFGBitmap表中的值 2)判断间接调用地址是否0x10对齐: 2.1) 如果是对齐的,函数地址的第4-8位的值,就是上面获取的CFGBitmap表值的位偏移 2.2) 如果不是对齐的,函数地址的第4-8位的值,再或上1,得到的值就是CFGBitmap表值的位偏移 3)验证CFGBitmap表值的位偏移处的bit位,如果是1,则说明这个函数是有效的,否则产生异常 在PE结构的loadConfig信息中保存了: 1)_guard_check_icall的函数地址 2)CFGBitmap表的RVA,这里面是该程序的每个函数的RVA转换成1bit的值,制作成的一个CFGBitmap表 3)CFGBitmap中函数的数量 来源: https://blog.csdn.net/cssxn/article/details/101285088 因此我们也必须要识别是否是来自cfg控制区段的跳转,否则我们是没办法检测到函数的 基本代码如下:  值得注意的是,这个只是最基本的函数识别,这没办法处理其他情况,尤其是对于VT函数,我们应该需要搜索rdata去处理,除此之外,我们还需要从如下地方找: 1. 暴力特征函数序言 2. 异常处理表 3. TLS回调 4. 虚表/class 除了直接的call/jmp指令识别,我们还需要处理以下情况: ```cpp // 典型的间接跳转模式 mov rax, [some_address] call rax ``` 跳转表 ```cpp // 常见于switch-case实现 jmp qword ptr [rax*8 + jump_table] ``` 这些情况需要进行数据流分析才能准确识别目标函数。 一个完整的代码如下:  ## function symbol ida的symbol有几种: 1. 导入表 2. 导出表 3. lumina 4. pdb 5. ida sig 比如如下是基于导入表的符号识别:  而如下是基于lumina的识别  这是基于IDA sigs的函数识别  而我们先做简单一点,只识别导入/导出表  ## function stub 对于调试的程序你可以看到很多 function stub,这些都是指向真实函数,这方便调试,对于我们来说需要特殊处理这些.否则,背后跳转的函数我们会被忽略  而我们对其的处理可以粗暴一点,直接看是否是rip跳转  注意: 除了RIP相对寻址,function stub还有其他形式: 间接寻址 ```cpp mov rax, gs:[60h] ; TEB mov rax, [rax+...] ; 通过TEB间接寻址 jmp rax ``` 导入表跳板 ```cpp jmp cs:__imp_Function ; 通过导入表间接跳转 ``` 我们需要处理这些特殊情况,才能完整地识别所有函数。 ## 效果 IDA寻找出了320个函数  我们寻找出了297个函数  而作为对比,BN是460  > 一个显而易见的事实是,各家算法不同,差异也就越大.比如我们是不看debug jmp table的,而bn和ida是看的。而BN和IDA通过模拟执行能发现我们目前无法发现的间接call,而通过crt sigs,能直接标记已知的函数.这些是我们暂时没有的 ## 函数识别的难点与挑战 在基础函数识别之外,实际场景中还存在许多复杂情况需要处理: 编译器优化导致的函数识别困难 ```cpp // 尾调用优化 void funcA() { return funcB(); // 编译器会直接优化为 jmp funcB } // 内联优化 inline void funcC() { // 函数体直接被插入调用处 } ``` 反调试技术的影响 一些程序会使用特殊技术干扰函数识别: ```cpp // 花指令 push rax jmp $+5 db 0xE8 // 假call指令 pop rax // 真实代码继续... ``` 虚函数表的处理 需要专门的算法来处理C++的虚函数表: ```cpp struct VTable { void* func1; void* func2; }; class Base { virtual void vfunc() = 0; VTable* vtable; }; ``` 改进的函数识别算法 我们可以通过以下方式提高函数识别的准确率: 1. 启发式规则 检查函数序言模式(非仅prolog) 分析栈平衡情况 追踪寄存器使用模式 2. 交叉引用 3. 数据流分析 ## 未完待续 下一章,我们将介绍如何做程序控制流识别,以及一些关于IDA控制流追踪为什么那么不好使的原因.当文章阅读过1000就马上更新! **另外帮鸭鸭解决了滞销的米塔帽的兄弟们,可以在本公众号的微信群询问鸭哥要关于本章的DEMO以及指导(如果对做IDA感兴趣的话).感谢各位兄弟们的支持!** 本文由 huoji 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。 点赞 10
还不快抢沙发