用 Ghidra 逆向一个简单的 License 校验程序
本文介绍了如何使用Ghidra逆向分析一个简单的License校验程序。首先编写一个C程序实现License校验逻辑,编译后通过Ghidra导入二进制文件进行自动分析。重点讲解了如何定位main函数和关键校验逻辑,通过反编译结果还原出License的4条规则:长度必须为12字符、前四位固定为"GHDR"、中间四位数字之和为20、最后四位是中间数字的镜像排列。文章通过实际案例展示
一、前言
上一篇我们从宏观层面聊了 Ghidra 是什么、能干什么。这篇我们来点实战:
从零开始,写一个简单的 C 程序做 License 校验
➜ 编译成二进制
➜ 用 Ghidra 打开
➜ 找到校验逻辑、还原正确 License
这类例子在 CTF / 安全面试 / 逆向入门里非常常见:
输入一个“序列号”,程序判断对/错。
我们要做的事就是反推它的规则。
二、准备一个目标程序:简单 License 校验
先写一个非常简单(但足够展示 Ghidra 功能)的例子。
假设源码如下(你可以保存为 license.c):
#include <stdio.h>
#include <string.h>
int check_license(const char *key) {
// 1. 长度必须为 12
if (strlen(key) != 12) {
return 0;
}
// 2. 前四位固定为 "GHDR"
if (key[0] != 'G' || key[1] != 'H' || key[2] != 'D' || key[3] != 'R') {
return 0;
}
// 3. 中间四位为数字,且它们的和要等于 20
int sum = 0;
for (int i = 4; i < 8; i++) {
if (key[i] < '0' || key[i] > '9') {
return 0;
}
sum += key[i] - '0';
}
if (sum != 20) {
return 0;
}
// 4. 最后四位是前四位数字的“镜像”
// key[8] == key[7], key[9] == key[6], key[10] == key[5], key[11] == key[4]
if (key[8] != key[7] ||
key[9] != key[6] ||
key[10] != key[5] ||
key[11] != key[4]) {
return 0;
}
return 1;
}
int main() {
char buf[256];
printf("Please input license: ");
if (scanf("%255s", buf) != 1) {
printf("Input error\n");
return 1;
}
if (check_license(buf)) {
printf("OK! License is valid.\n");
} else {
printf("Invalid license.\n");
}
return 0;
}
⚠️ 实战中你是拿不到源码的,但为了讲解,我们先从“已知真相”出发,等会再从 Ghidra 视角反推一次。
2.1 编译成二进制
Linux / macOS:
gcc -O2 -s license.c -o license
-O2:开启优化(实战中常见)-s:strip 符号表,让逆向更接近真实场景(没有函数名)
Windows(MinGW)示例:
gcc -O2 -s license.c -o license.exe
接下来,你就只保留二进制文件(license / license.exe),假装源码丢了,用 Ghidra 从头分析。
三、在 Ghidra 中导入与自动分析
3.1 创建工程 & 导入文件
-
启动 Ghidra,选择:
File -> New Project- 选
Non-Shared Project,随便起个名字,比如ghidra_license_demo
-
在左侧 Project 窗格空白处右键:
Import File...- 选中刚才编译出的
license/license.exe
-
导入时 Ghidra 会识别出:
- 文件格式(ELF / PE)
- 处理器架构(x86 / x64)
确认无误后,点击 OK。
3.2 打开 Program & 执行 Auto Analysis
在 Project 窗格里双击导入的程序,Ghidra 会问你:
“Analyze this program?”
直接选择 Yes。
在弹出的 Analysis Options 窗口里:
- 先保持默认勾选(函数分析、字符串分析、指针分析等)
- 点击
Analyze
稍等片刻,左侧 Symbol Tree 会出现一堆自动识别出的函数。
四、定位 main 函数和关键逻辑
4.1 找到 main / 程序入口
strip 之后,main 可能不叫 main,而是被识别成类似:
_startentryFUN_00101234(Ghidra 自动起的名称)
可以这样做:
-
打开
Symbol Tree -> Functions -
在比较新的编译环境中,Ghidra 仍然能识别出
main(有时会显示为main) -
如果没有明显标识:
- 先打开
_start,一路往下跟调用 - 通常会看到某个函数负责:设置栈、调用
__libc_start_main,后者的参数里就有main的地址
- 先打开
这部分如果你第一次玩可以大概看下,不用追求搞懂所有启动流程,先找到“真正的业务函数”就行。
4.2 打开 Decompiler 视图
找到 main 之后:
- 双击
main函数,打开反汇编视图 - 打开
Window -> Decompiler - 把 Decompiler 窗口拖到右侧固定
这时右侧应该能看到类似 C 的伪代码,其中包括:
printf("Please input license: ");scanf(...)- 调用一个内部函数(名称类似
FUN_00101320)
这个内部函数,基本就是我们的 check_license。
小技巧:
在 Decompiler 窗口中,选中函数名右键,选择Rename,把它改成check_license,后续阅读更友好。
五、分析 check_license 函数:从伪代码还原规则
现在我们把注意力放在 check_license 上。
5.1 函数基本结构
打开 check_license 后,在 Decompiler 中你会看到一个大概这样的结构(不同编译器/优化级别略有差异):
int check_license(char * key)
{
size_t len;
int i;
int sum;
len = strlen(key);
if (len != 12) {
return 0;
}
if ((key[0] != 'G') || (key[1] != 'H') || (key[2] != 'D') || (key[3] != 'R')) {
return 0;
}
sum = 0;
for (i = 4; i < 8; i = i + 1) {
if ((key[i] < '0') || ('9' < key[i])) {
return 0;
}
sum = sum + (int)key[i] - 0x30;
}
if (sum != 20) {
return 0;
}
if (((((key[8] != key[7]) || (key[9] != key[6])) || (key[10] != key[5])) || (key[11] != key[4]))) {
return 0;
}
return 1;
}
现在,假装你没有源码,我们来“逆向还原”规则。
5.2 规则一:长度校验
len = strlen(key);
if (len != 12) {
return 0;
}
非常直观:
- License 必须长度为 12 个字符。
5.3 规则二:前缀校验
if ((key[0] != 'G') || (key[1] != 'H') || (key[2] != 'D') || (key[3] != 'R')) {
return 0;
}
这说明:
key[0..3]必须是"GHDR"- 即 License 格式为:
GHDR????????
你可以在 Ghidra 把这些部分加上注释,例如在 Decompiler 中右键 -> Add Comment:
// key[0..3] must be "GHDR"
5.4 规则三:中间四位为数字且和为 20
sum = 0;
for (i = 4; i < 8; i = i + 1) {
if ((key[i] < '0') || ('9' < key[i])) {
return 0;
}
sum = sum + (int)key[i] - 0x30;
}
if (sum != 20) {
return 0;
}
拆解一下:
-
for (i = 4; i < 8; i++):循环处理key[4] ~ key[7] -
if (key[i] < '0' || '9' < key[i]) return 0;:- 这一位必须是
0-9的字符(ASCII 数字)
- 这一位必须是
-
sum += key[i] - 0x30;- 把字符转成数字(‘0’ 的 ASCII 是 0x30)
-
if (sum != 20) return 0;- 这四个数字之和必须等于 20
于是我们可以归纳出:
- 中间四位是数字,且四位数字之和为 20
例如:1235(1+2+3+5=11)不行,5681(5+6+8+1=20)就行。
5.5 规则四:最后四位是前四位数字的“镜像”
if (((((key[8] != key[7]) || (key[9] != key[6])) ||
(key[10] != key[5])) || (key[11] != key[4]))) {
return 0;
}
逐条看:
key[8] != key[7]→ 要求key[8] == key[7]key[9] != key[6]→ 要求key[9] == key[6]key[10] != key[5]→ 要求key[10] == key[5]key[11] != key[4]→ 要求key[11] == key[4]
也就是说:
key[8] == key[7]key[9] == key[6]key[10] == key[5]key[11] == key[4]
最后四位,是中间四位数字的“反向拷贝”:
- 设中间四位为:
a b c d(对应 key[4]~key[7]) - 最后四位为:
d c b a(对应 key[8]~key[11])
六、根据逆向结果构造一个合法 License
现在我们从 Ghidra 里已经“还原”出所有规则:
- 总长度 = 12
- 前四位固定:
GHDR - 中间四位是数字且和为 20
- 最后四位是中间四位的镜像
随便构造一个满足条件的数字组合,例如:
- 取中间四位:
5 6 8 1 - 5 + 6 + 8 + 1 = 20 ✅
- 镜像后:
1 8 6 5
那么整个 License 可以写成:
GHDR56811865
你可以试着运行程序验证:
./license
Please input license: GHDR56811865
OK! License is valid.
至此,我们完成了一个完整的“从零到一”的逆向过程:
- 只有程序的二进制
- 用 Ghidra 分析结构
- 在 Decompiler 中逐行理解逻辑
- 总结出规则
- 构造出合法 License
七、这次实战中用到的 Ghidra 核心能力回顾
在整个分析过程中,我们实际用到了 Ghidra 的哪几类能力?
-
自动分析(Auto Analysis)
- 函数识别、字符串分析、控制流构建
- 让我们可以直接从
main开始,而不是在一坨汇编里迷路
-
多视图协作
- 汇编视图:可以看到真实的指令
- Decompiler 视图:更接近源码,利于快速理解
- Symbol Tree:方便浏览函数和符号列表
-
重命名与注释
- 把
FUN_00101320改成check_license - 给变量加语义化名字(如
len,sum) - 给关键代码块加注释,这样以后再打开不会忘
- 把
-
数据流 & 条件分析
- 通过观察条件判断(
if (...) return 0;) - 推导出真正的业务规则:长度 / 前缀 / 数字和 / 镜像
- 通过观察条件判断(
八、你可以继续玩的几个进阶方向
这只是一个非常简单的入门 demo,真正的程序会复杂很多。你可以在此基础上往下玩:
-
增加反调试、字符串混淆
- 把字符串
GHDR加密存放,运行时解密 - 使用
memcmp、strncpy等多种方式混淆逻辑
- 把字符串
-
把 License 规则改成更“数学化”的
- 某几位数字做异或 / 乘法 / 取模
- 用一个小的自定义 hash 函数
-
尝试开启更高级别优化
- 用
-O3或-Os编译,看看 Ghidra 反编译出的结构有什么变化
- 用
-
给自己出题
- 写一个你自己都容易忘记逻辑的校验程序
- 过一段时间后删掉源码,只留下二进制
- 用 Ghidra 把规则完整还原一遍
这会非常锻炼你对“汇编 + 伪代码”的综合理解能力。
九、总结
这篇文章我们做了一次完整的 Ghidra 实战演练:
- 从一个简单的 License 校验程序出发
- 编译为无符号的二进制
- 在 Ghidra 中导入分析
- 从
main函数顺藤摸瓜找到核心校验函数 - 用 Decompiler 视图和一点点耐心,把算法和规则完整还原
相比纯理论,这种“自己写一个 ➜ 自己反编译 ➜ 自己逆向”的方式,会让你对 Ghidra 和逆向的理解扎实很多。
更多推荐



所有评论(0)