简介
在《2024年终: 木马病毒自动化特征提取&云端机器学习的思路分享》
2024年终: 木马病毒自动化特征提取&云端机器学习的思路分享
中我提到过
具体细节在明年2025年我会开一整个系列介绍实现,原理与思路,其实这种操作效果就是手写了一套IDA,但是比IDA更小,更可控。让我们回到正题,继续介绍模式匹配的具体用法
现在,2025年,让我们开始系统从头设计一个”IDA”,从基本原理入手,逐步变成可”一键F5”的工具。
免责声明: 我不是专业搞编译器的,整个过程的路线是能跑就行,要是有高手发现我写的有问题,评论区跟我说一下,我们一起探讨一下
原理
IDA识别到能一键F5实际上可以分为几个具体步骤(略有不同,IDA是微码, 不是IL):
-
PE识别,导入表,导出表,TLS 等等 -
基于(1),进行函数识别,标记哪些位置是函数 -
基于(2),提升为 LLIL 也就是 low level IL,这个主要负责将 汇编 转为自己的通用翻译语言。这代表上层不需要关系汇编的架构什么的(比如,X64,MIPS,等等 转IL后都不需要关心了) -
基于(3) 提升为MIDDLE LEVEL IL,而这个过程我们需要根据RSP/RBP 访问标记函数,解析符号,并且识别函数内容 -
基于(4) 做进一步优化,总而成为HIGHT LLIL。这一步后,就已经是IDA F5后的形状了
需要注意的是,这个过程并不是严格线性的。比如在LLIL提升过程中,可能会发现新的函数,这时需要回到第2步重新进行函数识别。整个过程是迭代式的,每一步都可能影响其他步骤。
我们以BN为例子:
-
bn的汇编模式:
你可以看到,在这个阶段,bn只是显示纯汇编(虽然可能有一些标注) -
BN的llil模式:
这个时候,原来的汇编已经变成了BN的一种中间码,这样BN就能很愉快的处理各种乱七八糟的平台的汇编,比如ARM,mips等等
3.MLIL模式:
这个模式下的BN,在LLIL的基础上已经标识了
函数调用/函数参数/符号及代码做了优化 -
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机制,则该函数不会生效。
如下就是一个经典的例子:
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去处理,除此之外,我们还需要从如下地方找:
-
暴力特征函数序言 -
异常处理表 -
TLS回调 -
虚表/class
除了直接的call/jmp指令识别,我们还需要处理以下情况:
// 典型的间接跳转模式
mov rax, [some_address]
call rax
跳转表
// 常见于switch-case实现
jmp qword ptr [rax*8 + jump_table]
这些情况需要进行数据流分析才能准确识别目标函数。
一个完整的代码如下:
function symbol
ida的symbol有几种:
-
导入表 -
导出表 -
lumina -
pdb -
ida sig
比如如下是基于导入表的符号识别:
而如下是基于lumina的识别
这是基于IDA sigs的函数识别
而我们先做简单一点,只识别导入/导出表
function stub
对于调试的程序你可以看到很多 function stub,这些都是指向真实函数,这方便调试,对于我们来说需要特殊处理这些.否则,背后跳转的函数我们会被忽略
而我们对其的处理可以粗暴一点,直接看是否是rip跳转
注意:
除了RIP相对寻址,function stub还有其他形式:
间接寻址
mov rax, gs:[60h] ; TEB
mov rax, [rax+...] ; 通过TEB间接寻址
jmp rax
导入表跳板
jmp cs:__imp_Function ; 通过导入表间接跳转
我们需要处理这些特殊情况,才能完整地识别所有函数。
效果
IDA寻找出了320个函数
我们寻找出了297个函数
而作为对比,BN是460
一个显而易见的事实是,各家算法不同,差异也就越大.比如我们是不看debug jmp table的,而bn和ida是看的。而BN和IDA通过模拟执行能发现我们目前无法发现的间接call,而通过crt sigs,能直接标记已知的函数.这些是我们暂时没有的
函数识别的难点与挑战
在基础函数识别之外,实际场景中还存在许多复杂情况需要处理:
编译器优化导致的函数识别困难
// 尾调用优化
void funcA() {
return funcB(); // 编译器会直接优化为 jmp funcB
}
// 内联优化
inline void funcC() {
// 函数体直接被插入调用处
}
反调试技术的影响
一些程序会使用特殊技术干扰函数识别:
// 花指令
push rax
jmp $+5
db 0xE8 // 假call指令
pop rax
// 真实代码继续...
虚函数表的处理
需要专门的算法来处理C++的虚函数表:
struct VTable {
void* func1;
void* func2;
};
class Base {
virtual void vfunc() = 0;
VTable* vtable;
};
改进的函数识别算法
我们可以通过以下方式提高函数识别的准确率:
-
启发式规则
检查函数序言模式(非仅prolog)
分析栈平衡情况
追踪寄存器使用模式 -
交叉引用 -
数据流分析
未完待续
下一章,我们将介绍如何做程序控制流识别,以及一些关于IDA控制流追踪为什么那么不好使的原因.当文章阅读过1000就马上更新!
另外帮鸭鸭解决了滞销的米塔帽的兄弟们,可以在本公众号的微信群询问鸭哥要关于本章的DEMO以及指导(如果对做IDA感兴趣的话).感谢各位兄弟们的支持!
原文始发于微信公众号(冲鸭安全):IDA背后的原理入门(一): 简介&函数识别
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论