-
-
[原创] KCTF 2026 第七题 (AI)
-
发表于: 2026-8-24 01:34 144
-
题目概述
已知数据如下:
密钥:121
TLU -> 94AA48550495
Hello -> 34BB405504B5223594B94C53
2026 -> A48844556485223322356483
abcd! -> 547B475584B5223564BB4553
目标是根据样例恢复固定加密流程,再解密 FLAG:
14CC4655547594BC475584C5848A43551495448C445584C5D4C9475564C534A84B55A4B574BA4355F495A48844556485648C495534A5548C4F5584A5B4BB405554B522332235A4B3
最终结果是:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
1. 从长度入手
统计样例长度:
| 明文 | 明文长度 | 密文长度 |
|---|---|---|
| TLU | 3 字节 | 12 个十六进制字符 |
| Hello | 5 字节 | 24 个十六进制字符 |
| 2026 | 4 字节 | 24 个十六进制字符 |
| abcd! | 5 字节 | 24 个十六进制字符 |
密文总是按 12 个十六进制字符分组。一个 ASCII 字节对应两个十六进制半字节,因此 3 个明文字节对应 6 个有效半字节,最后被扩展成一个 12 字符的密文块。
例如:
Hello:
34BB405504B5 | 223594B94C53
2026:
A48844556485 | 223322356483
这说明算法以 3 个明文字节为一个完整块。最后不足 3 个字节的部分使用长度相关的固定模板。
2. 找出完整块中的固定位置
先只看完整的 3 字节块:
TLU -> 94AA48550495
Hel -> 34BB405504B5
202 -> A48844556485
abc -> 547B475584B5
按下标展开:
下标: 0 1 2 3 4 5 6 7 8 9 10 11
TLU: 9 4 A A 4 8 5 5 0 4 9 5
Hel: 3 4 B B 4 0 5 5 0 4 B 5
202: A 4 8 8 4 4 5 5 6 4 8 5
abc: 5 4 7 B 4 7 5 5 8 4 B 5
固定位置为:
1、4、6、7、9、11
它们的值始终是:
4、4、5、5、4、5
因此,完整块中的 6 个有效半字节位于:
0、2、3、5、8、10
有效半字节还经过了位置重排。结合四组样例,可以确定正确的提取顺序:
[0, 2, 8, 10, 5, 3]
对一个 12 字符密文块 c,依次提取:
c[0], c[2], c[8], c[10], c[5], c[3]
以 TLU 为例:
密文:94AA48550495
c[0] = 9
c[2] = A
c[8] = 0
c[10] = 9
c[5] = 8
c[3] = A
提取结果是:
9A098A
3. 恢复尾块模板
最后一块的固定内容只和剩余明文长度有关。
剩余 2 个明文字节
Hello 和 abcd! 都有一个包含 2 个字节的尾块:
Hello 的尾块:223594B94C53
abcd! 的尾块:223564BB4553
两者的结构为:
2235 r0 4 r1 r3 4 r2 53
有效半字节的提取位置为:
[4, 6, 9, 7]
例如,223594B94C53 按该顺序提取后得到:
9BC9
剩余 1 个明文字节
2026 的最后一个字符是 6,尾块为:
223322356483
模板为:
22332235 r0 4 r1 3
提取位置为:
[8, 10]
所以 2026 的尾块提取结果为:
68
FLAG 的最后一块是:
22332235A4B3
它也符合 1 字节尾块模板。因此 FLAG 长度为:
11 个完整块 × 3 字节 + 1 字节 = 34 字节
4. 找到整体逆序
将 TLU 的完整块按上面的顺序提取:
94AA48550495 -> 9A098A
TLU 的 ASCII 十六进制为:
T = 54
L = 4C
U = 55
TLU = 544C55
将提取结果反转:
9A098A -> A890A9
逐个半字节比较:
变换结果:A 8 9 0 A 9
明文数值:5 4 4 C 5 5
差值: 5 4 5 4 5 4
差值在模 16 下交替出现 5 和 4。因此:
高半字节:+5 mod 16
低半字节:+4 mod 16
解密时执行相反操作:
高半字节:-5 mod 16
低半字节:-4 mod 16
例如:
C + 4 = 10(hex) -> 0
这是模 16 循环偏移。
5. 密钥 121 的作用
密钥 121 按三个数字使用,而不是作为一个十进制整数整体参与运算:
1、2、1
三轮模 16 偏移的基础总量为:
1 + 2 + 1 = 4
低半字节使用基础偏移 4。高半字节再使用首位 1 进行额外扰动,因此偏移量为 5:
base = 1 + 2 + 1
低半字节:value + base = value + 4
高半字节:value + base + 1 = value + 5
三个数字的作用可以概括为:
| 数字 | 作用 |
|---|---|
| 第一个 1 | 第一轮偏移,并提供高半字节的额外偏移 |
| 中间的 2 | 第二轮模 16 偏移 |
| 最后的 1 | 第三轮模 16 偏移 |
6. 完整加密流程
加密流程如下:
- 将明文转为 ASCII 字节。
- 将每个字节转换为两个十六进制半字节。
- 高半字节加 5,低半字节加 4,结果对 16 取模。
- 将整个半字节序列反转。
- 每 6 个半字节分成一块。
- 根据剩余半字节数量,写入对应的固定模板。
- 输出大写十六进制字符串。
三个模板为:
完整块,6 个有效半字节:
r0 4 r1 r5 4 r4 5 5 r2 4 r3 5
尾块,4 个有效半字节:
2235 r0 4 r1 r3 4 r2 53
尾块,2 个有效半字节:
22332235 r0 4 r1 3
7. 手工验证 Hello
Hello 的 ASCII 十六进制为:
48656C6C6F
逐个半字节偏移:
4 + 5 = 9
8 + 4 = C
6 + 5 = B
5 + 4 = 9
6 + 5 = B
C + 4 = 0
6 + 5 = B
C + 4 = 0
6 + 5 = B
F + 4 = 3
得到:
9CB9B0B0B3
整体反转:
3B0B0B9BC9
分块:
3B0B0B | 9BC9
第一块使用完整块模板,得到:
34BB405504B5
第二块使用 4 半字节尾模板,得到:
223594B94C53
拼接后为:
34BB405504B5223594B94C53
这与题目给出的 Hello 密文一致。
8. 解密 FLAG
按照完整块和尾块的提取规则处理 FLAG,得到处于逆序状态的数据流:
1C576C9B8C7C88193A488C4CDC6C793AABB87BF93AA86848683A9C588AFCBB5B0BAB
反转后:
BAB0B5BBCFA885C9A38684868AA39FB78BBAA397C6CDC4C884A39188C7C8B9C675C1
交替减去 5 和 4,得到明文十六进制:
666C61677B5431755F323032365F4B6337665F4372797074305F4D3473746572217D
按每两个十六进制字符转换为 ASCII:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
9. Python 解密脚本
FLAG_CIPHER = (
"14CC4655547594BC475584C5848A43551495448C445584C5"
"D4C9475564C534A84B55A4B574BA4355F495A48844556485"
"648C495534A5548C4F5584A5B4BB405554B522332235A4B3"
)
def extract_block(block, nibble_count):
if nibble_count == 6:
# 完整块:r0 4 r1 r5 4 r4 5 5 r2 4 r3 5
positions = [0, 2, 8, 10, 5, 3]
elif nibble_count == 4:
# 2 字节尾块:2235 r0 4 r1 r3 4 r2 53
positions = [4, 6, 9, 7]
elif nibble_count == 2:
# 1 字节尾块:22332235 r0 4 r1 3
positions = [8, 10]
else:
raise ValueError("invalid nibble count")
return "".join(block[i] for i in positions)
def detect_tail_size(block):
if block.startswith("22332235"):
return 2
if block.startswith("2235"):
return 4
return 6
def decrypt(ciphertext, key="121"):
if len(ciphertext) % 12 != 0:
raise ValueError("ciphertext length must be a multiple of 12")
blocks = [
ciphertext[i:i + 12]
for i in range(0, len(ciphertext), 12)
]
# 恢复偏移后的逆序半字节流。
reversed_stream = []
for block in blocks[:-1]:
reversed_stream.append(extract_block(block, 6))
tail_size = detect_tail_size(blocks[-1])
reversed_stream.append(
extract_block(blocks[-1], tail_size)
)
shifted_stream = "".join(reversed_stream)[::-1]
digits = [int(x) for x in key]
base_shift = sum(digits) # 4
high_extra = digits[0] # 1
plain_nibbles = []
for i, char in enumerate(shifted_stream):
value = int(char, 16)
if i % 2 == 0:
value = (value - base_shift - high_extra) % 16
else:
value = (value - base_shift) % 16
plain_nibbles.append(format(value, "X"))
plain_hex = "".join(plain_nibbles)
return bytes.fromhex(plain_hex).decode("ascii")
print(decrypt(FLAG_CIPHER))
输出:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
10. 正向验证
把恢复出的明文重新加密,可以验证算法不仅能解出 FLAG,也能重现所有已知样例:
def encrypt(plaintext, key="121"):
plain_hex = plaintext.encode("ascii").hex().upper()
digits = [int(x) for x in key]
base_shift = sum(digits)
high_extra = digits[0]
shifted = []
for i, char in enumerate(plain_hex):
value = int(char, 16)
if i % 2 == 0:
value = (value + base_shift + high_extra) % 16
else:
value = (value + base_shift) % 16
shifted.append(format(value, "X"))
reversed_stream = "".join(shifted)[::-1]
result = []
for i in range(0, len(reversed_stream), 6):
r = reversed_stream[i:i + 6]
if len(r) == 6:
block = (
f"{r[0]}4{r[1]}{r[5]}4{r[4]}"
f"55{r[2]}4{r[3]}5"
)
elif len(r) == 4:
block = (
f"2235{r[0]}4{r[1]}{r[3]}"
f"4{r[2]}53"
)
elif len(r) == 2:
block = f"22332235{r[0]}4{r[1]}3"
else:
raise ValueError("invalid block length")
result.append(block)
return "".join(result)
tests = {
"TLU": "94AA48550495",
"Hello": "34BB405504B5223594B94C53",
"2026": "A48844556485223322356483",
"abcd!": "547B475584B5223564BB4553",
}
for plaintext, expected in tests.items():
calculated = encrypt(plaintext)
print(plaintext, calculated == expected)
flag = "flag{T1u_2026_Kc7f_Crypt0_M4ster!}"
print(encrypt(flag))
四组样例和 FLAG 都能重新生成题目中的密文,说明恢复出的流程完整一致。