本地模型自动出逆向题,编译验证形成闭环
开源工具 SlothCTF 用本地开放权重模型生成 C 语言逆向题,并自动编译、运行和验证,输出源码、二进制及解题文档。作者报告 20 个题目均通过验证,同时指出让模型计算隐藏字节曾频繁出错。
完整中文译文
Hacktoberfest 开源 AI 挑战赛第 3 周:“慢而稳”参赛作品 🦥
这是我提交给 Hacktoberfest 开源 AI 挑战赛第 3 周的作品,主题是“慢而稳”。🦥
项目仓库:garanews/slothctf
手工编写逆向工程 CTF 题目很烦琐:你得构思方案、写 C 代码、编译、写求解脚本,再反复确认确实能拿到 flag,还得确保 strings 不会直接泄露答案……每道题都得来一遍。
我想要的是,输入一句话,就能得到一道已经完成并验证通过的题目:
python main.py generate --name xor_demo \
--prompt "Create an easy reverse engineering challenge with XOR encryption" \
--model qwen2.5-coder:32b
SlothCTF 做的正是这件事。本地运行的开放权重模型用 C 写出整道题;工具负责编译、运行,并在我查看之前验证它确实能正常工作。我把一直想做的 20 个题目创意交给它,最终得到了 20/20 道有效题目——每道题都有源码、编译好的二进制文件、面向选手的 README,以及面向出题人的解题说明。现在,那些缓慢又无聊的工作会在我喝咖啡的时候自动完成。倒是很符合国际树懒日的气质。
一个没人提醒你的坑:LLM 算不对字节运算
最初的设计里,我让模型包办一切,包括计算隐藏的字节——“把 flag 与 0x42 做 XOR 运算,再存成数组”。我试过的所有模型都算错了,从 Gemma 3 4B 到 Qwen2.5-Coder 14B,无一例外。它会声明 flag 是 sloth{easy_reverse},然后输出一个解码后却是 LCTE… 的数组。LLM 就是没法可靠地在脑子里对 20 个字节逐个计算 flag[i] ^ key。
这和其他人遇到的“LLM 在逆向工程中自信地产生幻觉”是同一个问题——可以看看 qisanxi 的《遏制 LLM 在逆向工程中的幻觉:认识 Reverify》;从 CTF 出题的角度,也可以看看 aleff 的《NiceTryGPT:少一点模式匹配,多一点真正的破解》。
解决办法:让机器做算术
于是,我不再要求模型计算任何东西,而是让它写两个 C 程序,再交给机器运行:
model → encoder.c (organizer tool) + challenge.c (has a placeholder for the bytes)
→ compile & run encoder on the secret → EXACT bytes (machine math)
→ inject bytes into challenge.c, compile, run → behaviour check
→ decode: stdout == flag | check: right input accepted, wrong rejected
→ strings(binary) must NOT contain the plaintext secret
↺ any failure is fed back to the model for another attempt
模型负责构思方案、编写代码;它不擅长的精确字节运算,则交给编译器完成。不需要模板,也不需要为每种方案堆一串 if 分支。下面是一组实际生成的程序,使用 XOR:
/* organizer/encoder.c — run on the flag to produce the bytes */
int main(int argc, char **argv) {
for (char *p = argv[1]; *p; ++p) printf("0x%02x,", (unsigned char)(*p ^ 0x42));
}
/* src/challenge.c — the player's binary; the machine filled in the array */
static unsigned char data[] = { 0x31, 0x2e, 0x2d, 0x36, 0x2a, 0x39, /* … */ };
int main(void) { for (size_t i = 0; i < sizeof data; ++i) putchar(data[i] ^ 0x42); }
两类题目
decode——二进制程序解码内嵌的数组,然后打印 flag。
check——二进制程序读取 stdin,只接受正确的秘密值,可以实现密码校验、哈希、密码算法、状态机、反调试关卡等。
第二种模式覆盖了经典的“破解二进制程序”类题目。验证器会分别输入真正的秘密值和一个错误值,确认前者必须被接受、后者必须被拒绝,同时确认 strings 的输出里不会出现明文。
我的 20 条 prompt 全部经过了独立复验——每道题都会重新编译、运行、检查 flag,并接受 strings 检查:
$ for n in generated/*; do python main.py validate --dir "$n"; done
xor_demo OK state_machine_demo OK
obfuscation_demo OK func_pointers_demo OK
anti_debug_demo OK constructor_demo OK
custom_b64_demo OK fnv1a_hash_demo OK
tea_crypto_demo OK sbox_crypto_demo OK
rc4_cipher_demo OK dynamic_imports_demo OK
junk_code_demo OK math_equations_demo OK
multithread_check_demo OK custom_vm_demo OK
timing_attack_demo OK anti_vm_demo OK
self_modifying_demo OK seh_exception_demo OK
VALID 20/20
一道 decode 题目的完整示例:
$ cat generated/xor_demo/challenge.json
{ "title": "XORed Flag", "difficulty": "easy",
"flag": "sloth{easy_xor}", "challenge_kind": "decode" }
$ ./generated/xor_demo/bin/challenge
sloth{easy_xor}
一道 check 题目,也就是反调试题——输入正确就成功,输入错误就失败,而且二进制文件里始终没有明文秘密值:
$ echo 'sloth{DebugMeNot}' | ./generated/anti_debug_demo/bin/challenge
Access Granted
$ echo 'wrongpass' | ./generated/anti_debug_demo/bin/challenge
Denied
$ strings ./generated/anti_debug_demo/bin/challenge | grep DebugMeNot
(nothing — the plaintext secret never appears in the binary)
几条让我多试了几次才学会的经验
告诉模型,代码会在哪里编译。Windows/MinGW 上没有 ptrace(Linux),也没有 MSVC 的 __try/__except。在 prompt 里加上一行“BUILD TARGET: Windows/MinGW, no ptrace/SEH”,反调试题和异常处理题就从验证失败变成了首次尝试即通过,模型转而使用了 IsDebuggerPresent。
哈希和分组密码是难点。FNV-1a 和 TEA 需要两份在字节层面完全一致的实现,也就是 encoder 和 challenge,两者的结果必须吻合。解决办法有两个:输出目标数据时,每个 token 只放一个字节,不要把一个 32 位字作为单个 token 打印出来——关于这个坑,也值得看看 nearshi 的《用 Ollama 和 Python 干净地提取 JSON》;另外,要求模型把变换函数原封不动地复制到两个程序里。这样,我拿下了 20 道题里最后剩下的 2 道。
为什么开放创新在这里很重要
这套方案能成立,靠的就是开放和本地运行:
它通过 Ollama,在开放权重模型上离线运行。不需要云端 API,没有按调用计费的成本,题目创意也不会离开我的机器——当这些“题目”本身就是安全任务时,这一点很重要。
我可以自由更换模型。我亲眼看着完全相同的流水线在 4B 模型上失败,又在 32B 模型上成功;只有模型掌握在自己手里,才能做这样的实验。(如果想按 VRAM 容量选择本地编程模型,mrsaynothing 的《最适合编程的本地 LLM:8GB 到 24GB VRAM 的选择》是一份不错的指南。)
“慢而稳”正是核心。模型可以花上几分钟,尝试好几次,因为确定性的验证器会抓住每一个错误。我不需要它快,我需要它正确,而且等着它的人也不是我。
在借来的 A100 上运行 Qwen2.5-Coder 32B,我的 20/20 条 prompt 都生成了有效且通过验证的题目——包括 XOR、自定义 Base64、S-box、RC4、TEA、FNV-1a、字节码 VM、状态机、函数指针校验、反调试、计时侧信道等。小模型不太能遵守这套双程序协议;32B 是个恰到好处的选择。
这也引出了我唯一一条认真的工程建议:很遗憾,要让本地 32B 模型的迭代体验舒服起来,最便宜的办法就是给自己买一张本地 A100。🦥💸(听说配有 512 GB RAM 的工作站也能跑,就是慢一点。树懒表示赞同。)
git clone https://github.com/garanews/slothctf
cd slothctf
python main.py generate --name demo --prompt "a reverse challenge with a custom cipher" \
--model qwen2.5-coder:32b --ollama-url http://localhost:11434
python main.py validate --dir generated/demo
所有内容都在 garanews/slothctf 仓库里:生成器、自验证循环、两类题目、远程 Ollama 支持,以及测试。
下一步:做一个带小型数据库的 GUI,按类型和难度浏览生成的题目,并支持其他经典 CTF 类型,包括 pcap、内存转储、磁盘取证、日志和 OSINT。
如需进一步处理,可以考虑屏蔽此人,以及/或者举报滥用行为。