攻防世界逆向题 CatFly 完整解题思路与过程
一、题目背景与核心目标
1. 题目基础信息
- 类型:Linux 平台逆向工程(RE)题,可执行程序无输入,仅输出滚动乱码和递增次数字符串;
- 现象:运行程序后,顶部是乱码(加密 Flag),底部循环输出
You have nyaned for X times!(X 从 0 开始递增); - 核心目标:通过逆向分析程序逻辑,还原加密乱码对应的原始 Flag。
2. 逆向解题核心思路(通用)

二、阶段 1:程序初分析 —— 从现象提取线索(逆向第一步)
1. 运行程序,收集直观线索
- 操作步骤:
- 给程序加执行权限:
chmod +x CatFly; - 运行程序:
./CatFly; - 记录现象:
- 线索 1:乱码随次数 X 递增同步变化 → 乱码生成依赖 X;
- 线索 2:X 从 0 开始递增 → 次数变量有明确初始值;
- 线索 3:无输入仅输出 → Flag 加密逻辑在程序内部,无需外部输入参与。
- 给程序加执行权限:
- 逆向知识点:先运行后分析,避免直接陷入汇编代码,用现象缩小分析范围。
2. IDA 静态分析准备
- 操作步骤:
- 用 IDA Pro 打开程序(64 位 Linux 程序选「ELF64 for x86-64」);
- 等待 IDA 完成反汇编,按
F5生成 main 函数伪代码(核心); - 打开字符串窗口(
Shift+F12),搜索关键词:- 找到
You have nyaned for %d times!→ 关联次数 X; - 找到潜在的
CatCTF→ Flag 前缀(后续验证用)。
- 找到
- 逆向知识点:
- IDA 核心快捷键:
F5(伪代码)、Shift+F12(字符串窗口)、X(交叉引用); - 字符串窗口是逆向 “捷径”,Flag 前缀、提示语常藏于此。
- IDA 核心快捷键:
三、阶段 2:核心逻辑拆解 ——IDA 分析关键变量与函数
1. 关键变量定位与分析(逆向核心)
(1)次数变量:dword_108E0(对应 X)
- 定位步骤:
- 在字符串窗口右键
You have nyaned for %d times!→ 「Jump to cross reference」→ 跳转到 printf 调用处; - 伪代码中
%d对应的参数就是dword_108E0; - 右键
dword_108E0→ 「Jump to operand」→ 跳转到.bss段定义处:
- 在字符串窗口右键
.bss:00000000000108E0 dword_108E0 dd ?
- 分析结论:
dword_108E0是.bss段全局变量(未初始化数据段);- 按 C 语言规则:未显式初始化的全局整数变量默认值为 0 → X 初始值 = 0。
- 逆向知识点:
.bss段变量dd ?表示未显式初始化,全局变量默认值为 0;变量名 = 类型(dword)+ 地址后几位(108E0)是 IDA 命名习惯。
(2)种子变量:dword_E1E8(随机数种子)
- 定位步骤:
- 在 main 函数伪代码中搜索「随机数」相关逻辑(或异或
^操作); - 找到调用
sub_62B5(随机数生成函数)的地方,参数 / 返回值关联dword_E1E8; - 右键
dword_E1E8→ 「Find cross references to」→ 找到初始化处:dword_E1E8 = 0x1106。
- 在 main 函数伪代码中搜索「随机数」相关逻辑(或异或
- 分析结论:
dword_E1E8是随机数种子,初始值 = 0x1106,每次循环后会被更新。
(3)加密数组:e120(存储加密 Flag)
- 定位步骤:
- 在伪代码中找到异或操作(
^),操作数之一就是e120数组; - 右键
e120→ 「Jump to operand」→ 跳转到.data段定义处(已初始化数据段):
- 在伪代码中找到异或操作(
.data:000000000001E120 dword_E120 dd 27FBh, 27A4h, 464Eh, ...
- 分析结论:
e120是 dword 数组(每个元素 4 字节),存储加密后的 Flag 原始数据,解密需提取其有效字节。 - 逆向知识点:
.data段存储已初始化全局变量,数组、常量常在此处。
2. 核心函数分析(解密逻辑的核心)
(1)sub_62B5:伪随机数生成函数(LCG 算法)
- 伪代码核心逻辑:
int sub_62B5() { dword_E1E8 = 1103515245 * dword_E1E8 + 12345; // LCG核心公式 return (dword_E1E8 >> 10) & 0x7FFF; // 提取15位随机数 }
- 逐行解释:
1103515245 * dword_E1E8 + 12345:经典 LCG 算法(A=1103515245,C=12345,M=2³¹);>> 10:右移 10 位,跳过低 10 位(LCG 低位数随机性差);& 0x7FFF:按位与 15 个 1,限定随机数范围 0~32767(15 位)。
- 逆向知识点:LCG 算法是逆向高频伪随机数算法,公式为
Xn+1 = (A*Xn + C) mod M。程序中dword_E1E8的类型是32 位有符号整数(C 语言的int类型)——32 位有符号int的取值范围是[-2^31, 2^31 - 1](即[-2147483648, 2147483647])。 -
为什么不需要显式写
mod 2^31?当计算1103515245 * dword_E1E8 + 12345时,结果很容易超过2^31 - 1(32 位有符号int的最大值),此时会触发整数溢出。而 C 语言中,32 位有符号整数的溢出规则是:溢出后的结果会自动以2^32为模进行 “截断”,但由于是有符号数,最高位(第 31 位)是符号位 —— 这就等价于对2^31取模(因为2^32截断后,符号位会自动区分正负,最终效果和mod 2^31一致)。举个例子:
假设dword_E1E8的某个值导致1103515245 * dword_E1E8 + 12345 = 2^31 + 100(超过最大值),32 位有符号int存储时会溢出,最终值会变成-2^31 + 100—— 这个结果恰好是(2^31 + 100) mod 2^31的结果(因为2^31 mod 2^31=0,所以结果是 100,但由于符号位的存在,表现为负数,但后续的>>10和&0x7FFF会处理符号位,不影响最终随机数)。 -
总结程序中通过32 位有符号
int的溢出特性,隐式实现了mod 2^31的效果,所以逆向代码里不需要显式写出mod 2^31的操作 —— 这是 C 语言中 LCG 算法的常见 “隐式实现” 方式(利用整数类型的存储范围替代显式取模)
(2)sub_6314:Flag 解密核心函数
- 伪代码核心逻辑:
void sub_6314(int a2, int a3) { int random = sub_62B5(); // 调用随机数函数 char ch = e120[a3] ^ random; // 异或解密 ch = ch & 0x7F; // 限定ASCII范围(0~127) sub_62E3(ch); // 筛选可见字符 }
- 分析结论:
- 解密逻辑:
e120元素 ^ 随机数 & 0x7F→ 得到候选字符; - 仅当
a2=18、a3∈[5,54]时执行解密 → 遍历 e120 的 50 个元素。此处需要注意sub_6314函数有个循环在调用,每次循环都会用上一个生成的随机数(未进行位移和模运算的随机数,即sub_62B5()中的dword_E1E8的值,而不是函数返回值)作为种子生成随机数在与下一个字符异或(即50个字符每个都与不同的随机数异或)。
- 解密逻辑:
(3)sub_62E3:可见字符筛选函数
- 核心逻辑:判断字符 ASCII 码是否在 32~126 之间(可见字符),否则替换为空格。
3. 种子更新逻辑(本题唯一难点)
- 伪代码关键逻辑:
// 输出次数字符串,获取printf返回值(字符串长度) int len = printf("You have nyaned for %d times!", dword_108E0); dword_E1E8 += len; // 种子 += printf返回值 dword_108E0 += 1; // 次数X自增
- 分析结论:
- printf 返回值 = 输出字符串长度 = 固定长度 41 + 次数 X 的位数(如 X=100,位数 3→返回 44);
- 种子更新依赖字符串长度 → 直接解密 e120 无法得到 Flag,必须模拟 X 递增 + 种子更新。
- 逆向知识点:库函数(printf/scanf)的返回值常是隐藏关键,需跟踪变量更新路径。
四、阶段 3:关键数据提取 —— 从 IDA 获取解密所需数据
1. 提取 e120 数组(核心数据)
- 提取步骤:
- 跳转到 e120 的
.data段定义处,按D切换显示格式为byte(1 字节); - 按小端序规则提取每个 dword 的低字节(小端序:低字节存低地址,解密仅用低字节):
- IDA 显示 dword 值
27FBh→ 低字节0xFB; - IDA 显示 dword 值
27A4h→ 低字节0xA4;
- IDA 显示 dword 值
- 提取 50 个低字节,整理为列表:
- 跳转到 e120 的
int e120[] = {
0x27FB, 0x27A4, 0x464E, 0x0E36, 0x7B70, 0x5E7A, 0x1A4A, 0x45C1,
0x2BDF, 0x23BD, 0x3A15, 0x5B83, 0x1E15, 0x5367, 0x50B8, 0x20CA,
0x41F5, 0x57D1, 0x7750, 0x2ADF, 0x11F8, 0x09BB, 0x5724, 0x7374,
0x3CE6, 0x646E, 0x010C, 0x6E10, 0x64F4, 0x3263, 0x3137, 0x00B8,
0x229C, 0x7BCD, 0x73BD, 0x480C, 0x14DB, 0x68B9, 0x5C8A, 0x1B61,
0x6C59, 0x5707, 0x09E6, 0x1FB9, 0x2AD3, 0x76D4, 0x3113, 0x7C7E,
0x11E0, 0x6C70
};
0x27FB, 0x27A4, 0x464E, 0x0E36, 0x7B70, 0x5E7A, 0x1A4A, 0x45C1,
0x2BDF, 0x23BD, 0x3A15, 0x5B83, 0x1E15, 0x5367, 0x50B8, 0x20CA,
0x41F5, 0x57D1, 0x7750, 0x2ADF, 0x11F8, 0x09BB, 0x5724, 0x7374,
0x3CE6, 0x646E, 0x010C, 0x6E10, 0x64F4, 0x3263, 0x3137, 0x00B8,
0x229C, 0x7BCD, 0x73BD, 0x480C, 0x14DB, 0x68B9, 0x5C8A, 0x1B61,
0x6C59, 0x5707, 0x09E6, 0x1FB9, 0x2AD3, 0x76D4, 0x3113, 0x7C7E,
0x11E0, 0x6C70
};
- 逆向知识点:x86/x64 程序是小端序存储,多字节数据的低字节是解密核心。
2. 提取核心参数
| 参数名称 | 取值 | 说明 |
|---|---|---|
| 种子初始值 | 0x1106 | dword_E1E8 初始值 |
| 次数初始值 | 0 | dword_108E0 初始值 |
| LCG_A | 1103515245 | 0x41C64E6D |
| LCG_C | 12345 | 0x3039 |
| 随机数掩码 | 0x7FFF | 截取 15 位 |
| printf 固定长度 | 41 | "You have nyaned for " + " times!" |
| Flag 前缀 | "CatCTF" |
验证解密结果 |
五、阶段 4:编写解密代码 —— 模拟核心逻辑(关键步骤)
1. 代码编写思路
- 核心:模拟「种子更新→生成随机数→异或解密→验证 Flag」的循环,优化 printf 返回值计算(手动算长度,提升速度)。
2. 完整解密代码(带详细注释)
#include <stdio.h> #include <string.h> #include <stdbool.h> #include <stdint.h> // ===================== 核心数据(完全对齐你的参考代码) ===================== // 种子初始值(对应dword_E1E8) int pass = 0x1106; // encryptFlag数组:直接使用dword原值(无需提取低字节) int encryptFlag[50] = { 0x27fb, 0x27a4, 0x464e, 0x0e36, 0x7b70, 0x5e7a, 0x1a4a, 0x45c1, 0x2bdf, 0x23bd, 0x3a15, 0x5b83, 0x1e15, 0x5367, 0x50b8, 0x20ca, 0x41f5, 0x57d1, 0x7750, 0x2adf, 0x11f8, 0x09bb, 0x5724, 0x7374, 0x3ce6, 0x646e, 0x010c, 0x6e10, 0x64f4, 0x3263, 0x3137, 0x00b8, 0x229c, 0x7bcd, 0x73bd, 0x480c, 0x14db, 0x68b9, 0x5c8a, 0x1b61, 0x6c59, 0x5707, 0x09e6, 0x1fb9, 0x2ad3, 0x76d4, 0x3113, 0x7c7e, 0x11e0, 0x6c70 }; // Flag前缀(用于memcmp验证) const char TARGET[] = "CatCTF"; #define TARGET_LEN 6 // ===================== 核心函数(完全对齐你的参考代码) ===================== /** * LCG随机数生成(对应sub_62B5,直接更新全局种子pass) */ int getPass() { pass = 1103515245 * pass + 12345; return (pass >> 10) & 0x7FFF; } /** * 计算数字位数(纯数学方法,对齐你的charNum函数) */ int charNum(int count) { int i = 0; while (count) { count /= 10; i++; } return i; } /** * 可见字符判断(对齐你的isRightFlag函数) */ bool isRightFlag(char a1) { return (a1 & 0x7Fu) <= 0x7E && (a1 & 0x7Fu) > 0x20; } // ===================== 主函数(核心解密逻辑) ===================== int main() { int count = 0; // 对应dword_108E0,次数X初始值0 unsigned char flag[51] = {0}; // 解密结果(50字符+结束符) // 无限循环,仅找到Flag时退出 while (1) { // 步骤1:解密encryptFlag数组(逐元素异或随机数) for (int i = 0; i < 50; i++) { encryptFlag[i] ^= getPass(); // 直接对dword数组异或,无需预处理 // 筛选可见字符,非可见设为空格(32) if (isRightFlag(encryptFlag[i])) { flag[i] = encryptFlag[i] & 0xff; // 取低字节,和提取低字节效果一致 } else { flag[i] = 32; } } flag[50] = '\0'; // 字符串结束符 // 步骤2:高效验证Flag(memcmp对比前6字节,对齐你的逻辑) if (memcmp(TARGET, flag, TARGET_LEN) == 0) { puts(flag); // 仅找到Flag时输出,无冗余IO break; } // 步骤3:更新次数和种子(核心!完全对齐你的更新逻辑) count += 1; pass += 41; // printf固定长度41 pass += charNum(count); // 次数位数增量 } return 0; }
3. 代码关键优化点
- 手动计算 printf 返回值(41 + X 的位数);
- 严格还原 LCG 的
>>10 & 0x7FFF逻辑,确保随机数和原程序一致; - 按
&0x7F限定 ASCII 范围,匹配原程序的字符筛选逻辑。
六、阶段 5:运行代码与验证结果
1. 运行代码
- 时长:因需循环约 1 亿次,速度较慢。
2. 最终结果
运行后会输出:
CatCTF{Fly1NG_NyAnC4t_Cha5eS_the_FL4G_in_The_Sky}
浙公网安备 33010602011771号