ISCC2026 WriteUp
Reverse——Conway’s trap
解题思路
1. 程序整体观察
运行附件中的 PE 程序后,终端先提示 “Enter seed:",
等待输入 32 位 hex 字符串;通过后显示 “Correct Seed.",
接着提示 “Enter flag:"。
输入格式 `ISCC{…}` 后若花括号匹配则给出 “Correct format! But wrong content."。注意到这里有两类不同反馈——格式正确但内容错误——暗示 flag 校验阶段存在不止一条代码路径。
使用 IDA 定位到这些字符串的交叉引用,找到 main 函数的核心逻辑。main 大致分为两段:前半段处理 seed,后半段处理 flag。
2. Seed 校验逆推
seed 输入为 32 个 hex 字符,程序通过 `sscanf` 格式化为 16 字节,然后调 `sub_401990` 迭代 5 轮,最终结果转 hex 后与全局常量比对:
df7b6a5d4da0f5facf32c4ee4b28b792
深入 `sub_401990` 发现每轮操作分两层:
把 16 字节两两分组(共 8 组),对每组的 (a, b) 做 a’ = (a + b) & 0xff, b’ = ROL(b ^ a’, 1)
一轮结束后整体左循环移位一个字节
由于加法模 256 和 ROL 均可逆,直接从目标常量反推即可。逆轮函数步骤为:先右循环移位恢复位置,对每组用 ROR 和减法反推原始值,如此迭代 5 次得到真正的 seed:
0b5e321c68e0e4fb2d972226e8c70f8d
为了验证正确性,将得到的 seed 再正向跑 5 轮,结果与目标常量完全一致。
3. Flag 校验阶段分析
flag 输入经过格式验证(以 `ISCC{` 开头、`}` 结尾)后,调用 `sub_401C10` 提取花括号中间的正文,再将其传入 `sub_401EB0`。
初看 `sub_401EB0`,容易直接关注到函数末尾那条长 hex 比较——它把正文编码后与一个 53 字节的目标串比对:
5986898a77726599708c726f716a72757596868f82657b8a8b8a6f6e6f7a8b727998789075697a92758d7669837570876d8e789582
但仔细回溯 main 的初始化代码,发现在进入 flag 校验前,程序通过 `sub_401E00` 注册了一个异常处理器(SEH),同时把地址 `sub_401CB0` 的首字节修改为 `0xCC`(即 int3 断点指令)。这样一来,每当 `sub_401CB0` 被调用时,就会触发断点异常,执行流被 SEH handler 劫持转向 `sub_401D00`。
而 `sub_401EB0` 在逐字节编码正文的循环中,恰好每轮都调用 `sub_401CB0`。这意味着实际参与处理的并非表面上的长 hex 比较逻辑,而是 `sub_401D00` 中的隐藏校验。
4. 提取隐藏校验常量与逆推 flag
`sub_401D00` 内部仅在首次被调用时执行完整校验。它先通过一系列 mov 指令在栈上构造一段密文,再与 `0xCC` 异或得到 23 字节的真实目标值。校验公式可抽象为:
for i in 0..22:
t = seed[i & 0xF] \^ body[i]
t = (t + i * 0x17) & 0xFF
t = ROL(t, 3) \^ 0xAA
if t != secret[i]: fail
seed[0:2] = [0xDE, 0xAD]
关键信号:校验通过后 seed 前两字节被置为 `0xDE 0xAD`,而 `sub_401EB0` 返回前会检查这两个字节——如果是 `0xDE 0xAD` 则提前返回成功,完全跳过末尾的长 hex 串比对。这解释了为何“格式正确但内容错误”和长 hex 串都存在却并非真正的判据。
从二进制中定位到栈上 mov 指令的立即数,异或 `0xCC` 后得到隐藏目标:
80e8512fb19a9b1332a47888df95efc2ddbf8dab146f76
逆推公式为:
body[i] = seed[i & 0xF] \^ ((ROR(target[i] \^ 0xAA, 3) - i * 0x17) & 0xFF)
代入 seed 和 target 计算出 23 字节正文:
NocwosxmveVar{i9uEtwc5E
最终 flag:
ISCC{NocwosxmveVar{i9uEtwc5E}
5. Exp
import re
from pathlib import Path
TARGET_SEED = 'df7b6a5d4da0f5facf32c4ee4b28b792'
def _rol8(v: int, n: int) -> int:
return ((v << n) | (v >> (8 - n))) & 0xFF
def _ror8(v: int, n: int) -> int:
return ((v >> n) | (v << (8 - n))) & 0xFF
def undo_one_round(data: bytes) -> bytes:
shifted = bytes([data[-1]]) + data[:-1]
res = bytearray(16)
for i in range(0, 16, 2):
nl, nr = shifted[i], shifted[i + 1]
orig_r = _ror8(nr, 1) \^ nl
orig_l = (nl - orig_r) & 0xFF
res[i], res[i + 1] = orig_l, orig_r
return bytes(res)
def do_one_round(data: bytes) -> bytes:
buf = bytearray(data)
for i in range(0, 16, 2):
a, b = buf[i], buf[i + 1]
na = (a + b) & 0xFF
nb = _rol8(na \^ b, 1)
buf[i], buf[i + 1] = na, nb
return bytes(buf[1:]) + bytes([buf[0]])
def recover_seed() -> bytes:
state = bytes.fromhex(TARGET_SEED)
for _ in range(5):
state = undo_one_round(state)
# sanity check
assert state == bytes.fromhex('0b5e321c68e0e4fb2d972226e8c70f8d')
ck = state
for _ in range(5):
ck = do_one_round(ck)
assert ck.hex() == TARGET_SEED
return state
def fetch_hidden_constants(binary: bytes) -> bytes:
pat = (
rb'\\xc7\\x45\\xe4(.{4})\\xc7\\x45\\xe8(.{4})\\xc7\\x45\\xec(.{4})'
rb'\\xc7\\x45\\xf0(.{4})\\xc7\\x45\\xf4(.{4})\\x66\\xc7\\x45\\xf8(.{2})\\xc6\\x45\\xfa(.)'
)
m = re.search(pat, binary, re.DOTALL)
if m is None:
raise SystemExit('hidden constants not located in binary')
raw = b''.join(m.groups())
return bytes(b \^ 0xCC for b in raw)
def decode_body(seed: bytes, secret: bytes) -> str:
chars = []
for idx, ct in enumerate(secret):
v = _ror8(ct \^ 0xAA, 3)
v = (v - (idx * 0x17)) & 0xFF
pt = v \^ seed[idx & 0xF]
chars.append(pt)
body = bytes(chars).decode('latin-1')
# sanity check
check = bytearray()
for i, ch in enumerate(body.encode('latin-1')):
t = seed[i & 0xF] \^ ch
t = (t + (i * 0x17)) & 0xFF
t = _rol8(t, 3) \^ 0xAA
check.append(t)
assert bytes(check) == secret
return body
def main():
exe = Path(__file__).resolve().parent / "Conway's Trap.exe"
if not exe.is_file():
alt = next(Path(__file__).resolve().parent.parent.glob('Reverse2-*'), None)
if alt:
exe = alt / "Conway's Trap.exe"
raw = exe.read_bytes()
seed = recover_seed()
print(f'[+] seed : {seed.hex()}')
hidden = fetch_hidden_constants(raw)
print(f'[+] secret: {hidden.hex()}')
body = decode_body(seed, hidden)
print(f'[+] body : {body}')
print(f'[*] flag : ISCC{{{body}}}')
if __name__ == '__main__':
main()
运行结果输出:
[+] seed : 0b5e321c68e0e4fb2d972226e8c70f8d
[+] secret: 80e8512fb19a9b1332a47888df95efc2ddbf8dab146f76
[+] body : NocwosxmveVar{i9uEtwc5E
[*] flag : ISCC{NocwosxmveVar{i9uEtwc5E}
评论