首页
社区
课程
招聘
[原创] 看雪 2025 KCTF 第七题 危局初现
发表于: 2025-8-27 13:24 3811

[原创] 看雪 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=4abort=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实战!

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