首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创]第八题:亥子合辰·塔影迷楼(AI)
发表于: 2026-8-21 22:41
67
[原创]第八题:亥子合辰·塔影迷楼(AI)
onetry
2026-8-21 22:41
67
1、程序入口 start start 干三件事:先用 0x5A 异或解密 [0x1800, 0x510) 这块代码(偏移和大小存在 .tgt 段的 0x14000A028/0x14000A020),然后打印 Input: 读输入,最后调用 sub_140001070 和 sub_1400018D0。 F5 看 start(0x1400012C0)很直白:前半段一个 64 字节为单位的 XOR 循环(XMM 寄存器 4 个一组异或密钥),VirtualProtect 改成可写执行后解完再 FlushInstructionCache 还原;后半段 qmemcpy(Input:, "Input:", 6) 之后读输入,`if (n16 != 16)` 直接 fault——输入必须正好 16 字节。 sub_140001070 是 FNV 风格滚动哈希,结果等于 0x1111111111111111 就改个全局变量,但 start 根本没用它的返回值,纯诱饵,跳过。 2、校验函数 sub_1400018D0(第一层解密出来的代码) 这个函数是第二层解密器:先对自己解密后的代码区 [0x1800, 0x510) 做 FNV-1a 哈希(基 0xCBF29CE484222325、乘数 0x100000001B3),同时用 TEA delta 0x9E3779B97F4A7C15 做初值再算一个哈希,两个哈希值当流密码种子,解密 [0x1D10, 0x4962)(.tgt 段 0x14000A008/0x14000A000 存的偏移和大小),然后调用 0x140001D10,返回后再用同样的流密码把代码加密回去,最后 VirtualProtect 还原页属性。伪代码里这段特征很明显:两个初值不同的哈希循环,后面跟着大段的 SSE 流密码(vpmullq + 查表取密钥字节异或)。 0x140001D10 是真正的校验入口。它用 (input[0]*0x9E3779B97F4A7C15 ^ input[15]<<33) & 7 做下标去跳表取函数调用,但跳表里 8 个表项全是同一个地址 0x140004380——在 .rdata 的 0x140007100 能看到 8 个一模一样的 qword,纯混淆。另外这里有个 sum 检查:`(sum(input) & 0xFFFFFF) == 0x501` 时直接返回 0,所以总和不能是 0x501。 3、核心校验 sub_140004380(0x4380,8.9KB,重度混淆) 这个函数被 LLVM 风格的混淆折腾过,F5 出来 1989 行,全是不透明谓词、SIMD、128 位乘法展开。硬啃不划算,我的做法是:先把它的算法用 Python 完全复刻出来,再拿 Unicorn 模拟器跑真实代码当对照,逐条对中间值,直到 128 位全对上。 3-1、前置检查和反调试 函数开头先检查 16 字节的和:`(sum & 0xFFFFFF) != dword_14000A010` 直接返回 0,而 .tgt 段 0x14000A010 存的是 0x500,所以输入总和必须是 1280。 然后是四组反调试:PEB.BeingDebugged 和 ApiSetMap 检查;GetThreadContext 查 Dr0-Dr3 硬件断点;扫 ntdll 的 NtQueryInformationProcess 找 0F 05(syscall) 和 B8 imm32 提取 syscall 号,直接 syscall 调 ProcessDebugPort 和 ProcessDebugObjectHandle;两次 rdtsc 夹住一个 18KB 的哈希循环,超过 0x2FAF080 个周期就算被调试。任何一组命中都会污染后面的 PRNG 种子,正常无调试运行下这些标志全是 0。 3-2、PRNG 和常数 种子是 payload(解密后的 [0x1D10, 0x4962))的两个 FNV-1a 哈希: ``` v408 = FNV1a(payload, 基 0xCBF29CE484222325) v407 = FNV1a(payload, 基 0x9E3779B97F4A7C15) s0 = v408 ^ 反调试标志 s1 = v407 ^ 0xCBF29CE484222325 ``` 然后 43 轮,每轮出两个 64 位: ``` s0 = (s0 + 0x9E3779B97F4A7C15) & M64 s1 = (s1 + 0xBF58476D1CE4E5B9) & M64 out1 = mix(s0 + 0x9E3779B97F4A7C15) out2 = mix(s1 + 0x9E3779B97F4A7C15) s0 ^= out2; s1 ^= out1 mix(x): y = ((x ^ x>>30) * 0xBF58476D1CE4E5B9) & M64 z = y ^ (y >> 27) o = (z * 0x94D049BB133111EB) & M64 return o ^ (o >> 31) ``` 汇编里(0x140004740 起)这个循环很好认:一连串 imul 两个固定常数、shr 30/27/31、43 次循环(mov ebx, 2Bh)。86 个输出依次覆盖 CONTEXT 结构 0x00-0x2A8——GetThreadContext 先被调用一次,然后它的输出缓冲区整个被 PRNG 重写当工作区用。 各块用途: | CONTEXT 偏移 | 用途 | |---|---| | 0x00/0x10/0x20/0x30/0x40 | 多项式常数 C1-C5 | | 0x50 | 模乘常数 K | | 0x60-0x15F | 256 字节 S 盒 | | 0x160-0x1F8 | 循环 1 的 20 个轮字 w1 | | 0x200-0x298 | 循环 2 的 20 个轮字 w2 | | 0x2A0/0x2A8 | 目标掩码 X1/X2 | 这里有个坑:反编译出来的字段名有 union 歧义,循环 2 的轮字起点我一开始按伪代码理解成 0x1C0,实际是 0x200,回汇编层看 [rbp+var_5B8+8] 才对。 3-3、关键:真实模数是 2^127-39 这一步是整个题的核心。代码里乘完 128 位数之后都会跟一段"折叠"和一堆不透明谓词,一开始我以为是 mod 2^128-78(折叠是 P_lo + 78*P_hi,2^128 ≡ 78),结果模拟器对出来差了 0x8000000000000027 = 2^127 + 39。回头看那堆谓词(比如 0x140005AF7 开始的块): ``` if (hi >= 0x7FFFFFFFFFFFFFFF) { hi = hi - (lo < 0xFFFFFFFFFFFFFFD9) + 0x8000000000000001; lo += 39; } ``` 它根本不是 no-op:值 ≥ 2^127 的时候就把最高位清掉、低 64 位加 39,也就是减掉 2^127-39。所以全部算术的真实模数是 M = 2^127-39(素数),而 78 = 2*39 恰好是 2^128 对 M 的余数。改过来之后模拟器 128 位全部吻合。 3-4、输入 -> A(base-94 累加) ``` A = 0 for i in 0..15: A = (94 * A + (input[i] - 33)) mod M ``` 就是把 16 个字符当基 94 的数,每位 = 字符 - 33('!' 是 0)。汇编里对应的就是 `mov r9d, 5Eh`(乘 94)和 `add r13, 0FFFFFFFFFFFFFFDFh`(-33)。可打印输入下 A < 94^16 < M,不会触发取模。 3-5、五次多项式 ``` R5 = ((((A + C1)*A + C2)*A + C3)*A + C4)*A + C5 (mod M) = A^5 + C1*A^4 + C2*A^3 + C3*A^2 + C4*A + C5 ``` 伪代码里是四段相同的模式:128 位乘法展开 + 78 折叠 + 谓词归约,然后加一个 C 常数,最后 R5 存进 m_15/v239 作为循环初态。 3-6、两个 20 轮混合循环 循环 1(初态 (H,L,P) = (R5.hi, R5.lo, R5.hi),轮字 w1,h 按轮次奇偶取 v408/v407): ``` x = w ^ h Y = ((H << 64 | x) * K) mod M z = Y.lo ^ x ^ ror64(Y.hi, 47) H' = L ^ S8(z) ^ ror64(z, 51) t = x ^ 0x9E3779B97F4A7C15 U = ((H' << 64 | t) * K) mod M v = U.lo ^ t ^ ror64(U.hi, 47) L' = P ^ S8(v) ^ ror64(v, 51) P = H'; H, L = H', L' ``` 循环 2 结构一样,轮字 w2、h 奇偶反过来、最后一步是 L' = H ^ S8(v) ^ ror64(v, 51)。伪代码里 S 盒查表那行很长很显眼:v423[v280 & 0xFF]、v423[BYTE1(v280)]...一路 <<8 拼成一个 qword。 3-7、最终四个比较 循环 1 的输出 (H:L) 要等于 v420 = xmmword_14000A050 ^ (X1:X2);循环 2 的输出等于 v421 = xmmword_14000A040 ^ (X1:X2);再补两道 FNV-1a:FNV(A.lo, A.hi, T1.lo, T1.hi, T2.lo, T2.hi) == qword_14000A030 ^ X2,FNV(T1.lo, T1.hi, T2.lo, T2.hi) == qword_14000A060 ^ X1。后一道跟输入无关,是作者自己的自洽校验,我复刻 PRNG 后用它当免费的验证器——它一过,说明 PRNG 和所有 .tgt 常数全对。 4、求解 4-1、逆推两个循环拿 R5 循环的逆是精确的,从输出状态 (H, L) 往回走一轮: ``` U = ((H << 64 | t) * K) mod M, v = U.lo ^ t ^ ror64(U.hi, 47) H_prev = L ^ S8(v) ^ ror64(v, 51) Y = ((H_prev << 64 | x) * K) mod M, z = Y.lo ^ x ^ ror64(Y.hi, 47) L_prev = H ^ S8(z) ^ ror64(z, 51) ``` 两条循环独立逆推,得到同一个值,正好互相印证: ``` R5 = 4be831b0ad3a2d361489375bba3fb8de ``` 4-2、解五次多项式 M 是素数,在 F_M 上对 f(x) = x^5 + C1x^4 + C2x^3 + C3x^2 + C4x + (C5 - R5) 做 Cantor-Zassenhaus(gcd(x^p - x, f) 提线性因子,再随机分裂),得到唯一根: ``` A = 00000174a5e1d4cf4f146e72f70092ac ``` 4-3、A 转基 94 得 flag ``` A 的 16 位基 94 数字 = [74, 64, 77, 87, 84, 68, 31, 17, 78, 17, 21, 78, 23, 0, 31, 2] 每位 + 33 = 'k','a','n','x','u','e','@','2','o','2','6','o','8','!','@','#' ``` 获得 flag:kanxue@2o26o8!@#
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
onetry
3
发帖
8
回帖
162
RANK
关注
私信
他的文章
[原创]第八题:亥子合辰·塔影迷楼(AI)
67
第七题 星际移民_WP
1051
[原创]第四题 神秘信号_WP
6834
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部