-
-
[原创]第七题:戌时·暗能潜流
-
发表于: 4天前 10
-
HexMaze Writeup
奇奇怪怪的题目(),看第一第二两个大哥都是opus5出的,果然还是claude更适合misc吗,不懂crypto,不清楚在开始那种约束下为什么能解出来flag,gpt跟我说好像不唯一,等作者分析了()
题目数据
题目给出 4 组明文和密文,密钥为 121:
TLU -> 94AA48550495
Hello -> 34BB405504B5223594B94C53
2026 -> A48844556485223322356483
abcd! -> 547B475584B5223564BB4553
FLAG 密文为:
14CC4655547594BC475584C5848A43551495448C445584C5D4C9475564C534A84B55A4B574BA4355F495A48844556485648C495534A5548C4F5584A5B4BB405554B522332235A4B3
题目说明算法处理 ASCII 字符串,输出只包含 0-9A-F,并且包含字节编码、十六进制位运算、模 16 循环偏移、固定位置重排和固定摘要扰动。
外层格式剥离
观察样例可知,密文每 12 个十六进制字符为一组。满 3 字节块的格式为:
q = ab cd ef
ct = a4 bc 4d 55 e4 f5
也就是在真实中间码 abcdef 中插入固定字符 4、55、4、5。因此满块取密文块的第 0,2,3,5,8,10 个字符即可恢复 3 个中间字节。
例如:
TLU -> 94AA48550495
9 4 A A 4 8 5 5 0 4 9 5
^ ^ ^ ^ ^ ^
剥离得到:9A A8 09
尾块格式由明文长度对 3 取模决定。
剩 2 字节时:
q = ab cd
ct = 2235 a4 bc 4d 53
真实中间码位于第 4,6,7,9 个字符。
剩 1 字节时:
q = ab
ct = 22332235 a4 b3
真实中间码位于第 8,10 个字符。
按这个规则剥离样例,得到 inner 中间码:
TLU -> 9AA809
Hello -> 3BB00B9B9C
2026 -> A8846868
abcd! -> 57B78B6BB5
FLAG 外层剥离后得到:
1CC6579BC78C88A31948C48CDC976C3A8BAB7BA3F9A8846868C93A58CF8ABBB05BAB
外层只依赖块长度插入固定模板,尾部的 2235、22332235 也是长度相关标记,内容不参与这些固定字符的计算。
inner 字节变换
先看完整 3 字节样例 TLU。
明文字节为:
T = 0x54
L = 0x4C
U = 0x55
inner 为:
9A A8 09
若先把字节拆成高、低两个十六进制半字节,并尝试模 16 偏移:
hi = high + 5 mod 16
lo = low + 4 mod 16
则:
U = 0x55 -> (5+5, 5+4) = A9
T = 0x54 -> (5+5, 4+4) = A8
L = 0x4C -> (4+5, C+4) = 90
再结合密钥 121,把每个输出位置的两个半字节做循环旋转:
key digit = 1 -> 两个半字节旋转 1 位,即交换 hi/lo
key digit = 2 -> 两个半字节旋转 2 位,即回到原序
得到:
输出位置 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:
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 的字节级编码为:
byte -> high/low nibble
high += 5 mod 16
low += 4 mod 16
按输出位置循环使用 key=121 旋转两个 nibble
密钥 121 中每个数字的作用为:
1: 两个 nibble 旋转 1 位,即交换
2: 两个 nibble 旋转 2 位,即保持原序
1: 两个 nibble 旋转 1 位,即交换
按输出位置循环后就是:
swap, keep, swap, swap, keep, swap, ...
位置重排
撤销 nibble 偏移和 key 旋转后,可以得到一个“重排后的明文字节流”。
对四个样例分别处理:
9AA809 -> UTL
3BB00B9B9C -> olleH
A8846868 -> 6022
57B78B6BB5 -> !cdba
这些结果和原文之间的关系如下:
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]
可以归纳出重排规则:
- 先将所有明文字节整体逆序;
- 再对每 3 个输出位置做局部重排
[0,2,1],也就是交换每组三元组里的第 2 项和第 3 项。
用下标表示,长度为 n 时:
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]
几个样例长度对应的顺序为:
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。
第一步,计算输出字节的源下标顺序:
order = reversed(range(n))
每 3 个位置交换第 2、3 项
第二步,按 order 取明文字节,对每个字节做十六进制半字节偏移:
hi = (byte >> 4) + 5 mod 16
lo = (byte & 0xf) + 4 mod 16
第三步,按输出位置 j 使用 key[j % 3]:
key 1 -> swap(hi, lo)
key 2 -> keep(hi, lo)
第四步,将得到的 inner 字节按外层模板写成最终十六进制密文:
满 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 为:
1CC6579BC78C88A31948C48CDC976C3A8BAB7BA3F9A8846868C93A58CF8ABBB05BAB
先逐字节撤销 key 旋转和模 16 偏移,可得到重排后的明文:
}r!est4_M0ptyCr_7fc_K6022u_1{Tglaf
FLAG 长度为 34。长度 34 的位置顺序为:
[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]
把重排后的明文字节按该顺序放回原位置,得到:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
并且将该 flag 按完整加密流程重新加密,可以得到题目给出的 FLAG 密文。
Exp
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()
运行结果:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
唯一性讨论
如果题目只给出“固定摘要扰动只与输入长度有关”,同时没有限定扰动表的生成方式和规模,样例无法证明所有可能算法中只有这一种还原方式。长度相关扰动可以是一张固定表。
设上面还原出的可逆主体算法为 Core,外层剥离后的 FLAG 中间码为 C34。任选另一个 34 字节、格式正确、全可打印的字符串:
flag{not_unique_without_hint_0000}
定义新的长度扰动表:
Mask[3] = 0
Mask[4] = 0
Mask[5] = 0
Mask[34] = C34 xor Core(flag{not_unique_without_hint_0000})
令新算法中间层为:
AltInner(p) = Core(p) xor Mask[len(p)]
该构造仍然只使用字节编码、十六进制位运算、模 16 偏移、固定位置重排,以及只由输入长度决定的固定扰动。由于样例长度分别为 3、4、5,扰动均为 0,四组样例全部保持一致;长度为 34 的 FLAG 密文会被解成上面选定的替代字符串。
该替代字符串对应的长度 34 固定扰动为:
00421FD364C4041E32830CB6402B57B64070416A62F436B3F401006300A100000000
严格唯一性需要额外限定摘要扰动的具体生成方式或表规模。在由样例直接归纳出的最小自然模型中,算法可逆,FLAG 为:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}