应用安全 --- 逆向技巧 之 IDA未知函数如何识别
什么是未知函数
在逆向中有一类函数没有名字,但是客观存在,ida会给这类函数重命名一个函数名称,sub_xxxx,如何准确识别此类函数至关重要。
如何快速识别函数
sub_xxxx 函数识别操作方法
一、先看调用了什么函数(最快)
text
看到这些调用组合 -> 直接判定
malloc + realloc + free + std::terminate
-> 动态容器扩容函数
__cxa_allocate_exception + __cxa_throw + std::logic_error
-> STL异常包装函数
__cxa_begin_catch + __cxa_end_catch
-> 异常捕获处理函数
__dynamic_cast + strcmp
-> RTTI类型匹配函数
pthread_mutex_lock + pthread_mutex_unlock
-> 线程安全操作函数
_ReadStatusReg(TPIDR_EL0)
-> ARM64运行时/TLS相关函数
memcpy + memset + operator new + operator delete
-> 对象构造/复制函数
fprintf + fflush + abort
-> 错误诊断/断言函数
二、看魔数(第二快)
text
函数体内出现这些数值 -> 直接判定
22 或 0x16
-> std::string SSO操作
4 或 0x4(与字符串操作同时出现)
-> std::wstring SSO操作
0xFFFFFFFFFFFFFFF0
-> 16字节内存对齐操作
0x1000
-> 页式Arena分配器
0xFF0 / 0xFFF0 / 4032 / 4048
-> Arena页剩余空间阈值检查
0x434C4E47432B2B
-> libcxxabi异常魔数 "GNUC++"
0x7FFFFFFFFFFFFFE6 / 0x3FFFFFFFFFFFFFF0
-> std::string/wstring最大长度检查
23 或 0x17
-> std::string容量分配边界
三、看参数结构(第三快)
text
参数是 _QWORD* 且函数内访问 [0][1][2]
-> 三元组结构体 大概率是容器或string对象
[0]=数据指针/flags
[1]=size/end指针
[2]=capacity指针
参数是 char** 且函数内访问 [0][1][2]
-> 输出缓冲区结构
[0]=缓冲区基址
[1]=当前写入位置
[2]=总容量
-> 判定为某种emit/print/序列化函数
参数是 __int64 且访问 +4912 偏移
-> C++符号解码器的parser状态
-> 判定为demangler内部函数
参数只有一个 __int64 且访问大量固定正偏移
如 +16 +24 +32 +40 +48
-> AST节点操作函数
四、看字符串常量(直接定性)
text
出现这些字符串 -> 直接命名
"libunwind: %s - %s\n" -> libunwind诊断函数
"getRegister" -> 寄存器读取函数
"unsupported arm64 register" -> ARM64寄存器错误处理
"stoi" / "stol" / "stoul" -> STL字符串转整数函数
": out of range" -> STL越界异常构建
": no conversion" -> STL无效参数异常构建
"operator " -> C++符号解码operator节点
"decltype(" -> C++符号解码decltype节点
"template<" -> C++符号解码模板节点
" [enable_if:" -> C++符号解码SFINAE节点
"enum" "struct" "union" -> elaborated type解析
"allocator<T>::allocate" -> 分配器容量超限错误
"unwind_phase2" -> libunwind第二阶段展开
五、看控制流形状(快速分类)
text
函数体只有一条jmp
-> thunk/桩函数 j_前缀 不用分析
函数体只有ret或xor返回0
-> nullsub空函数 跳过
有两层嵌套if且都检查同一个指针的不同位
-> SSO string判断 std::string或std::wstring操作
有 offset - 阈值 <= 大负数 的无符号比较
后跟 malloc(0x1000)
-> Arena分配器扩容逻辑
有 v >> 3 和 v >> 2 同时出现
-> vector/容器的按元素大小计算指针
8字节元素容器
switch case -2,-1,0,29,30,31,32,34
-> ARM64寄存器编号映射函数
while循环体内有 & 0x80000000 检查
-> libunwind栈帧遍历函数
六、看vtable调用偏移(C++虚函数定位)
text
*(vtable + 0) -> 析构函数
*(vtable + 8) -> 析构函数(带delete)
*(vtable + 16) -> 第一个虚函数
*(vtable + 32) -> 常见print前置/emit函数
*(vtable + 40) -> 常见print后置/emit函数
*(vtable + 56) -> search_above_dst(RTTI搜索)
节点偏移规律:
node + 8 -> 节点类型标志(4字节int)
node + 16 -> 第一个子节点或字符串指针
node + 24 -> 第二个子节点
node + 32 -> 第三个子节点
-> 判定为AST节点操作函数
七、FLIRT签名匹配操作
text
IDA操作步骤:
1. File -> Load File -> FLIRT Signature File
2. 选择对应平台的 .sig 文件
iOS/Android ARM64 -> 用 ios_arm64.sig 或 android_ndk.sig
3. 自动识别标准库函数
手动生成签名:
1. 找到已知的.a静态库
2. pelf -> sigmake -> 生成.sig
3. 加载到IDA
常用签名库来源:
- IDA自带 sig目录
- https://github.com/push0ebp/sig-database
- https://github.com/Maktm/FLIRTDB
八、Bindiff/Diaphora 交叉比对
text
操作步骤:
1. 找到开源版本的同名库(libc++/libunwind/libc++abi)
2. 编译相同架构版本
3. 用Bindiff或Diaphora对比两个idb
4. 相似度>0.9的函数直接采用开源名称
适用场景:
- 目标二进制静态链接了开源库
- libc++ libunwind libc++abi libstdc++
- OpenSSL mbedTLS等常见第三方库
九、优先级顺序总结
text
拿到一个 sub_xxxx 按以下顺序操作:
第1步 看有没有字符串常量 5秒内得出结论
第2步 看调用了哪些已知API 10秒内得出结论
第3步 看参数个数和类型结构 15秒内得出结论
第4步 看函数内的魔数 20秒内得出结论
第5步 看控制流形状和switch结构 30秒内得出结论
第6步 看vtable偏移和调用模式 1分钟内得出结论
第7步 FLIRT签名自动匹配 批量处理
第8步 Bindiff交叉比对 深度分析时使用
能在前3步确认的不要拖到后面
多个维度同时印证才能提高准确率
单一维度匹配要保持怀疑
免责声明
本文档所有内容仅供安全研究、学术交流与技术学习使用,严禁用于任何未经授权的逆向破解、网络攻击、隐私窃取、恶意软件开发及其他违反《中华人民共和国网络安全法》《数据安全法》等法律法规的行为,使用者应确保已获得目标软件权利人的合法授权并自行承担因使用本文档内容所产生的一切法律责任与后果,作者不对任何直接或间接损害承担任何责任,继续阅读即视为您已知悉并同意上述全部条款。
浙公网安备 33010602011771号