-
-
[原创] 看雪 2025 KCTF 第七题 危局初现
-
发表于: 2025-8-27 13:24 3811
-
打开main函数,稍微修几个变量类型,然后贴给AI一发入魂:
提示词:
ChatGPT的返回:
已思考 1m 20s
下面我把给出的 QNX 程序的校验逻辑复刻、简化、并给出可直接运行的逆算法(用来产生能使程序输出 ok 的 key)。我先把关键点归纳,再给出验证器和反解(生成器),最后给出一个已验证的示例 key。
一个可通过验证并打印 ok 的 key(长度 14):
程序做了这些主要工作(我按逻辑顺序抽象、去掉与系统调用有关的噪声):
读取用户输入字符串 s(最长 29),以 \n 处 NUL 结束。
将前至多 10 个字符解析成 nibble(0..15):
'0'..'9' → 0..9(注意原码对 '0' 有特殊分支,结果仍为 0)
'a'..'f' → 10..15
其它字符 → 16(但 16 会导致后续约束失败,所以有效字符应是 0-9 或 a-f)
把这最多 10 个 nibble(按输入顺序)按固定顺序放入一个 5×5 的表 v39 与一段中间数组 v38[68..82]。这个“放置”是硬编码的 — 程序在遍历 i=0..4, j=0..4 时,遇到特定 (i,j) 对会从 nibble 数组顺序地弹出一个值放入 v39[i][j],同时把它写入若干 v38 的特定位置。
关键的中间数组 v38[68..82] 被组织成 5 组(每组 3 个连续元素),对每一组 i(i=0..4)要求:
(原代码是每次循环 v55 += value; v55 -= 5,最后 v55 必须等于 19,等价于上式)
另外一个代数约束(把先前放进去的一些 v39 值代入)化简为:
其中 v24 = 8 * v5[0] + (v38[2] & 0x80),而 v38[2] & 0x80 在正常情况下为 0,所以通常 v24 = 8 * v5[0]。v51 在程序中相当于 sum of v39[4][j](也就是第 4 行的 5 个值的和),经代数推导可化为 24 + v5[9](其余元素为常数 i+j),再加上两个关于 open("/proc/self/as") 与 devctl(...) 的可能性(每个失败会把 v51 增加 1)。在典型静态/受控环境下,这两项我们可以视为 0。
最后一组字符检查把 v54 从 123 调整到最后要等于 20*(reset + abort),因 reset=4,abort=1,所以右边 = 100。程序通过检查 s[10], s[11], s[12], s[13] 是否分别为 'a'/'s' 来做 ±45、±67、±8、±9 的调整。穷举可知,使 v54 == 100 的唯一字符组合为:
即末尾四字符为 "asas"(注意字符串长度需要覆盖这些索引,但最终程序还要求 strlen(s) <= 14,所以长度应为 14)。
总结:有效 key 要满足
前 10 个字符(index 0..9)是 hex-like(0-9,a-f),按特定位置映射为 10 个 nibble(记为 v5[0..9])
这 10 个 nibble 要满足 5 组线性加和约束(每组和为 34),以及 24 + v5[9] == 8*v5[0] - 34(假设 open/devctl 没造成额外加数)
字符 10..13 必须恰好是 asas
总长度 ≤ 14(因此长度应为 14)
把输入的 v5[0..9](就是前 10 个解析出的 nibble)按顺序分配到 v39 与 v38 的映射如下(这是代码静态决定的):
v5[0] → v39[0][3] → v38[68]
v5[1] → v39[1][0] → v38[71]
v5[2] → v39[1][2] → v38[69] and v38[74]
v5[3] → v39[2][1] → v38[70] and v38[72]
v5[4] → v39[2][3] → v38[75] and v38[77]
v5[5] → v39[3][0] → v38[80]
v5[6] → v39[3][2] → v38[73] and v38[81]
v5[7] → v39[3][3] → v38[78] and v38[82]
v5[8] → v39[3][4] → v38[76]
v5[9] → v39[4][2] → v38[79]
因此,5 组求和约束(每组和为 34)具体展开为:
组 i=0: v38[68] + v38[69] + v38[70] = v5[0] + v5[2] + v5[3] = 34
组 i=1: v38[71] + v38[72] + v38[73] = v5[1] + v5[3] + v5[6] = 34
组 i=2: v38[74] + v38[75] + v38[76] = v5[2] + v5[4] + v5[8] = 34
组 i=3: v38[77] + v38[78] + v38[79] = v5[4] + v5[7] + v5[9] = 34
组 i=4: v38[80] + v38[81] + v38[82] = v5[5] + v5[6] + v5[7] = 34
另一个关键代数式(假设 v38[2] & 0x80 == 0):
(上式在程序中还有可能出现 +1/+2 的偏移,取决于 open/devctl 的返回情况。上面解法用的是 extras=0 情形,即没有额外 +1/+1。)
下面是一个把原程序规则复刻成 Python 的验证函数(可直接运行,用来检验任意 key 是否会输出 ok):
思路是把上面的代数系统解出来。具体步骤:
选择 v5[0](为 0..15)。
用 v5[9] = 8*v5[0] - 58(在 extras=0 情形)。 v5[9] 必须在 0..15。
根据 5 组和为 34 的方程,解出 v5[1]..v5[8]。这是一组线性整数方程组(每个未知都应在 0..15)。存在很多解(不是唯一)。
把得到的 nibble 值映射回字符(0..9 → '0'..'9',10..15 → 'a'..'f')。
把字符 10..13 设置为 "asas",把总长度调整为 14(即前 10 个字符 + "asas")。
得到的字符串即为一个有效 key。
使用上面方法(在 extras=0 条件下),我找到了很多可行解。给出一个具体可直接用的例子(再次列出):
前 10 nibble 对应值 (v5[0]..v5[9]) =(8, 4, 11, 15, 13, 4, 15, 15, 10, 6)
把这些值映射为字符: 8 4 b f d 4 f f a 6,再加上末尾 "asas" 得到完整 key:
把上面的 check_key 函数跑一遍会返回 True。我在对原逻辑的 Python 模拟中验证该字符串能让复刻的验证器通过(并且长度恰好为 14)。
我把 open("/proc/self/as") 与 devctl(...) 的系统行为当成了“不改变 v51 的情况”(extras=0)。如果在真实运行时这些系统调用失败,v51 会被加 1 或 2,那么 v5[9] 与 v5[0] 的关系要相应修正(等式右边会多 1 或 2)。因此如果在某台机器上复现不成功,请检查那两次系统调用是否返回了错误(会改变所需的 nibble 值关系)。
我限制前 10 个字符使用 0-9 与 a-f;程序中其他字符会被解析为 16,几乎必然导致和不满足 34 的约束。
我假设 v38[2] & 0x80 == 0(原程序中 v38[2] 没被赋值,通常为 0)。如果运行环境把 v38[2] 填入非零并且高位 0x80 被置位,会改变代数式。
我给出的 Python 验证器是对原函数的简化复刻(含重要约束)。你可以用它在本地快速验证 candidate keys。
如果你需要,我可以:
把复刻的验证器打包成一个可运行的小脚本(Python),并列出所有满足条件的前 N 个解;
或者在考虑 open/devctl 增量(extras = 0/1/2)三种情况分别列出解集;
或者把上述验证与逆解过程用 C 语言实现(方便直接在目标 QNX 环境编译运行)。
你想要哪一种扩展?
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!