IDA背后的原理入门(二): 函数大小计算

admin 2025年2月8日00:58:34评论3 views字数 3540阅读11分48秒阅读模式

简介

我们上一章已经成功的得到了函数的列表:

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的情况下,是这样:
IDA背后的原理入门(二): 函数大小计算

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优化后,会变成这样
IDA背后的原理入门(二): 函数大小计算

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不可取,并且导致了一个麻烦的结论

所有软件对函数大小的计算,都是启发性的,并不能精准识别.

我们需要一个更加聪明的办法.

聪明的办法

要准确计算函数大小,我们需要分析函数的控制流。主要思路是:追踪所有可能的执行路径,直到找到所有可能的结束点。

具体来说,我们需要:

  1. 追踪所有的跳转指令(jmp, jz, jnz等)
  2. 分析条件分支创造的多个执行路径
  3. 找到每个路径的终点(ret指令)
  4. 取所有终点中地址最大的那个作为函数结束位置

这将会尽可能的找到我们需要的函数方向.

具体实现

基本的实现流程如下:

function FindFunctionEnd(startAddress):
    1. 反汇编当前地址的指令
    2. 如果是返回指令,记录当前位置+指令长度
    3. 如果是跳转指令:
       - 验证跳转目标的合法性
       - 递归分析跳转目标
       - 继续分析当前路径
    4. 返回找到的最远结束地址

为了避免重复分析和无限递归,我们需要:

  1. 使用哈希表记录已分析过的地址
  2. 检查跳转目标是否在合理范围内
  3. 防止向低地址的非法跳转

关键点处理

跳转指令分析:

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背后的原理入门(二): 函数大小计算

测试

上面的代码,IDA里面大小是0x50:
IDA背后的原理入门(二): 函数大小计算
而以上的实现,也是0x50:
IDA背后的原理入门(二): 函数大小计算
如果直接看单独ret的存在,我们是没办法这样匹配的

未完待续

下一章,我们将介绍如何做程序控制流访问控制.当文章阅读过1000就马上更新!

另外加入了鸭鸭粉丝俱乐部的各位,可以在本公众号的微信群询问鸭哥要关于本章的DEMO以及指导(如果对做IDA感兴趣的话).感谢各位兄弟们的支持!

如果没有加入,速速私聊加入.

原文始发于微信公众号(冲鸭安全):IDA背后的原理入门(二): 函数大小计算

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月8日00:58:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   IDA背后的原理入门(二): 函数大小计算https://cn-sec.com/archives/3704102.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息