5571 字
28 分钟
Rainbow 逆向分析:C+SDL2 图形题,以及和 33.exe 的对比

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 = 0x5A
flag = "".join(chr(b ^ key) for b in encrypted_bytes) # 每字节 XOR 0x5A
print(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 内容。


二、文件识别#

Terminal window
$ file rainbow
ELF 64-bit LSB pie executable, x86-64, dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, not stripped
意义
格式ELF 64 x86-64 PIELinux 可执行文件,非 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(见第四节)
Terminal window
$ strings rainbow
SDL_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_decryptcalculate_key_from_color 没有任何调用者(REA procedure_callers 返回空)。calculate_key_from_color 的名字暗示”从颜色算出密钥”,配合彩虹动画,像是在提示”flag 藏在颜色里”——纯属误导。跟着调用关系走,别被函数名带偏


四、两份密文:程序代码块输出 vs output.txt(关键对比)#

检查结论:程序 hide_flag(IDA 反编译代码块)加密的输出,与 output.txt 的内容【不同】!

两份密文都符合”每字节 XOR 0x5A → 转 hex 去掉 0x”的加密格式,前 5 字节都是 3C363B3D21flag{ ^ 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 内容
密文3C363B3D212E323329053329053C363B3D273C363B3D2128306F0A34...2027
长度36 hex(18 字节)86 hex(43 字节)
加密对象flag{this_is_flag}(演示)43 字节 flag
解密结果flag{this_is_flag}(示例)flag{rj5Pnm6UGyGFc01BllivJliGIz37gxXfaj85z}
前 5 字节3C363B3D213C363B3D21(相同,迷惑点)

两份密文前 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(密文 ③)为准(第七节有专门踩坑分析)。


五、解题步骤(做题人视角)#

Terminal window
# 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.txt
print(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 的密文)

本题核心考点

  1. 从二进制还原加密算法(每字节 XOR 0x5A → hex 去 0x)——注意 flag 以 movabs 立即数”移位”藏在指令流,strings 只见碎片;
  2. 解密 output.txt 里的加密 flag(答案所在);
  3. 区分”演示 flag”(flag{this_is_flag})与”真 flag”(output.txt 解出的 QHCTF{…});
  4. 识破图形动画 + 死代码函数两个烟雾弹。

六、Rainbow vs 33.exe 对比#

维度33.exerainbow
平台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、xdisfile / strings / objdump / IDA / Ghidra / python
核心考点会脱壳、会拆 PyInstaller、会反汇编 pyc会读指令流、会异或、会解数据文件、会忽略装饰
难度定位babyRe(防护 2 层,逻辑一眼)babyRe(伪装 1 层 + 误导 + 算法/数据分离,逻辑一眼)

相同点

  • 都是 babyRe 入门题:逻辑零难度(base64 / 单字节 XOR),难度全在”包装/伪装”上。
  • 都没有输入校验、没有反调试,静态分析几分钟可解。
  • 出题思路同源:写一个一眼能还原的变换,再包一层”挡 strings 的东西”,flag 内容都用 UUID。

不同点(本质差异)

  1. 语言与格式:PE+Python vs ELF+C。33 的难点在”拆包”(壳+打包器),rainbow 的难点在”找算法 + 解数据文件”。
  2. 答案载体:33 的 flag 在程序常量表里(拆包即得);rainbow 的 flag 在独立数据文件 output.txt 里,二进制只演示算法——算法/数据分离
  3. 防护哲学:33 是”包装型”(多层壳);rainbow 是”伪装型”(图形外衣 + 误导代码 + 演示 flag)。
  4. 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。

方法论总结(做题通用)#

  1. 真相源优先级:程序反编译代码块(学算法)→ 汇编交叉验证 → 题目数据文件(取答案,以出题方为准)strings
  2. 任何解密结果做闭环验证:明文 XOR 0x5A 能还原密文才算对;但闭环 ≠ 是答案,多来源时以权威数据为准
  3. 跟着 xref/调用关系走,别被函数名、动画、装饰带节奏。
  4. 字符串不在字符串表时,找指令流立即数(movabs 小端),strings 碎片是最强线索。
  5. 算法/数据分离:程序输出与数据文件内容不一致 ≠ 数据是干扰,而是”演示 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 sys
flag = 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 步:编译 + 打包

Terminal window
gcc -o rainbow rainbow.c -lSDL2 -O0 # 留符号,方便自己先验证
# 发布可选:strip rainbow # 去掉符号 → 难度+1
# 可选再加壳:upx -9 rainbow # ELF 也能 upx
zip rainbow.zip rainbow output.txt # 交付:程序(算法演示) + 数据(真密文)

第 5 步:以选手视角自测(最重要,别跳过)

Terminal window
file rainbow
strings rainbow | grep -i flag # 不该有真 flag(只能有演示 flag 的碎片)
objdump -d rainbow # 从 hide_flag 学算法
python3 gen.py "QHCTF{<你的真flag>}" # 生成 output.txt
python3 -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 难度旋钮(同一套模板,从易到难)#

  1. 不藏字符串:flag 直接放 printf → 新生第一题(strings 秒解)。
  2. 算法/数据分离 + 单字节 XOR(本题)→ 会读指令流学算法、会解数据文件。
  3. 多字节 XOR / 伪随机密钥(如 srand(time) 生成密钥)→ 需要动态调试或重放随机序列。
  4. 输入校验 + 整体哈希sha256(input)==TARGET)→ 无法爆破,必须逆算法。
  5. strip 去符号 → 增加定位核心函数的成本(从 xref 反推)。
  6. 反调试 / 花指令 / 加壳(upx -9 ELF) → 进阶题。
  7. 真陷阱:把 calculate_key_from_color 从死代码改成”画线颜色=密文”(密文藏在 300 条线的 RGB 里)→ 选手需要抓帧/动态提取——图形题的高级形态。

每加一档,务必回到第 5 步重新自测——“题目能出”和”题目能解”是两个方向,必须双向验证

8.4 通用出题铁律#

  1. 真 flag 永远不进字符串常量表——那是 strings 白嫖区。要么指令流立即数、要么加密、要么独立数据文件。
  2. 算法与数据分离时,程序里演示用的 flag 要与真 flag 区分(rainbow 用 flag{this_is_flag} 做示例,真 flag 在 output.txt——选手必须先学会”演示 ≠ 答案”)。
  3. 发布前核对交付文件output.txt 必须是 gen.py 生成的正确密文(本次踩坑就是交付副本与正确密文不一致)。
  4. 所有干扰必须可识破:死代码要有明显”没被调用”的破绽(xref 为空);演示 flag 要有”长度/内容与数据文件对不上”的破绽——否则就是坑题。
  5. 先自己当选手完整解一遍,记录每一步和耗时,据此定难度。
  6. 工具清单:识别 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}
  • 注意区分三份密文:程序代码块演示输出(3C363B3D212E323329053329053C363B3D27flag{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_flagstrcpy(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_colorobjdump -d 交叉验证指令流与立即数(小端字节序);readelf/file/strings 确认格式与符号;python 对正确密文 0B12190E... 做 XOR 0x5A 解密并闭环验证(明文重加密还原密文),flag 为 UUID v4 结构。