🎨 颜色标记说明

  • 粉色:特别重要、主线必掌握、优先复盘的内容。
  • 蓝色:核心概念、术语、分类名。
  • 橙色:需要关注的边界、易错点、注意事项。
  • 绿色:解释类内容、帮助理解知识点。

从栈帧回溯调用关系

函数分析的核心不是只读当前函数,而是回答三个问题:

  1. 谁调用了它?
  2. 参数从哪里来?
  3. 返回后数据流向哪里?

在传统 x86 栈帧中,可以从当前 EBP 读取上一层栈帧和返回地址:

1
2
3
[EBP]     → 上一层 EBP
[EBP+4] → 返回地址
[EBP+8] → 第一个栈参数

沿着保存的 EBP 逐层查看,可以恢复一条基本调用链。

返回地址落在哪个模块、哪个函数附近,是判断调用来源的重要依据。

模块、地址与符号

调试器中的地址只有和模块基址、符号信息结合后才更有意义。

1
实际地址 = 模块基址 + RVA

例如某函数位于:

1
demo.exe + 0x1230

即使 ASLR 使模块每次加载到不同位置,RVA 通常仍能帮助定位同一段代码。

符号信息可能提供:

  • 函数名和源码文件。
  • 局部变量和参数名。
  • 源码行与机器指令的对应关系。
  • 类型、结构体和类成员信息。

发布版本经常缺少完整符号,因此需要保留“地址、模块、偏移”三件套,而不是只记录一次运行中的绝对地址。

动态调试基本流程

使用 x32dbg、x64dbg 或其他 DBG 工具时,可以按下面的顺序建立分析节奏:

  1. 确认目标程序架构。
  2. 查看模块列表和入口点。
  3. 在关键 API、字符串引用或可疑分支处设置断点。
  4. 单步进入目标函数,记录寄存器和栈变化。
  5. 在返回前确认返回值和被修改的内存。
  6. 回到调用点验证自己的函数原型推测。

常用执行方式的区别:

操作 用途
Step Into 进入 CALL 的目标函数
Step Over 执行 CALL,但停在调用后的下一条指令
Run Until Return 执行到当前函数返回
Run to User Code 从系统模块尽快回到目标模块

IDA 中的交叉引用

静态分析时,交叉引用可以把数据和控制流连接起来:

  • 函数的代码引用能帮助寻找调用者。
  • 字符串的数据引用能定位输入、错误提示和业务分支。
  • 全局变量引用能显示哪些函数读取或修改它。
  • 导入函数引用能快速找到文件、网络、注册表等功能入口。

推荐先从一个有语义的锚点出发:

1
字符串或导入 API → 交叉引用 → 所在函数 → 调用者 → 上层业务逻辑

参数传递

32 位常见规则

32 位代码中,参数经常通过栈传递,也可能使用寄存器:

调用约定 常见参数位置 栈清理
__cdecl 调用者
__stdcall 被调用者
__fastcall 常见为 ECX、EDX 后再入栈 通常被调用者
__thiscall ECX 常保存 this,其余参数入栈 依编译器规则

64 位 Windows 规则

前四个整数或指针参数通常使用:

1
RCX, RDX, R8, R9

更多参数放在栈中。调用者还要为前四个参数预留影子空间,并保证栈满足对齐要求。

浮点参数会使用对应的 XMM 寄存器,具体位置要结合函数原型和调用点判断。

返回值

常见返回位置包括:

  • x86 整数或指针:EAX
  • x86 较宽整数:可能使用 EDX:EAX
  • x64 整数或指针:RAX
  • 浮点值:常见于 XMM0,旧代码也可能使用 x87 FPU 栈。
  • 大型结构体:编译器可能加入隐藏的返回缓冲区指针。

返回寄存器只是线索。最终类型必须结合调用者如何使用该值来确认。

从反汇编还原函数

看到下面的代码:

1
2
3
4
mov eax, [ebp+8]
imul eax, [ebp+0Ch]
add eax, [ebp+10h]
ret

可以先写出一个候选原型:

1
2
3
4
int calc(int a, int b, int c)
{
return a * b + c;
}

但还要回到调用点验证:

  • 调用前是否确实准备了三个 4 字节参数。
  • 返回后的 EAX 是否按整数使用。
  • 函数是否还读取全局变量或修改外部内存。
  • 编译器优化是否合并或重排了表达式。

反编译结果是帮助阅读的假设,不是源代码的精确复原。

静态与动态分析配合

比较稳定的工作方式是:

1
2
3
4
5
静态分析提出假设

动态调试观察真实参数和分支

回到静态视图修正函数名、类型和注释

静态分析适合看全局结构,动态调试适合验证某一次执行。两者来回切换,比单独依赖某一种工具更可靠。

栈异常与控制流风险

返回地址保存在栈中,所以越界写入、错误的栈平衡或异常的栈指针都可能改变函数返回路径。

从防御分析角度,遇到以下现象应重点检查:

  • RET 前的栈顶不在正常调用链中。
  • 返回地址不属于可执行模块的代码区。
  • 函数尾声没有恢复预期的 ESP/RSP
  • 局部缓冲区附近出现越界写入。

这一部分的重点是理解崩溃与异常控制流的成因,并通过边界检查、编译器保护和内存权限降低风险。

线程栈边界

Windows 会在线程环境块中维护与线程栈相关的信息,例如栈顶边界和栈底边界。调试器通常可以直接展示这些数据。

查看栈地址时可以判断:

  • 当前 ESP/RSP 是否落在线程栈范围内。
  • 某个地址是栈变量、堆对象还是模块数据。
  • 栈空间是否接近保护页或发生异常增长。

不同 Windows 版本和架构下内部结构存在差异,实际分析时优先使用调试器符号和官方结构定义,不依赖固定偏移。

一次完整的函数分析记录

建议每次练习都按固定模板记录:

1
2
3
4
5
6
7
8
9
10
函数地址:模块 + RVA
调用者:
调用约定:
候选函数原型:
参数来源:
返回值位置:
读取的数据:
修改的数据:
关键分支:
动态验证结果:

这样做的价值在于把零散指令整理成可复查的结论,也方便后续继续给函数和变量命名。

学习检查清单

检查项 状态
能根据栈帧和返回地址回溯调用者 待复盘
能使用模块基址与 RVA 记录地址 待复盘
能通过字符串、API 和交叉引用定位函数 待复盘
能区分常见 x86 调用约定 待复盘
能说明 Windows x64 前四个整数参数的位置 待复盘
能根据调用点验证候选函数原型 待复盘
能结合静态分析与动态调试修正结论 待复盘
能识别明显的栈失衡和异常返回地址 待复盘