文章总结: 本文讲解了IDA计算函数大小的原理。指出直接查找首个ret指令不准确,因为编译器优化会产生多个ret。正确方法是进行控制流分析,追踪所有跳转路径,记录合法范围内的所有ret位置,取最大地址作为函数结束点。通过对比IDA结果验证了该启发式算法的有效性,并说明了地址合法性校验的重要性。 综合评分: 86 文章分类: 逆向分析,二进制安全,安全工具
IDA背后的原理入门(二): 函数大小计算
原创
冲鸭安全
冲鸭安全
2025年2月6日 10:02 北京
简介
我们上一章已经成功的得到了函数的列表:
IDA背后的原理入门(一): 简介&函数识别
现在,我们遇到了一个麻烦: 函数大小计算
可能有一些逆向人认为,直接查找ret即可完成任务。知道了函数开头,直接查找第一次出现的ret就是函数的大小。 但是事实并非如此,并且函数大小计算是一个充满启发性的计算。不是想象中那么容易能算出来的。让我们一步一步的说明为什么
存在的问题
如果我们直接看函数的ret会怎么样?看第一次出现的ret,就能非常轻松的确定是哪个。 如果实际这样做过,你就会发现,大小完全的不准。这是因为各个编译器编译参数不一样,不一定是以ret为函数结尾
比如这个函数:
int test_function(int x) {
volatile int a = x;
if (a > 0) {
for (int i = 0; i < a; i++) {
if (i * i > a) {
return a + i;
}
}
return a + 1;
}
return a - 1;
}
在clang的情况下,是这样:
test_function(int):
push rbp
mov rbp, rsp
mov dword ptr [rbp - 8], edi
mov eax, dword ptr [rbp - 8]
mov dword ptr [rbp - 12], eax
mov eax, dword ptr [rbp - 12]
cmp eax, 0
jle .LBB0_8
mov dword ptr [rbp - 16], 0
.LBB0_2:
mov eax, dword ptr [rbp - 16]
mov ecx, dword ptr [rbp - 12]
cmp eax, ecx
jge .LBB0_7
mov eax, dword ptr [rbp - 16]
imul eax, dword ptr [rbp - 16]
mov ecx, dword ptr [rbp - 12]
cmp eax, ecx
jle .LBB0_5
mov eax, dword ptr [rbp - 12]
add eax, dword ptr [rbp - 16]
mov dword ptr [rbp - 4], eax
jmp .LBB0_9
.LBB0_5:
jmp .LBB0_6
.LBB0_6:
mov eax, dword ptr [rbp - 16]
add eax, 1
mov dword ptr [rbp - 16], eax
jmp .LBB0_2
.LBB0_7:
mov eax, dword ptr [rbp - 12]
add eax, 1
mov dword ptr [rbp - 4], eax
jmp .LBB0_9
.LBB0_8:
mov eax, dword ptr [rbp - 12]
sub eax, 1
mov dword ptr [rbp - 4], eax
.LBB0_9:
mov eax, dword ptr [rbp - 4]
pop rbp
ret
开了-o2优化后,会变成这样
test_function(int):
mov dword ptr [rsp - 4], edi
cmp dword ptr [rsp - 4], 0
mov eax, dword ptr [rsp - 4]
jle .LBB0_7
test eax, eax
jle .LBB0_6
xor eax, eax
.LBB0_3:
mov ecx, eax
imul ecx, eax
cmp ecx, dword ptr [rsp - 4]
jg .LBB0_4
inc eax
cmp eax, dword ptr [rsp - 4]
jl .LBB0_3
.LBB0_6:
mov eax, dword ptr [rsp - 4]
inc eax
ret
.LBB0_7:
dec eax
ret
.LBB0_4:
add eax, dword ptr [rsp - 4]
ret
非常明显,我们多了几个RET,这是因为: 不开优化(-O0)时:
编译器会按照代码的字面顺序直接翻译 所有返回语句通常会跳转到函数末尾的一个公共返回点 这样做便于调试,因为执行路径更直观
开启优化(-O2)时:
编译器会尝试优化执行路径,减少指令数量 如果发现直接返回比跳转到公共返回点更高效,就会生成多个ret 这样可以省去额外的跳转指令
所以,直接找ret不可取,并且导致了一个麻烦的结论
所有软件对函数大小的计算,都是启发性的,并不能精准识别.
我们需要一个更加聪明的办法.
聪明的办法
要准确计算函数大小,我们需要分析函数的控制流。主要思路是:追踪所有可能的执行路径,直到找到所有可能的结束点。
具体来说,我们需要:
- 追踪所有的跳转指令(jmp, jz, jnz等)
- 分析条件分支创造的多个执行路径
- 找到每个路径的终点(ret指令)
- 取所有终点中地址最大的那个作为函数结束位置
这将会尽可能的找到我们需要的函数方向.
具体实现
基本的实现流程如下:
function FindFunctionEnd(startAddress):
1. 反汇编当前地址的指令
2. 如果是返回指令,记录当前位置+指令长度
3. 如果是跳转指令:
- 验证跳转目标的合法性
- 递归分析跳转目标
- 继续分析当前路径
4. 返回找到的最远结束地址
为了避免重复分析和无限递归,我们需要:
- 使用哈希表记录已分析过的地址
- 检查跳转目标是否在合理范围内
- 防止向低地址的非法跳转
关键点处理
跳转指令分析:
if (isJump(instruction)) {
// 获取跳转目标
targetAddress = getJumpTarget(instruction);
// 验证目标地址
if (isValidTarget(targetAddress)) {
// 递归分析新路径
endAddr = max(endAddr, FindFunctionEnd(targetAddress));
}
}
这样会追踪所有可能的执行路径
示例代码:
if (x > 0) {
return 1;
} else {
return 2;
}
汇编代码:
cmp eax, 0
jle else_branch // 条件跳转,创建两条路径
mov eax, 1
ret // 路径1的结束点
else_branch:
mov eax, 2
ret // 路径2的结束点
如果不分析跳转,就会漏掉else分支的ret,导致函数大小计算错误
另外这个不允许向上跳转,向上跳转则认为这个跳转没意义,我们假设代码是从下到上的.因此还需要地址合法性检查
地址合法性检查代码:
bool isValidTarget(targetAddress) {
// 不允许向低地址跳转
if (targetAddress < functionStart)
return false;
// 不允许跳出代码段
if (targetAddress > codeSegmentEnd)
return false;
// 避免重复分析
if (alreadyAnalyzed(targetAddress))
return false;
return true;
}
地址合法性检查存在的意义是,防止向下跳转导致的误判,如下所示:
function_A:
...
function_B:
jmp function_A // 如果允许向下跳转,可能误判为function_B的一部分
以及避免跨段访问
.text:
function_start:
jmp data_section // 不允许跳转到数据段
.data:
data_section:
db "Hello"
这样,我们终于能安心的寻找最后一个RET了:
测试
上面的代码,IDA里面大小是0x50:
而以上的实现,也是0x50:
如果直接看单独ret的存在,我们是没办法这样匹配的
未完待续
下一章,我们将介绍如何做程序控制流访问控制.当文章阅读过1000就马上更新!
另外加入了鸭鸭粉丝俱乐部的各位,可以在本公众号的微信群询问鸭哥要关于本章的DEMO以及指导(如果对做IDA感兴趣的话).感谢各位兄弟们的支持!
如果没有加入,速速私聊加入.
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:冲鸭安全 冲鸭安全《IDA背后的原理入门(二): 函数大小计算》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论