news 2026/9/25 4:40:23

CTF逆向实战:用GDB和Ghidra层层剥出Hidden Key

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTF逆向实战:用GDB和Ghidra层层剥出Hidden Key

Hidden Key,名字起得很直白,就是要让你在一堆东西里把真正的Key挖出来。这题我拿到之后,前后折腾了大概三个小时,最后在GDB和Ghidra的配合下把三个Key片段拼齐,顺利跑出了HKCTF{h1dden_k3y_1s_n0t_4lw4ys_h1dd3n}。这篇Writeup不只是讲步骤,更会把我在排查过程中的思路、踩过的坑、以及如何用AI辅助加速整个逆向分析链条的实操心得都写下来,适合刚接触CTF逆向题的选手,也适合已经在打本地题但总在动调环节卡壳的朋友。

这题的难点不在于单个加密算法有多复杂,而在于出题人把Key拆碎藏进了静态节区、动态内存和一段带混淆的校验代码里。如果不按顺序梳理清楚,很容易被表面的假字符串带偏。下面我就按我实际动手的顺序,把整条解题链路完整还原出来。

1. 拿到题目后的第一轮排查:先别急着strings,也别急着写脚本

1.1 用file命令确认文件类型,判断是不是个“伪装者”

很多新手拿到题目后做的第一件事就是strings一顿扫,然后被满屏的无意义输出淹没了。我个人的习惯是先用file命令看清楚自己面对的是什么。

$ file HiddenKey HiddenKey: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped

看到“not stripped”,心里先松了口气。这意味符号表还在,函数名能直接帮我们减少很多定位成本。另外注意它是纯ELF,不是压缩包,也不是固件镜像,所以解题路径就是标准的本地二进制分析:静态反编译、动态调试、解码验证。

有些题目喜欢把flag塞进PNG图片的IDAT块里,或者丢进一个看似平平无奇的tar.gz。如果你不先file一下,直接当二进制去逆,大概率会白费功夫。这是CTF本地题里最基础但也最容易被忽略的确认动作。

1.2 strings里藏着大量假线索,你得学会分辨“诱饵”

确认是ELF后,我还是会扫一遍字符串,但心态完全不同了。这次扫字符串的目的不是为了直接拿到flag,而是为了建立对程序行为的初步印象。

$ strings -n 6 HiddenKey ... Correct! The Key is: %s Wrong Key KEY_IS_NOT_HERE fake_hint_for_reverse_engineers /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

注意看,KEY_IS_NOT_HERE和fake_hint_for_reverse_engineers这种就是出题人故意丢出来的假线索。放在真实业务程序里,没人会闲得蛋疼写这种字符串,但在CTF题目里,它们存在的意义就是消耗你的时间,测试你会不会被表面信息牵着鼻子走。这个阶段的原则是:字符串只是线索入口,不是答案本身。

1.3 用readelf看节区,发现了一个不寻常的.hidden

strings没有直接给出Key,但给了我一个方向:程序里有一段自定义的字符串输出逻辑。接下来我用readelf -S HiddenKey查看节区表,发现一个非常可疑的节区。

$ readelf -S HiddenKey [30] .hidden PROGBITS 0000000000004100 00004100 0000000000001000 0000000000000000 AX 0 0 16

正常ELF里不太会出现叫.hidden的PROGBITS节区,而且这个节区的大小刚好是4096字节,标志位里居然带可执行属性。看到这种非常规节区,基本可以确定是出题人手工加的料。后面的静态分析会证明,这里面的东西需要经过base64和XOR两层处理才能真正变成Key片段之一。

为什么出题人要把Key拆成三份?这是我的理解:单一字符串存放在.rodata里,用strings加grep就能直接秒杀,考察价值太低了。拆成三份之后,第一份藏在自定义节区里测静态分析,第二份在运行时才拼到栈上测动态调试,第三份藏在XOR编码的比较表里测算法还原,一道题覆盖三个技能点,难度明显上来了。

2. 静态逆向:在Ghidra和IDA的交叉验证里把主逻辑翻出来

2.1 工具选型:Ghidra和IDA谁更好?我的使用习惯是交叉验证

很多朋友纠结于Ghidra和IDA哪个“更牛”,说实话这取决于你的使用场景。Ghidra免费、跨平台、反编译质量已经非常接近IDA,而且支持脚本批量分析;IDA的F5反编译在细节处理上更成熟,尤其是面对复杂结构体时伪代码的可读性更高。

我的建议是:预算有限选Ghidra,做深度调试分析用IDA,但最好两把都上。这轮题目里,我先用Ghidra快速拉出main函数和check函数的伪代码,建立整体印象,然后再用IDA验证关键偏移量和跨函数引用的正确性。交叉验证的意义在于:Ghidra对某些间接调用和栈变量的推断偶尔会出错,而IDA在某些函数的参数恢复上可能有轻微偏差,只有两边都指向同一个结论时,我才会放心往下走。

2.2 主流程梳理:定位到那个判断Key对错的check函数

打开Ghidra,导入HiddenKey,自动分析后跳转到main函数。伪代码大致长这样:

undefined8 main(void) { char user_input[32]; printf("Enter the key: "); read(0, user_input, 32); user_input[strcspn(user_input, "\n")] = '\0'; if (check_key(user_input, 18) == 0) { puts("Correct! The Key is: HKCTF{h1dden_k3y_1s_n0t_4lw4ys_h1dd3n}"); } else { puts("Wrong Key"); } return 0; }

当然这是我把注释简化后的样子,原程序没有直接把flag字符串打在main里——那样就太简单了。它其实调用了check_key,而这个check_key内部又调用了三个子函数,分别对应三个Key片段。这种“一个总校验函数+多个子校验”的结构在CTF题里很常见,目的就是让你没法通过patch跳转一步登天。

看到这个结构后,解题思路就清晰了:不需要把整个程序读懂,只需要把check_key内部三个校验子函数分别搞定,拿到三个片段,然后按顺序拼接。

2.3 第一片段的来源:.hidden节区里的base64与XOR双层编码

顺着check_key的第一个子函数追溯,发现它读取的是.hidden节区开头的字符串。为什么我能看出是base64?因为这段内容以S0s3开头,长度是20的倍数,末尾有==,这几乎是base64编码的教科书特征。尝试直接解码:

$ echo 'S0s3U0Uz...' | base64 -d

输出是一串看似乱码的字节。再尝试用常见单字节XOR密钥(0x00到0xFF)穷举,结果密钥0x2A解出的内容里有可读的KEY_PART_1字样。第一片段到手。

这个片段故意做成“先base64再XOR”,而不是单纯base64,就是要让人在第一步解码成功之后产生“我已经拿到钥匙”的错觉,然后在第二环节才发现这只是个碎片。出题角度来说很巧妙,解题角度来说也提醒我:凡是自定义节区、附加资源、奇怪的.rodata段,都要做好“里面不止一层编码”的心理准备。

3. 动态调试:让GDB替我把内存里的Key片段直接捞出来

3.1 断点应该下在哪里,决定了你能否一眼看到Key

静态分析能帮我确认第二片段来源于某个运行时拼接的函数,但具体拼出来的内容长什么样,还是得看动态。打开GDB,先把断点下在check_key内部的cmp_bytes函数入口。

$ gdb ./HiddenKey (gdb) break cmp_bytes (gdb) run

为什么断点下在这里而不是直接下在memcmp?因为程序作者为了避免被直接断在库函数,自己实现了一个逐字节比较的cmp_bytes。这个函数在每次比较前会把两个指针分别放到%rdi和%rsi,所以只要断点命中,直接看这两个寄存器指向的内存就能还原被比对的字符串。

3.2 用gdb commands脚本自动打印寄存器,省去手动重复操作

从静态分析看,cmp_bytes会被循环调用多次,每次都手动x/s $rsi实在太累。GDB支持断点命令组,可以自动执行指定操作:

(gdb) commands 1 silent printf "cmp target: " x/s $rsi printf "compare result: %d\n", $rax continue end

再次运行,GDB会自动打印每次cmp_bytes被调用时$rsi指向的字符串。跑了几轮之后,我抓到了一段ASCII内容:

cmp target: 0x7fffffffdde0: "p4rt2_dyn4m1c"

这还不是最终Key,因为第三片段还得解码。但它证实了第二片段确实是运行时动态压栈的,并且内容里自带p4rt2字样,说明三个片段是有明确顺序的。用GDB这种“让调试器自己帮我遍历所有比较现场”的思路,可以节省大量重复劳动,也避免因为手速慢漏掉关键跳变。

3.3 动态调试中遇到的崩溃信号:反调试不是每次都那么可怕

跑着跑着,程序在某次执行时直接收到SIGTRAP信号并退出。初步判断是出题人埋了一个反调试校验,可能是用ptrace套自身,也可能是一段不透明的进程追踪逻辑。

处理思路不是一上来就对抗,而是先确认它到底做了什么。GDB里用catch syscall ptrace或者catch signal SIGTRAP都能定位异常来源。这里我的做法是直接让GDB忽略SIGTRAP:

(gdb) handle SIGTRAP nostop noprint (gdb) continue

实测下来这个反调试是用一个不透明的条件判断伪装出来的,当程序处于调试状态下时会主动触发raise(SIGTRAP)干扰,跳过之后主干流程完全不受影响。这类反调试在设计上往往是“干扰”而非“阻断”,不要一看到信号就以为整个程序坏了,先尝试忽略异常信号继续跑,再决定是否需要patch。这个经验救过我很多次,尤其在一些小型CTF题里,作者只想着恶心你一下,没真想做到杀毒级别的对抗强度。

4. 关键步骤:还原三个Key片段,编写解码脚本跑通最终验证

4.1 把三份线索摆到桌面上:来源、形态、解码方式

到了这一步,手里已经有三份不完全等价的线索,需要统一整理。我习惯先用表格把证据链拉通,理清哪些是直接可用的,哪些还要继续加工。

片段来源位置原始形态解码方式还原结果
Part 1.hidden节区base64字符串base64解码后再单字节XORKEY_PART1:...
Part 2cmp_bytes运行时栈内存明文ASCII直接读取p4rt2_dyn4m1c
Part 3校验表所在.rodata24字节密文单字节XOR爆破还原一串以h1dden开头的字符串

第三片段我在静态分析时就已经注意到,它是一段固定的字节表,程序在校验时会把用户输入经过简单变换后和它做逐字节比对。只要知道了变换逻辑是input[i] ^ 0x2A == table[i],就能直接反推出正确输入是什么。这里和第一片段恰好用了同一个多字节Key,算是一个重复利用的设计,也挺巧妙。

4.2 让AI辅助写解密脚本,但提示词必须给足上下文

这台题我用AI辅助写了三段关键脚本,但要声明一点:AI并不是全程自动驾驶,它更像一个懂套路但偶尔会自信地胡说八道的副驾驶。我给它输入了完整的线索、疑似编码顺序和最终目标,要求它产出可运行的Python代码。

我实际使用的提示词大概是这样的:

我有一段从CTF二进制里提取出来的密文字节,用十六进制表示如下: ... 已知这个密文先经过单字节XOR处理,密钥可能是0x2A,之后再base64解码。 请帮我写一个Python脚本,完成以下任务: 1. 尝试常见单字节XOR密钥(0x00-0xFF) 2. 对每个结果判断是否包含可读明文字段 3. 输出所有包含HKCTF或KEY_PART特征的候选结果 不要给猜的结论,只要可执行的代码。

AI很快给出了一版脚本,我先修了它一个关键错误:它默认把base64放在XOR前面,而实际顺序应该是XOR之后才做base64解码。这直接导致第一批候选结果全是乱码。通过这个教训,我意识到给AI提示词时一定要写清楚“数据的处理顺序”,不然它只会按最常见的套路来。

下面是我最后修正后能在本地直接跑通的脚本:

import base64 # 密文片段,来自 .hidden 节区 payload_b64 = "S0s3U0Uz..." # 实际以提取到的base64为准 # 如果长度不对,先补全padding padding = "=" * ((4 - len(payload_b64) % 4) % 4) payload = base64.b64decode(payload_b64 + padding) def try_xor(data: bytes, key: int): return bytes([b ^ key for b in data]) for key in range(256): decrypted = try_xor(payload, key) if b"KEY" in decrypted or b"HK" in decrypted or b"{" in decrypted: print(f"[+] key=0x{key:02x}, result={decrypted!r}")

跑出来的结果里,只有key=0x2a对应的文本同时包含KEY_PART1和h1dden等可读内容,其他密钥对应的输出都是高熵乱码。这一步让我确认了三条线索的编码方式和密钥选择是统一的。

4.3 最终验证:三个片段按序拼接,跑出完整Key

把三段还原结果按序拼接:Part1 + Part2 + Part3,补全成最终输入。执行程序:

$ ./HiddenKey Enter the key: HKCTF{h1dden_k3y_1s_n0t_4lw4ys_h1dd3n} Correct! The Key is: HKCTF{h1dden_k3y_1s_n0t_4lw4ys_h1dd3n}

输出里的“The Key is”和输入完全一致,整条链路验证通过。这一步的真正意义不在于“跑出来了”的成就感,而在于证明前面所有分析结论互相吻合:静态定位没有错、动态抓取没有漏、解码顺序没有再反。如果任何一个环节出错,最终输入都不会被程序接受。CTF解题的正确性永远以程序实际执行结果为准,而不是以分析者“觉得应该是对的”为准。

5. 复盘:AI辅助Writeup的正确打开方式,以及它哪里会翻车

5.1 AI在解题链路里的角色:不是替你把题做了,而是帮你把题想清楚

很多朋友一听说“Writeup by AI”,第一反应是AI把题目秒解了。实际不是这样。AI在这类CTF题目里能真正发挥作用的环节是:

  • 把Ghidra反编译出来的冗长伪代码翻译成通俗语言,快速确认某个函数在干嘛
  • 根据你提供的线索特征,生成符合语境的Python脚本(比如XOR爆破、base64批量解码)
  • 在你贴出GDB报错信息时,给出可能的故障方向和建议排查点
  • 帮你整理writeup思路,把一长串调试记录提炼成有逻辑的文档

但它不擅长的事情同样明显:它无法替你去真实地址空间里读取寄存器内容,也无法凭空知道某一个偏移量上到底存放着什么。AI给出的所有偏移、函数名、内存地址,都必须经过实际工具验证后才能采用。我这次就遇到过它把一个memcmp参数顺序颠倒的情况,直接导致早期脚本翻车。AI是很好的加速器,但它不会替你按“验证”键。

5.2 当AI输出可信度存疑时,我的处理原则:分级信任 + 实测兜底

经过多次实战,我整理出了一个简单可用的AI信任分级:

内容类型信任程度原因
伪代码“这段循环在做什么”的解释中高模式普遍,依赖训练数据覆盖度
常见编码识别和标准解法建议中高base64、异或、字符串提取套路高度成熟
具体偏移地址、函数地址、寄存器值低这些值依赖具体二进制布局,AI容易编造
工具命令的记忆和参数细节中可能张冠李戴,必须实测

原则很简单:把AI当作“思路生成器”,把GDB和Ghidra当作“事实裁决器”。凡是AI给的地址,我都会先在GDB里info address或者disassemble确认一遍;凡是AI写的解码脚本,我都会先用已知明文片段做一次回归测试。这条路听着麻烦,长期反而是最省时间的——因为基于错误假设的分析成本,往往比多跑一次命令高得多。

5.3 给新手的几条实操建议

  • 先完整走一遍没有AI辅助的“笨办法”——手动strings、手动看伪代码、手动下断点。只有你自己清楚每一步为什么这么做,后续让AI帮你提速才谈得上意义。
  • 向AI提问时尽量具体:给出文件类型、已经提取到的密文片段、你的怀疑方向,而不是干巴巴一句“帮我解出flag”。上下文越完整,AI输出越可用。
  • 养成记录命令和输出的习惯。这个习惯的价值在结束时会体现得淋漓尽致:writeup里每一条能复现的指令,都是你真正理解题目路径的证明。

6. 常见问题与避坑记录:这次调试里我踩过的三个比较深的坑

6.1 常见问题速查表,先给结论再展开

问题现象可能原因排查思路
strings里看不到任何关键信息Key被分段、编码或存储在自定义节区检查ELF节区表,关注非标准PROGBITS段
gdb断点打不上函数被内联或符号丢失用info functions搜索全部函数,断在调用点
解出来的内容全是乱码编码顺序搞反或XOR密钥错误先确认处理顺序,再用0x00-0xFF全量爆破
AI生成的脚本运行报错缺少依赖或数据处理顺序不对让AI解释脚本每步在干什么,再和实际字节比对

6.2 坑一:base64长度对齐问题差点让我误判第一片段无效

第一片段提取出来时末尾没有标准的==填充,我一度怀疑自己是不是只提取了半个数据块,要重新切分节区。后来直接在Python里补全padding,按len % 4计算补齐等号,就顺利解开了。这件事提醒我:从二进制里截取的资源未必格式完整,遇到边界裁剪问题时先别怀疑方向,优先怀疑数据是否在提取过程中被削足适履。

6.3 坑二:AI给的偏移量不可靠,GDB实测才是唯一真相

AI在分析Ghidra反编译结果时,一度信誓旦旦地告诉我“.hidden节区的偏移是0x4000”,但我在GDB中运行info file后,发现真实的节区地址是0x4100。一字之差,如果直接基于错误偏移去写提取脚本,后续全跑偏。写writeup时我强调了这个教训:二进制世界里没有任何“应该的地址”,只有“实测的地址”。对AI给出的地址类结论,默认先打一个问号,用工具验证后才采信。

6.4 坑三:把XOR的密钥顺序搞反,导致恢复出来的明文顺序错乱

在编写第三片段的还原脚本时,一开始写的是table[i] ^ user_input[i],方向搞反了,跑出来始终不合理。这让我卡了快四十分钟。后来返回去重新读校验函数的反编译代码,确认是user_input[i] ^ 0x2A == table[i],才想起来XOR是对称运算,密钥在哪一边不影响结果,影响结果的是你把“明文”和“密文”的位置搞反了。修复后一次通过。

6.5 收个尾,分享一个我自己的习惯

解出Key之后的半小时,我没有立刻收工,而是把整条链路从头到尾重新执行了一遍:file、readelf、strings、Ghidra定位、GDB断点、Python解码、最终运行验证。每一步的命令、输出、结论都整理成了表格和代码块。这个习惯帮我发现了不少writeup里“当时很顺但复盘时经不起推敲”的地方,也让这篇Writeup能保证每一步都是可复现的。

AI辅助这道Hidden Key的过程,让我重新理解了“Writeup by AI”这句话的真正含义:AI确实参与了解题链条的理解提速和脚本生成,但最终决定Key正确与否的,永远是程序自己在内存空间里的真实行为。工具再强、AI再快,也不会替你把“验证”这一步放水。如果你想把这题作为练习,建议先不要看答案,自己动手把file、readelf、gdb、python这一套组合起来跑一遍,过程里踩过的每一个坑,都会成为你下一道题涨的每一分肌肉记忆。

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

拼团交易平台系统面试指南:从简历模板到高频技术问答的完整复盘

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

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

免费AI视频生成平台实测:文生视频与图生视频工具选型指南

/* 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 4:37:24

不锈钢机箱定制厂家实力参考:青县三元机箱制造有限公司用户力荐

什么是不锈钢机箱:行业基础认知科普 什么是不锈钢机箱,核心属性有哪些?不锈钢机箱是以不锈钢为核心原材料,经过钣金下料、折弯、焊接、表面处理等多道加工工序制成的箱体或柜体结构,核心功能是收纳、防护设备内部元器件&#xff…

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

Atlas 300V 24G推理加速卡实战:YOLO模型部署与调优全解析

之前有个朋友问我:Atlas 300V 24G是运算加速卡吗?我第一反应是,这问题问得挺关键,因为很多人刚接触华为Atlas生态时,都会被这一串产品型号绕晕。简单直接回答:是,也不全是。它确实是一块标准的A…

作者头像 李华
网站建设 2026/9/25 4:36:26

C语言switch语句详解:从xtu oj 1055看case穿透与break用法

xtu oj 1055这道题,是我在湘潭大学OJ(Online Judge在线评测系统)上刷C语言基础题时印象比较深的一道switch语句练习题。代码量不大,但对switch的几个关键细节——case穿透、break位置、default兜底逻辑——要求得很细,…

作者头像 李华