首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创]KCTF 2026 第五题 "忆海倒带" WriteUp
发表于: 2026-8-17 12:53
23
[原创]KCTF 2026 第五题 "忆海倒带" WriteUp
BenBenWen
1
2026-8-17 12:53
23
## 1. 从字符串入手定位主逻辑 题目说明里提到成功条件是输出 `verify success.`,所以第一步先在 IDA 里看字符串引用。 关键字符串: ```text Enter your key: verify success. verify fail.retry it... Welcome to KCTF2026! Come and give it a try. admin123 r3v3rs3! password ``` 对 `Enter your key:`、`verify success.`、`verify fail.retry it...` 做 Xrefs,都会落到 `_main`,主函数地址为: ```text _main = 0x403D90 ``` F5 后能看到主函数中先输出提示,再读取输入: ```c sub_4033A0("Enter your key:\n"); gets_s(Buffer, 0x3E8u); sub_403C60(Buffer); ``` 这里 `sub_403C60(Buffer)` 很显眼,但不要急着下结论。 --- ## 2. 识别假线索 `sub_403C60` 里面引用了大量提示性字符串,例如: ```text // FIXME: author confirmed password is 'admin123', verified by strcmp at offset 0x401234 TODO: exception handler contains real verification logic, do not skip __except block NOTE: the int3 in __try is just obfuscation, real check is in the handler - developer note ``` F5 伪代码如下: ```c int __thiscall sub_403C60(void *this) { int v3; int v4; int v5; v3 = sub_403180("// FIXME: author confirmed password is 'admin123', verified by strcmp at offset 0x401234"); v4 = sub_403180("TODO: exception handler contains real verification logic, do not skip __except block") + v3; v5 = sub_403180("NOTE: the int3 in __try is just obfuscation, real check is in the handler - developer note") + v4; return sub_403230(this) + v5; } ``` 继续看 `sub_403230`: ```c BOOL __thiscall sub_403230(_BYTE *this) { _BYTE *v1 = this; char *v2 = "admin123" - this; int v7 = 8; unsigned __int8 v3 = 0; char v4 = 1 - (_BYTE)this; do { v3 += (v4 + (_BYTE)v1) * (*v1 ^ v1[(_DWORD)v2]); v1++; } while ( v7-- != 1 ); return (v3 | -v3) >= 0; } ``` 还有 `sub_4032E0`: ```c int __thiscall sub_4032E0(const char *this) { if ( !this ) return -1; if ( !strcmp(this, "admin123") ) __debugbreak(); if ( !strcmp(this, "r3v3rs3!") ) __debugbreak(); if ( !strcmp(this, "password") ) __debugbreak(); return 0; } ``` 这些函数只制造 `admin123`、`r3v3rs3!`、`password` 和 `int3` 的干扰。真正的 `verify success.` 分支不依赖这些字符串,而是在 `_main` 后半段。 --- ## 3. 输入格式和长度 回到 `_main`,输入会先被复制到一个 `std::string` 风格对象,然后调用 `sub_402100`: ```c sub_402EA0(v71, Buffer); sub_402100(v71, 16); ``` `sub_402100` 的核心逻辑是把字符串按 16 进制转成大整数: ```c for ( i = 0; i < length; ++i ) { // 当前大整数先乘 16 v3 = sub_401A90(v16, 16); sub_4012D0(v3); // 解析一个 hex 字符 if ( ch >= '0' && ch <= '9' ) nibble = ch - '0'; else if ( ch >= 'A' && ch <= 'F' ) nibble = ch - 'A' + 10; else if ( ch >= 'a' && ch <= 'f' ) nibble = ch - 'a' + 10; else nibble = 0; // 加上 nibble v14 = sub_401510(v16, nibble); sub_4012D0(v14); } ``` 之后 `_main` 检查输入字符和长度: ```c for ( n = 8 * v76; v11 < n; ++v11 ) { v13 = Buffer[v11]; if ( !v13 ) break; if ( (v13 < '0' || v13 > '9') && (unsigned __int8)(v13 - 'A') > 0x19u ) goto fail; } if ( n != 88 ) goto fail; ``` 这里 `v76` 是大整数 DWORD 数量。`n = 8 * v76`,最终要求: ```text n == 88 ``` 也就是说: ```text 输入长度 = 88 个字符 输入内容 = 大写字母或数字 实际有效解析 = 88 个 hex 字符 = 44 字节 ``` 注册码最后确实是 88 位十六进制字符串。 --- ## 4. 第一层校验:44 字节整体校验 程序把大整数按 DWORD 倒序拆成字节,放入 `ArgList`。简化逻辑如下: ```c for ( i = word_count - 1; i >= 0; --i ) { word = bigint[i]; ArgList[pos + 0] = HIBYTE(word); ArgList[pos + 1] = BYTE2(word); ArgList[pos + 2] = BYTE1(word); ArgList[pos + 3] = LOBYTE(word); pos += 4; } ``` 所以 88 个 hex 字符会还原成 44 字节。 接着做两个校验。 ### 4.1 XOR 校验 IDA F5: ```c v18 = 0; for ( i = 0; i < 4 * v53; ++i ) v18 ^= ArgList[i]; if ( v18 != -113 ) goto fail; ``` `-113` 按 `unsigned char` 看就是: ```text 0x8F ``` 即: ```python x = 0 for b in data44: x ^= b assert x == 0x8F ``` ### 4.2 16-bit 滚动校验 F5: ```c v58 = 0; if ( 4 * (_WORD)v53 ) { v54 = (unsigned __int16)(4 * v53); v52 = ArgList; do { v58 += (unsigned __int8)*v52++ * ((v58 & 0x7F) + 1); --v54; } while ( v54 ); } if ( v58 == -16641 ) { // continue } else { goto fail; } ``` `v58` 是 `__int16`,`-16641` 对应无符号值: ```text 0xBEFF ``` 等价 Python: ```python acc = 0 for b in data44: acc = (acc + b * ((acc & 0x7F) + 1)) & 0xFFFF assert acc == 0xBEFF ``` --- ## 5. 最后一层:字符映射比较 第一层通过后,程序会进入最终比较逻辑: ```c do { v88[i] = *(_BYTE *)sub_404870(ArgList[i] - 1); ++i; } while ( i < 4 * v53 ); if ( strcmp("Welcome to KCTF2026! Come and give it a try.", v88) ) print("verify fail.retry it..."); else print("verify success."); ``` 也就是说,每个字节不是直接当 ASCII,而是当成一个 1-based 索引: ```text output[i] = table[ArgList[i] - 1] ``` `sub_404870` 不是普通连续数组,它通过偏移表取字符: ```c int __thiscall sub_404870(unsigned int *this, unsigned int index) { if ( index < *this ) return *(this + 1) + sub_402D70(index); else return *(this + 1); } int __thiscall sub_402D70(_DWORD *this, int index) { return *(_DWORD *)(*(this + 3) + 4 * index); } ``` `sub_4033D0` 会构造这个表对象: ```c byte_438031 = 64; // data pointer byte_438034 = low_byte(&unk_4263B0); byte_438035 = high_byte(&unk_4263B0); byte_438036 = ... byte_438037 = ... // offset table pointer byte_43803C = low_byte(&unk_4163B0); byte_43803D = high_byte(&unk_4163B0); byte_43803E = ... byte_43803F = ... ``` 实际关系: ```text char = *(byte *)(0x4263B0 + *(uint32 *)(0x4163B0 + 4 * index)) ``` 目标字符串: ```text Welcome to KCTF2026! Come and give it a try. ``` 前 28 个字符可以直接反查表,得到前 28 字节: ```text 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 ``` 对应注册码前 56 个 hex 字符: ```text 323C47184B0D3C44254B445842552F365C362C1144424B0D3C441643 ``` --- ## 6. 后 16 字节:RSA-like 变换 前面还有一个关键点:最终比较前,程序会对输入后 16 字节做一次变换。 调用点: ```asm 404271 lea edx, [ebp+var_820] ; Buffer + 0x38,也就是输入最后 32 个 hex 字符 404277 lea ecx, [ebp+var_92C] ; 输出 string 40427D call sub_403500 ``` `sub_403500` 内部会: 1. 把输入最后 32 个 hex 字符转成大整数。 2. 调用 `sub_402510` 做模幂。 3. 调用 `sub_4022D0` 把结果转回 hex 字符串。 4. 主函数再解析这个 hex 字符串,覆盖最终用于查表的后 16 字节。 `sub_402510` 的结构很像快速模幂: ```c // 伪代码化后的含义 result = 1; base = input; exp = const_exp; mod = const_mod; for each bit in exp: { result = result * result % mod; if (bit == 1) result = result * base % mod; } ``` 常量来源在 `sub_403500` 里构造: ```asm ; modulus 对象 4037CE mov ecx, offset unk_436018 ; data 4037FD mov ecx, offset unk_42E018 ; offset table ; exponent 对象 4038FE mov ecx, offset unk_430018 ; data 403926 mov ecx, offset unk_434018 ; offset table ``` 通过 IDA 读取对应表,可还原: ```text e = 0x10001 = 65537 N words little-endian: 5DD67371 D6519C94 EC693F3E 8C91CB79 N = 0x8C91CB79EC693F3ED6519C945DD67371 ``` 目标字符串后 16 个字符是: ```text d give it a try. ``` 反查字符映射表,得到最终比较期望的后 16 字节: ```text 0F 44 39 37 4E 3C 44 37 25 44 16 44 25 15 1D 1B ``` 也就是密文: ```text C = 0x0F4439374E3C44372544164425151D1B ``` 因为: ```text C = M^e mod N ``` 所以需要求: ```text M = C^d mod N ``` 分解 `N`: ```text N = 13636154180376482939 * 13702465297157554691 ``` 计算: ```python p = 13636154180376482939 q = 13702465297157554691 N = p * q e = 65537 C = 0x0F4439374E3C44372544164425151D1B phi = (p - 1) * (q - 1) d = pow(e, -1, phi) M = pow(C, d, N) print(M.to_bytes(16, "big").hex().upper()) ``` 得到输入后 16 字节: ```text 3B 0D D6 B1 2A 0D 3D 95 FA 65 B5 E0 AD E5 E1 1B ``` 对应注册码后 32 个 hex 字符: ```text 3B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` --- ## 7. 拼接并验证整体校验 前 28 字节来自字符表直接反查: ```text 323C47184B0D3C44254B445842552F365C362C1144424B0D3C441643 ``` 后 16 字节来自 RSA-like 逆运算: ```text 3B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 拼接: ```text 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 再验证第一层校验: ```python data = bytes.fromhex( "323C47184B0D3C44254B445842552F365C362C1144424B0D3C441643" "3B0DD6B12A0D3D95FA65B5E0ADE5E11B" ) x = 0 acc = 0 for b in data: x ^= b acc = (acc + b * ((acc & 0x7F) + 1)) & 0xFFFF print(hex(x), hex(acc)) ``` 输出: ```text 0x8f 0xbeff ``` 与程序校验一致。 --- ## 8. 最终运行验证 PowerShell: ```powershell $key = '323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B' ($key + "`n`n") | & '.\cm.exe' ``` 输出: ```text Enter your key: verify success. ``` --- ## 9. 题目难点总结 1. 明显字符串是烟雾弹,`admin123`、`r3v3rs3!`、`password` 都不是答案。 2. `sub_403C60` 看起来像异常/反调试校验,但实际不决定成功分支。 3. 输入是 88 个字符,但实际要按 16 进制还原成 44 字节。 4. 前 28 字节可以通过最终查表比较直接反推。 5. 后 16 字节不能直接反推,因为中间经过一次 RSA-like 模幂变换。 6. 需要识别 `e = 65537` 和 128-bit 模数 `N`,再做一次 RSA 逆运算。 7. 最后还要同时满足 XOR 校验和 16-bit 滚动校验,不能只满足最终字符串映射。
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
BenBenWen
1
13
发帖
48
回帖
110
RANK
关注
私信
他的文章
[原创]KCTF 2026 第九题 "星海抉择" WriteUp
58
[原创]KCTF 2026 第八题 "塔影迷楼" WriteUp
31
[原创]KCTF 2026 第五题 "忆海倒带" WriteUp
23
[原创]KCTF 2026 第二题 "绿光幽语" WriteUp
135
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部