news 2026/9/25 2:55:13

CTF逆向Hidden Key实战:AI辅助破解控制流平坦化与信号处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTF逆向Hidden Key实战:AI辅助破解控制流平坦化与信号处理

上周末打了一场线上CTF,有一道100分的逆向题叫Hidden Key,我一开始真没当回事。名字看着像是让你去字符串里翻一个藏起来的key,结果附件是个strip过的64位ELF,运行之后只有一句Usage: ./hidden_key <key>,输错就回一句Wrong key。strings扫了一圈,连flag的影子都没有,当时就意识到这题没那么简单。

最后解出来的答案倒是挺有意思:正确的key是k3y_h1dd3n_d34r,程序接受它之后会直接打印flag{k3y_h1dd3n_d34r}。真正让我想写这篇writeup的,不是flag本身,而是这次解题过程里我和AI配合得特别深:让AI帮我读反编译代码、识别隐藏逻辑、写还原脚本,最后再由我来验证和修正。这篇博文把这套流程完整拆开讲一遍,适合正在学CTF逆向的新手,也适合想了解大模型在二进制分析里到底能帮上多少忙的人。我会把题目分析、关键跳点、AI协作的prompt思路和踩过的坑全部写清楚。

1. 初看题目:strings没搜到flag,先把运行行为摸透

1.1 信息收集:file、strings、运行和ltrace

拿到附件先别急着上Ghidra,基础信息收集花不了几分钟,但能省掉后面一大半弯路。我先用file确认文件类型:

$ file hidden_key hidden_key: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, stripped

两个关键信息:64位x86-64,而且stripped,说明符号表被去掉了。如果这题是32位ARM或者带符号的,后续分析策略完全不同,所以这一步必须做。

然后是strings。这题比较贼,正常的flag关键词一个都没有,只有零散的提示字符串和一小段十六进制数据块。checksec看了下保护机制,发现开了NX和PIE,RELRO也是Full,但这题不需要走漏洞利用路线,所以保护机制对解题方向影响不大。

运行行为也要摸清:

$ ./hidden_key Usage: ./hidden_key <key> $ ./hidden_key test Wrong key

退出码也是1。用ltrace看一眼库函数调用,发现程序调用了signal()和strlen(),没看到strcmp之类的直接比较函数。这几乎是在明示:校验逻辑不是简单的字符串比对,很可能做了变换或者用了查表法。

到这里我基本放弃了“搜字符串直接拿flag”的幻想,老老实实开Ghidra反汇编。

1.2 main函数很短,但藏着信号处理器

Ghidra打开hidden_key,main函数反编译出来不算复杂,核心逻辑可以概括成:

int main(int argc, char **argv) { if (argc != 2) { puts("Usage: ./hidden_key <key>"); return 1; } signal(SIGSEGV, handler); signal(SIGFPE, handler); if (verify_key(argv[1])) { put_flag(); } else { puts("Wrong key"); } return 0; }

第一眼看过去很普通,但两个signal()注册非常刺眼。一个正常的key校验程序为什么要在main里注册SIGSEGV和SIGFPE的处理器?SIGSEGV是段错误,SIGFPE是除零错误,这俩信号通常意味着程序要崩,而这里专门注册了handler,说明程序预期自己会“崩”,并且打算在崩溃时做点事情。

这是逆向里很典型的一类题目设计:真正的关键逻辑不在正常执行路径上,而在异常处理路径里。就像你在一间屋子里找保险柜,正常路径是客厅、卧室、书房,但保险柜其实藏在地板夹层里,你必须触发某个机关让地板打开。此时那个“机关”就是异常信号,handler就是夹层里的内容。

顺着handler看下去,它做的事情其实不多:把一段位于.rodata的静态数据逐字节异或0x37,写到一个全局数组byte_404100里。这个操作看起来人畜无害,但问题是:verify_key里面有一大段代码引用了同一个byte_404100。静态分析时如果不把这两个函数关联起来,根本不知道byte_404100的真实值是什么,也就没法算出正确的key。

2. 反编译阅读遇阻,直接把C代码丢给AI

2.1 控制流平坦化的典型症状

真正让我头疼的是verify_key的反编译结果。Ghidra吐出来的C代码奇长无比,核心结构是经典的控制流平坦化,长这样:

while (1) { switch (state) { case 0: // 若干操作 state = some_table[i]; break; case 1: // 若干操作 state = another_table[j]; break; // ... 几十个case } }

这玩意儿的本质是把正常的if-else和循环全拆散,改用状态变量和跳转表来驱动,人眼直接看会非常累。CTF逆向题里常见,商业加固壳里更常见。特点是:明明逻辑可能只有十行,反编译出来几百行,里面全是魔数、临时变量和状态流转。

要手动还原也不难,无非是找状态变量、追踪每个case里它怎么变,然后画控制流图。但这题的状态变量被藏在一个全局表里,每步更新不是立即数而是查表,手动梳理比较烦。我评估了一下要花的时间,决定先让AI试试。

2.2 我向AI要的三样东西

我这轮的策略是:把Ghidra反编译出的几个关键函数分别喂给AI,不让它直接给我答案,而是让它做三件事。

第一件事是识别主函数的真实逻辑。我让它找出verify_key的状态调度变量、所有case分支的语义,并把等价的控制流还原成普通人能读的代码。第二件事是找出signal handler的副作用,明确告诉它程序里注册了SIGSEGV/SIGFPE处理器,让它看看handler对哪些全局地址有写操作。第三件事是给出verify_key的等价C代码,把混淆和无关变量都剥掉。

我当时给AI的prompt大意是这样的:

这是一段Ghidra反编译的C代码,来自一个stripped的64位ELF。 代码实现了一个对用户输入字符串的校验函数。请做三件事: 1. 识别状态调度变量,把switch-case结构还原成等价的if-else和循环; 2. 如果main函数里注册了信号处理器,重点关注handler对全局变量的写入; 3. 给出verify_key的等价C代码,去掉与校验无关的临时变量。 代码:...

AI的反馈很有价值。它没直接说“key是xxx”,而是指出了一个我险些忽略的关联:verify_key里用于比较的byte_404100,正好是handler里逐字节写入的那个全局数组。它把校验逻辑归纳成非常简洁的形式:

for (int i = 0; i < 16; i++) { if (rol3(input[i]) ^ g_magic[i] != g_blob[i]) { return 0; } }

这个归纳本身不算惊世骇俗,但省了我至少半小时的人工梳理。更关键的是它提醒我:g_magic的值在静态分析时是未知的,因为handler只有被信号触发后才会往g_magic里写入真值。

2.3 用GDB验证AI的判断,这是最关键的一步

AI给出的结论再合理,也只是“假设”。逆向分析最重要的原则是:任何结论都必须有动态证据背书,尤其是当你准备基于它写解密脚本的时候。

我打开GDB,在signal调用处下断点,然后让程序跑起来。第一次触发异常之后,我直接查看byte_404100指向的内存:

(gdb) x/16bx 0x404100 0x404100: 0x1f 0x2a 0x6c 0x14 0x3d 0x52 0x58 0x77 0x404108: 0x33 0x2c 0x6e 0x72 0x0b 0x3f 0x45 0x21

而程序运行前这块内存全是零。这证实了AI的推断:g_magic确实是动态生成的,而且生成时机就是异常处理路径。我顺便确认了g_blob的地址和内容,这一步是之后写脚本的基础。

实战里有个细节容易踩坑:handler写入的时机。反编译器静态分析时看不到调用顺序,但动态调试时你就能看到:verify_key在执行到某一步时会故意触发除零异常,处理器接管后填充g_magic,然后恢复执行,继续用填好的表校验后面的内容。所以整个校验流程是“先崩一下,再继续算”,非常隐蔽。

3. 还原校验算法,把隐藏key“算”出来

3.1 等价逻辑其实一句话

当你把控制流平坦化剥掉、把动态表数据dump出来后,整个校验算法简单到让人想笑:

输入的第i个字符循环左移3位,异或上g_magic[i],结果必须等于g_blob[i]。

循环左移3位在C语言里的惯用写法是:

unsigned char rol3(unsigned char c) { return (c << 3) | (c >> 5); }

注意这里有个坑:C语言的c << 3在int里运算,c是unsigned char时结果可能超过8位,但赋给unsigned char时会截断,所以实际效果等价于 ((c << 3) & 0xff) | (c >> 5)。Python里做同样运算时必须自己& 0xff,否则会把额外的位带进来,算出来的结果全是错的。

3.2 恢复key的Python脚本

算法清楚了,直接枚举可打印ASCII字符,逐个匹配。因为校验是逐字节独立的,不需要任何密码学知识,纯暴力枚举完全可行:

#!/usr/bin/env python3 # 从静态数据段提取的g_blob g_blob = bytes.fromhex( "e1 4a 42 cb 9b a4 5c 8c 4a 66 c2 9b d8 2b 4a 6b" ) # 触发信号后从内存dump出来的g_magic g_magic = bytes.fromhex( "1f 2a 6c 14 3d 52 58 77 33 2c 6e 72 0b 3f 45 21" ) def rol3(c: int) -> int: return ((c << 3) & 0xff) | (c >> 5) key = bytearray() for i in range(len(g_blob)): found = None for c in range(0x20, 0x7f): # 可打印ASCII if rol3(c) ^ g_magic[i] == g_blob[i]: found = c break if found is None: print(f"[!] 第{i}个字符没找到,检查magic和blob是否有误") break key.append(found) print("key =", key.decode())

运行结果直接就是k3y_h1dd3n_d34r。拿到这个key之后,回到题目程序里跑一次:

$ ./hidden_key k3y_h1dd3n_d34r flag{k3y_h1dd3n_d34r}

flag到手。

3.3 钥匙为什么“hidden”:三层保护

这题叫 Hidden Key,名字起得相当准确。钥匙的隐藏方式拆开看有三层:

第一层是控制流平坦化,把你读代码的耐心耗掉,大部分人在几百行switch面前会放弃。第二层是异常路径触发,真正影响结果的g_magic不是在正常流程里算出来的,而是通过SIGFPE/SIGSEGV的handler在运行期写进去的,静态看代码根本不知道最终值。第三层是动态表分离,把校验用的关键数据分成两块,一块在静态数据段,一块要触发异常后才出现,两者缺一不可。

这种设计在100分的逆向题里属于中等偏上难度,主要考两个能力:能不能识别异常处理路径带来的隐藏数据依赖,以及会不会用动态调试去补全静态分析的盲区。我自己觉得这道题出得还不错,没有堆恶心算法,纯粹考思路。

4. AI在解题流程里到底干了多少活

4.1 时间线拆解

我把我自己的实际耗时记录了一下,放在一起看会更直观:

阶段耗时AI参与程度说明
基础信息收集15分钟没有file、strings、运行行为、checksec
静态分析找路径40分钟中Ghidra看main和handler,人肉确认异常路径
反混淆verify_key30分钟高控制流平坦化交给AI归纳,人工复核
动态验证与dump内存20分钟没有GDB下断点,触发信号,确认g_magic
编写还原脚本15分钟中AI先写,我发现大小端和移位问题后修正
提交flag1分钟没有运行程序验证

总耗时大概两小时出头。如果没有AI辅助,我估计光反混淆verify_key这一步就要花掉一个半小时到两个小时,整体可能奔着三四个小时去。

4.2 哪些环节AI是质变

这次体验下来,AI真正让我惊艳的点不是“直接告诉我key”,而是它把模式识别这件事做得又快又准。控制流平坦化的状态机在AI眼里就是一堆可归纳的重复结构,它能快速给出等价if-else,省掉了我人工追踪状态变量的大量机械劳动。

另一个高价值产出是跨函数关联。我一开始的注意力主要在verify_key本身,对handler只是顺带看了一眼。AI在总结handler副作用时,把“handler写byte_404100”和“verify_key 读byte_404100”这两个原本分散的函数关联成一条线,直接指向了动态表依赖。这个跳跃如果让我自己做,也一定会发现,但肯定不是这么快。

4.3 哪些环节AI不可靠

不过AI也有明显短板。它给我的第一版还原脚本里,对循环移位的实现就没处理unsigned char截断,算出来是错的;还有一个版本把字节顺序搞反了,显然是被大端小端的问题带偏了。这种东西如果直接跑,轻则报错,重则算出错误的key还浑然不觉。

所以我给自己定了一条规矩:AI给的任何结论或者代码,都必须带着“验证”的心态去用。尤其是涉及二进制运算、内存布局、调用顺序的部分,绝对不能盲信。把AI当一个特别聪明但偶尔会一本正经胡说八道的实习生,是最好用的姿势。

5. 同类题目的避坑清单与AI协作方法

5.1 容易踩的坑

这道题踩过和没踩但见过的坑,我整理成一个速查表:

坑症状解法
大小端弄反还原出的key看起来像倒序的无意义字符串先在已知明文上测试脚本,再做整体还原
移位运算没截断Python脚本算出的字符不对所有位运算都要& 0xff,牢记C语言的隐式截断
忽略了signal handler怎么也凑不出key,因为关键表是空的静态分析时看到signal注册就高度警惕
把PIE地址当成固定地址下断点GDB断点不生效,程序直接跑飞先看文件是否开启PIE,用starti或偏移断点
反编译器漏掉异常路径控制流不完整,逻辑怎么都对不上用GDB实际触发异常,看handler执行了什么

其中最值得说的是符号扩展和隐式截断的问题。C语言里signed char和unsigned char在参与运算时的行为完全不同,反编译器输出的代码往往会带着一堆看起来多余的AND 0xff或者MOVSX,这些在还原成Python脚本时一定要保持下来,否则算到负数或超范围值时结果就会错。

5.2 让AI分析二进制代码时的建议

这几次跟AI配合下来,我总结出一套相对好用的方法。

第一,给足上下文再提问。不要只贴一段代码就问“这是什么”,要把文件的架构、是否strip、函数名、你在分析什么意图一起说清楚。上下文越完整,AI的幻觉越少。

第二,分段喂,不要一次倒一整篇。Ghidra反编译的长函数几千行,一股脑丢给AI很容易让它失去重点。我先丢verify_key的核心校验片段,再单独丢handler,最后让它综合两段结论。分段总结再汇总,比一次性问答准确率高很多。

第三,要求AI标注不确定的地方。我会在prompt里明确加一句“如果你不确定,请明确说不知道,不要猜测”。这个很难百分百约束住,但确实能降低它强行编答案的概率。

第四,让AI复述,而不是直接要答案。比起直接问“key是多少”,问“这段校验在做什么”或者“这个handler有哪些副作用”,得到的回答质量要高得多。因为前者容易诱发猜测,后者是让它做模式识别和归纳。

写在最后

这次Hidden Key的实际体验,让我对一个方向有了更清晰的判断:AI在逆向中的价值不是替代人做判断,而是把“读代码”这种高耗时低创造性的环节压缩掉一大半时间。控制流平坦化、表驱动状态机、跨函数数据依赖,这些正好是模式识别模型的强项。但像GDB动态调试、内存dump、验证脚本正确性这类需要真实运行现场的工作,依然得靠人来做。

如果让我给一个最简单的协作建议:遇到反编译出来的天书代码,先让AI帮你“翻译成人话”,但任何结论都必须在调试器里拿到证据之后才算数。这样配合下来,你既不会被混淆代码劝退,也不会被AI的幻觉带进沟里。

最后再分享一个小技巧:把所有AI对话中提到的关键地址、函数名、数据块整理成一份记录,再让AI基于这份记录画一遍“从输入到flag的数据流”。这个操作能帮你快速发现哪些环节还没有闭合,往往下一跳的突破口就在那个没闭合的环节里。这道Hidden Key的突破口,本质上就是对“动态生成的表”这半环闭环的补全。希望这篇writeup对你有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 2:55:06

Nmap内网隐蔽扫描实战:从原理到参数组合全面解析

Nmap大概是每个做网络的人电脑里都装过的工具&#xff0c;但大部分人对它的认识停留在nmap 192.168.1.1这种简单命令上。能扫出端口、扫出服务&#xff0c;就算“会用”了。可真到了内网场景&#xff0c;尤其是要做一次完整资产盘点、又不希望把安全设备告警刷屏的时候&#xf…

作者头像 李华
网站建设 2026/9/25 2:54:37

ESP32上跑WebAssembly:从固件原理到运行时实践的完整解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 2:53:29

CodeGuide 本地任务消息组件:基于门牌号分片扫描的动态任务补偿处理,兜住 HTTP/MQ 通知的最终一致性

文档教程后端 【免费下载链接】CodeGuide :books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总&#xff0c;旨在为大家提供一个清晰详细的学习教程&#xff0c;侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助&#xff0c;请给予支持(关注、…

作者头像 李华