首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创] 看雪·2026 KCTF 第五题:申时·忆海倒带
发表于: 2026-8-17 12:57
26
[原创] 看雪·2026 KCTF 第五题:申时·忆海倒带
mb_mgodlfyn
16
2026-8-17 12:57
26
题是AI做的,人都没打开看,题解贴AI自动生成的长篇writeup毫无意义(更没必要搞什么点赞/回复可见,发论坛是为了公开交流),不如写点给人看的话。 在第二题的题解中提到过Harness的重要性。我的codex似乎被上下文配置或长期记忆等污染的很严重,5.6 Sol每句对话都会触发外部审查,然后是安全风险+无法显式此对话;5.5能跑,但明显感觉没有5.6有洞察力。 第三题 “只管出不管做” 大佬的 wp 很震撼 [https://bbs.kanxue.com/thread-292518.htm](https://bbs.kanxue.com/thread-292518.htm) ,ChatGPT 自己自动解决了,而且每轮对话都能跑 120min;我在网页尝试复现的时候,刚开始总是几分钟就停一下说一句小发现,后来发现是记忆的影响,关掉记忆后终于能一次对话跑 60min 了(但差不多也只能跑这么长,感觉像 Plus 订阅的限制),然而还是始终没有突破。可惜 5.6 Sol拒答,Plus订阅也无法选择xHigh等更高思考等级,估计Pro订阅才能能跑到120min的深度。 第四题情况类似,由于 Codex 额度剩的不多了,只能在 ChatGPT 网页对话用 GPT 5.5 跑;对话上下文快用尽时让它把文档和脚本打包zip下载然后开新对话。跑了周末两天,它真的一直在改进windows环境模拟,然后最后还是没能把核心逻辑完全逆清楚。 (p.s. ChatGPT的网页现在功能很强,基本相当于一个不能定制环境的云端Codex,最重要的是不耗额度,否则周末的实验根本烧不起) 切实感受,对于强模型,越多的上下文和依赖反而会制约它;而弱模型,一百个臭皮匠也顶不了一个诸葛亮,灵感和洞察力才是强模型的竞争力(不过,弱模型配一个强Harness可以在实际任务中获得超越强模型配弱Harness的效果,这是工程优化和人的力量弥补了模型的不足,那如果是强Harness配强Harness,上限会有多大呢)。模型的中间思考值得人一直关注,有时里面会涌现一些关键点但被它自我否决。 关于第五题,感觉我的本地Codex还是没调整好,上下文仍然有污染;给了它IDA和x64dbg的mcp和Skills,以及Agents.md的指令,反而耗时10分钟才做出来;相同的GPT 5.5 High,在网页ChatGPT上只给题目附件,5分钟出答案。 --- (Human) 分隔线 (AI) --- 网页chatgpt,5分钟解决(GPT 5.5 High) <a href="elink@a2eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0K9r3q4@1k6%4m8@1i4K6u0W2j5$3!0E0i4K6u0r3M7$3S2S2M7X3g2Q4x3V1j5$3j5e0R3J5z5r3y4V1x3g2)9J5k6o6M7H3j5U0c8Q4x3X3b7^5x3$3g2S2i4K6u0V1j5U0M7$3x3#2)9J5k6o6f1%4x3K6R3#2x3r3g2X3k6h3g2V1k6l9`.`."><mark class="encrypted">8d0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0K9r3q4@1k6%4m8@1i4K6u0W2j5$3!0E0i4K6u0r3M7$3S2S2M7X3g2Q4x3V1j5$3j5e0R3J5z5r3y4V1x3g2)9J5k6o6M7H3j5U0c8Q4x3X3b7^5x3$3g2S2i4K6u0V1j5U0M7$3x3#2)9J5k6o6f1%4x3K6R3#2x3r3g2X3k6h3g2V1k6l9`.`.</mark></a>  本地codex,10分钟才解决(GPT 5.5 High)(另外并行开了一个 GPT 5.6 Terra xHigh,中间还触发了外置审查,用时21分钟出答案,二者合计烧了 8% 的 Plus 周额度;GPT 5.6 Sol 触发 Cyber Risk 拒答)     --- 附录1:GPT 5.5 High 的解题报告 第五题_分析报告.md ## 样本信息 - 样本:`cm.exe` - SHA1:`D1C8124C5964AF1531C6E311328854A05CA40BCA` - 架构:Windows PE 32-bit - 判胜输出:`verify success.` ## 最终输入 ```text 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 本地验证: ```text Enter your key: verify success. ``` ## 关键流程 `main` 地址为 `0x403D90`。明文字符串 `admin123`、`r3v3rs3!` 以及若干开发者注释均为干扰项,真正逻辑集中在 `main` 的输入解析、校验和最终查表比较中。 主要校验点: - `0x404092`:长度检查。输入经十六进制解析后必须为 11 个 32-bit limb,即输入长度为 88 个字符。 - `0x404070` 至 `0x404095`:字符范围检查。明面允许 `0-9` 和 `A-Z`,但后续十六进制解析函数 `sub_402100` 只把 `0-9 A-F a-f` 作为有效 hex digit,其他字母按 0 处理。 - `0x4041BC`:原始 44 字节 XOR 必须为 `0x8F`。 - `0x404264`:原始 44 字节滚动校验低 16 位必须为 `0xBEFF`。 - `0x40427D`:调用 `sub_403500`,只取输入从第 57 个字符开始的 32 个 hex 字符,即原始 44 字节中的最后 16 字节,计算模幂结果。 - `0x40450E`:最终查表映射。44 字节缓冲逐字节映射后,必须等于字符串 `Welcome to KCTF2026! Come and give it a try.` ## 大整数与 RSA 部分 `sub_402100` 是十六进制字符串转大整数: ```text num = num * 16 + digit ``` 大整数使用 32-bit little-endian limb。`sub_402510` 是模幂计算,`sub_4022D0` 将大整数转回大写 hex 字符串。 在 `sub_403500` 中恢复出的参数为: ```text n = 0x8c91cb79ec693f3ed6519c945dd67371 e = 0x10001 ``` `n` 的两个因子: ```text p = 0xbd3d59fac7cc547b q = 0xbe28ef96c8318203 ``` 最终目标字符串经查表逆映射得到 44 字节目标内部缓冲: ```text 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416430F4439374E3C44372544164425151D1B ``` 其中前 28 字节直接作为输入前 56 个 hex 字符。后 16 字节是模幂输出目标: ```text c = 0x0F4439374E3C44372544164425151D1B ``` 用私钥指数求逆: ```text m = c^d mod n = 0x3B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 于是输入最后 32 个 hex 字符为: ```text 3B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 拼接得到最终序列号。 ## 复现脚本 复现脚本已保存为: ```text q5_solve.py ``` 运行: ```powershell C:\Users\hz\opt\Python313\python.exe -u q5_solve.py ``` 脚本会: 1. 从 PE 中抽取稀疏存储的大整数常量与查表数据。 2. 逆出目标字符串对应的 44 字节内部缓冲。 3. 分解 RSA 模数并求私钥指数。 4. 生成最终 key。 5. 启动 `cm.exe` 做本地验证。 q5_solve.py ```python from __future__ import annotations import math import pathlib import random import struct import subprocess BASE = 0x400000 ROOT = pathlib.Path(__file__).resolve().parent EXE = ROOT / "cm.exe" def load_image(path: pathlib.Path): data = path.read_bytes() pe = struct.unpack_from("<I", data, 0x3C)[0] nsects = struct.unpack_from("<H", data, pe + 6)[0] opt_size = struct.unpack_from("<H", data, pe + 20)[0] sec_off = pe + 24 + opt_size sections = [] for i in range(nsects): off = sec_off + 40 * i name = data[off : off + 8].split(b"\0", 1)[0].decode() vsize, va, raw_size, raw_ptr = struct.unpack_from("<IIII", data, off + 8) sections.append((name, BASE + va, max(vsize, raw_size), raw_ptr, raw_size)) return data, sections DATA, SECTIONS = load_image(EXE) def va_to_off(va: int) -> int: for _name, start, size, raw_ptr, raw_size in SECTIONS: if start <= va < start + size: delta = va - start if delta >= raw_size: return None return raw_ptr + delta raise ValueError(hex(va)) def rb(va: int, size: int) -> bytes: off = va_to_off(va) if off is None: return b"\0" * size return DATA[off : off + size] def u32(va: int) -> int: b = rb(va, 4) if len(b) < 4: b = b.ljust(4, b"\0") return struct.unpack("<I", b)[0] def sparse_bigint(data_ptr: int, perm_ptr: int, limbs: int) -> int: out = 0 vals = [] for i in range(limbs): idx = u32(perm_ptr + 4 * i) val = u32(data_ptr + 4 * idx) vals.append(val) out |= val << (32 * i) print(f"limbs @ {hex(data_ptr)} via {hex(perm_ptr)}:", [hex(v) for v in vals]) return out def char_preimages(ch: int) -> list[int]: # sub_4033D0 patches byte_438031 to 0x40, then sub_404870 indexes: # data[perm[input_byte - 1]]. data_ptr = 0x4263B0 perm_ptr = 0x4163B0 hits = [] for x in range(1, 256): idx = u32(perm_ptr + 4 * (x - 1)) val = rb(data_ptr + idx, 1) if val and val[0] == ch: hits.append(x) return hits def miller_rabin(n: int) -> bool: if n < 2: return False small = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] for p in small: if n % p == 0: return n == p d = n - 1 s = 0 while d % 2 == 0: s += 1 d //= 2 for a in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]: if a >= n: continue x = pow(a, d, n) if x == 1 or x == n - 1: continue for _ in range(s - 1): x = (x * x) % n if x == n - 1: break else: return False return True def pollard_rho(n: int) -> int: if n % 2 == 0: return 2 if n % 3 == 0: return 3 while True: c = random.randrange(1, n - 1) x = random.randrange(2, n - 1) y = x d = 1 while d == 1: x = (x * x + c) % n y = (y * y + c) % n y = (y * y + c) % n d = math.gcd(abs(x - y), n) if d != n: return d def factor(n: int) -> list[int]: try: import sympy as sp out = [] for p, e in sp.factorint(n).items(): out.extend([int(p)] * int(e)) return out except Exception: pass if n == 1: return [] if miller_rabin(n): return [n] d = pollard_rho(n) return factor(d) + factor(n // d) def checksum(bs: bytes) -> tuple[int, int]: x = 0 s = 0 for b in bs: x ^= b s = (s + ((s & 0x7F) + 1) * b) & 0xFFFF return x, s def main() -> None: n = sparse_bigint(0x436018, 0x42E018, 4) e = sparse_bigint(0x430018, 0x434018, 1) print("n =", hex(n)) print("e =", hex(e), e) target = b"Welcome to KCTF2026! Come and give it a try." print("target len =", len(target)) choices = [char_preimages(c) for c in target] for i, (c, xs) in enumerate(zip(target, choices)): print(i, chr(c), [hex(x) for x in xs]) if not xs: raise SystemExit(f"no preimage for {i} {chr(c)!r}") direct = bytes(xs[0] for xs in choices) c_tail = int.from_bytes(direct[28:44], "big") print("mapped bytes =", direct.hex().upper()) print("cipher tail =", direct[28:44].hex().upper()) fs = sorted(factor(n)) print("factors =", [hex(x) for x in fs]) phi = 1 for p in fs: phi *= p - 1 d = pow(e, -1, phi) m_tail = pow(c_tail, d, n) assert pow(m_tail, e, n) == c_tail all_bytes = direct[:28] + m_tail.to_bytes(16, "big") x, s = checksum(all_bytes) key = all_bytes.hex().upper() print("tail input =", m_tail.to_bytes(16, "big").hex().upper()) print("checksum =", hex(x), hex(s)) print("key =", key) proc = subprocess.run( [str(EXE)], input=(key + "\n").encode(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=str(ROOT), timeout=10, ) print(proc.stdout.decode(errors="replace")) if proc.stderr: print(proc.stderr.decode(errors="replace")) if __name__ == "__main__": main() ``` --- 附录2:GPT 5.6 Terra xHigh 的解题报告 (与上面对比,阅读感受差别还挺大的) 第五题_分析与验证报告.md ## 最终答案 ``` 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 这是 88 个大写十六进制字符。将其输入原始 `cm.exe` 后,程序输出: ``` Enter your key: verify success. ``` 分析样本的 SHA-1 为 `d1c8124c5964af1531c6e311328854a05ca40bca`,与题目 `readme.txt` 给出的值一致。 ## 校验链 `_main`(`0x403D90`)在 `0x403F81` 读取输入,并在 `0x404040` 以 16 进制解析成自定义大整数。其前置约束为: | 程序位置 | 已证实的约束 | 本答案的结果 | |---|---|---| | `0x404055`–`0x404095` | 输入对应 11 个 32 位字,故字符数必须为 88;每字符仅可为 `0-9` / `A-Z` | 88 字符,均为合法十六进制字符 | | `0x4041BC` | 44 个输入字节的 XOR 为 `0x8F` | `0x8F` | | `0x404218`–`0x40426B` | 16 位滚动累加器必须为 `0xBEFF` | `0xBEFF` | 显示的 `admin123`、`r3v3rs3!`、`password` 及其“开发者注释”均为诱饵:`sub_403C60` (`0x403C60`) 在入口调用的 `sub_403230`/`sub_4032E0` 只用于干扰和异常路径,未参与最终成功分支。 ## 核心变换 完整性门通过后,`0x40427D` 调用 `sub_403500`。在其 `0x403A21` 调用点进行运行时取证得到固定 RSA 参数: ``` e = 0x10001 n = 0x8C91CB79EC693F3ED6519C945DD67371 ``` `n` 可分解为: ``` p = 13636154180376482939 q = 13702465297157554691 ``` 因此可计算私钥指数 `d = e^-1 mod ((p-1)(q-1))`,进而反解最后 16 个输入字节。动态交叉验证中,令该 16 字节输入全为 `0x11` 时,程序在结果缓冲区写入: ``` 506ABC5131EEE215BA202F97A58FB1AA ``` 它与独立计算的 `0x1111...1111 ^ 65537 mod n`(大端字节序)完全一致。 ## 最终查表和逆向方法 `0x4044F8`–`0x404540` 遍历 44 个字节,并在 `0x40450E` 调用 `sub_404870` 进行最终查表。查表状态由 `sub_4033D0` (`0x4033D0`) 建立: - 置换表:`0x4163B0`(255 个 `uint32_t`); - 字节表:`0x4263B0`(长度 `0x408F`); - 目标明文:`0x416380`,即 `Welcome to KCTF2026! Come and give it a try.`。 对目标明文的每个字符,在 255 个可用源字节中反查唯一前像,得到 44 字节待映射数据。前 28 字节直接组成注册码的前 56 个十六进制字符;后 16 字节是 RSA 密文,使用上述私钥指数反解后组成后 32 个十六进制字符。最后再次计算两道前置校验并通过。 ## 可复现脚本 [solve_q5.py](solve_q5.py) 直接从未修改的 `cm.exe` 提取置换表和字节表,反解 RSA,并在输出前断言:查表可逆、RSA 逆变换正确、XOR 校验正确、滚动校验正确。 运行: ```powershell C:\Users\hz\opt\Python313\python.exe .\solve_q5.py ``` solve_q5.py ```python """Derive and verify the registration key for KCTF 2026 question 5. The program maps the 44 bytes encoded by the 88 hexadecimal input characters through an in-binary lookup table. The final 16 bytes are RSA encrypted before that lookup; this script inverts both transformations and verifies the two early integrity checks used by cm.exe. """ from __future__ import annotations import struct from pathlib import Path HERE = Path(__file__).resolve().parent SAMPLE = HERE / "cm.exe" # Runtime-confirmed public RSA parameters. The modulus is encoded as four # little-endian limbs in cm.exe, written below in conventional big-endian hex. N = int("8C91CB79EC693F3ED6519C945DD67371", 16) E = 0x10001 P = 13636154180376482939 Q = 13702465297157554691 TARGET = b"Welcome to KCTF2026! Come and give it a try." # Image-base virtual addresses of the lookup permutation and its byte table. PERM_VA = 0x4163B0 DATA_VA = 0x4263B0 LOOKUP_COUNT = 0x408F def rva_to_offset(image: bytes, rva: int) -> int: """Translate an RVA through the PE section table without external deps.""" nt = struct.unpack_from("<I", image, 0x3C)[0] section_count = struct.unpack_from("<H", image, nt + 6)[0] optional_size = struct.unpack_from("<H", image, nt + 20)[0] image_base = struct.unpack_from("<I", image, nt + 24 + 28)[0] wanted = rva - image_base section = nt + 24 + optional_size for _ in range(section_count): virtual_size, virtual_address, raw_size, raw_offset = struct.unpack_from( "<IIII", image, section + 8 ) span = max(virtual_size, raw_size) if virtual_address <= wanted < virtual_address + span: return raw_offset + wanted - virtual_address section += 40 raise ValueError(f"RVA 0x{wanted:X} is outside all PE sections") def build_lookup(image: bytes) -> list[int]: perm_at = rva_to_offset(image, PERM_VA) data_at = rva_to_offset(image, DATA_VA) permutation = struct.unpack_from("<255I", image, perm_at) table = image[data_at : data_at + LOOKUP_COUNT] if len(table) != LOOKUP_COUNT: raise ValueError("lookup byte table is truncated") return [table[offset] for offset in permutation] def invert_lookup(lookup: list[int], plaintext: bytes) -> bytes: answer = bytearray() for char in plaintext: matches = [index + 1 for index, mapped in enumerate(lookup) if mapped == char] if len(matches) != 1: raise ValueError( f"lookup for {chr(char)!r} is not unique among valid source bytes: {matches}" ) answer.append(matches[0]) return bytes(answer) def rolling_check(data: bytes) -> int: state = 0 for byte in data: state = (state + byte * ((state & 0x7F) + 1)) & 0xFFFF return state def xor_check(data: bytes) -> int: state = 0 for byte in data: state ^= byte return state def solve(image: bytes) -> str: lookup = build_lookup(image) mapped_source = invert_lookup(lookup, TARGET) direct_prefix, ciphertext = mapped_source[:28], mapped_source[28:] phi = (P - 1) * (Q - 1) private_exponent = pow(E, -1, phi) rsa_input = pow(int.from_bytes(ciphertext, "big"), private_exponent, N).to_bytes(16, "big") source = direct_prefix + rsa_input if len(source) != 44: raise AssertionError("wrong input byte length") if bytes(lookup[value - 1] for value in direct_prefix + ciphertext) != TARGET: raise AssertionError("lookup inversion failed") if pow(int.from_bytes(rsa_input, "big"), E, N).to_bytes(16, "big") != ciphertext: raise AssertionError("RSA inversion failed") if xor_check(source) != 0x8F: raise AssertionError("XOR integrity check failed") if rolling_check(source) != 0xBEFF: raise AssertionError("rolling integrity check failed") return source.hex().upper() if __name__ == "__main__": key = solve(SAMPLE.read_bytes()) print(f"key={key}") print("bytes=44, chars=88") print("xor=0x8F, rolling=0xBEFF") ```
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2026-8-17 20:52 被mb_mgodlfyn编辑 ,原因:
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mb_mgodlfyn
16
58
发帖
61
回帖
2100
RANK
关注
私信
他的文章
[原创] 看雪·2026 KCTF 第十题:卯时·曦光初现
1455
[原创] 看雪·2026 KCTF 第九题:丑寅同墟·星海抉择
18
[原创] 看雪·2026 KCTF 第八题:亥子合辰·塔影迷楼
25
[原创] 看雪·2026 KCTF 第七题:戌时·暗能潜流
1265
[原创] 看雪·2026 KCTF 第五题:申时·忆海倒带
26
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部