简介
我们上一章已经成功的得到了函数的列表:
现在,我们遇到了一个麻烦: 函数大小计算
可能有一些逆向人认为,直接查找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背后的原理入门(二): 函数大小计算
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论