-
-
[原创] 看雪·2026 KCTF > 第五题:申时·忆海倒带
-
发表于: 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_403500 内 0x403A26 冻结,读线程栈上的 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 0x16380(Welcome 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 0x16380(Welcome 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] - 1,sub_404870(&v81, idx):
off_table:运行时0x6963B0(RVA0x163B0),256 个 dword 偏移。- pool:运行时
0x6A63B0(RVA0x263B0),0x4000 字节,内容为 95 个可打印字符0x20~0x7E打散存放。 - 动态 dump 后验证:
keybyte -> out是双射,目标串每个字符唯一对应一个 Key 字节。
- 首次
0x80000003(初始断点)后写入补丁:0x41BE、0x426B:NOP 掉两个预校验跳转;0x4515:把装载器存字节改成写向自建 scratch(VirtualAllocEx的out_addr):88 86 <out_addr> 90(mov [esi+out_addr], al,esi=循环下标);- 在关键点写自旋补丁
C6 05 <mark> 41 EB FE冻结进程,再ReadProcessMemory读内存。
- 冻结点:
0x404542(装载循环之后、比较之前):dumpArgList、var_88、v81、pool、off_table;0x403A26(sub_403500内模幂返回后):dumpv44(模数)、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
- pool:
0x41BE、0x426B:NOP 掉两个预校验跳转;0x4515:把装载器存字节改成写向自建 scratch(VirtualAllocEx的out_addr):88 86 <out_addr> 90(mov [esi+out_addr], al,esi=循环下标);- 在关键点写自旋补丁
C6 05 <mark> 41 EB FE冻结进程,再ReadProcessMemory读内存。
0x404542(装载循环之后、比较之前):dumpArgList、var_88、v81、pool、off_table;0x403A26(sub_403500内模幂返回后):dumpv44(模数)、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.exe:verify success.✓