首页
社区
课程
招聘
[原创]KCTF 2026 第八题:亥子合辰·塔影迷楼 writeup
发表于: 2026-8-22 10:43 630

[原创]KCTF 2026 第八题:亥子合辰·塔影迷楼 writeup

2026-8-22 10:43
630

查看导入表与节区特征:

导入表仅 8 个 KERNEL32 基础 API,无 I/O 相关函数(采用 Direct Syscall)。

包含 VirtualProtect、FlushInstructionCache 以及自定义的 .tgt 节,存在典型的自修改代码(SMC)。

.tgt 节内包含两组偏移与长度参数:(0x1800, 0x510) 与 (0x1d10, 0x4962)。

存在级联校验,动态调试下软断点(0xCC)会破坏 Stage 2 密钥流,导致后续代码解密失败。

解出的明文可以直接 patch 进 IDA 中分析(起始地址 0x140001800,长度 0x4E72)。

解密后分析校验函数 0x140004380:

使用 Unicorn 构建模拟运行环境,Hook 掉 rdtsc,模拟 PEB/TEB 和 syscall 桩,确保 0x1400046ed 处的 rbx 累加器为 0。

主流程逻辑分两个阶段:

Base-94 编码:
将 16 字节可见字符(ASCII 33~126)按 Horner 算法转成 128 位大整数 X:
X=∑i=015​(extinput[i]−33)imes9415−i
因为 9416pprox2104.87<2127,转换过程无溢出,可完全双向还原。

模素数多项式:
在 GF(M)(模数 M=2127−39,为素数)下计算五次多项式:
P1​=X5+FE0​X4+FE1​X3+FE2​X2+FE3​X+FE4​(modM)
(FE0​∼FE4​ 为运行时 PRNG 导出的常量)。

P1​ 被拆为高低 64 位 (P, Q),分别输入两组各 20 轮的双子轮结构(Loop 1 输出 A,Loop 2 输出 B):

利用终点比较的已知目标值 T1​,T2​(由 .tgt 掩码解密得到),分别从 Loop 1 和 Loop 2 逆推回 P1​:

两路逆推均得到一致的中间值:
P1ext−target​=ext0x4be831b0ad3a2d361489375bba3fb8de

在有限域 GF(2127−39) 上求解多项式方程:
X5+FE0​X4+FE1​X3+FE2​X2+FE3​X+(FE4​−P1ext−target​)≡0(modM)

得到唯一有效实根:
X=ext0x00000174a5e1d4cf4f146e72f70092ac

将 X 连续模 94 展开为 16 位字符(+33),得到字符串:kanxue@2o26o8!@#。其 ASCII 和刚好为 1280,满足初始 Gate 约束。

运行:

import sys, pefile
from capstone import Cs, CS_ARCH_X86, CS_MODE_64

PATH = "kctf2026_CrackMe08.exe"
pe = pefile.PE(PATH)
IB = pe.OPTIONAL_HEADER.ImageBase
raw = bytearray(open(PATH, "rb").read())

def va2off(va):
    for s in pe.sections:
        start = IB + s.VirtualAddress
        if start <= va < start + max(s.Misc_VirtualSize, s.SizeOfRawData):
            return s.PointerToRawData + (va - start)
    return None

# 解 Stage 1
o1 = va2off(0x140001800)
for i in range(0x510):
    raw[o1 + i] ^= 0x5A

# 计算 FNV-1a 并解 Stage 2
M, PRIME, GOLD = (1 << 64) - 1, 0x100000001b3, 0x9e3779b97f4a7c15
H1, H2 = 0xcbf29ce484222325, GOLD
for b in raw[o1:o1 + 0x510]:
    H1 = ((H1 ^ b) * PRIME) & M
    H2 = ((H2 ^ b) * PRIME) & M

o2 = va2off(0x140001D10)
for p in range(0x4962):
    t = (p * GOLD) & M
    t = (t ^ H2 ^ H1) ^ ((t ^ H2 ^ H1) >> 33)
    t = (t * 0xff51afd7ed558ccd) & M
    raw[o2 + p] ^= (t >> 56) & 0xff

open("dec.bin", "wb").write(raw)
输入 (16 字节) 
   │
   ▼
[Phase 1] Base-94 编码 -> 128 位大整数 X -> 模多项式 P1 
   │
   ▼
[Phase 2] 两路独立的 Feistel 混淆器 (各 20 轮) -> 输出 (A, B) 与目标 (T1, T2) 比较
def bwd_round(P, Q, lo1, lo2):
    P_prev = Q ^ mix(F(P, lo2))
    Q_prev = P ^ mix(F(P_prev, lo1))
    return P_prev, Q_prev
# solve.py
import random
import model

M = (1 << 127) - 39

# ---------- GF(M) 上的多项式:系数列表,下标 = 次数 ----------
def trim(a):
    while a and a[-1] == 0: a.pop()
    return a

def pmul(a, b):
    if not a or not b: return []
    r = [0] * (len(a) + len(b) - 1)
    for i, x in enumerate(a):
        if x:
            for j, y in enumerate(b):
                if y: r[i+j] = (r[i+j] + x*y) % M
    return trim(r)

def pdivmod(a, b):
    a = a[:]; db = len(b) - 1; invb = pow(b[-1], M-2, M)
    q = [0] * max(0, len(a) - db)
    while len(a) - 1 >= db and a:
        d = len(a) - 1 - db
        c = a[-1] * invb % M
        q[d] = c
        for i, y in enumerate(b):
            a[i+d] = (a[i+d] - c*y) % M
        trim(a)
    return trim(q), a

def pmod(a, b): return pdivmod(a, b)[1]

def pgcd(a, b):
    a, b = trim(a[:]), trim(b[:])
    while b:
        a, b = b, pmod(a, b)
    if a:
        inv = pow(a[-1], M-2, M)
        a = [c*inv % M for c in a]
    return a

def ppowmod(base, e, f):
    r = [1]; b = pmod(base[:], f)
    while e:
        if e & 1: r = pmod(pmul(r, b), f)
        b = pmod(pmul(b, b), f); e >>= 1
    return r

def psub(a, b):
    n = max(len(a), len(b))
    return trim([((a[i] if i < len(a) else 0) - (b[i] if i < len(b) else 0)) % M for i in range(n)])

def roots(f):
    f = trim(f[:])
    inv = pow(f[-1], M-2, M); f = [c*inv % M for c in f]
    g = pgcd(f, psub(ppowmod([0, 1], M, f), [0, 1]))   # gcd(f, x^M - x)
    out = []
    def split(p):
        deg = len(p) - 1
        if deg <= 0: return
        if deg == 1:
            out.append((-p[0] * pow(p[1], M-2, M)) % M); return
        while True:
            a = random.randrange(M)
            t = psub(ppowmod([a, 1], (M-1)//2, p), [1])
            d = pgcd(p, t)
            if 0 < len(d)-1 < deg:
                split(d); split(pdivmod(p, d)[0]); return
    split(g)
    return sorted(set(out))

if __name__ == "__main__":
    MASK = (1 << 64) - 1
    cfg = model.dump()
    m = model.Mix(cfg)
    a = m.bwd(cfg['T1'] >> 64, cfg['T1'] & MASK, cfg['rc1'], False)
    b = m.bwd(cfg['T2'] >> 64, cfg['T2'] & MASK, cfg['rc2'], True)
    assert a == b, "loop1/loop2 不一致,模型有错"
    P1T = (a[0] << 64) | a[1]
    print("P1_target = %032x" % P1T)

    FE = cfg['FE']
    f = [(FE[4] - P1T) % M, FE[3], FE[2], FE[1], FE[0], 1]
    # 自检:已知的 (X, P1) 对必须满足未打靶的多项式
    chk = 0
    for c in [1] + FE: chk = (chk*cfg['X'] + c) % M
    assert chk == cfg['P1'], "多项式形式不对"

    rs = roots(f)
    print("五次方程的根个数:", len(rs))
    LIM = 94 ** 16
    for X in rs:
        print("  X = %032x   < 94^16 : %s" % (X, X < LIM))
        if X >= LIM: continue
        d, v = [], X
        for _ in range(16):
            d.append(v % 94); v //= 94
        d.reverse()
        s = "".join(chr(c + 33) for c in d)
        print("    字节和 = %d (需要 1280)   全可打印=%s" % (sum(d) + 528, all(0 <= c <= 93 for c in d)))
        print("    CANDIDATE = %r" % s)
$ python solve.py
P1_target = 4be831b0ad3a2d361489375bba3fb8de
五次方程的根个数: 1
  X = 00000174a5e1d4cf4f146e72f70092ac   < 94^16 : True
    字节和 = 1280 (需要 1280)   全可打印=True
    CANDIDATE = 'kanxue@2o26o8!@#'

real    0m0.454s

存在级联校验,动态调试下软断点(0xCC)会破坏 Stage 2 密钥流,导致后续代码解密失败。


[招生]科锐逆向工程师培训(2026年11月13日实地,远程教学同时开班, 第57期)

收藏
点赞 ・4
打赏
分享
最新回复 (1)
雪    币: 437
活跃值: (370)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2 楼
111
2026-9-8 17:18
0
游客
登录 | 注册 方可回帖
返回