第06节-从逆向视角看程序结构
第 6 节:从逆向视角看程序结构
所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 3 周
课程主题:调试器和逆向入门
本节目标:理解源代码视角和二进制视角的区别,掌握符号表、strip、反汇编、反编译、字符串、交叉引用、函数调用图等逆向分析基础概念。
1. 本节课你要学会什么
学完这一节,你应该能回答下面几个问题:
- 源代码视角和二进制视角有什么区别?
- 编译后,函数名、变量名、类型信息还在不在?
- 符号表是什么?
strip为什么会让逆向变难?- 反汇编和反编译有什么区别?
- Ghidra、IDA、x64dbg 各自更适合做什么?
- 字符串为什么是逆向分析的重要入口?
- 交叉引用是什么?
- 函数调用图能帮助我们看什么?
- 为什么逆向时不能完全相信反编译伪代码?
本节课的主线是:
1 | |
这节课会把前面学过的编译、汇编、调试器连接到真正的逆向分析流程中。
2. 什么是逆向工程
逆向工程可以先简单理解为:
在没有完整源码的情况下,通过分析二进制文件、运行行为和系统交互,推断程序的结构、逻辑和目的。
在软件安全中,逆向工程常用于:
- 漏洞分析
- 恶意代码分析
- 补丁对比
- 协议分析
- 加密逻辑分析
- CTF Reverse 题目
- 软件兼容性研究
- 安全审计
逆向工程不是“猜谜游戏”。
它依赖大量证据:
- 文件格式
- 汇编代码
- 反编译伪代码
- 字符串
- 导入 API
- 函数调用关系
- 调试器运行结果
- 文件/网络/进程行为
你的目标不是把每条指令翻译成 C,而是逐步回答:
1 | |
3. 源代码视角和二进制视角
写 C 程序时,你看到的是源代码视角。
例如:
1 | |
你能直接看到:
- 函数名
- 变量名
- 类型
if结构- 字符串
- 输入输出逻辑
但编译成二进制后,你看到的可能是:
1 | |
在二进制视角中,很多高级信息会消失或变得不明显。
4. 编译后哪些信息可能还在
编译后,二进制文件中可能还保留一些信息。
例如:
- 机器指令
- 字符串常量
- 导入函数名
- 导出函数名
- 动态库依赖
- 部分符号信息
- 调试信息
- 文件格式元数据
例如字符串:
1 | |
通常可能保留在 .rodata 或 .rdata 中。
导入 API 也可能保留,例如:
1 | |
这些信息是逆向分析的重要线索。
5. 编译后哪些信息可能消失
编译后,很多源代码级信息可能消失。
例如:
- 局部变量名
- 原始注释
- 宏定义原貌
- 高级控制结构
- 部分函数名
- 类型信息
- 源代码文件结构
- 代码格式
如果没有调试符号,你可能看不到:
1 | |
而只能看到:
1 | |
变量也可能变成:
1 | |
所以逆向不是恢复“原始源码”,而是恢复“程序逻辑”。
6. 符号是什么
符号可以理解为:
程序中函数、全局变量等对象的名字和地址之间的对应关系。
例如:
1 | |
这些名字能帮助调试器和逆向工具显示更友好的信息。
如果二进制保留符号,你可能看到:
1 | |
如果符号被去掉,你可能只看到:
1 | |
或者工具给它自动命名:
1 | |
7. 符号表是什么
符号表就是保存符号信息的数据结构。
在 ELF 文件中,常见符号相关节包括:
1 | |
简化理解:
| 名称 | 作用 |
|---|---|
.symtab |
普通符号表,可能包含较多函数和变量名 |
.dynsym |
动态符号表,动态链接需要用到 |
.strtab |
普通字符串表,保存符号名字符串 |
.dynstr |
动态字符串表,保存动态符号名等 |
即使普通符号被去掉,动态链接程序通常仍会保留动态符号。
因为动态链接器运行时需要知道导入导出符号。
8. 用 nm 查看符号
Linux 下可以用 nm 查看符号。
准备程序 symbol_demo.c:
1 | |
编译:
1 | |
查看符号:
1 | |
你可能看到:
1 | |
其中:
| 标记 | 含义 |
|---|---|
| T | 代码段中的符号,通常是函数 |
| D | 已初始化数据段中的符号 |
| B | BSS 中的符号 |
| U | 未定义符号,需要从外部库解析 |
9. strip 是什么
strip 用于移除二进制文件中的符号和调试信息。
例如:
1 | |
再执行:
1 | |
你可能看到:
1 | |
这说明普通符号被移除了。
再用 objdump 看:
1 | |
你会发现:
- 未 strip 的版本函数名更清晰
- strip 后很多函数名消失
- 工具只能根据地址或自动分析结果命名函数
10. strip 后程序还能运行吗
可以。
因为普通符号和调试信息主要是给人、调试器、分析工具看的。
CPU 执行程序并不需要这些名字。
CPU 只需要:
- 机器指令
- 地址
- 入口点
- 必要的动态链接信息
所以:
1 | |
这就是为什么真实商业软件、恶意代码、CTF 题目经常没有完整符号。
11. 调试信息是什么
调试信息比普通符号更丰富。
它可能包含:
- 源文件路径
- 行号
- 局部变量名
- 变量类型
- 函数参数
- 结构体定义
编译时加 -g:
1 | |
会把调试信息写入可执行文件。
有调试信息时,GDB 可以:
1 | |
并显示源码级信息。
去掉调试信息后,GDB 仍能调试,但更多只能靠:
- 汇编
- 地址
- 寄存器
- 内存
12. 反汇编是什么
反汇编是:
把机器码字节转换成汇编指令。
例如机器码:
1 | |
反汇编后:
1 | |
常用反汇编工具:
- objdump
- GDB
- IDA
- Ghidra
- x64dbg
反汇编结果比较接近 CPU 实际执行的内容。
它比反编译伪代码更底层,也更可靠。
但缺点是:
- 可读性差
- 需要理解寄存器和指令
- 高级结构不明显
13. 反编译是什么
反编译是:
尝试把机器码或汇编还原成类似 C 语言的伪代码。
例如汇编:
1 | |
反编译工具可能显示:
1 | |
常见反编译工具:
- Ghidra Decompiler
- IDA Hex-Rays
- Binary Ninja
反编译优点:
- 可读性强
- 更接近源代码
- 适合快速理解逻辑
缺点:
- 可能不准确
- 变量名是工具猜的
- 类型可能错
- 控制流可能被误识别
- 优化后的代码可能很难还原
- 混淆代码可能让伪代码非常混乱
所以要记住:
反编译伪代码是工具推测结果,不是原始源码。
遇到关键逻辑时,要回到汇编验证。
14. 反汇编和反编译的区别
| 对比项 | 反汇编 | 反编译 |
|---|---|---|
| 输入 | 机器码 | 机器码/汇编 |
| 输出 | 汇编指令 | 类 C 伪代码 |
| 可靠性 | 更接近真实执行 | 可能有误判 |
| 可读性 | 较差 | 较好 |
| 适合场景 | 精确分析、漏洞、控制流 | 快速理解程序逻辑 |
| 工具 | objdump、GDB、IDA、x64dbg | Ghidra、IDA Hex-Rays |
推荐方法:
1 | |
15. 字符串为什么重要
字符串是逆向分析中最常用的入口之一。
因为程序中很多行为会留下字符串线索。
例如:
- 菜单文本
- 错误信息
- 成功/失败提示
- 文件路径
- URL
- IP 地址
- 注册表路径
- 命令行参数
- 加密前后的明文
- API 名称
例如看到字符串:
1 | |
你可以推测程序可能有:
- 密码校验逻辑
- 文件写入行为
- 网络通信行为
- 注册表持久化行为
字符串往往能帮助你快速定位关键函数。
16. 用 strings 查看字符串
Linux 下可以用:
1 | |
例如:
1 | |
也可以结合 grep:
1 | |
注意:
strings 只能看到明文字符串。
如果字符串被加密、压缩或运行时生成,strings 可能看不到。
这时需要:
- 动态调试
- 内存搜索
- 找解密函数
- 跟踪字符串使用位置
17. 交叉引用是什么
交叉引用英文常写作:
1 | |
它表示:
某个函数、变量或字符串被哪里引用了。
例如程序中有字符串:
1 | |
你想知道哪里使用了它。
在 Ghidra 或 IDA 中,可以对这个字符串查看 XREF。
工具会告诉你:
1 | |
那么 FUN_00401180 很可能和密码校验或错误处理有关。
交叉引用非常重要,因为它能从线索跳到代码。
常见分析流程:
1 | |
18. 函数调用图是什么
函数调用图展示函数之间的调用关系。
例如:
1 | |
它能帮助你理解程序结构。
在复杂程序中,函数很多,单看汇编容易迷路。
调用图可以帮你判断:
- 哪个函数是入口逻辑
- 哪些函数是初始化
- 哪些函数处理输入
- 哪些函数调用敏感 API
- 哪些函数可能是加密/解密逻辑
- 哪些函数可能是网络通信逻辑
Ghidra、IDA、Binary Ninja 都能显示不同形式的函数图或调用图。
19. 控制流图是什么
控制流图通常展示一个函数内部的分支结构。
例如一个函数中有:
1 | |
反汇编中会表现为多个基本块和跳转。
控制流图能帮助你看:
- 条件判断
- 循环
- 错误分支
- 成功分支
- 复杂跳转
在逆向中,常见思路是:
1 | |
CTF Reverse 中尤其常见。
20. Ghidra 基本分析流程
Ghidra 是一个免费开源的逆向分析工具。
基本流程:
- 新建 Project
- Import File
- 选择目标二进制
- 让 Ghidra 自动分析
- 查看 Symbol Tree
- 查看 Functions
- 查看 Strings
- 查看 Decompiler
- 查看 Listing 汇编
- 使用 XREF 找引用
常见窗口:
| 窗口 | 用途 |
|---|---|
| Listing | 汇编和数据视图 |
| Decompiler | 反编译伪代码 |
| Symbol Tree | 函数、标签、符号 |
| Program Trees | 程序结构 |
| Data Type Manager | 类型信息 |
| Defined Strings | 字符串列表 |
初学时重点掌握:
- 找
main - 看字符串
- 看交叉引用
- 看反编译伪代码
- 对照汇编确认关键逻辑
21. IDA 基本分析思路
IDA 是经典逆向工具。
常见概念:
- Functions 窗口
- Strings 窗口
- Imports 窗口
- Exports 窗口
- Graph View
- Text View
- Xrefs
- Pseudocode
IDA 的图形视图非常适合看函数控制流。
如果使用 IDA Free,也可以完成很多入门分析。
基本思路和 Ghidra 类似:
1 | |
22. x64dbg 在逆向中的角色
Ghidra 和 IDA 更偏静态分析。
x64dbg 更偏 Windows 动态调试。
x64dbg 适合:
- 下断点
- 单步执行
- 看寄存器
- 看栈
- 看内存
- 看模块
- 跟踪 API 调用
- 修改运行时数据
- 观察解密后的字符串
- 分析简单壳或反调试
常见组合是:
1 | |
23. Linux 实验:debug 版和 strip 版对比
创建 reverse_demo.c:
1 | |
编译 debug 版:
1 | |
编译普通版:
1 | |
复制并 strip:
1 | |
对比:
1 | |
观察:
- 哪个版本符号最多?
- strip 后还能看到
check_password吗? - 程序是否仍然能运行?
24. Linux 实验:字符串和反汇编
查看字符串:
1 | |
你应该能看到:
1 | |
反汇编:
1 | |
虽然函数名可能消失,但字符串还在。
你可以继续用:
1 | |
查看只读数据区。
思考:
- 密码字符串在哪里?
- 成功/失败字符串在哪里?
- 如果没有函数名,字符串是否仍然能帮助定位逻辑?
25. Ghidra 实验:用字符串找关键函数
用 Ghidra 打开 reverse_demo_stripped。
步骤:
- Import File
- 运行自动分析
- 打开 Defined Strings
- 找到
open_sesame - 右键查看 References / XREF
- 跳到引用它的函数
- 查看 Decompiler 伪代码
- 尝试判断哪个函数是
check_password - 给函数重命名为
check_password - 给局部变量改一个更清晰的名字
这一步很重要。
真实逆向中,你经常要自己给函数和变量重新命名。
重命名不是为了好看,而是为了建立理解。
26. Ghidra 实验:对比反汇编和反编译
在 Ghidra 中找到关键函数后,同时观察:
- Listing 窗口中的汇编
- Decompiler 窗口中的伪代码
重点看:
strcmp调用- 参数是什么
- 返回值如何判断
- 成功和失败分支
思考:
- 反编译伪代码是否接近原始 C?
- 变量名是否还原正确?
- 函数名是否需要你手动修改?
- 如果伪代码看不懂,汇编能否提供更多确定信息?
27. Windows 对照实验:PE 中的字符串和导入表
如果你有 Windows 环境,可以写类似程序并编译成 exe。
使用工具:
- Detect It Easy
- PE-bear
- Ghidra
- x64dbg
观察:
- 文件是否是 PE
- 有哪些 Section
- Import Table 中有哪些函数
- 字符串窗口能否看到
success、fail、密码字符串 - Ghidra 中能否通过字符串 XREF 找到关键函数
- x64dbg 中能否在
strcmp或相关比较位置下断点
Windows 下导入函数可能来自:
1 | |
具体取决于编译器和运行库。
28. 逆向分析的基本流程
拿到一个陌生二进制时,可以按这个顺序来:
28.1 识别文件
Linux:
1 | |
Windows:
- Detect It Easy
- PE-bear
看:
- ELF / PE
- 32 位 / 64 位
- 是否动态链接
- 是否可能加壳
28.2 查看字符串
1 | |
关注:
- 错误信息
- 成功信息
- URL
- 文件路径
- 命令
- 注册表路径
- API 名称
28.3 查看导入函数
Linux:
1 | |
Windows:
- PE-bear Import Table
- Ghidra Imports
- IDA Imports
关注:
- 文件操作 API
- 网络 API
- 进程线程 API
- 内存管理 API
- 加密 API
28.4 找入口点和 main
入口点不一定是 main。
真实流程通常是:
1 | |
Ghidra / IDA 通常能帮助你定位 main 或类似函数。
28.5 用字符串和 XREF 找关键函数
这是最常用方法之一:
1 | |
28.6 动态调试验证
静态分析只是推测。
关键结论最好用调试器验证:
- 下断点
- 观察参数
- 观察返回值
- 观察内存
- 观察分支跳转
29. 从漏洞分析角度看程序结构
漏洞分析时,你也需要程序结构视角。
你要找:
- 输入入口在哪里
- 输入传给了哪些函数
- 是否有长度检查
- 是否调用危险函数
- 数据是否进入栈或堆缓冲区
- 崩溃点和输入处理函数之间的调用关系
常见危险函数线索:
1 | |
如果在导入表或反编译中看到这些函数,要提高警惕。
但注意:
出现危险函数不等于一定有漏洞,关键看输入是否可控、长度是否受限、目标缓冲区大小是否足够。
30. 从恶意代码分析角度看程序结构
恶意代码分析时,结构视角同样重要。
你要找:
- 初始化函数
- 反调试函数
- 解密函数
- 配置解析函数
- 网络通信函数
- 文件写入函数
- 持久化函数
- 注入或执行函数
常见可疑 API 线索包括:
Windows:
1 | |
Linux:
1 | |
这些 API 本身不一定恶意,但组合起来能描述行为。
例如:
1 | |
在安全分析中就非常值得关注。
31. 不要过度相信函数名
有些函数名是真实符号。
有些函数名是工具自动生成的。
有些函数名可能被开发者故意误导。
例如恶意代码可能把函数命名为:
1 | |
但实际做的是可疑行为。
所以逆向时不要只相信名字,要看证据:
- 函数调用了什么 API
- 参数是什么
- 写了哪些路径
- 连了哪些地址
- 改了哪些内存
- 运行时行为是什么
32. 不要过度相信反编译变量类型
反编译器会猜变量类型。
例如它可能把某个变量猜成:
1 | |
但这些类型不一定准确。
如果你分析关键逻辑,要验证:
- 这个变量被当作地址使用吗?
- 它传给了哪个函数?
- 它参与了整数运算还是指针运算?
- 它对应的寄存器和内存访问宽度是多少?
类型错误会导致伪代码误导你。
这就是为什么关键处要看汇编。
33. 本节重点总结
你需要记住这些核心结论:
- 逆向工程是在没有完整源码时推断程序结构和行为。
- 源代码编译后,很多高级信息会消失或变得不明显。
- 符号表保存函数、变量等名字和地址的对应关系。
strip会移除很多符号和调试信息,使逆向更困难。- 反汇编更接近真实机器执行,反编译更容易阅读但可能不准确。
- 字符串是逆向分析的重要入口。
- 交叉引用能帮助你从字符串、函数或变量跳到使用它们的位置。
- 函数调用图和控制流图能帮助理解程序结构。
- 静态分析和动态调试应该结合使用。
- 逆向时要看证据,不要盲目信任函数名、变量名或反编译伪代码。
34. 本节课后作业
作业 1:debug / release / stripped 对比
写 reverse_demo.c:
1 | |
编译:
1 | |
执行:
1 | |
提交内容:
1 | |
作业 2:字符串分析
执行:
1 | |
提交内容:
1 | |
作业 3:Ghidra 分析 stripped 程序
用 Ghidra 打开 reverse_demo_stripped。
要求:
- 找到 Defined Strings
- 找到
open_sesame - 查看它的 XREF
- 跳到引用它的函数
- 判断哪个函数是密码检查函数
- 给该函数重命名为
check_password - 找到 success/fail 分支
提交内容:
1 | |
作业 4:动态验证
用 GDB 或 x64dbg 动态调试程序。
Linux GDB 示例:
1 | |
1 | |
输入:
1 | |
再测试错误密码。
提交内容:
1 | |
35. 自测题
题 1
源代码视角和二进制视角最大的区别是什么?
题 2
符号表的作用是什么?
题 3
strip 会对逆向分析造成什么影响?
题 4
反汇编和反编译有什么区别?
题 5
为什么不能完全相信反编译伪代码?
题 6
字符串为什么是逆向分析的重要入口?
题 7
交叉引用 XREF 是什么?
题 8
拿到一个陌生二进制,你可以按什么基本流程分析?
36. 自测题参考答案
答 1
源代码视角保留函数名、变量名、类型、控制结构和注释等高级信息;二进制视角主要面对机器指令、地址、字符串、导入函数和有限符号,很多高级信息已经丢失或需要推断。
答 2
符号表保存函数、全局变量等对象的名字和地址之间的对应关系,帮助调试器和逆向工具显示更清晰的信息。
答 3
strip 会移除很多普通符号和调试信息,使函数名、变量信息、源码行号等消失,从而增加逆向分析难度,但通常不影响程序运行。
答 4
反汇编把机器码转换成汇编指令,更接近真实执行;反编译尝试把机器码还原成类 C 伪代码,可读性更好,但可能不准确。
答 5
因为反编译器需要猜测变量类型、控制结构和函数边界。优化、混淆、缺少符号等情况都可能导致伪代码不准确。
答 6
字符串常常包含错误信息、成功提示、文件路径、URL、命令、注册表路径等行为线索,可以帮助快速定位关键函数。
答 7
交叉引用表示某个函数、变量或字符串被哪些位置引用。通过 XREF 可以从一个线索跳到使用它的代码位置。
答 8
可以先识别文件格式,再查看字符串和导入函数,然后找入口点和 main,利用字符串和 XREF 找关键函数,最后用调试器动态验证分析结论。
37. 下一节预告
下一节课会讲:
1 | |
你会学习:
- 程序和进程的区别
- PID、父进程、子进程
- 进程状态
- PCB 的概念
- Linux
fork - Windows
CreateProcess - 为什么进程行为分析是恶意代码分析的重要基础