攻防世界逆向题 CatFly 完整解题思路与过程

一、题目背景与核心目标

1. 题目基础信息

  • 类型:Linux 平台逆向工程(RE)题,可执行程序无输入,仅输出滚动乱码和递增次数字符串;
  • 现象:运行程序后,顶部是乱码(加密 Flag),底部循环输出 You have nyaned for X times!(X 从 0 开始递增);
  • 核心目标:通过逆向分析程序逻辑,还原加密乱码对应的原始 Flag。

2. 逆向解题核心思路(通用)

image

二、阶段 1:程序初分析 —— 从现象提取线索(逆向第一步)

1. 运行程序,收集直观线索

  • 操作步骤:
    1. 给程序加执行权限:chmod +x CatFly
    2. 运行程序:./CatFly
    3. 记录现象:
      • 线索 1:乱码随次数 X 递增同步变化 → 乱码生成依赖 X;
      • 线索 2:X 从 0 开始递增 → 次数变量有明确初始值;
      • 线索 3:无输入仅输出 → Flag 加密逻辑在程序内部,无需外部输入参与。 
  • 逆向知识点:先运行后分析,避免直接陷入汇编代码,用现象缩小分析范围。

2. IDA 静态分析准备

  • 操作步骤:
    1. 用 IDA Pro 打开程序(64 位 Linux 程序选「ELF64 for x86-64」);
    2. 等待 IDA 完成反汇编,按F5生成 main 函数伪代码(核心);
    3. 打开字符串窗口(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(随机数种子)

  • 定位步骤:
    1. 在 main 函数伪代码中搜索「随机数」相关逻辑(或异或^操作);
    2. 找到调用sub_62B5(随机数生成函数)的地方,参数 / 返回值关联dword_E1E8
    3. 右键dword_E1E8 → 「Find cross references to」→ 找到初始化处:dword_E1E8 = 0x1106
  • 分析结论:dword_E1E8是随机数种子,初始值 = 0x1106,每次循环后会被更新。

(3)加密数组:e120(存储加密 Flag)

  • 定位步骤:
    1. 在伪代码中找到异或操作(^),操作数之一就是e120数组;
    2. 右键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位随机数
}
  • 逐行解释:
    1. 1103515245 * dword_E1E8 + 12345:经典 LCG 算法(A=1103515245,C=12345,M=2³¹);
    2. >> 10:右移 10 位,跳过低 10 位(LCG 低位数随机性差);
    3. & 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); // 筛选可见字符
}
  • 分析结论:
    1. 解密逻辑:e120元素 ^ 随机数 & 0x7F → 得到候选字符;
    2. 仅当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自增
  • 分析结论:
    1. printf 返回值 = 输出字符串长度 = 固定长度 41 + 次数 X 的位数(如 X=100,位数 3→返回 44);
    2. 种子更新依赖字符串长度 → 直接解密 e120 无法得到 Flag,必须模拟 X 递增 + 种子更新。
  • 逆向知识点:库函数(printf/scanf)的返回值常是隐藏关键,需跟踪变量更新路径。

四、阶段 3:关键数据提取 —— 从 IDA 获取解密所需数据

1. 提取 e120 数组(核心数据)

  • 提取步骤:
    1. 跳转到 e120 的.data段定义处,按D切换显示格式为byte(1 字节);
    2. 按小端序规则提取每个 dword 的低字节(小端序:低字节存低地址,解密仅用低字节):
      • IDA 显示 dword 值27FBh → 低字节0xFB
      • IDA 显示 dword 值27A4h → 低字节0xA4
    3. 提取 50 个低字节,整理为列表:
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
};
  • 逆向知识点: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}

 

posted @ 2026-01-25 10:25  夏了茶糜  阅读(0)  评论(0)    收藏  举报