如何逆向破解攻防世界CatFly题目?

摘要:一、题目背景与核心目标 1. 题目基础信息 类型:Linux 平台逆向工程(RE)题,可执行程序无输入,仅输出滚动乱码和递增次数字符串; 现象:运行程序后,顶部是乱码(加密 Flag),底部循环输出 You have nyaned for
一、题目背景与核心目标 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 前缀、提示语常藏于此。 三、阶段 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。 分析结论: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; 提取 50 个低字节,整理为列表: inte120[]={ 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}