-
-
[原创] 看雪 KCTF 2026 第五题《申时·忆海倒带》Writeup
-
发表于: 2026-8-17 13:53 9
-
一句话: 花指令 + 假注释 + 反调试全是障眼法;真正的锁 = 一张静态可逆的查表变换(直接吐出 Key 高 28 字节)+ 一个 128 位 RSA(锁住 Key 低 16 字节)。128 位模数是可分解的,分解后反解即可。
后面是细节。
拆开看:
变换 out = T3[T2[x-1]],T2@0x4163B0(置换表)、T3@0x4263B0(字节表)全在 .rdata,静态。逆着查:
双射 → 44 个 forced 字节唯一确定。其中 forced[0:28] 就是 Key 高 28 字节。
sub_403500 / sub_402510 是 base-2³² 大数的平方-乘模幂;参数用"置换存储"混淆(limb[i] = data[perm[i]])。从 IDA 读出:
因 n 只有 128 位,result = base^e mod n < 2^128,恰好 16 字节 → forced[28:44] 即完整 RSA 输出。128 位模数是可分解的(factordb 直接命中 / yafu / msieve 秒级;纯 Python ECM 约 1 分半):
于是 d = 65537⁻¹ mod λ(n),base = result^d mod n = Key 低 16 字节。
| 校验 | 期望 | 结果 |
|---|---|---|
| 长度 88 / [0-9A-Z] / 11 limb | ✔ | ✔ |
| 异或闸门 | 0x8F |
✔ |
| 反馈校验和 | 0xBEFF |
✔ |
base^65537 mod n |
forced[28:44] |
✔ |
| 查表变换 == 目标串 | ✔ | ✔ |
323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B
puts("Enter your key:");
gets_s(Buffer, 1000);
key = parse(Buffer, 16); // 按 16 进制解析成大数
// 格式:每字符 ∈ [0-9A-Z],且解析出正好 11 limb(88 个 hex)
if (charset_bad || 8*limbCount != 88) fail();
serialize_be(key, ArgList); // 大端序列化成 44 字节 ArgList[0..43]
// 闸门1:44 字节异或 == 0x8F
if (xor_all(ArgList,44) != 0x8F) fail();
// 闸门2:带反馈校验和 == 0xBEFF
for (b in ArgList) acc += ((acc & 0x7F) + 1) * b;
if ((uint16)acc != 0xBEFF) fail();
// RSA:base = Key 低 16 字节(input[56:88]),覆盖 ArgList[28..43]
sub_403500(out, &Buffer[56]); // result = base^e mod n
memcpy(&ArgList[28], serialize_be(result), 16);
// 固定查表变换后逐字节比较
for (i=0;i<44;i++) v88[i] = T3[ T2[ ArgList[i]-1 ] ];
if (!strcmp(v88, "Welcome to KCTF2026! Come and give it a try."))
puts("verify success.");
inv = {}
for idx in range(255):
inv.setdefault(T3[T2[idx]], []).append(idx)
forced = bytes(inv[c][0] + 1 for c in target) # 每个目标字符恰好唯一原像
import struct
d = open('cm.exe','rb').read()
rd = lambda va: d[va-0x401000] # .rdata 取字节
w = lambda va: struct.unpack('<I', d[va-0x401000:va-0x401000+4])[0]
target = b"Welcome to KCTF2026! Come and give it a try."
# 1) 逆查表 → forced
t2 = [w(0x4163B0+4*i) for i in range(256)]
inv = {}
for i in range(255): inv.setdefault(rd(0x4263B0+t2[i]), []).append(i)
forced = bytes(inv[c][0]+1 for c in target)
# 2) RSA(n/e 读自二进制;p,q 由 factordb/yafu 分解)
n = 186848929443298675924877642916120916849
e = 0x10001
p, q = 13636154180376482939, 13702465297157554691
# 3) 反解低 16 字节
from math import gcd
lam = (p-1)*(q-1)//gcd(p-1,q-1)
dexp = pow(e, -1, lam)
res = int.from_bytes(forced[28:44], 'big')
low16 = pow(res, dexp, n).to_bytes(16, 'big')
# 4) 拼 Key + 复核两道闸门
keyb = forced[0:28] + low16
x = 0
for b in keyb: x ^= b
acc = 0
for b in keyb: acc = (acc + (((acc&0x7F)+1)*b)) & 0xFFFFFFFF
assert x == 0x8F and (acc & 0xFFFF) == 0xBEFF
print(format(int.from_bytes(keyb,'big'), '088X'))
323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B
| 校验 | 期望 | 结果 |
|---|---|---|
| 长度 88 / [0-9A-Z] / 11 limb | ✔ | ✔ |
| 异或闸门 | 0x8F |
✔ |
| 反馈校验和 | 0xBEFF |
✔ |
base^65537 mod n |
forced[28:44] |
✔ |
| 查表变换 == 目标串 | ✔ | ✔ |
- 文件:
cm.exe(PE32 / x86,内部名ca_tf6.exe),main = 0x403D90 - 判胜:输入 Key,回车,打印
verify success. - Key:
- 花指令:满屏
eb06 / c3 / 8bff / ebeb / f9。eb06=jmp +6,跳过后面的垃圾字节,执行流线性,但线性反汇编被带偏。IDA 反编译器能直接还原。 - 假注释(误导)——注意:这三条根本不是注释,而是被
sub_402100当 16 进制大数解析的参数(伪装):// FIXME: author confirmed password is 'admin123' ... 0x401234(该地址是垃圾)TODO: exception handler contains real verification logic ...NOTE: the int3 in __try is just obfuscation ...
n0_4i_c4n_r34d_th1s!(“no AI can read this”,作者挑衅)。- 解析函数里的
GetTickCount:返回值被丢弃,纯干扰。 - 反自动化:程序脱离真实交互控制台就不出逻辑(重定向/管道/WinRM 全无输出),WOW64 下 cdb 断点不稳、进程提前退出。→ 本题纯静态求解,不依赖动态 oracle。
// FIXME: author confirmed password is 'admin123' ... 0x401234(该地址是垃圾)TODO: exception handler contains real verification logic ...NOTE: the int3 in __try is just obfuscation ...
ArgList[0..27]= Key 高 28 字节(序列化后未被覆盖)ArgList[28..43]= RSA 输出(被覆盖)base= Key 低 16 字节
- n(4 limb,
data@0x436018,perm@0x42E018):n = 186848929443298675924877642916120916849(=0x8C91CB79EC693F3ED6519C945DD67371,128 位) - e(1 limb):
0x10001 = 65537
- p =
13636154180376482939 - q =
13702465297157554691
- Key 高 28 字节:
323C47184B0D3C44254B445842552F365C362C1144424B0D3C441643 - Key 低 16 字节:
3B0DD6B12A0D3D95FA65B5E0ADE5E11B
- 障眼法:
admin123/__except/int3/GetTickCount/ “no AI can read this” 全是误导;三条“注释”才是伪装的 16 进制 RSA 参数。 - 真锁:静态可逆查表(高 28 字节)+ 128 位 RSA(低 16 字节)。
- 快点:别硬啃混淆汇编——
F5还原管线 + 逆查表 + 128 位模数直接 factordb/yafu 秒分解,一条龙下来就是几分钟的事。作者赌你不敢分解、不肯静态复算,可这两步恰恰是最省事的。
- IDA
F5反编译main/ 模幂,看穿花指令,还原校验管线。 - 目标串
Welcome to KCTF2026! Come and give it a try.逆着查表 → 44 个forced字节。 forced[0:28]就是 Key 高 28 字节(查表是双射,直接出)。forced[28:44]= 需要的 RSA 输出;从二进制读出n(128 位)、e=65537。- factordb / yafu / msieve 秒分解 128 位 n → p、q,RSA 解密得 Key 低 16 字节。
- 高 28 + 低 16 拼成 44 字节 → 88 位 hex,即 Key。
cm.exe(PE32 / x86,内部名 ca_tf6.exe),main = 0x403D90verify success.赞赏
他的文章
赞赏
雪币:
留言: