2024 强网拟态决赛 Crypto

特种兵 CTF 南京站,就打了两道密码,写一下。

notiv

题目

task.py 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# !/usr/bin/env python
from hashlib import sha256
import socketserver
import os
import sys
import random
import signal
import string
from hashlib import sha256
from mypad import *

from Crypto.Cipher import AES
from random import *
from Crypto.Util.number import *


class Task(socketserver.BaseRequestHandler):
def _recvall(self):
BUFF_SIZE = 2048
data = b''
while True:
part = self.request.recv(BUFF_SIZE)
data += part
if len(part) < BUFF_SIZE:
break
return data.strip()

def send(self, msg, newline=True):
try:
if newline:
msg += b'\n'
self.request.sendall(msg)
except:
pass

def recv(self, prompt=b'> '):
self.send(prompt, newline=False)
return self._recvall()

def close(self):
self.request.close()

def proof_of_work(self):
seed(os.urandom(8))
proof = ''.join(
[choice(string.ascii_letters+string.digits) for _ in range(20)])
_hexdigest = sha256(proof.encode()).hexdigest()
self.send(f"[+] sha256(XXXX+{proof[4:]}) == {_hexdigest}".encode())
x = self.recv(prompt=b'[+] Plz tell me XXXX: ')
if len(x) != 4 or sha256(x+proof[4:].encode()).hexdigest() != _hexdigest:
return False
return True

def handle(self):
try:
if not self.proof_of_work():
self.send(b"try again!")
self.close()
exit()

#signal.alarm(120)
count = 0
for _ in range(50):
seed(os.urandom(8))
key = pad_x923(b"")
chal = hex(getrandbits(64*3))[2:].zfill(16*3)

for i in range(200):
iv = long_to_bytes(getrandbits(128)).rjust(16, b"\00")
cipher = AES.new(key, AES.MODE_CBC, iv)
test = self.recv(b":> ").decode()
tb = bytes.fromhex(test)
ret = cipher.encrypt(pad_x923(tb))
if len(ret) > 16:
self.send(b"forbid")
continue
self.send((iv+ret).hex().encode())

s = self.recv(b">> ").decode()
iv = bytes.fromhex(s[:32])
ans = bytes.fromhex(s[32:])
cipher = AES.new(key, AES.MODE_CBC, iv)
att = cipher.decrypt(ans)
att = unpad_x923(att)
if att == chal.encode():
count += 1
self.send(b"you got %d score" % count)
if count >= 20:
f=open("./flag","rb")
FLAG=f.read()
f.close()
self.send(b"cong!your flag is "+FLAG)
else:
self.send(b"sorry,plz try again")
except:
self.send(b"something wrong! plz try again!")
self.close()


class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass


class ForkedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
pass


if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 12345
server = ForkedServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()

mypad.py 如下:

1
2
3
4
from Crypto.Random import get_random_bytes

pad_x923=lambda x,block_size=16:x+get_random_bytes((block_size-(len(x)%block_size)-1))+bytes([(block_size-len(x)%block_size)])
unpad_x923=lambda x,block_size=16:x[:-((x[-1]-1)%block_size)-1]

分析

主要考点就是一个 CBC mode 的 AES,如何利用多次随机 IV 的 oracle 搞出一个能解密出 chal 的 IV 和 ciphertext,因为 key 是无法还原的。
首先肯定是要逆出 chal,典型的 MT19937,注意到 getrandbits(128) 其实就是 4 个 getrandbits(32) 拼接出来的,题目给出的 IV 完全是满足 19968 bits 还原的条件,但是注意到 seed 是 8 字节随机数,也就是说不需要 624 个 getrandbits(32) 就可以还原出 seed,我用的是 272 个,也就是 68 个 getrandbits(128),实际更少应该也行,但是不会有很明显的优化了,吧。
然后很奇怪的一个点是 chal 虽然是 getrandbits(64*3),按理说是 24 字节,但是出题人 hex 了一下,变成了 48 字节,这是一个蛮迷惑的操作,当时我找工作人员跟他确认的时候,他说这是故意做的字符空间压缩,实际上我觉得没鸟蛋区别,因为碰撞的过程中是完全随机的。

然后就是构造的部分,这个 AES 的 blocksize 是 16 字节,那么就是要构造 4 个 block,因为 pad 会多补一个 block,这个 block 的末字节的低 4 位是 0。
注意到输入只给你输入 15 字节,所以构造的 block 只有前 15 字节是可控的,最后一个字节就要爆破,不做任何优化的情况下,第一个 block 可以直接改向量控制最后一个字节,第二、三个 block 都要爆 1/256,第四个 block 只需要爆 1/16,因为只需要令最后一个字节的低 4 位是 0。

exp

基本是稳定 30 分的,题目说的什么一次不行再来两次的,根本不需要好吧(除非你大非酋非到没边了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
from pwn import *
from Crypto.Util.number import *
from tqdm import trange, tqdm
from Crypto.Random import get_random_bytes
from seedrecovery import MT19937RecoverSeed
import random
import itertools as its
import string
from hashlib import sha256

pad_x923=lambda x,block_size=16:x+get_random_bytes((block_size-(len(x)%block_size)-1))+bytes([(block_size-len(x)%block_size)])
unpad_x923=lambda x,block_size=16:x[:-((x[-1]-1)%block_size)-1]

r = remote('127.0.0.1', 12345)

# context.log_level = 'debug'

def proof_of_work(suffix, hash, prelen=4):
table = string.ascii_letters+string.digits
r = its.product(table, repeat=prelen)
for i in tqdm(r):
i = ''.join(i)
str = i + suffix
str_256 = sha256(str.encode()).hexdigest()
if str_256 == hash:
return i
raise Exception('Not Found')

def iv2num(iv: bytes):
nums = []
iv = bytes_to_long(iv)
for _ in range(4):
nums.append(iv % 2**32)
iv >>= 32
return nums

def oracle(p: bytes):
r.sendlineafter(b":> ", p)
res = r.recvline().strip().decode()
iv = bytes.fromhex(res)[:16]
ciphertext = bytes.fromhex(res)[16:]
return iv, ciphertext

prefix = r.recvuntil(b')').decode()[-17:-1]
h = r.recvline().decode()[4:-1]
r.recvuntil(b'[+] Plz tell me XXXX: ')
r.sendline(proof_of_work(prefix, h, 4).encode())

win = 0
for _ in trange(50):
data = []

for _ in range(68):
iv, _ = oracle(b'00')
data += iv2num(iv)

assert len(data) == 272
recover = MT19937RecoverSeed([0]*6 + data, 8)
seed = recover.get_seed()
random.seed(seed)

# chal = long_to_bytes(random.getrandbits(64*3))
chal = hex(random.getrandbits(64*3))[2:].zfill(16*3).encode()
iv_list = [long_to_bytes(random.getrandbits(128)).rjust(16, b"\00") for _ in range(200)]

TRY_TABLE_LEN = 16
ivs = []
for _ in range(TRY_TABLE_LEN):
iv, enc = oracle(chal[:15].hex().encode())
ivs.append((200, iv, enc))

# now find the iv and enc
for i in range(TRY_TABLE_LEN):
_, iv, enc = ivs[i]
for j in range(68 + TRY_TABLE_LEN, 180):
iv2 = iv_list[j]
check = list(iv2)[-1] ^ list(enc)[-1] ^ 1 == chal[31]
if check:
# success(j)
ivs[i] = (j, iv, enc)
break
ivs = [i for i in ivs if i[0] != 200]
ivs.sort(key=lambda x: x[0])
# info(ivs)
info("%d possble ivs found" % len(ivs))

count = 68 + TRY_TABLE_LEN
while True:
iv3_found = False
if count > 199:
warn("GG")
break

result = next((iv for iv in ivs if iv[0] == count), None)
if result is None:
oracle(b'00')
count += 1
continue
else:
iv, enc = result[1], result[2]

iv2 = iv_list[count]
payload = bytes_to_long(chal[16:32]) ^ bytes_to_long(iv2) ^ bytes_to_long(enc)
payload = long_to_bytes(payload)
payload = payload[:15]

iv2_true, enc2 = oracle(payload.hex().encode())
assert iv2_true == iv2, (iv2_true, iv2)

for i in range(count, 190):
iv3 = iv_list[i]
check = list(iv3)[-1] ^ list(enc2)[-1] ^ 1 == chal[47]
if check:
iv3_found = True
break

count += 1
if iv3_found:
success("BLOCK1 PASS AT %d" % count)
break
else:
continue
# info(count)
while True:
if count > 199:
warn("GG")
enc3 = b'\x00'*16
break
iv3 = iv_list[count]
count += 1
payload = bytes_to_long(chal[32:48]) ^ bytes_to_long(iv3) ^ bytes_to_long(enc2)
payload = long_to_bytes(payload)
payload = payload[:15]

iv3_true, enc3 = oracle(payload.hex().encode())

check = list(iv3)[-1] ^ list(enc2)[-1] ^ 1 == chal[47]
if check:
success("BLOCK2 PASS AT %d" % count)
break
else:
continue

while True:
if count > 199:
warn("GG")
enc4 = b'\x00'*16
break
iv4 = iv_list[count]
count += 1
payload = bytes_to_long(b'\x00') ^ bytes_to_long(iv4) ^ bytes_to_long(enc3)
payload = long_to_bytes(payload)
payload = payload[:15]

iv4_true, enc4 = oracle(payload.hex().encode())

check = (list(iv4)[-1] ^ list(enc3)[-1] ^ 1) & 0xf == 0
if check:
success("BLOCK3 PASS AT %d OHHHHHHHH!!!!!!!" % count)
win += 1
break
else:
continue

info("Win times: (%d / 20)" % win)
for _ in range(200 - count):
oracle(b'00')
iv = list(iv)
iv[-1] = iv[-1] ^ 1 ^ chal[15]
iv = bytes(iv)
payload2 = iv + enc + enc2 + enc3 + enc4
r.sendlineafter(b">> ", payload2.hex().encode())
r.interactive()
作者

未央

发布于

2024-11-29

更新于

2024-12-17

许可协议

评论