首页
社区
课程
招聘
[原创] 看雪 KCTF 2026 第五题《申时·忆海倒带》Writeup
发表于: 2026-8-17 13:53 9

[原创] 看雪 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 / f9eb06 = jmp +6,跳过后面的垃圾字节,执行流线性,但线性反汇编被带偏。IDA 反编译器能直接还原
  • 假注释(误导)——注意:这三条根本不是注释,而是被 sub_40210016 进制大数解析的参数(伪装):
    • // 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 秒分解,一条龙下来就是几分钟的事。作者赌你不敢分解、不肯静态复算,可这两步恰恰是最省事的。
  1. IDA F5 反编译 main / 模幂,看穿花指令,还原校验管线。
  2. 目标串 Welcome to KCTF2026! Come and give it a try. 逆着查表 → 44 个 forced 字节。
  3. forced[0:28] 就是 Key 高 28 字节(查表是双射,直接出)。
  4. forced[28:44] = 需要的 RSA 输出;从二进制读出 n(128 位)、e=65537
  5. factordb / yafu / msieve 秒分解 128 位 n → p、q,RSA 解密得 Key 低 16 字节。
  6. 高 28 + 低 16 拼成 44 字节 → 88 位 hex,即 Key。
  • 文件:cm.exe(PE32 / x86,内部名 ca_tf6.exe),main = 0x403D90
  • 判胜:输入 Key,回车,打印 verify success.
  • Key:

  • 传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

    收藏
    免费 0
    打赏
    分享
    最新回复 (0)
    游客
    登录 | 注册 方可回帖
    返回