逆向工程系列(五):Ghidra 实战:从零开始分析一个可执行文件
在上一篇中,我们概述了静态分析的重要性以及 Ghidra 和 IDA Pro 这两大工具。现在,是时候将理论付诸实践了!本篇我们将聚焦于 Ghidra,通过一个具体的案例,从零开始演示如何使用 Ghidra 的反编译、交叉引用、重命名等核心功能来分析一个未知的可执行文件。
1. 准备目标文件
为了更好地进行实战,我们首先需要一个用于分析的目标文件。这里我们创建一个简单的 C 语言程序,它包含一些常见的控制流和字符串操作,足以展示 Ghidra 的强大之处。
创建 target_program.c
文件:
#include <stdio.h>
#include <string.h>
#include <stdbool.h> // 包含布尔类型头文件
// 简单的校验函数
bool check_key(const char* key) {
char correct_prefix[] = "ABC-";
char correct_suffix[] = "-XYZ";
int magic_number = 0x1A2B3C4D; // 一个魔法数字
if (strlen(key) != 12) {
return false;
}
// 检查前缀
if (strncmp(key, correct_prefix, strlen(correct_prefix)) != 0) {
return false;
}
// 检查后缀
if (strcmp(key + 8, correct_suffix) != 0) { // key + 8 指向 "-XYZ" 的开始
return false;
}
// 检查中间的数字部分是否符合某种逻辑
// 假设中间是 "1234"
char middle_part[5];
strncpy(middle_part, key + 4, 4); // 从key的第5个字符开始取4个
middle_part[4] = '\0'; // 确保字符串以空字符结尾
if (strcmp(middle_part, "1234") != 0) {
return false;
}
printf("Magic number check: %x\n", magic_number); // 打印魔法数字
return true; // 所有检查通过
}
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("用法: %s <序列号>\n", argv[0]);
return 1;
}
printf("正在检查序列号: %s\n", argv[1]);
if (check_key(argv[1])) {
printf("序列号正确!欢迎。\n");
} else {
printf("序列号错误!请重试。\n");
}
return 0;
}
编译程序 (使用 MinGW-w64 GCC,不带优化和调试信息,模拟未知二进制):
在命令行中,导航到文件所在的目录,然后执行以下命令:
gcc target_program.c -o target_program.exe -O0 -g0
-O0
: 禁用所有优化,这会使生成的汇编代码更直接地反映 C 语言的结构,便于学习。
-g0
: 不包含调试信息,模拟我们在实际逆向中通常会遇到的情况——没有符号信息。
现在,你得到了 target_program.exe
,这就是我们要用 Ghidra 分析的目标。
2. Ghidra 初体验:导入与初步分析
- 启动 Ghidra: 打开 Ghidra 运行时,它会首先显示项目管理器。
- 创建新项目: 点击
File
-\> New Project...
。
- 选择
Non-Shared Project
(非共享项目)。
- 为项目命名(例如
TargetProgramAnalysis
)并选择一个项目目录。点击 Finish
。
- 导入可执行文件:
- 在项目管理器中,点击
File
-\> Import File...
。
- 浏览并选择你刚刚编译的
target_program.exe
文件,点击 Select File
。
- Ghidra 会显示导入选项。通常,保持默认设置即可,它会自动识别文件类型、CPU 架构(例如
x86:LE:64:default:gcc
表示 64 位 Little Endian GCC 编译的 x86 程序)和格式(PE)。点击 OK
。
- 初步分析:
- 导入成功后,文件会出现在项目管理器中。双击
target_program.exe
。
- Ghidra 会询问你是否进行“初步分析 (Analyze)”。选择
Yes
。
- 在分析选项弹窗中,确保所有推荐的分析选项都被选中(特别是
Decompiler Parameter ID
、StackVarRecovery
、DWARF
等,尽管我们没有调试信息,但一些分析器依然有用)。点击 Analyze
。
- 等待分析完成。这可能需要一些时间,具体取决于文件大小和你的电脑性能。
3. Ghidra 界面导览与核心功能实践
分析完成后,Ghidra 的主分析界面会打开。让我们逐一探索其主要区域并进行分析。
3.1 探索 main
函数
- Symbol Tree (符号树) 窗口: (通常在左侧)
- 展开
Functions
节点。你会看到 Ghidra 识别出的一些函数。
- 找到并双击
main
函数。
- Listing (列表) 窗口: (通常在中间)
- 这里显示
main
函数的反汇编代码。你会看到机器码、地址和汇编指令。
- 注意指令前的注释,
CALL
指令通常会有目标函数名。
- Decompile (反编译) 窗口: (通常在右侧)
- 这是最重要的窗口!它显示了 Ghidra 尝试将汇编代码反编译成的 C 语言伪代码。
- 你会看到
main
函数的伪代码,包括 argc
和 argv
的使用、printf
和 check_key
的调用。
3.2 深入 check_key
函数
在 main
函数的伪代码中,你会看到调用了一个名为 FUN_00xxxxxxxx
的函数(具体名称可能不同),这就是我们的 check_key
函数。
- 重命名函数: 在
Decompile
窗口中,选中这个未知函数名(如 FUN_00xxxxxxxx
),按下 L
键 (或者右键 Rename Function
)。
- 将其重命名为
check_key
。你会发现 Listing
窗口和 Symbol Tree
窗口中的函数名也会同步更新。
- 进入
check_key
函数: 双击 Decompile
窗口中重命名后的 check_key
函数名,或者在 Symbol Tree
中双击 check_key
。
- 现在,你在
Decompile
窗口中看到的是 check_key
函数的伪代码。
3.3 分析 check_key
的伪代码
在 check_key
的伪代码中,Ghidra 会尽可能地还原 C 语言的逻辑。
- 识别局部变量:
- 你会看到类似
param_1
、param_2
、local_c
等变量名。
- 根据其上下文(例如,
strlen(param_1)
),我们可以推断 param_1
就是 const char* key
。
- 选中
param_1
,按 L
键重命名为 key
。
local_c
可能是我们的 correct_prefix
数组。观察其如何被赋值和使用。
- 识别字符串常量:
- 你会看到像
s_ABC-_00xxxxxxxx
这样的字符串引用。
- 双击这些字符串引用,Ghidra 会跳转到数据段中对应的字符串位置。
- 在
Listing
窗口中,你可以看到这些字符串的实际内容(ABC-
、-XYZ
、1234
等)。
- 回到
Decompile
窗口,你可以根据字符串内容重命名伪代码中的相关变量(如将 s_ABC-_00xxxxxxxx
相关的变量重命名为 correct_prefix
等)。
- 理解控制流:
- 观察
if
语句和 return false
(return 0
) 的对应关系。
strlen(key) != 12
的判断会在伪代码中清晰地显示出来。
strncmp
和 strcmp
函数的调用也会被 Ghidra 识别出来。如果你不确定这些标准库函数的原型,可以在 Ghidra 中查看其导入函数的交叉引用,或者在网上cha询其定义。
- 发现“魔法数字”:
- 留意伪代码中的常量。你会发现一个
0x1A2B3C4D
的十六进制常量。这个就是我们源代码中的 magic_number
。这表明 Ghidra 成功识别并保留了这个常量。
3.4 使用交叉引用 (Cross-References)
交叉引用是理解数据和代码如何相互关联的强大工具。
- 查找字符串引用:
- 在
Symbol Tree
窗口中,展开 Strings
节点。你会看到程序中所有的字符串常量。
- 双击字符串
恭喜!序列号正确!\n
。
- 在
Listing
窗口中,右键该字符串地址,选择 References
-\> Show References to Address(es)
。
- 你会看到哪些指令引用了这个字符串。通常,这会带你到
main
函数中打印成功信息的 printf
调用处。
- 查找函数调用:
- 在
Symbol Tree
中,找到 check_key
函数。
- 右键
check_key
,选择 References
-\> Show References to
。
- 你会看到
main
函数调用了 check_key
。这验证了我们的分析。
4. 结合动态调试验证静态分析
静态分析为你提供了程序的“地图”,动态调试则是按图索骥,验证你的猜测,并观察运行时行为。
- 设定目标: 通过 Ghidra 的分析,我们已经知道正确的序列号是 "ABC-1234-XYZ",长度为 12。
- 启动 x64dbg: 使用 x64dbg 打开
target_program.exe
。
- 设置断点: 在
main
函数的 check_key
调用处设置一个断点。
- 命令行参数: 在 x64dbg 中,通过
File
-\> Change Commandline
,在命令行中输入你的测试序列号,例如:target_program.exe ABC-1234-XYZ
。
- 运行并观察:
- 运行程序,它会在
check_key
调用前暂停。
F7
单步进入 check_key
函数。
- 观察寄存器(尤其是
RCX
,它应该包含你输入的序列号字符串地址)、栈和标志位的变化。
- 对照 Ghidra 的伪代码,单步执行,验证 Ghidra 的反编译是否准确。你会看到
strlen
、strncmp
、strcmp
的返回值如何影响程序流程。
- 如果输入了错误的序列号,观察是哪个条件判断导致函数返回
false
。
5. 总结与展望
通过本篇的实战,你已经初步掌握了 Ghidra 的核心功能,包括导入文件、初步分析、重命名、识别数据、理解控制流以及利用交叉引用。更重要的是,你体验了静态分析与动态调试结合的强大威力,它们是逆向工程中不可或缺的互补工具。
在下一篇中,我们将探讨在逆向工程中常用的其他工具和技术,如 PE 文件结构分析、脱壳、以及一些自动化分析工具。