第四周
ISCC 2026破阵夺旗赛校级赛-消失的密钥(Web)
前期探测
访问首页
访问首页后,可以看到一个认证界面,页面要求输入一个名为step1的授权值。表面上看,页面只暴露了一个输入框,没有给出其它功能入口。
黑盒探测源码入口
在不知道源码的前提下,可以尝试 PHP CTF 题里常见的调试参数,例如:
?source
?debug
?view
?highlight
该题中,访问:http://39.105.213.28:12601/?source会直接返回高亮后的 PHP 源码。
源码审计
过滤
$step1 = $_GET['step1'];
$filtered = str_replace("key", "", $step1);先把输入中的所有小写
key删除;删除之后的结果必须严格等于字符串
key。
POST 数据结构
$a = $_POST['a'] ?? null;
$obj_a = (object)$a;
$user_key = $obj_a->key;
if (isset($user_key) && $user_key === "1337") {$_POST['a']必须存在;转成对象后,能访问到
$obj_a->key;该属性值必须严格等于字符串
"1337"。
哈希比较
$val_a = $_GET['a'] ?? "";
$val_b = $_GET['b'] ?? "";
if ($val_a !== "" && $val_b !== "" && $val_a !== $val_b) {
if (md5($val_a) == md5($val_b)) {a和b都不能为空;a和b不能是相同字符串;但
md5($a)与md5($b)在 PHP 的 == 判断下要“相等”。
第一关详细利用
通过“重复片段嵌套”构造输入:kkeyey,服务端执行:str_replace("key", "", "kkeyey") => "key",于是条件:$filtered === "key"成立,第一关通过。
第二关详细利用
在 PHP 表单处理中,像这样的 POST 数据:a[key]=1337,会被自动解析为:
$_POST['a'] = [
'key' => '1337'
];接着执行:$obj_a = (object)$a;,就会得到一个等价对象:$obj_a->key === "1337",于是第二关通过。
第三关详细利用
第三关判断的是:$val_a !== "" && $val_b !== "" && $val_a !== $val_b,以及:md5($val_a) == md5($val_b),也就是说a和b必须是两个不同的非空字符串,但它们的 MD5 值在弱比较下要相等。
可以使用这组经典输入:
a=QNKCDZO
b=240610708它们是两个不同字符串,满足:$val_a !== $val_b,但对应的 MD5 分别为:
md5("QNKCDZO") = 0e830400451993494058024219903391
md5("240610708") = 0e462097431906509019562988736854在 PHP 的弱比较 == 中,这类字符串会被解释成数值:0 × 10^若干次方,结果都等于数字0。因此:md5("QNKCDZO") == md5("240610708"),会被判断为真。
如果题目写成:md5($val_a) === md5($val_b),那么 PHP 会按字符串逐字节比较,两串 MD5 不完全相同,就一定失败。
完整利用链
GET 参数
?step1=kkeyey&a=QNKCDZO&b=240610708
POST 数据
a[key]=1337
利用效果
这组数据会依次满足:
step1=kkeyey经过str_replace("key", "", ...)后得到key;a[key]=1337被 PHP 解析后,可让$obj_a->key === "1337";QNKCDZO与240610708的 MD5 在 PHP == 下相等;服务端进入最终分支并输出 flag。
FLAG:CREDENTIAL_FOUND: ISCC{hash_collision_v1_0e_2x_stable}
ISCC 2026破阵夺旗赛校级赛-where's bunny(Reverse)
解题过程
本题合理的做法是:
先看 Strings;
通过字符串交叉引用(Xref)定位主逻辑。;
优先分析 main 或者校验函数;
把输入、输出、比较目标、固定常量先找出来。
使用快捷键 Shift+F12 打开字符串窗口,可以看到:
Enter Flag:
Congratulations! Flag is correct!
Flag is incorrect!
15727A0205634B35CA3E14B73A7DA4157D6B004BD9EB1D65也可以在窗口中按下 Ctrl+F 调出搜索框,优先搜索这些特征字符串:
Enter Flag
Congratulations
incorrect
ISCC{输入目标字符串并回车,即可快速定位。双击字符串,按 X 或 Ctrl+X 看交叉引用。定位到主函数,有这样一段常量初始化:
v78[0] = 5;
v78[1] = 344;
v78[2] = 13;
v78[3] = 21;
v78[4] = 34;
v78[5] = 55;
v78[6] = 89;
v78[7] = 144;
v78[8] = 233;
v78[9] = 377;这是一组很眼熟的数字:5、13、21、34、55、89、144、233、377,其中多数属于斐波那契数列。程序随后把十个洞的“状态”先全部设为1:
v68 = (void *)1;
v69 = 1;
v70 = 1;
v71 = 1;
v72 = 1;
v73 = 1;
v74 = 1;
v75 = 1;
v76 = 1;
v77 = 1;
v17 = 0;然后进入关键循环:
for ( i = 1; i < 1000; ++i )
{
v19 = v17 % -10;
v17 = v17 % -10 + i + 1;
*(&v68 + v19) = nullptr;
}这段代码虽然写得别扭,但本质上是在不断把某些洞位置为0,表示“这个洞被排除掉了”。把这段循环翻译成 Python 后,十个洞最终的状态是:
v = [1] * 10
s = 0
for i in range(1, 1000):
idx = s % -10
s = s % -10 + i + 1
v[idx] = 0
print(v)结果是:[0, 1, 0, 1, 0, 0, 1, 0, 1, 0],也就是只有 4 个位置还活着:索引:1、3、6、8,对应 v78 中的数字分别是:344、21、89、233。这一步得出的结论极其重要:程序真正会用到的key,不是 10 个,而是 4 个:344、21、89、233。程序随后遍历 10 个洞。如果洞是存活状态,就把对应数字转成字符串,然后按“第几个存活洞”决定做哪种变换。
先看sub_401AC0。这个函数先把数字转成字符串,例如:233 -> "233",然后调用sub_4024B0。sub_4024B0很长,但它的结构特征是标准 SHA-256:
64 轮常量
8 个状态字
消息扩展
大量右旋和逻辑函数
所以这一段不是 MD5,而是 SHA-256。sub_401AC0随后把 SHA-256 的结果取前 16 个字节,转成 32 个十六进制字符。所以我们可以理解为:233 -> "233" -> SHA-256 -> 取前16字节 -> 十六进制字符串,这会得到:c0509a487a18b003ba05e505419ebb63,这 16 字节会被后续函数作为分组加密的key。
sub_401B80这个函数有两个关键特点。它先把key的十六进制字符串还原成 16 字节,然后把它拆成 4 个 32 位整数。
sub_401880它对输入做 8 字节分组加密,这个函数的常量和结构特征非常明显:
常量 0x9E3779B9
两个 32 位字反复更新
共 32 轮
这是标准 TEA(Tiny Encryption Algorithm)风格。
在sub_401B80里,程序先调用:sub_403D20(8 - (*(_DWORD )(a2 + 16) & 7), 8 - ((_BYTE *)(a2 + 16) & 7));,这表示它会把数据补齐到 8 字节整数倍。补齐方式是:
如果差 1 字节,就补 0x01
如果差 2 字节,就补 0x02 0x02
这就是典型的 PKCS#7 风格填充。
函数sub_402FD0的功能很简单:
逐字节取二进制输出;
拆成高 4 位和低 4 位;
查表 "0123456789ABCDEF";
生成大写十六进制字符串。
最终和常量:15727A0205634B35CA3E14B73A7DA4157D6B004BD9EB1D65进行比较。所以完整正向流程已经可以写成:
inner_flag
-> RC4(key="344")
-> XOR(key="21")
-> ADD(key="89")
-> PKCS#7 padding to 8 bytes
-> TEA encrypt(key = SHA256("233")[:16])
-> Upper Hex
-> compare with 15727A0205634B35CA3E14B73A7DA4157D6B004BD9EB1D65反向步骤如下:
把目标十六进制串还原成二进制;
用 233 派生出 TEA key;
对每个 8 字节块做 TEA 解密;
去掉末尾填充;
撤销第 3 段:减去循环 key "89";
撤销第 2 段:再异或循环 key "21";
撤销第 1 段:再跑一次 RC4(key="344"),因为 RC4 加解密对称。
得到的就是花括号内部明文。
import hashlib
TARGET = bytes.fromhex("15727A0205634B35CA3E14B73A7DA4157D6B004BD9EB1D65")
def rc4(data: bytes, key: bytes) -> bytes:
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) & 0xff
s[i], s[j] = s[j], s[i]
i = 0
j = 0
out = bytearray()
for b in data:
i = (i + 1) & 0xff
j = (j + s[i]) & 0xff
s[i], s[j] = s[j], s[i]
out.append(b ^ s[(s[i] + s[j]) & 0xff])
return bytes(out)
def tea_decrypt_block(block: bytes, key_words):
v0 = int.from_bytes(block[:4], "little")
v1 = int.from_bytes(block[4:], "little")
delta = 0x9E3779B9
total = (delta * 32) & 0xffffffff
k0, k1, k2, k3 = key_words
for _ in range(32):
v1 = (v1 - ((((v0 << 4) & 0xffffffff) + k2) ^ ((v0 + total) & 0xffffffff) ^ ((v0 >> 5) + k3))) & 0xffffffff
v0 = (v0 - ((((v1 << 4) & 0xffffffff) + k0) ^ ((v1 + total) & 0xffffffff) ^ ((v1 >> 5) + k1))) & 0xffffffff
total = (total - delta) & 0xffffffff
return v0.to_bytes(4, "little") + v1.to_bytes(4, "little")
# 计算 TEA 密钥
sha16 = hashlib.sha256(b"233").digest()[:16]
tea_key = [int.from_bytes(sha16[i:i+4], "little") for i in range(0, 16, 4)]
# Stage 3: TEA 解密
stage3_padded = b"".join(
tea_decrypt_block(TARGET[i:i+8], tea_key)
for i in range(0, len(TARGET), 8)
)
# 去除 Padding
pad = stage3_padded[-1]
stage3 = stage3_padded[:-pad]
# Stage 2 & 1: 简单的位移和异或变换
stage2 = bytes((b - ord("89"[i % 2])) % 256 for i, b in enumerate(stage3))
stage1 = bytes(b ^ ord("21"[i % 2]) for i, b in enumerate(stage2))
# Stage 0: RC4 解密
plain = rc4(stage1, b"344")
print("ISCC{" + plain.decode("latin1") + "}")FLAG:ISCC{deIylyrlsluhowuetocIiaa}
附录:IDA Pro 常用快捷键
视图与导航
| 快捷键 | 功能说明 |
|---|---|
| Space (空格) | 切换 文本视图 与 图形视图 |
| Esc | 返回上一个操作地址(后退) |
| G | 快速跳转到指定地址或符号 |
| Ctrl + S | 查看并选择程序段(Segments)信息 |
| Ctrl + 鼠标滚轮 | 在图形视图下放大或缩小界面 |
| Alt + T | 搜索文本内容 |
| Alt + B | 搜索十六进制字节序列 |
符号与数据定义
| 快捷键 | 功能说明 |
|---|---|
| N | 对符号/变量/函数进行 重命名 |
| Y | 修改变量或函数的 数据类型 |
| U (Undefine) | 取消定义(将代码/数据还原为原始字节) |
| A | 将选中数据解释为 字符串 |
| X | 查看当前函数、变量的 交叉引用 (Cross Reference) |
| Shift + F12 | 打开 字符串窗口,一键检索所有字符串 |
注释与标签
| 快捷键 | 功能说明 |
|---|---|
| : (冒号) | 添加常规注释(仅显示在当前位置) |
| ; (分号) | 添加可重复注释(所有引用处均会显示) |
| Alt + M | 添加标签(Mark) |
| Ctrl + M | 查看已添加的标签列表 |
数据库管理
| 快捷键 | 功能说明 |
|---|---|
| Ctrl + W | 保存当前 IDA 数据库 |
| Ctrl + Shift + W | 拍摄 IDA 快照(备份当前进度) |
Hex-Rays 伪代码 (F5) 专项
在按下 F5 进入反编译界面后,以下快捷键最为常用:
| 快捷键 | 功能说明 |
|---|---|
| / | 在伪代码界面添加注释 |
| ** | 显示/隐藏 变量与函数的类型描述(精简视图) |
| N / Y | 在伪代码中同样生效,用于重命名和修改类型 |
动态调试执行控制
| 快捷键 | 功能说明 |
|---|---|
| F9 | 运行程序(直到遇到断点) |
| F7 / F8 | 单步步入 (进入函数) / 单步步过 (跳过函数) |
| F4 | 运行到当前光标所在行(常用于跳出循环) |
| Ctrl + F2 | 重新开始调试(Restart) |
| Alt + F2 | 结束调试会话 |
| Ctrl + F9 | 执行直到返回(执行到当前函数结束) |
| Alt + F9 | 执行直到回到用户代码段 |
动态调试断点与查找
| 快捷键 | 功能说明 |
|---|---|
| F2 | 设置/取消 软件断点 |
| Alt + B | 显示断点列表窗口 |
| Ctrl + G | 输入地址并跟随 |
| Ctrl + N | 查找导入/导出符号名 |
| Shift + F4 | 设置条件断点 |
动态调试窗口切换 (Alt 系列)
| 快捷键 | 对应窗口 | 快捷键 | 对应窗口 |
|---|---|---|---|
| Alt + C | CPU 主窗口 | Alt + M | 内存映射窗口 |
| Alt + E | 模块列表 | Alt + K | 呼叫堆栈 (Stack) |
| Alt + L | 日志记录 | Alt + O | 调试选项 |
提示:IDA 中的 Esc 是逆向工程中最常用的按键之一,类似于浏览器的“后退”按钮。动态调试时,如果不小心跑进了系统 DLL,可以使用 Alt + F9 快速回到程序自身的代码区域。