首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创]第七题:戌时·暗能潜流
发表于: 2026-8-20 14:47
18
[原创]第七题:戌时·暗能潜流
mxym_
2
2026-8-20 14:47
18
# HexMaze Writeup 奇奇怪怪的题目(),看第一第二两个大哥都是opus5出的,果然还是claude更适合misc吗,不懂crypto,不清楚在开始那种约束下为什么能解出来flag,gpt跟我说好像不唯一,等作者分析了() ## 题目数据 题目给出 4 组明文和密文,密钥为 `121`: ```text TLU -> 94AA48550495 Hello -> 34BB405504B5223594B94C53 2026 -> A48844556485223322356483 abcd! -> 547B475584B5223564BB4553 ``` FLAG 密文为: ```text 14CC4655547594BC475584C5848A43551495448C445584C5D4C9475564C534A84B55A4B574BA4355F495A48844556485648C495534A5548C4F5584A5B4BB405554B522332235A4B3 ``` 题目说明算法处理 ASCII 字符串,输出只包含 `0-9A-F`,并且包含字节编码、十六进制位运算、模 16 循环偏移、固定位置重排和固定摘要扰动。 ## 外层格式剥离 观察样例可知,密文每 12 个十六进制字符为一组。满 3 字节块的格式为: ```text q = ab cd ef ct = a4 bc 4d 55 e4 f5 ``` 也就是在真实中间码 `abcdef` 中插入固定字符 `4`、`55`、`4`、`5`。因此满块取密文块的第 `0,2,3,5,8,10` 个字符即可恢复 3 个中间字节。 例如: ```text TLU -> 94AA48550495 9 4 A A 4 8 5 5 0 4 9 5 ^ ^ ^ ^ ^ ^ 剥离得到:9A A8 09 ``` 尾块格式由明文长度对 3 取模决定。 剩 2 字节时: ```text q = ab cd ct = 2235 a4 bc 4d 53 ``` 真实中间码位于第 `4,6,7,9` 个字符。 剩 1 字节时: ```text q = ab ct = 22332235 a4 b3 ``` 真实中间码位于第 `8,10` 个字符。 按这个规则剥离样例,得到 inner 中间码: ```text TLU -> 9AA809 Hello -> 3BB00B9B9C 2026 -> A8846868 abcd! -> 57B78B6BB5 ``` FLAG 外层剥离后得到: ```text 1CC6579BC78C88A31948C48CDC976C3A8BAB7BA3F9A8846868C93A58CF8ABBB05BAB ``` 外层只依赖块长度插入固定模板,尾部的 `2235`、`22332235` 也是长度相关标记,内容不参与这些固定字符的计算。 ## inner 字节变换 先看完整 3 字节样例 `TLU`。 明文字节为: ```text T = 0x54 L = 0x4C U = 0x55 ``` inner 为: ```text 9A A8 09 ``` 若先把字节拆成高、低两个十六进制半字节,并尝试模 16 偏移: ```text hi = high + 5 mod 16 lo = low + 4 mod 16 ``` 则: ```text U = 0x55 -> (5+5, 5+4) = A9 T = 0x54 -> (5+5, 4+4) = A8 L = 0x4C -> (4+5, C+4) = 90 ``` 再结合密钥 `121`,把每个输出位置的两个半字节做循环旋转: ```text key digit = 1 -> 两个半字节旋转 1 位,即交换 hi/lo key digit = 2 -> 两个半字节旋转 2 位,即回到原序 ``` 得到: ```text 输出位置 0,源字节 U:A9 经过 key 1 交换 -> 9A 输出位置 1,源字节 T:A8 经过 key 2 保持 -> A8 输出位置 2,源字节 L:90 经过 key 1 交换 -> 09 ``` 正好匹配 `TLU -> 9AA809`。 继续验证 `2026`。如果输出源字节顺序为 `6,0,2,2`: ```text 6 = 0x36 -> (3+5, 6+4) = 8A -> key 1 交换 -> A8 0 = 0x30 -> (3+5, 0+4) = 84 -> key 2 保持 -> 84 2 = 0x32 -> (3+5, 2+4) = 86 -> key 1 交换 -> 68 2 = 0x32 -> (3+5, 2+4) = 86 -> key 1 交换 -> 68 ``` 得到 `A8846868`,与样例一致。 因此 inner 的字节级编码为: ```text byte -> high/low nibble high += 5 mod 16 low += 4 mod 16 按输出位置循环使用 key=121 旋转两个 nibble ``` 密钥 `121` 中每个数字的作用为: ```text 1: 两个 nibble 旋转 1 位,即交换 2: 两个 nibble 旋转 2 位,即保持原序 1: 两个 nibble 旋转 1 位,即交换 ``` 按输出位置循环后就是: ```text swap, keep, swap, swap, keep, swap, ... ``` ## 位置重排 撤销 nibble 偏移和 key 旋转后,可以得到一个“重排后的明文字节流”。 对四个样例分别处理: ```text 9AA809 -> UTL 3BB00B9B9C -> olleH A8846868 -> 6022 57B78B6BB5 -> !cdba ``` 这些结果和原文之间的关系如下: ```text TLU -> UTL 下标顺序 [2,0,1] 2026 -> 6022 下标顺序 [3,1,2,0] Hello -> olleH 下标顺序 [4,2,3,1,0] abcd! -> !cdba 下标顺序 [4,2,3,1,0] ``` 可以归纳出重排规则: 1. 先将所有明文字节整体逆序; 2. 再对每 3 个输出位置做局部重排 `[0,2,1]`,也就是交换每组三元组里的第 2 项和第 3 项。 用下标表示,长度为 `n` 时: ```python order = list(reversed(range(n))) for i in range(0, n, 3): if i + 2 < n: order[i + 1], order[i + 2] = order[i + 2], order[i + 1] ``` 几个样例长度对应的顺序为: ```text n = 3: reverse [2,1,0] -> [2,0,1] n = 4: reverse [3,2,1,0] -> [3,1,2,0] n = 5: reverse [4,3,2,1,0] -> [4,2,3,1,0] ``` 这正好同时解释 4 组样例。 ## 完整加密流程 设明文长度为 `n`。 第一步,计算输出字节的源下标顺序: ```text order = reversed(range(n)) 每 3 个位置交换第 2、3 项 ``` 第二步,按 `order` 取明文字节,对每个字节做十六进制半字节偏移: ```text hi = (byte >> 4) + 5 mod 16 lo = (byte & 0xf) + 4 mod 16 ``` 第三步,按输出位置 `j` 使用 `key[j % 3]`: ```text key 1 -> swap(hi, lo) key 2 -> keep(hi, lo) ``` 第四步,将得到的 inner 字节按外层模板写成最终十六进制密文: ```text 满 3 字节:ab cd ef -> a4 bc 4d 55 e4 f5 剩 2 字节:ab cd -> 2235 a4 bc 4d 53 剩 1 字节:ab -> 22332235 a4 b3 ``` ## 解密 FLAG FLAG 外层剥离后的 inner 为: ```text 1CC6579BC78C88A31948C48CDC976C3A8BAB7BA3F9A8846868C93A58CF8ABBB05BAB ``` 先逐字节撤销 key 旋转和模 16 偏移,可得到重排后的明文: ```text }r!est4_M0ptyCr_7fc_K6022u_1{Tglaf ``` FLAG 长度为 34。长度 34 的位置顺序为: ```text [33, 31, 32, 30, 28, 29, 27, 25, 26, 24, 22, 23, 21, 19, 20, 18, 16, 17, 15, 13, 14, 12, 10, 11, 9, 7, 8, 6, 4, 5, 3, 1, 2, 0] ``` 把重排后的明文字节按该顺序放回原位置,得到: ```text flag{T1u_2026_Kc7f_Crypt0_M4ster!} ``` 并且将该 flag 按完整加密流程重新加密,可以得到题目给出的 FLAG 密文。 ## Exp ```python from __future__ import annotations KEY = "121" SAMPLES = { b"TLU": "94AA48550495", b"Hello": "34BB405504B5223594B94C53", b"2026": "A48844556485223322356483", b"abcd!": "547B475584B5223564BB4553", } FLAG_CT = ( "14CC4655547594BC475584C5848A43551495448C445584C5D4C9475564C534A84B55" "A4B574BA4355F495A48844556485648C495534A5548C4F5584A5B4BB405554B522332235A4B3" ) def byte_order(n: int) -> list[int]: order = list(reversed(range(n))) for i in range(0, n, 3): if i + 2 < n: order[i + 1], order[i + 2] = order[i + 2], order[i + 1] return order def encode_inner(plain: bytes) -> bytes: out = bytearray() order = byte_order(len(plain)) for j, src in enumerate(order): b = plain[src] hi = ((b >> 4) + 5) & 0xF lo = ((b & 0xF) + 4) & 0xF if int(KEY[j % len(KEY)]) % 2: hi, lo = lo, hi out.append((hi << 4) | lo) return bytes(out) def wrap_outer(q: bytes) -> str: parts: list[str] = [] full = len(q) // 3 rem = len(q) % 3 for i in range(full): hx = f"{q[3 * i]:02X}{q[3 * i + 1]:02X}{q[3 * i + 2]:02X}" parts.append( hx[0] + "4" + hx[1] + hx[2] + "4" + hx[3] + "55" + hx[4] + "4" + hx[5] + "5" ) if rem == 1: hx = f"{q[3 * full]:02X}" parts.append("22332235" + hx[0] + "4" + hx[1] + "3") elif rem == 2: hx = f"{q[3 * full]:02X}{q[3 * full + 1]:02X}" parts.append("2235" + hx[0] + "4" + hx[1] + hx[2] + "4" + hx[3] + "53") return "".join(parts) def encrypt(plain: bytes) -> str: return wrap_outer(encode_inner(plain)) def peel_outer(cipher_hex: str, plain_len: int) -> bytes: blocks = [cipher_hex[i : i + 12] for i in range(0, len(cipher_hex), 12)] out = bytearray() full = plain_len // 3 rem = plain_len % 3 for i in range(full): c = blocks[i] out.extend(bytes.fromhex(c[0] + c[2] + c[3] + c[5] + c[8] + c[10])) if rem == 1: c = blocks[full] out.extend(bytes.fromhex(c[8] + c[10])) elif rem == 2: c = blocks[full] out.extend(bytes.fromhex(c[4] + c[6] + c[7] + c[9])) return bytes(out) def decode_inner(q: bytes) -> bytes: plain = bytearray(b"\x00" * len(q)) order = byte_order(len(q)) for j, y in enumerate(q): hi = y >> 4 lo = y & 0xF if int(KEY[j % len(KEY)]) % 2: hi, lo = lo, hi b = (((hi - 5) & 0xF) << 4) | ((lo - 4) & 0xF) plain[order[j]] = b return bytes(plain) def decrypt(cipher_hex: str, plain_len: int) -> bytes: return decode_inner(peel_outer(cipher_hex, plain_len)) def main() -> None: for pt, ct in SAMPLES.items(): assert encrypt(pt) == ct assert decrypt(ct, len(pt)) == pt flag = decrypt(FLAG_CT, 34) assert encrypt(flag) == FLAG_CT print(flag.decode()) if __name__ == "__main__": main() ``` 运行结果: ```text flag{T1u_2026_Kc7f_Crypt0_M4ster!} ``` ## 唯一性讨论 如果题目只给出“固定摘要扰动只与输入长度有关”,同时没有限定扰动表的生成方式和规模,样例无法证明所有可能算法中只有这一种还原方式。长度相关扰动可以是一张固定表。 设上面还原出的可逆主体算法为 `Core`,外层剥离后的 FLAG 中间码为 `C34`。任选另一个 34 字节、格式正确、全可打印的字符串: ```text flag{not_unique_without_hint_0000} ``` 定义新的长度扰动表: ```text Mask[3] = 0 Mask[4] = 0 Mask[5] = 0 Mask[34] = C34 xor Core(flag{not_unique_without_hint_0000}) ``` 令新算法中间层为: ```text AltInner(p) = Core(p) xor Mask[len(p)] ``` 该构造仍然只使用字节编码、十六进制位运算、模 16 偏移、固定位置重排,以及只由输入长度决定的固定扰动。由于样例长度分别为 3、4、5,扰动均为 0,四组样例全部保持一致;长度为 34 的 FLAG 密文会被解成上面选定的替代字符串。 该替代字符串对应的长度 34 固定扰动为: ```text 00421FD364C4041E32830CB6402B57B64070416A62F436B3F401006300A100000000 ``` 严格唯一性需要额外限定摘要扰动的具体生成方式或表规模。在由样例直接归纳出的最小自然模型中,算法可逆,FLAG 为: ```text flag{T1u_2026_Kc7f_Crypt0_M4ster!} ```
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mxym_
2
11
发帖
2
回帖
120
RANK
关注
私信
他的文章
[原创] 第十题:卯时·曦光初现 WP
1512
[原创]第九题:丑寅同墟·星海抉择 WP
63
[原创]第八题:亥子合辰·塔影迷楼 WP
44
[原创]第七题:戌时·暗能潜流
18
[原创]第六题:酉时·书院迷局 WP
1500
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部