IDA原理入门(四): 函数参数识别

admin 2025年5月19日15:01:50评论3 views字数 4908阅读16分21秒阅读模式

简介

没看过的请看之前的第三篇。
本篇是第四篇,讲述怎么计算函数参数

IDA原理入门(三): 控制流追踪与CFG Blocks构建

函数参数识别

我们必须要知道一个事实是, 在没PDB之前,是没有一个准确的函数识别办法的,现如今所有的办法都是启发式的办法,这也是为什么逆向工具在遇到混淆的时候会拉闸。我们这边不介绍复杂的启发算法,只说一个简单的: 堆栈遍历+模式匹配 (只说X64的)

Windows x64调用约定

在讨论参数识别之前,我们需要了解Windows x64调用约定:

前4个参数通过寄存器传递:RCX, RDX, R8, R9
超过4个的参数通过栈传递,从RSP+0x20开始
即使参数少于4个,编译器也会在栈上为它们预留空间(称为”shadow space”)
以下是参数在栈上的布局:

RSP+0x08: 第1个参数的影子空间 (RCX)
RSP+0x10: 第2个参数的影子空间 (RDX)
RSP+0x18: 第3个参数的影子空间 (R8)
RSP+0x20: 第4个参数的影子空间 (R9)
RSP+0x28: 第5个参数(第一个栈参数)
RSP+0x30: 第6个参数(第二个栈参数)
以此类推…

实际案例分析:8参数函数

让我们通过一个具体例子来理解参数识别过程。以下是一个接受8个整型参数的函数的C代码和反汇编:

int __fastcall sub_140012510(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  j___CheckForDebuggerJustMyCode(byte_140023029);
  return j_printf("wtf: %d n", (unsigned int)(a8 + a7 + a6 + a5 + a4 + a3 + a2 + a1));
}

关键汇编指令:

.text:0000000140012510 44 89 4C 24 20          mov     [rsp-8+arg_18], r9d     ; 保存第4个参数(a4)到影子空间
.text:0000000140012515 44 89 44 24 18          mov     [rsp-8+arg_10], r8d     ; 保存第3个参数(a3)到影子空间
.text:000000014001251A 89 54 24 10             mov     [rsp-8+arg_8], edx      ; 保存第2个参数(a2)到影子空间
.text:000000014001251E 89 4C 24 08             mov     [rsp-8+arg_0], ecx      ; 保存第1个参数(a1)到影子空间
.text:0000000140012522 55                      push    rbp                     ; 函数序言开始
.text:0000000140012523 57                      push    rdi
.text:0000000140012524 48 81 EC E8 00 00 00    sub     rsp, 0E8h               ; 分配栈空间
.text:000000014001252B 48 8D 6C 24 20          lea     rbp, [rsp+20h]          ; 设置帧指针
...
.text:000000014001253C 8B 85 E8 00 00 00       mov     eax, [rbp+0D0h+arg_8]   ; 访问a2
.text:0000000140012542 8B 8D E0 00 00 00       mov     ecx, [rbp+0D0h+arg_0]   ; 访问a1
.text:0000000140012548 03 C8                   add     ecx, eax                ; a1 + a2
.text:000000014001254A 8B C1                   mov     eax, ecx
.text:000000014001254C 03 85 F0 00 00 00       add     eax, [rbp+0D0h+arg_10]  ; + a3
.text:0000000140012552 03 85 F8 00 00 00       add     eax, [rbp+0D0h+arg_18]  ; + a4
.text:0000000140012558 03 85 00 01 00 00       add     eax, [rbp+0D0h+arg_20]  ; + a5
.text:000000014001255E 03 85 08 01 00 00       add     eax, [rbp+0D0h+arg_28]  ; + a6
.text:0000000140012564 03 85 10 01 00 00       add     eax, [rbp+0D0h+arg_30]  ; + a7
.text:000000014001256A 03 85 18 01 00 00       add     eax, [rbp+0D0h+arg_38]  ; + a8

“启发式”参数识别过程

原理

简单来说我给每个寄存器访问的偏移搜集了一次,然后做置信度,一旦置信度超过X我们就认为是一个参数了.

分析函数序言

函数序言是识别栈帧结构的关键.通过跟踪PUSH RBP和SUB RSP, xxx等指令,我们可以计算栈调整大小并确定序言何时结束。

if (llil_ins->type == LLIL::LLILInstruction::ARITHMETIC &&
    llil_ins->op == LLIL::LLILInstruction::PUSH) {
    func->stackAdjustment += 8;
    if (!llil_ins->operands.empty() &&
        llil_ins->operands[0]->type == LLIL::LLILOperand::REGISTER &&
        llil_ins->operands[0]->value == "rbp") {
        func->foundPushRbp = true;
    }
} else if (llil_ins->type == LLIL::LLILInstruction::ARITHMETIC &&
           llil_ins->op == LLIL::LLILInstruction::SUB &&
           llil_ins->operands.size() >= 2 &&
           llil_ins->operands[0]->type == LLIL::LLILOperand::REGISTER &&
           llil_ins->operands[0]->value == "rsp" &&
           llil_ins->operands[1]->type == LLIL::LLILOperand::IMMEDIATE) {
    func->stackAdjustment += llil_ins->operands[1]->immediate;
    func->inPrologue = false;
}

匹配全部栈读写的操作

首先,我们遍历函数的所有指令,收集对栈的读写操作:
我们跟踪每个内存操作,区分读取和写入,并记录指令类型和位置。尤其重要的是,我们识别出是否在函数序言(prologue)之前发生了访问,这通常表明是在访问参数。

// 记录栈访问
auto processMemOperand = [&](const LLIL::LLILOperand op, bool isDest) {
    if (op.type != LLIL::LLILOperand::MEMORY) return;

    int64_t offset;
    if (op.base == "rsp") {
        offset = op.offset;
    } else if (op.base == "rbp") {
        if (func->inPrologue) {
            offset = func->rbpRspOffset + op.offset;
        } else {
            offset = op.offset;  // 直接使用相对rbp的偏移
        }
    } else {
        return;
    }
    StackAccess access;
    access.isRead = !isDest;
    access.isLea = llil_ins->type == LLIL::LLILInstruction::LEA;
    access.insIndex = insIndex;
    access.isBeforePrologue = func->inPrologue;
    access.ins = llil_ins.get();
    stackAccesses[offset].push_back(access);
};

以我们的汇编为例,我们搜集到了12处栈访问的信息:
IDA原理入门(四): 函数参数识别
实际上IDA识别出来是有8个:
IDA原理入门(四): 函数参数识别
仔细看我们前四个的搜集的栈访问参数位置:
IDA原理入门(四): 函数参数识别
其实都是因为X64的传参造成的栈访问,这前四个很快就会被接下里的可信度计算过滤了:
IDA原理入门(四): 函数参数识别
观察剩下的0xe0的栈访问
IDA原理入门(四): 函数参数识别
实际上他就是一个”参数”
IDA原理入门(四): 函数参数识别

参数识别和置信度计算

收集完栈访问信息后,我们分析每个偏移处的访问模式:

for (const auto& [offset, accesses] : stackAccesses) {
    if (offset < 0x8 || (offset % 8) != 0) continue;
    if (handledOffsets.find(offset) != handledOffsets.end()) continue;

    bool hasRealAccess = false;
    bool onlyLea = true;
    bool usedBeforePrologue = false;
    uint64_t firstAccessAddr = 0;

    for (const auto& access : accesses) {
        if (!access.isLea) {
            hasRealAccess = true;
            onlyLea = false;
        }
        if (access.isBeforePrologue) {
            usedBeforePrologue = true;
        }
        if (firstAccessAddr == 0) {
            firstAccessAddr = access.ins->address;
        }
    }

我们根据以下几点计算参数的置信度:

  1. 是否在序言之前被访问(增加置信度)
  2. 是否只有LEA指令访问(可能只是局部变量)
  3. 对于栈参数,是否有写操作(参数通常是只读的)
auto info = std::make_shared<ParamInfo>();
info->offset = offset;
info->confidence = 0.6f;  // 基础置信度
info->insMemAddress = firstAccessAddr;
if (usedBeforePrologue) info->confidence += 0.1f;

// 对于栈参数,检查是否有写操作
if (offset >= 0x28) {  // 栈参数
    bool hasWrite = false;
    for (const auto& access : accesses) {
        if (!access.isRead && !access.isLea) {
            hasWrite = true;
            break;
        }
    }
    // 如果没有写操作,增加置信度
    if (!hasWrite) info->confidence += 0.1f;
}

这确保了参数按照预期的顺序排列:先是寄存器参数(RCX, RDX, R8, R9),然后是栈参数。
一个简单的函数识别就OK了,更复杂一点的,我们可以看

  1. call这个函数之前的模式匹配,结合函数内的匹配会很精准
  2. 在(1)的基础上,多次匹配其他的call的地方.

最终结果

那么,我们来看看我们的最终结果吧:
最后,我们看到,函数参数有8个,跟IDA的结果是一样的:
IDA原理入门(四): 函数参数识别
我们也正确的显示了参数列表:

IDA原理入门(四): 函数参数识别
IDA原理入门(四): 函数参数识别

后续问题

如您所见,这并不是最优解,比如在某些情况下,会多一个参数或者少一个参数
IDA原理入门(四): 函数参数识别
这并不是最优解,最优解是我们还需要根据函数的调用方去分析一次参数,然后两者比对.这个我们会在后面会说

原文始发于微信公众号(冲鸭安全):IDA原理入门(四): 函数参数识别

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

发表评论

匿名网友 填写信息