首页
社区
课程
招聘
[原创] 看雪·2026 KCTF > 第五题:申时·忆海倒带
发表于: 5天前 12

[原创] 看雪·2026 KCTF > 第五题:申时·忆海倒带

htg 活跃值
7
5天前
12

程序运行时会对代码段进行解密(自解密/壳),静态 IDB 中已把运行时解密后的代码回填。运行时代码基址 0x680000,IDA 中以 0x400000 为基址显示,两者相差 0x280000;数据区立即数均为运行时绝对地址。

主函数(0x404000 附近)流程:

0x4041B0:44 字节 ArgList 异或结果必须等于 0x8F

0x404218

out[i] = *(0x6A63B0 + off_table[keybyte-1])

sub_403500(&var_92C, &var_820) 的输入是 Key 尾部 16 字节(ArgList[28..43])的 32 位大写十六进制串。函数内部:

动态抓取(在 sub_4035000x403A26 冻结,读线程栈上的 v44/v46/v29 结构体,再按置换表读出 limb 值):

验证:pow(0x0F4439374E3C44372544164425151D1B, 0x10001, mod) == 0x2CDFE7C7F5FEE09E2CDF7CAB86775A85(与运行观察一致)。即典型的 RSA 加密,e = 65537

注:大整数 limb 通过置换表访问:limb(idx) = *(limbs + 4*perm[idx]),perm 表在 0x6AE018/0x6B4018(模数/指数)。

sub_402100 再次解析 var_92C 的十六进制串,解压成 16 字节大端序写入 var_88,随后 0x40436B 的拷贝循环把 var_88[0..15] 拷到 ArgList[28..43][ebp-0x454..-0x445],16 字节)。

因此装载器实际使用的 ArgList 为:key[0..27] 拼接 RSA(key[28..43]) 的大端序结果。动态验证:probe Key 的 ArgList 尾部被替换为 2c df e7 c7 f5 fe e0 9e 2c df 7c ab 86 77 5a 85,与 var_88 完全一致。

采用 DebugActiveProcess + WaitForDebugEvent 的方式(ctypes 直接调用,64 位 Python 对 32 位进程用 Wow64GetThreadContext 取寄存器):

目标串 44 字节。由双射关系 out[i] = pool[table[b-1]],对每个目标字符求出唯一 Key 字节:

前 28 字节即 key[0..27];后 16 字节为 RSA 输出必须等于 0x0F4439374E3C44372544164425151D1B

分解模数:

求私钥指数:

验证:pow(key_tail, e, n) == 0x0F4439374E3C44372544164425151D1B

运行时基址 0x680000
目标串 RVA 0x16380Welcome to KCTF2026! ...
off_table RVA 0x163B0(运行时 0x6963B0
pool RVA 0x263B0(运行时 0x6A63B0
v81 模板 运行时 0x6B8030(20 字节)
RSA e 0x10001
RSA n 0x8C91CB79EC693F3ED6519C945DD67371
n = p×q 13636154180376482939 × 13702465297157554691
私钥指数 d 0x721A022D849DE5CC5AD9BBB701F9FB65
acc = 0;
for (i = 0; i < 44; i++)
    acc = (acc + ((acc & 0x7F) + 1) * key[i]) & 0xFFFF;
// 结果必须等于 0xBEFF
v81 = 8f 40 00 00 | b0 63 6a 00 | 01 2c b8 70 | b0 63 69 00 | 80 63 68 00
     count=0x408F  base=0x6A63B0  heap  ptr       off_table=0x6963B0  pool2=0x686380
int sub_404870(unsigned int *this, unsigned int idx) {
    if (idx < this[0])                       // 0x408F,恒真
        return this[1] + sub_402D70(idx);    // 0x6A63B0 + off_table[idx]
    return this[1];
}
int sub_402D70(unsigned int *this, int idx) {
    return *(DWORD*)(this[0xC] + 4*idx);     // *(0x6963B0 + 4*idx)
}
base = int(key[28..43])           // 大端序 16 字节
exp  = 0x10001                    // 65537
mod  = 0x8C91CB79EC693F3ED6519C945DD67371
out  = pow(base, exp, mod)
req[0..43] = 32 3c 47 18 4b 0d 3c 44 25 4b 44 58 42 55 2f
             36 5c 36 2c 11 44 42 4b 0d 3c 44 16 43
             0f 44 39 37 4e 3c 44 37 25 44 16 44 25 15 1d 1b
n = 0x8C91CB79EC693F3ED6519C945DD67371
  = 13636154180376482939 × 13702465297157554691
e = 0x10001
phi(n) = (p-1)*(q-1)
d = e^{-1} mod phi(n) = 0x721A022D849DE5CC5AD9BBB701F9FB65
key_tail = 0x0F4439374E3C44372544164425151D1B^d mod n
         = 0x3B0DD6B12A0D3D95FA65B5E0ADE5E11B
key = req[0..27] || key_tail(16 字节大端)
    = 323c47184b0d3c44254b445842552f365c362c1144424b0d3c441643
      3b0dd6b12a0d3d95fa65b5e0ade5e11b
323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B

注:大整数 limb 通过置换表访问:limb(idx) = *(limbs + 4*perm[idx]),perm 表在 0x6AE018/0x6B4018(模数/指数)。

运行时基址 0x680000
目标串 RVA 0x16380Welcome to KCTF2026! ...
off_table RVA 0x163B0(运行时 0x6963B0
pool RVA 0x263B0(运行时 0x6A63B0
v81 模板 运行时 0x6B8030(20 字节)
RSA e 0x10001
RSA n 0x8C91CB79EC693F3ED6519C945DD67371
n = p×q 13636154180376482939 × 13702465297157554691
私钥指数 d 0x721A022D849DE5CC5AD9BBB701F9FB65
  • 程序:cm.exe(Windows 32 位)
  • SHA1:d1c8124c5964af1531c6e311328854a05ca40bca
  • 判定条件:打开 cm.exe,输入序列号 Key 后回车,若显示 verify success. 即成功
  • 目标:求出可通过校验的 88 位十六进制 Key
  • sub_4033D0(&v81) 把全局 0x6B8030 处 20 字节拷入 v81[ebp-0x86C]),动态抓取:
  • 装载器(0x404504 起)逐字节:idx = ArgList[i] - 1sub_404870(&v81, idx)
  • off_table:运行时 0x6963B0(RVA 0x163B0),256 个 dword 偏移。
  • pool:运行时 0x6A63B0(RVA 0x263B0),0x4000 字节,内容为 95 个可打印字符 0x20~0x7E 打散存放。
  • 动态 dump 后验证:keybyte -> out 是双射,目标串每个字符唯一对应一个 Key 字节。
  • 首次 0x80000003(初始断点)后写入补丁:
    • 0x41BE0x426B:NOP 掉两个预校验跳转;
    • 0x4515:把装载器存字节改成写向自建 scratch(VirtualAllocExout_addr):88 86 <out_addr> 90mov [esi+out_addr], al,esi=循环下标);
    • 在关键点写自旋补丁 C6 05 <mark> 41 EB FE 冻结进程,再 ReadProcessMemory 读内存。
  • 冻结点:
    • 0x404542(装载循环之后、比较之前):dump ArgListvar_88v81、pool、off_table;
    • 0x403A26sub_403500 内模幂返回后):dump v44(模数)、v46(指数)、v29(base/result) 结构体及其 limb/perm 表;
    • 0x40410A(ArgList 构造后):确认 ArgList 初始等于输入十六进制字节。
  • 关键内存:
    • pool:0x6A63B0(0x4000 字节,pool2.bin
    • off_table:0x6963B0(0x400 字节,table2.bin
    • 模数 limbs:0x6B6018,perm:0x6AE018;指数 limbs:0x6B0018,perm:0x6B4018
  • 0x41BE0x426B:NOP 掉两个预校验跳转;
  • 0x4515:把装载器存字节改成写向自建 scratch(VirtualAllocExout_addr):88 86 <out_addr> 90mov [esi+out_addr], al,esi=循环下标);
  • 在关键点写自旋补丁 C6 05 <mark> 41 EB FE 冻结进程,再 ReadProcessMemory 读内存。
  • 0x404542(装载循环之后、比较之前):dump ArgListvar_88v81、pool、off_table;
  • 0x403A26sub_403500 内模幂返回后):dump v44(模数)、v46(指数)、v29(base/result) 结构体及其 limb/perm 表;
  • 0x40410A(ArgList 构造后):确认 ArgList 初始等于输入十六进制字节。
  • pool:0x6A63B0(0x4000 字节,pool2.bin
  • off_table:0x6963B0(0x400 字节,table2.bin
  • 模数 limbs:0x6B6018,perm:0x6AE018;指数 limbs:0x6B0018,perm:0x6B4018
  • 异或和:0x8F
  • 加权和:0xBEFF
  • 实跑 cm.exeverify success.

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

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