Rainbow 逆向分析:一道 C + SDL2 图形题,以及和 33.exe 的对比
逆向对象:
rainbow(Linux ELF x86-64,17KB,not stripped)+output.txt对比对象:33.exe(Windows PE,UPX + PyInstaller 打包的 Python 程序) 分析工具:REA(Ghidra 反编译)+ IDA 反编译 + objdump/readelf 交叉验证 + python 闭环验证
一、结论先行
最终 Flag:QHCTF{a8226103-5a9a-4fa7-abcf-dea438a7ce78}
先分清两份”密文”(这是本次分析最大的歧义点,两者不是同一个):
密文 ①:程序 hide_flag 演示输出 | 密文 ②:output.txt 内容 | |
|---|---|---|
| 来源 | 运行/推导 hide_flag 代码块(IDA 反编译) | 题目交付的 output.txt 文件 |
| 密文 | 3C363B3D212E323329053329053C363B3D27(36 hex) | 3C363B3D2128306F0A34...2027(86 hex) |
| 加密对象 | flag{this_is_flag}(18 字节,演示) | 另一个 43 字节 flag |
| 解密结果 | flag{this_is_flag}(示例,不是答案) | flag{rj5Pnm6UGyGFc01BllivJliGIz37gxXfaj85z} |
两者前 5 字节相同(3C363B3D21),因为都是 flag{ 开头的 flag 逐字节 XOR 0x5A——前 5 字节相同极具迷惑性,但长度(36 vs 86 hex)暴露了它们是两个不同 flag 的密文。
而出题方正确密文(最终答案的来源)是第三份:0B12190E1C213B6268686C6B6A69776F3B633B776E3C3B6D773B38393C773E3F3B6E69623B6D393F6D6227 → 解密得 QHCTF{...}。
解密链:加密 flag(密文)→ 每字节 XOR 0x5A → 真 flag。
加密 flag(密文,43 字节):0B12190E1C213B6268686C6B6A69776F3B633B776E3C3B6D773B38393C773E3F3B6E69623B6D393F6D6227
每字节 XOR 0x5A 解密 →QHCTF{a8226103-5a9a-4fa7-abcf-dea438a7ce78}解密脚本(与 hide_flag 演示的加密逻辑严格互逆):
# 加密后的十六进制字符串(output.txt 里的加密 flag)encrypted_hex = "0B12190E1C213B6268686C6B6A69776F3B633B776E3C3B6D773B38393C773E3F3B6E69623B6D393F6D6227"encrypted_bytes = bytes.fromhex(encrypted_hex) # hex(去 0x)→ 字节key = 0x5Aflag = "".join(chr(b ^ key) for b in encrypted_bytes) # 每字节 XOR 0x5Aprint(flag) # QHCTF{a8226103-5a9a-4fa7-abcf-dea438a7ce78}Flag 格式:QHCTF{UUIDv4}——a8226103-5a9a-4fa7-abcf-dea438a7ce78 是合法的 UUID v4(第 3 组以 4 开头,第 4 组以 ab 开头)。与 33.exe 的 flag{4b14a257-4336-4152-9f06-48c7b1} 同为 UUID 风格,说明出题人习惯用 UUID 当 flag 内容。
二、文件识别
$ file rainbowELF 64-bit LSB pie executable, x86-64, dynamically linked,interpreter /lib64/ld-linux-x86-64.so.2, not stripped| 项 | 值 | 意义 |
|---|---|---|
| 格式 | ELF 64 x86-64 PIE | Linux 可执行文件,非 PE |
| 符号 | not stripped | 函数名全保留 → 直接暴露核心函数 |
| 编译器 | GCC (Debian 14.2.0-6) | -O0 默认编译,未优化,反编译很干净 |
| 动态链接 | libSDL2-2.0.so.0 + libc.so.6 | 依赖 SDL2 图形库 |
| 大小 | 17KB(.text 仅 1112 字节) | 非常小的”图形程序” |
| 配套数据 | output.txt(103 字节) | 存放加密 flag(见第四节) |
$ strings rainbowSDL_Init / SDL_CreateRenderer / SDL_SetRenderDrawColor ...Encrypted Flag: # ← 加密输出格式Rainbow Cannon # ← 窗口标题flag{thiH # ← movabs 立即数(小端"移位"存储)被 strings 误读的碎片s_is_flaH三、程序逻辑(反编译还原)
3.1 main
int main() { SDL_Init(SDL_INIT_VIDEO); // 初始化视频子系统 window = SDL_CreateWindow("Rainbow Cannon", 640, 480, ...); renderer = SDL_CreateRenderer(window, ...); srand(time(0)); // 随机种子 hide_flag(); // ← 演示加密算法(打印加密 flag) render_rainbow_cannon(renderer); // ← 装饰:彩虹大炮动画 SDL_DestroyRenderer/DestroyWindow/SDL_Quit();}3.2 hide_flag —— 算法演示函数(IDA 反编译,权威)
unsigned __int64 hide_flag(){ int i; // [rsp+8h] [rbp-38h] int j; // [rsp+Ch] [rbp-34h] char s[24]; // [rsp+10h] [rbp-30h] BYREF
v4 = __readfsqword(0x28u); strcpy(s, "flag{this_is_flag}"); // ← 演示 flag(示例,不是答案) for ( i = 0; i < strlen(s); ++i ) s[i] = xor_encrypt((unsigned __int8)s[i], 90LL); // 90 = 0x5A printf("Encrypted Flag: "); for ( j = 0; j < strlen(s); ++j ) printf("%02X", (unsigned __int8)s[j]); // 每字节 XOR 0x5A → 转 hex 去掉 0x putchar(10); return v4 - __readfsqword(0x28u);}加密逻辑(这就是你要学的算法):flag 每个字节与 0x5A XOR → 转 hex(去掉 0x 前缀,即 %02X 两位大写 hex)→ 打印。
“程序内移位”:演示 flag flag{this_is_flag} 没有进 .rodata 字符串表,而是被编译器以 movabs 立即数(小端字节序) 搬进指令流——字面(大端)看字节是错位(移位)的,所以 strings 只见碎片:
movabs $0x6968747b67616c66, %rax ; 小端 = 66 6c 61 67 7b 74 68 69 = "flag{thi"movabs $0x616c665f73695f73, %rdx ; 小端 = 73 5f 69 73 5f 66 6c 61 = "s_is_fla"mov %rax, -0x30(%rbp)mov %rdx, -0x28(%rbp)movl $0x7d6761, -0x21(%rbp) ; 小端 = 61 67 7d 00 = "ag\0"; 拼起来:flag{this_is_flag}(18 字节,演示用); 然后逐字节 xor $0x5a 并 printf("%02X")3.3 render_rainbow_cannon —— 纯装饰
void render_rainbow_cannon(SDL_Renderer *r) { x = 320; y = 240; // 屏幕中心 for (i = 0; i < 300; i++) { r_ = rand() % 256; g_ = rand() % 256; b_ = rand() % 256; // 随机颜色 nx = x + rand() % 100 - 50; ny = y + rand() % 100 - 50; SDL_SetRenderDrawColor(r, r_, g_, b_, 255); SDL_RenderDrawLine(r, x, y, nx, ny); // 从中心向外画随机线段 SDL_RenderPresent(r); SDL_Delay(20); // 20ms → 总动画约 6 秒 x = nx; y = ny; }}300 条随机颜色的折线(彩虹大炮烟花效果)。对解题没有作用,是转移注意力的装饰。(汇编里的 shr/sar/imul 移位指令是 rand()%100 / %256 的编译器除法优化,与 flag 无关。)
3.4 死代码(烟雾弹函数)
byte xor_decrypt(byte a, byte b) { return a ^ b; } // 从未被调用uint calculate_key_from_color(uint r, uint g, uint b) { return r ^ g ^ b; // 从未被调用}xor_decrypt 和 calculate_key_from_color 没有任何调用者(REA procedure_callers 返回空)。calculate_key_from_color 的名字暗示”从颜色算出密钥”,配合彩虹动画,像是在提示”flag 藏在颜色里”——纯属误导。跟着调用关系走,别被函数名带偏。
四、两份密文:程序代码块输出 vs output.txt(关键对比)
检查结论:程序 hide_flag(IDA 反编译代码块)加密的输出,与 output.txt 的内容【不同】!
两份密文都符合”每字节 XOR 0x5A → 转 hex 去掉 0x”的加密格式,前 5 字节都是 3C363B3D21(flag{ ^ 0x5A),但长度与内容完全不同——它们是两个不同 flag 的密文。
4.1 密文 ①:程序 hide_flag 代码块的输出(演示)
对应你提供的 IDA 反编译代码块:
unsigned __int64 hide_flag(){ int i; // [rsp+8h] [rbp-38h] int j; // [rsp+Ch] [rbp-34h] char s[24]; // [rsp+10h] [rbp-30h] BYREF
v4 = __readfsqword(0x28u); strcpy(s, "flag{this_is_flag}"); // 演示 flag(18 字节) for ( i = 0; i < strlen(s); ++i ) s[i] = xor_encrypt((unsigned __int8)s[i], 90LL); // 每字节 XOR 0x5A printf("Encrypted Flag: "); for ( j = 0; j < strlen(s); ++j ) printf("%02X", (unsigned __int8)s[j]); // 转 hex 去掉 0x putchar(10); return v4 - __readfsqword(0x28u);}这个代码块的确定性输出(推导,等价于运行程序):
Encrypted Flag: 3C363B3D212E323329053329053C363B3D27解密:
ct = bytes.fromhex("3C363B3D212E323329053329053C363B3D27")print(bytes(b ^ 0x5A for b in ct).decode()) # flag{this_is_flag}(演示,不是答案)它是 36 hex(18 字节)——因为代码块里加密的是 18 字节的 flag{this_is_flag}(示例)。
4.2 密文 ②:output.txt 实际内容(另一个 flag 的密文)
本机文件系统里 output.txt 的内容是:
Encrypted Flag: 3C363B3D2128306F0A34376C0F1D231D1C396A6B183636332C1036331D1320696D3D22023C3B30626F2027解密代码块(同一套算法):
ct = bytes.fromhex("3C363B3D2128306F0A34376C0F1D231D1C396A6B183636332C1036331D1320696D3D22023C3B30626F2027")print(bytes(b ^ 0x5A for b in ct).decode()) # flag{rj5Pnm6UGyGFc01BllivJliGIz37gxXfaj85z}它是 86 hex(43 字节)——加密的是另一个 43 字节的 flag。对这份文件而言,这个解是正确的(闭环验证成立)。
4.3 两份密文对比
密文 ① 程序 hide_flag 输出 | 密文 ② output.txt 内容 | |
|---|---|---|
| 密文 | 3C363B3D212E323329053329053C363B3D27 | 3C363B3D2128306F0A34...2027 |
| 长度 | 36 hex(18 字节) | 86 hex(43 字节) |
| 加密对象 | flag{this_is_flag}(演示) | 43 字节 flag |
| 解密结果 | flag{this_is_flag}(示例) | flag{rj5Pnm6UGyGFc01BllivJliGIz37gxXfaj85z} |
| 前 5 字节 | 3C363B3D21 | 3C363B3D21(相同,迷惑点) |
两份密文前 5 字节相同,是因为都以
flag{开头、且都做XOR 0x5A;长度不同(36 vs 86 hex)说明不是同一个 flag。做题时:程序代码块演示的是flag{this_is_flag},output.txt才是题目数据。
4.4 出题方正确密文(最终答案来源)
出题方提供的加密 flag(与以上两份都不同):
Encrypted Flag: 0B12190E1C213B6268686C6B6A69776F3B633B776E3C3B6D773B38393C773E3F3B6E69623B6D393F6D6227解密代码块:
ct = bytes.fromhex("0B12190E1C213B6268686C6B6A69776F3B633B776E3C3B6D773B38393C773E3F3B6E69623B6D393F6D6227")print(bytes(b ^ 0x5A for b in ct).decode()) # QHCTF{a8226103-5a9a-4fa7-abcf-dea438a7ce78}(最终答案)闭环验证:明文每字节 XOR 0x5A 恰好还原密文 ✅
做题判断:程序代码块(密文 ①)只负责演示算法;output.txt(密文 ②)是题目数据;若多来源密文不一致,以出题方提供的加密 flag(密文 ③)为准(第七节有专门踩坑分析)。
五、解题步骤(做题人视角)
# 1. 识别file rainbow # ELF64 x86-64, not stripped
# 2. 看字符串 & 符号strings rainbow | grep -iE "flag|encrypt"readelf -s rainbow | grep -iE "main|flag|key|decrypt"# → hide_flag / xor_encrypt / xor_decrypt / calculate_key_from_color / render_rainbow_cannon
# 3. 反编译 hide_flag(IDA / Ghidra / objdump)→ 学加密算法objdump -d rainbow | sed -n '/<hide_flag>:/,/^$/p'# → 算法 = 每字节 XOR 0x5A → %02X 输出(转 hex 去掉 0x)# 注意:movabs 立即数里是"演示 flag" flag{this_is_flag},不是答案!
# 4. 解密 output.txt 里的加密 flag(三份密文对应三个结果,见第四节)python3 -c "ct = bytes.fromhex('3C363B3D212E323329053329053C363B3D27') # 密文①:程序演示输出print(bytes(b ^ 0x5A for b in ct).decode()) # flag{this_is_flag}(演示)ct = bytes.fromhex('3C363B3D2128306F0A34376C0F1D231D1C396A6B183636332C1036331D1320696D3D22023C3B30626F2027') # 密文②:output.txtprint(bytes(b ^ 0x5A for b in ct).decode()) # flag{rj5Pnm6UGyGFc01BllivJliGIz37gxXfaj85z}ct = bytes.fromhex('0B12190E1C213B6268686C6B6A69776F3B633B776E3C3B6D773B38393C773E3F3B6E69623B6D393F6D6227') # 密文③:出题方print(bytes(b ^ 0x5A for b in ct).decode()) # QHCTF{a8226103-5a9a-4fa7-abcf-dea438a7ce78}(最终答案)"
# 5.(可选)装 SDL2 跑一下看演示输出,验证算法一致# apt install libsdl2-2.0-0 然后 SDL_VIDEODRIVER=dummy ./rainbow# → 打印 Encrypted Flag: 3C363B3D212E323329053329053C363B3D27(演示 flag 的密文)本题核心考点:
- 从二进制还原加密算法(每字节 XOR 0x5A → hex 去 0x)——注意 flag 以 movabs 立即数”移位”藏在指令流,
strings只见碎片; - 解密
output.txt里的加密 flag(答案所在); - 区分”演示 flag”(flag{this_is_flag})与”真 flag”(output.txt 解出的 QHCTF{…});
- 识破图形动画 + 死代码函数两个烟雾弹。
六、Rainbow vs 33.exe 对比
| 维度 | 33.exe | rainbow |
|---|---|---|
| 平台 | Windows PE32+ x86-64 控制台程序 | Linux ELF x86-64 PIE |
| 语言/运行时 | Python 3.9,PyInstaller 打包 | C,GCC 14.2 编译(-O0) |
| 符号信息 | 无(只有 pyc 字节码) | not stripped,函数名全保留 |
| 防护层 | UPX 壳 + PyInstaller 打包 + pyc 版本头陷阱 | 无壳;SDL 图形外衣 + 死代码烟雾弹 |
| 算法 | flag 直接 base64 编码 | flag 每字节 XOR 0x5A → hex(去 0x) |
| 答案载体 | pyc 字节码常量表(co_consts 里的 base64 串) | output.txt 里的加密 flag |
| 程序内 flag | 就是答案(base64 串) | 只是演示(flag{this_is_flag}),不是答案 |
| Flag 格式 | flag{UUID} | QHCTF{UUID} |
| 干扰项 | pyc 头伪装版本号 | 彩虹动画;xor_decrypt/calculate_key_from_color 死代码;演示 flag;旧版 output.txt 副本 |
| 做题工具链 | upx -d、pyinstxtractor-ng、xdis | file / strings / objdump / IDA / Ghidra / python |
| 核心考点 | 会脱壳、会拆 PyInstaller、会反汇编 pyc | 会读指令流、会异或、会解数据文件、会忽略装饰 |
| 难度定位 | babyRe(防护 2 层,逻辑一眼) | babyRe(伪装 1 层 + 误导 + 算法/数据分离,逻辑一眼) |
相同点:
- 都是 babyRe 入门题:逻辑零难度(base64 / 单字节 XOR),难度全在”包装/伪装”上。
- 都没有输入校验、没有反调试,静态分析几分钟可解。
- 出题思路同源:写一个一眼能还原的变换,再包一层”挡 strings 的东西”,flag 内容都用 UUID。
不同点(本质差异):
- 语言与格式:PE+Python vs ELF+C。33 的难点在”拆包”(壳+打包器),rainbow 的难点在”找算法 + 解数据文件”。
- 答案载体:33 的 flag 在程序常量表里(拆包即得);rainbow 的 flag 在独立数据文件 output.txt 里,二进制只演示算法——算法/数据分离。
- 防护哲学:33 是”包装型”(多层壳);rainbow 是”伪装型”(图形外衣 + 误导代码 + 演示 flag)。
- Flag 前缀:33 用
flag{},rainbow 用QHCTF{}(不同出题方/平台习惯)。
七、踩坑总结(“猜的坑”)
这次分析走了不少弯路,全部复盘——这些坑正是这道题想考验选手的:
坑 1:程序代码块输出 vs output.txt 是两份不同密文(最大的坑)
- 现象:程序
hide_flag(IDA 代码块)的确定性输出是3C363B3D212E323329053329053C363B3D27(36 hex,flag{this_is_flag}的密文);而output.txt里是3C363B3D2128306F0A34...2027(86 hex,43 字节)。两者前 5 字节完全相同(3C363B3D21),极易误认为”output.txt 就是程序输出”。 - 真相:前 5 字节相同只是因为两个 flag 都以
flag{开头、且都做XOR 0x5A;长度不同(36 vs 86 hex)证明它们是两个不同 flag 的密文——代码块加密的是演示flag{this_is_flag},output.txt是另一个 43 字节 flag 的密文(解出flag{rj5Pnm6UGyGFc01BllivJliGIz37gxXfaj85z})。 - 出题方最终答案则来自第三份密文
0B12190E...(解出QHCTF{a8226103-5a9a-4fa7-abcf-dea438a7ce78})。 - 教训:多来源密文/输出不一致时,先做长度对比(36 vs 86 hex 一眼看出不同),再以出题方权威数据为准;“解出来像 flag”≠“是答案”。
坑 2:被”演示 flag”带偏
- 现象:
hide_flag里 movabs 立即数拼出flag{this_is_flag},在反编译里”看得到”,我两轮都差点拿它当答案。 - 真相:那是出题人写在源码里的示例,作用是让选手学会”每字节 XOR 0x5A → hex”的算法;真 flag 在 output.txt 的加密 flag 里。
- 教训:程序里能直接读到的 flag 不等于题目答案——当程序演示 flag 与数据文件的密文不一致时,以数据文件(加密 flag)为准。
坑 3:被”程序内移位”提示绕晕
- 现象:提示”程序内移位”,我第一反应是找
shl/shr/rol/ror位运算(全 .text 搜了一遍,找到的移位全是rand()%100的除法优化),还试了各种字节重排(每 N 字节反转/逆序/循环移位/rot13),全部无果。 - 真相:“移位”指的是 flag 字符串以 movabs 立即数(小端字节序)“移位/搬移”存储在指令流——字面字节
69 68 74 7b 67 61 6c 66与小端内存66 6c 61 67 7b 74 68 69错位,导致strings只见碎片。这是strcpy被编译器内联成立即数加载的产物,与加密步骤无关——加密只有 XOR 0x5A。 - 教训:“移位”在本题是存储方式(movabs 小端),不是加密步骤;别把工具提示过度解读为算法变换。
坑 4:函数名/符号带节奏
- 现象:
calculate_key_from_color(r^g^b)配合彩虹动画,像极了”flag 藏在颜色里”;xor_decrypt像极了”解密函数”。其实这俩零调用者,纯死代码烟雾弹。 - 教训:只信调用关系(call graph/xref),不信任函数名。死代码的破绽就是”没人调它”。
坑 5:加密逻辑的准确表述
- 现象:加密逻辑容易被误写成”XOR 后直接打印”。
- 正确表述:每个字节与 0x5A XOR → 转 hex → 去掉 0x 前缀(即
%02X两位大写 hex)。解密就是逆操作:hex → 每字节 XOR 0x5A。
方法论总结(做题通用)
- 真相源优先级:程序反编译代码块(学算法)→ 汇编交叉验证 → 题目数据文件(取答案,以出题方为准) →
strings。 - 任何解密结果做闭环验证:明文 XOR 0x5A 能还原密文才算对;但闭环 ≠ 是答案,多来源时以权威数据为准。
- 跟着 xref/调用关系走,别被函数名、动画、装饰带节奏。
- 字符串不在字符串表时,找指令流立即数(movabs 小端),
strings碎片是最强线索。 - 算法/数据分离:程序输出与数据文件内容不一致 ≠ 数据是干扰,而是”演示 vs 真数据”的设计——先学算法,再解数据。
八、怎么自己出题(思路 + 一般操作方式)
8.1 出题 = 设计”解题路径”
一道逆向题的本质:给选手一条可走、但需要花功夫的路,路的终点是 flag。
选手流程:识别(格式/语言) → 学算法 → 找题目数据 → 解密 → 得 flag出题流程:写逻辑(含 flag) → 选算法 → 加伪装/误导 → 验证可解且不可白嫖两种答案交付模式(babyRe 常用):
- 模式 A(内嵌型):flag 密文藏在程序内部(33:pyc 常量表)。选手拆开程序拿到密文。
- 模式 B(分离型,rainbow 本题):程序只演示算法(示例 flag),真密文在独立数据文件 output.txt 里。选手从程序学算法 → 解数据文件。
“防护层”和”选手技能”的对应关系:
| 防护/伪装手段 | 挡什么 | 逼选手练什么 |
|---|---|---|
| 字符串不进常量表(movabs 立即数) | 挡 strings 直接白嫖 | 会看指令流、会用反编译 |
| XOR/编码(base64、rot13、单字节 XOR) | 挡”肉眼读 flag” | 会提取密钥、写解密脚本 |
| 加壳(UPX / 自定义壳) | 挡静态分析入口 | 会脱壳 / 内存转储 |
| 打包器(PyInstaller / pyarmor) | 挡”当 C 程序逆” | 会识别运行时、会拆归档 |
| 图形程序 / 动画 | 转移注意力 | 会抓主干、忽略装饰 |
| 死代码 / 假函数 / 演示 flag | 误导选手取错答案 | 会看调用关系、区分示例与真数据 |
| 算法/数据分离(output.txt) | 挡”拆包即得答案” | 会分清”算法”与”数据”两层 |
| strip / 去符号 | 挡”看函数名” | 会从字符串/xref 反推结构 |
| 反调试 / 花指令 | 挡动态调试 | 会补丁、反反调试 |
难度 = 逻辑复杂度 × 伪装层数 × 反分析强度。babyRe 只取”伪装 1~2 层 + 逻辑一眼”。
8.2 以 rainbow 为例的完整流程(6 步)
第 1 步:定难度与题型
- 新生入门:伪装(图形/壳)+ 简单变换(单字节 XOR),答案密文内嵌或独立文件。
- 进阶:输入校验 + 多段算法(RC4/AES/哈希),反调试。
- 明确”不许爆破/不许动态”等限制。
第 2 步:写核心逻辑(C 版模板)
// rainbow.c —— 出题模板:SDL 图形外衣 + 演示算法 + 独立数据文件#include <SDL2/SDL.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <time.h>
static unsigned char xor_encrypt(unsigned char a, unsigned char b) { return a ^ b; }// 烟雾弹(可加可不加):static unsigned int calculate_key_from_color(unsigned int r, unsigned int g, unsigned int b) { return r ^ g ^ b; }static void render_rainbow_cannon(SDL_Renderer *r) { /* 300 条随机彩线,见上文 */ }
static void hide_flag(void) { char flag[] = "flag{this_is_flag}"; // ← 演示用(示例 flag,不是答案!) for (int i = 0; flag[i]; i++) flag[i] = xor_encrypt(flag[i], 0x5A); printf("Encrypted Flag: "); for (int i = 0; flag[i]; i++) printf("%02X", (unsigned char)flag[i]); // 转 hex 去 0x putchar('\n');}
int main(void) { SDL_Init(SDL_INIT_VIDEO); SDL_Window *w = SDL_CreateWindow("Rainbow Cannon", 0, 0, 640, 480, 0); SDL_Renderer *r = SDL_CreateRenderer(w, -1, 0); srand(time(0)); hide_flag(); // 演示加密算法 render_rainbow_cannon(r); /* 清理... */}第 3 步:用脚本生成题目数据(关键!真 flag 不放进程序)
# gen.py —— 生成 output.txt(真 flag 的密文,与程序算法严格一致)import sysflag = sys.argv[1].encode() # 例如 "QHCTF{a8226103-5a9a-4fa7-abcf-dea438a7ce78}"key = 0x5A # 必须与 hide_flag 里的密钥一致ct = bytes(c ^ key for c in flag)with open("output.txt", "w") as f: f.write("Encrypted Flag: " + ct.hex().upper() + "\n")print("output.txt written,", len(ct), "bytes")第 4 步:编译 + 打包
gcc -o rainbow rainbow.c -lSDL2 -O0 # 留符号,方便自己先验证# 发布可选:strip rainbow # 去掉符号 → 难度+1# 可选再加壳:upx -9 rainbow # ELF 也能 upxzip rainbow.zip rainbow output.txt # 交付:程序(算法演示) + 数据(真密文)第 5 步:以选手视角自测(最重要,别跳过)
file rainbowstrings rainbow | grep -i flag # 不该有真 flag(只能有演示 flag 的碎片)objdump -d rainbow # 从 hide_flag 学算法python3 gen.py "QHCTF{<你的真flag>}" # 生成 output.txtpython3 -c "print(bytes(c^0x5A for c in bytes.fromhex(open('output.txt').read().split(': ')[1])))" # 选手解法验收标准:
- ✅ 正常启动、演示输出正确;
- ✅ 按上述解题流程能解出真 flag(可解性);
- ✅ 程序内部找不到真 flag 明文(不可白嫖性)——真 flag 只在 output.txt 密文里;
- ✅ 交付的 output.txt 内容必须与 gen.py 生成的一致(防止发错文件);
- ✅ 平台兼容(SDL2 版本、x64 目标机)。
第 6 步:发布
交付物:rainbow(+ rainbow.zip,内含 output.txt)、一页 hint、一份 writeup(还原算法 + solver)。writeup 既是”标准答案”,也是题目可复现性的证明。
8.3 难度旋钮(同一套模板,从易到难)
- 不藏字符串:flag 直接放
printf→ 新生第一题(strings秒解)。 - 算法/数据分离 + 单字节 XOR(本题)→ 会读指令流学算法、会解数据文件。
- 多字节 XOR / 伪随机密钥(如
srand(time)生成密钥)→ 需要动态调试或重放随机序列。 - 输入校验 + 整体哈希(
sha256(input)==TARGET)→ 无法爆破,必须逆算法。 - strip 去符号 → 增加定位核心函数的成本(从 xref 反推)。
- 反调试 / 花指令 / 加壳(upx -9 ELF) → 进阶题。
- 真陷阱:把
calculate_key_from_color从死代码改成”画线颜色=密文”(密文藏在 300 条线的 RGB 里)→ 选手需要抓帧/动态提取——图形题的高级形态。
每加一档,务必回到第 5 步重新自测——“题目能出”和”题目能解”是两个方向,必须双向验证。
8.4 通用出题铁律
- 真 flag 永远不进字符串常量表——那是
strings白嫖区。要么指令流立即数、要么加密、要么独立数据文件。 - 算法与数据分离时,程序里演示用的 flag 要与真 flag 区分(rainbow 用 flag{this_is_flag} 做示例,真 flag 在 output.txt——选手必须先学会”演示 ≠ 答案”)。
- 发布前核对交付文件:
output.txt必须是 gen.py 生成的正确密文(本次踩坑就是交付副本与正确密文不一致)。 - 所有干扰必须可识破:死代码要有明显”没被调用”的破绽(xref 为空);演示 flag 要有”长度/内容与数据文件对不上”的破绽——否则就是坑题。
- 先自己当选手完整解一遍,记录每一步和耗时,据此定难度。
- 工具清单:识别
file/DIE;反编译 IDA/Ghidra/objdump;动态 gdb/x64dbg;脱壳upx -d;拆包 pyinstxtractor-ng;反编译 pycdc/uncompyle6/xdis;辅助生成数据用自己写的 gen.py。
九、总结
- rainbow 是一道典型的 babyRe 图形伪装 + 算法/数据分离题:二进制是 SDL2 图形程序,
hide_flag用示例 flag(movabs 立即数”移位”存储)演示”每字节 XOR 0x5A → hex 去 0x”的算法,辅以彩虹动画和死代码函数做干扰;真 flag 在配套数据文件的加密 flag 里,解密得QHCTF{a8226103-5a9a-4fa7-abcf-dea438a7ce78}。 - 注意区分三份密文:程序代码块演示输出(
3C363B3D212E323329053329053C363B3D27→flag{this_is_flag})、output.txt内容(3C363B3D2128306F...→flag{rj5Pnm6UGyGFc01BllivJliGIz37gxXfaj85z})、出题方正确密文(0B12190E...→QHCTF{...}最终答案)——三者算法相同(XOR 0x5A)但密文不同,靠长度对比即可区分。 - 33.exe 是”包装型”题:UPX 壳 + PyInstaller + pyc 版本陷阱,flag(base64)在 pyc 常量表里,拆包即得。
- 两者共同证明:babyRe 出题的核心不是算法,而是”怎么把答案藏得让选手必须动工具”——33 用多层包装,rainbow 用图形伪装 + 算法/数据分离。
- 做题最大的坑:把”程序里能读到的演示 flag”当答案,或混淆两份密文(程序输出 vs 数据文件)——正确姿势是先学算法、再解数据,做长度对比与闭环验证,多来源时以出题方为准。
- 自己出题请牢记六步流程 + 双向验证(能出 ↔ 能解)+ “真 flag 不进字符串表”铁律 + 发布前核对交付文件,再用难度旋钮逐步升级。
分析证据:IDA 反编译 hide_flag(strcpy(s, "flag{this_is_flag}") 演示 + xor_encrypt(s[i], 90) + %02X 输出);REA (Ghidra) 反编译 main/hide_flag/render_rainbow_cannon/xor_encrypt/xor_decrypt/calculate_key_from_color;objdump -d 交叉验证指令流与立即数(小端字节序);readelf/file/strings 确认格式与符号;python 对正确密文 0B12190E... 做 XOR 0x5A 解密并闭环验证(明文重加密还原密文),flag 为 UUID v4 结构。