AES题型

First Post:

Last Update:

1.单独涉及ECB模式

最常见的是RSA中套了一个简单的AES_ECB加密,就是AES加密flag,只要先将用RSA加密的key解出,就能解的flag

2.单独涉及CBC模式

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
from Crypto.Cipher import AES
import os
from hashlib import sha256
import socketserver
import signal
import string
import random

table = string.ascii_letters + string.digits
BANNER = br'''
.d8888b. d8b 888 888
d88P Y88b Y8P 888 888
888 888 888 888
888 888 888 888 .d88b. 888888 .d8888b 8888b. 888888
888 888 888 888 d8P Y8b 888 d88P" "88b 888
888 888 888 Y88 88P 88888888 888 888 .d888888 888
Y88b d88P 888 Y8bd8P Y8b. Y88b. Y88b. 888 888 Y88b.
"Y8888P" 888 Y88P "Y8888 "Y888 "Y8888P "Y888888 "Y888



.d888 8888888b. d8b
d88P" 888 Y88b Y8P
888 888 888
888888 .d88b. 888d888 888 d88P 888d888 888 88888b. .d8888b .d88b.
888 d88""88b 888P" 8888888P" 888P" 888 888 "88b d88P" d8P Y8b
888 888 888 888 888 888 888 888 888 888 88888888
888 Y88..88P 888 888 888 888 888 888 Y88b. Y8b.
888 "Y88P" 888 888 888 888 888 888 "Y8888P "Y8888
'''

guard_menu = br'''
1.Tell the guard my name
2.Go away
'''

cat_menu = br'''1.getpermission
2.getmessage
3.say Goodbye
'''


def Pad(msg):
return msg + os.urandom((16 - len(msg) % 16) % 16)


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 proof_of_work(self):
proof = (''.join([random.choice(table) for _ in range(12)])).encode()
sha = sha256(proof).hexdigest().encode()
self.send(b"[+] sha256(XXXX+" + proof[4:] + b") == " + sha)
XXXX = self.recv(prompt=b'[+] Give Me XXXX :')
if len(XXXX) != 4 or sha256(XXXX + proof[4:]).hexdigest().encode() != sha:
return False
return True

def register(self):
self.send(b'')
username = self.recv()
return username

def getpermission(self, name, iv, key):
aes = AES.new(key, AES.MODE_CBC, iv)
plain = Pad(name)+b"a_cat_permission"
return aes.encrypt(plain)

def getmessage(self, iv, key, permission):
aes = AES.new(key, AES.MODE_CBC, iv)
return aes.decrypt(permission)

def handle(self):
signal.alarm(50)
if not self.proof_of_work():
return
self.send(BANNER, newline=False)
self.key = os.urandom(16)
self.iv = os.urandom(16)
self.send(b"I'm the guard, responsible for protecting the prince's safety.")
self.send(b"You shall not pass, unless you have the permission of the prince.")
self.send(b"You have two choices now. Tell me who you are or leave now!")
self.send(guard_menu, newline=False)
option = self.recv()
if option == b'1':
try:
self.name = self.register()
self.send(b"Hello " + self.name)
self.send(b"Nice to meet you. But I can't let you pass. I can give you a cat. She will play with you")
self.send(b'Miao~ ' + self.iv)
for i in range(3):
self.send(b"I'm a magic cat. What can I help you")
self.send(cat_menu, newline=False)
op = self.recv()
if op == b'1':
self.send(b"Looks like you want permission. Here you are~")
permission = self.getpermission(self.name, self.iv, self.key)
self.send(b"Permission:" + permission)
elif op == b'2':
self.send(b"Looks like you want to know something. Give me your permission:")
permission = self.recv()
self.send(b"Miao~ ")
iv = self.recv()
plain = self.getmessage(iv, self.key, permission)
self.send(b"The message is " + plain)
elif op == b'3':
self.send(b"I'm leaving. Bye~")
break
self.send(b"Oh, you're here again. Let me check your permission.")
self.send(b"Give me your permission:")
cipher = self.recv()
self.send(b"What's the cat tell you?")
iv = self.recv()
plain = self.getmessage(iv, self.key, cipher)
prs, uid = plain[16:],plain[:16]
if prs != b'Princepermission' or uid != self.name:
self.send(b"You don't have the Prince Permission. Go away!")
return
else:
self.send(b"Unbelievable! How did you get it!")
self.send(b"The prince asked me to tell you this:")
f = open('flag.txt', 'rb')
flag = f.read()
f.close()
self.send(flag)
except:
self.request.close()
if option == b'2':
self.send(b"Stay away from here!")
self.request.close()


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


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


if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 10005
print("HOST:POST " + HOST + ":" + str(PORT))
server = ForkedServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()

分析:

首先是proof of work函数,简单的爆破四个字符使其连接上剩余已知字符的sha256等于函数给出的sha256值,然后进入正题,第一段守卫处没有有用信息,然后输入你的name(m1),之后可以在魔法喵喵那里获得

  • getpermission
    得到对m1+b’a_cat_permission’的AES_CBC加密之后得到的密文,其中iv是系统生成的已知量,key未知
    1
    2
    3
    4
    def getpermission(self, name, iv, key):
    aes = AES.new(key, AES.MODE_CBC, iv)
    plain = Pad(name)+b"a_cat_permission"
    return aes.encrypt(plain)
  • getmessage()
    输入一段密文进行AES_CBC解密,iv由我们给出,key是之前用的,得到解密之后的明文
    1
    2
    3
    def getmessage(self, iv, key, permission):
    aes = AES.new(key, AES.MODE_CBC, iv)
    return aes.decrypt(permission)
    共有三次询问的机会(循环次数为3),循环完成之后由守卫进行验证
    提交给守卫某个密文和自己给出的偏移量iv,使得进行相同的key的AES_CBC解密之后得到的明文是m1
    +b’Princeperimission’
    1
    2
    3
    4
    5
    6
    7
    self.send(b"Give me your permission:")
    cipher = self.recv()
    self.send(b"What's the cat tell you?")
    iv = self.recv()
    plain = self.getmessage(iv, self.key, cipher)
    prs, uid = plain[16:],plain[:16]
    if prs != b'Princepermission' or uid != self.name:
    简单来说,我们就是需要想办法把从magic cat处得到的perimission = m1 + b’a_cat_perimission’的密文转换为m1 + b’Princepermission’的密文,可以用到的是magic cat会提供一次或两次的解密机会(iv要自己给),显然没有办法知道key进而求解,但是我们可以构造合适的iv,代入最后的AES_CBC解密
    先令
    m1=input_name
    m2=a_cat_permission
    need_m1=m1
    need_m2=Princepermission

由于AES_CBC加密是分块加密,而每一块的大小是128bits也就是16字节,而由于题目要求,m1和m2以及need_ m2都是16字节大小的
这就意味着加密得到的密文块会有特殊的性质

令c1 = permission[:16] c_1=permission[:16]
其中permission是 magic cat通过getpermission()给出的密文

通过图片明确一下AES_CBC加解密过程

加密过程是,先将明文按16字节一分组拆开,然后进行如图加密(先进行异或,非明文分组1的异或对象是前一个密文分组或者是明文分组1的异或对象初始化向量iv),再分别投入加密器,得到各个密文分组后拼接起来成为最后的密文

解密过程是,将密文按16字节一分组拆开,然后分别投入解密器,得到的结果与前一个密文分组或者初始化向量进行异或,最后得到各个明文分组再直接拼接在一起组成最后的明文
alt text
其中加密器和解密器在本题算是黑盒,只需要知道进去的数据和原来是不一样的即可

我们构造的密文要先经过一次解密器,这就意味着非经过AES_CBC加密产生的密文(比如c1,c2)都会产生一个未知数据,那么这时候就需要用到magic cat的解密机会了
经过解密器异或之后,就是本题关键了,异或性质就是两个一样的数异或都是0,也就是说对其他一起的数不会有影响,利用这个性质构造:
alt text
alt text
解密代码:

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
from re import L
from pwn import *
import hashlib,string,random
from Crypto.Cipher import AES

io = remote("node4.buuoj.cn","27370")
temp = io.recvline()
# print(temp)
temp1 = temp.split(b"==")
# print(temp1)
part_proof = bytes.decode(temp1[0].split(b"XXXX")[1])[1:-2]
sha = bytes.decode(temp1[1]).strip()
table = string.ascii_letters + string.digits
while True:
XXXX = "".join([random.choice(table)for _ in range(4)])
temp_proof = XXXX + part_proof
temp_sha = hashlib.sha256(temp_proof.encode()).hexdigest()
if sha == temp_sha:
io.recvuntil(b"[+] Give Me XXXX :")
io.sendline(XXXX.encode())
break
io.recvuntil(b"[-] ")
io.sendline(b"1")
io.sendline(b"1" * 16)
io.recvuntil(b'Miao~ ')
iv = io.recvuntil(b"\n")[:-1]
# print(iv)
io.recvuntil(b'[-]')
io.sendline(b'1')
io.recvuntil(b'Permission:')
cat_permission = io.recvline()[:-1]
# print(cat_permission)
io.recvuntil(b"[-]")
io.sendline(b'2')
io.recvuntil(b"Looks like you want to know something. Give me your permission:")

m2 = b"a_cat_permission"
m1 = b'1' * 16 # your name,直接16字节,不会进行pad()函数,如果那样的话,m1也会成为一个未知量
need_m2 = b"Princepermission"
need_m1 = m1
c1 = cat_permission[:16]
c2 = cat_permission[16:]
fake_c1 = xor(xor(c1,m2),need_m2)

io.sendline(fake_c1)
io.recvuntil(b"Miao~ ")
io.sendline(iv)
io.recvuntil(b"The message is ")
m = io.recvuntil(b"\n")[:-1]
# print(plain)
io.recvuntil(b"[-]")
io.sendline(b'3')
io.recvuntil(b"Give me your permission:")

fake_permission = fake_c1 + c2
fake_iv = xor(xor(m,m1),iv)

io.sendline(fake_permission)
io.recvuntil(b"What's the cat tell you?")
io.sendline(fake_iv)
io.interactive()

3.CBC和ECB的联合求解

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
from Crypto.Cipher import AES
import binascii
from Crypto.Util.number import bytes_to_long
from flag import flag
from key import key

iv = flag.strip(b'd0g3{').strip(b'}')

LENGTH = len(key)
assert LENGTH == 16

hint = os.urandom(4) * 8
print(bytes_to_long(hint)^bytes_to_long(key))

msg = b'Welcome to this competition, I hope you can have fun today!!!!!!'

def encrypto(message):
aes = AES.new(key,AES.MODE_CBC,iv)
return aes.encrypt(message)

print(binascii.hexlify(encrypto(msg))[-32:])

'''
56631233292325412205528754798133970783633216936302049893130220461139160682777
b'3c976c92aff4095a23e885b195077b66'
'''

从题目可知iv是由flag去掉前缀和后缀赋值得到的,所以解出iv就解出了flag,而题目给出的一大串数字是异或后的key,我们可以由
LENGTH = len(key)
assert LENGTH == 16

hint = os.urandom(4) * 8
print(bytes_to_long(hint)^bytes_to_long(key))
知道,现key是由随机的四个重复字节与原key进行异或得到的,将这一串数字转为字节可以看到:

1
2
3
4
>>> from Crypto.Util.number import *
>>> hint_xor_key = 56631233292325412205528754798133970783633216936302049893130220461139160682777
>>> long_to_bytes(hint_xor_key)
b'}4$d}4$d}4$d}4$d\x19\x04CW\x06CA\x08\x1e[I\x01\x04[Q\x19'

有重复的字节}4$d,这就是hint的重复字节,那么:

1
2
3
4
hint_xor_key = 56631233292325412205528754798133970783633216936302049893130220461139160682777
hint = long_to_bytes(hint_xor_key)[:4]
key = hint_xor_key ^ bytes_to_long(hint * 8)
key = long_to_bytes(key)

题目最后是让消息转为十六进制并输出最后32个字符,说明输出的是最后一组密文,
则我们可以得到最终解密iv的代码:

Crypto.Cipher import AES
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from Crypto.Util.number import *
from pwn import *
import binascii
hint_xor_key = 56631233292325412205528754798133970783633216936302049893130220461139160682777
part_enc = b'3c976c92aff4095a23e885b195077b66'
msg = b'Welcome to this competition, I hope you can have fun today!!!!!!'
# print(long_to_bytes(hint_xor_key))
hint = long_to_bytes(hint_xor_key)[:4]
key = hint_xor_key ^ bytes_to_long(hint * 8)
key = long_to_bytes(key)
# print(len(msg))
aes = AES.new(key,AES.MODE_ECB)
part_enc = binascii.unhexlify(part_enc)
enc1 = part_enc
m1 = aes.decrypt(enc1)
enc2 = xor(msg[-16:],m1)
m2 = aes.decrypt(enc2)
enc3 = xor(msg[-32:-16],m2)
m3 = aes.decrypt(enc3)
enc4 = xor(msg[-48:-32],m3)
m4 = aes.decrypt(enc4)
enc5 = xor(msg[-64:-48],m4)
iv = enc5
print(iv)

4.单独涉及CTR模式

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
import socketserver
import random
import os
import string
import binascii
import hashlib
from Crypto.Cipher import AES
from Crypto.Util import Counter
from Crypto.Util.number import getPrime
from hashlib import sha256
import gmpy2
from flag import flag

def init():
q = getPrime(512)
p = getPrime(512)
e = getPrime(64)
n = q*p
phi = (q-1) * (p-1)
d = gmpy2.invert(e, phi)
hint = 2 * d + random.randint(0, 2**16) * e * phi
mac = random.randint(0, 2**64)
c = pow(mac, e, n)
counter = random.randint(0, 2**128)
key = os.urandom(16)
score = 0
return n, hint, c, counter, key, mac, score

class task(socketserver.BaseRequestHandler):

def POW(self):
random.seed(os.urandom(8))
proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
result = hashlib.sha256(proof.encode('utf-8')).hexdigest()
self.request.sendall(("sha256(XXXX+%s) == %s\n" % (proof[4:],result)).encode())
self.request.sendall(b'Give me XXXX:\n')
x = self.recv()

if len(x) != 4 or hashlib.sha256((x+proof[4:].encode())).hexdigest() != result:
return False
return True

def recv(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 padding(self, msg):
return msg + chr((16 - len(msg)%16)).encode() * (16 - len(msg)%16)

def encrypt(self, msg):
msg = self.padding(msg)
if self.r != -1:
self.r += 1
aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r))
return aes.encrypt(msg)
else:
return msg

def send(self, msg, enc=True):
print(msg, end= ' ')
if enc:
msg = self.encrypt(msg)
print(msg, self.r)
self.request.sendall(binascii.hexlify(msg) + b'\n')

def set_key(self, rec):
if self.mac == int(rec[8:]):
self.r = self.counter

def guess_num(self, rec):
num = random.randint(0, 2**128)
if num == int(rec[10:]):
self.send(b'right')
self.score += 1
else:
self.send(b'wrong')

def get_flag(self, rec):
assert self.r != -1
if self.score == 5:
self.send(flag, enc=False)
else:
self.send(os.urandom(32) + flag)

def handle(self):
self.r = -1

if not self.POW():
self.send(b'Error Hash!', enc= False)
return

self.n, self.hint, self.c ,self.counter, self.key, self.mac, self.score = init()

self.send(str(self.n).encode(), enc = False)
self.send(str(self.hint).encode(), enc = False)
self.send(str(self.c).encode(), enc = False)

for _ in range(6):
rec = self.recv()
if rec[:8] == b'set key:':
self.set_key(rec)
elif rec[:10] == b'guess num:':
self.guess_num(rec)
elif rec[:8] == b'get flag':
self.get_flag(rec)
else:
self.send(b'something wrong, check your input')

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

def main():
HOST, PORT = '127.0.0.1', 10086
server = ForkedServer((HOST, PORT), task)
server.allow_reuse_address = True
server.serve_forever()

if __name__ == '__main__':
main()

这是一道经典的nc连接题,用了套接字模块,重点函数是handle()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def handle(self):
self.r = -1

if not self.POW():
self.send(b'Error Hash!', enc= False)
return

self.n, self.hint, self.c ,self.counter, self.key, self.mac, self.score = init()

self.send(str(self.n).encode(), enc = False)
self.send(str(self.hint).encode(), enc = False)
self.send(str(self.c).encode(), enc = False)

for _ in range(6):
rec = self.recv()
if rec[:8] == b'set key:':
self.set_key(rec)
elif rec[:10] == b'guess num:':
self.guess_num(rec)
elif rec[:8] == b'get flag':
self.get_flag(rec)
else:
self.send(b'something wrong, check your input')

先进行POW函数验证,就是判断hash值,爆破就行(就是去爆破下图的XXXX)
alt text
按照RSA加密的方式给出了n,c和hint(服务端这几个值都是十六进制,要先转十进制),且加密的明文是mac,先求它:
alt text
所以mac=gmpy2.iroot(pow(c,hint,n),2)[0];需要知道mac作用
再来看到之后提供三种选择,set key,guess num,get flag

1
2
3
def set_key(self, rec):
if self.mac == int(rec[8:]):
self.r = self.counter

似乎是判定输入的内容是否和mac一致,若一致,则进行一个赋值操作,看到是将counter赋值给r
寻找counter的生成:

1
2
3
def init():
...
counter = random.randint(0, 2**128)

就是一个随机数,而self.r的初始值为-1
由encrypt()函数可以看到

1
2
3
4
5
6
7
8
def encrypt(self, msg):
msg = self.padding(msg)
if self.r != -1:
self.r += 1
aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r))
return aes.encrypt(msg)
else:
return msg

如果self.r的值没有改变的话,也就是说没有进行set key操作的话,AES_CRT加密过程就不会生效;加密不生效就可以直接拿flag吗,再来看到get flag操作

1
2
3
4
5
6
def get_flag(self, rec):
assert self.r != -1
if self.score == 5:
self.send(flag, enc=False)
else:
self.send(os.urandom(32) + flag)

有个assert self.r != -1,意味着必须self.r被随机生成的counter赋值才能进行get flag,否则会直接assert报错

三种选择中还剩下一个guess num操作

1
2
3
4
5
6
7
def guess_num(self, rec):
num = random.randint(0, 2**128)
if num == int(rec[10:]):
self.send(b'right')
self.score += 1
else:
self.send(b'wrong')

如果猜对随即生成的数字,则使得self.score += 1,而self.score的作用是在get flag中

1
2
if self.score ==  5:
self.send(flag, enc=False)

意味着连续猜中5次则能使得get flag操作直接返回flag

但是细看一下,首先随机数生成的范围很大,不可能爆破;只能猜测一次,否则会在6次选择中浪费一次机会;最重要的是,6次选择中有一次机会必须用来set key(否则无法进行get flag操作),有一次机会用来get flag,只剩下4次机会,而我们需要连续猜中5次随机数;所以guess num操作在现在看来就是一个幌子

那么要获得flag只能从encrypt函数下手
这是一个CTR加密
alt text
计数器CTR就是可以生成随机的初始值IV,而后每次加密序号都有递增
生成过程:

1
2
from Crypto.Util import Counter 
aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r))

看得出来是Counter.new(128, initial_value=self.r))中initial_value变量在设置CTR的初始值

而CTR所表示的值也不是简单的00,01,而是一组由随机数nonce和分组序号组成的初始值
每加密一个明文分组,CTR的分组序号则会+1,使得进行加密操作之后的加密流与之前的加密流完全不同,达到与各个明文分组进行异或的字节串互不相同
注:加密操作是指CTR生成后马上进行加密器的操作

  • 解密过程

    可以发现就是重置计数器CTR使其在对应的密文分组上产生与加密时一样的CTR,再将其进行加密操作,生成与加密过程中一样的加密流,与密文分组异或即得到明文分组
    我们发现加解密其实就两大块,CTR加密和异或;

    • 再回到题目脚本,这里容易发现,在每次服务器脚本send的时候,函数中很多地方都是send(x,enc = False),可以找到脚本中自己定义的send函数
1
2
3
4
5
6
def send(self, msg, enc=True):
print(msg, end= ' ')
if enc:
msg = self.encrypt(msg)
print(msg, self.r)
self.request.sendall(binascii.hexlify(msg) + b'\n')

作用就是写出enc = False的send函数里面,会直接输出内容,也就是正常交互过程中的send函数;而当enc = True,或者说send函数里面没有对enc进行再赋值,那么输出的内容会进行encrypt()加密,也就是AES_CTR加密
仔细观察可以发现:

1
2
3
4
5
6
self.send(b'something wrong, check your input')
if num == int(rec[10:]):
self.send(b'right')
self.score += 1
else:
self.send(b'wrong')

这里的send函数返回的实际上不是明文right,something wrong…;而是AES_CTR加密后的密文(也可以在本地测试debug的时候发现这个特征)

不可能无缘无故地把这些返回内容设置为加密之后再返回,所以这里一定是突破点

现在我们可以得到一对明密文,AES_CTR加密后的flag;加密密钥key未知也无法知道,显然不能通过正常求密钥key来解密

总共给予了6次选择的机会,那如果我们多得到几组明密文,有什么用呢

在AES_CTR加密中,如果我们已知明文以及对应的密文,将两者异或即可得到加密流,因为加密过程中
alt text
而加密流是由不同的CTR生成的,如果我们多收集一些加密流拼接在一起,再用来和加密后的flag进行异或,不就模拟了AES_CTR模式的解密过程吗

那么最终的问题就是我们需要哪些加密流,也就是说哪些加密流是加密flag的时候真正使用的

加密流当然是越长越好,所以我们使用something wrong…作为明密文组求得加密流,默认经过padding函数后明文长度为48

正常情况下(本题不是这样的正常情况,之后会提到,也是关键点之一)是得到的加密流应该是由CTR,CTR+1,CTR+2生成的,那么很可能不够长足以使得与加密之后的flag异或可以得到完整的flag(因为flag加密过程中是作为padding(os.urandom(32)+flag)进行加密的,所以密文会比较长)

那么现在有一个疑惑就是如果连续加密两次及以上的话,counter会不会自动继续+1,使得我们确实可以将对不同的加密过程(虽然明文确实是相同的)中生成的多个加密流按照CTR+i的顺序拼接在一起,作为一整个加密流

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> from Crypto.Cipher import AES
>>> from Crypto.Util import Counter
>>> from pwn import *
>>> key = b'\x01' * 16
>>> msg = b"flag1111111111111111111111111111111111111111111111111111111"
>>> t = Counter.new(128, initial_value=123)
>>> aes = AES.new(key, AES.MODE_CTR, counter = t)
>>> enc1 = aes.encrypt(msg)
>>> enc1
b"0=J\xe6\x87\xec\x07r\xf1{L\x94\xf7\xf2\xfcp|-\xf0\xca\xba\xe4:\x89\x7f\rI\xb5\x84\xb8\x00\xe2%(\x02o\xa5\x8c\xa3\x93\x18'\xb0\xa2\xe2\xd4]\xb1*\xb5\x8fH7\xd8\xfa\x8f\x08\xe7X"
>>> enc2 = aes.encrypt(msg)
>>> enc2
b'\x97\xf5\xe3_\xdc\xb3\x1c\x11\x1fM\n\x16\x06}:hvU\x1d\x93"\xf6\x89\xc3\x05\x94\x8b>6ha\xce\'\x9f\xb5$\x07Hm\xa5\xd1]y)P\xff\xd3e\xea\x7f\xa9\xb3\xe9\xcd\x88\x97>\x8d\xad'

实践证明确实可以将不同加密过程中得到的加密流拼接在一起,但是如何确保对flag加密的加密流和我们求得的加密流一致呢,那就是重复使用set key操作

大致重演一下重复使用set key的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> from Crypto.Cipher import AES
>>> from Crypto.Util import Counter
>>> from pwn import *
>>> key = b'\x01' * 16
>>> msg = b"flag1111111111111111111111111111111111111111111111111111111"
>>> t = Counter.new(128, initial_value=123)
>>> aes = AES.new(key, AES.MODE_CTR, counter = t)
>>> enc1 = aes.encrypt(msg)
>>> enc2 = aes.encrypt(msg)
>>> t = Counter.new(128, initial_value=123)
>>> aes = AES.new(key, AES.MODE_CTR, counter = t)
>>> enc3 = aes.encrypt(msg)
>>> enc4 = aes.encrypt(msg)
>>> assert xor(enc1,msg) == xor(enc3,msg)
>>> assert xor(enc2,msg) == xor(enc4,msg)
>>>

意思就是当我们重复对counter进行赋相同的值时,整个CTR初始值会被刷新

那么我们是不是只需要两到三个something wrong…明密文对得到的加密流拼接在一起与flag进行异或就好了呢

实践发现只能得到一半的flag,刚好就是前48位的padding(os.urandom(32)+flag);之后的加密流为什么不是正确对应的呢

发现作者在self.r和aes上面动了手脚,在每次加密整个明文之前self.r会自己首先+1,也就是CTR + 1;并且在每次加密整个明文之前都会重新定义一遍 aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r)),这就意味着对两次明文(不是明文分组)加密中所使用两个的CTR生成的加密流中,上一个明文的最后一组明文分组使用的是CTR + i,下一个明文的第一组明文分组的不是CTR + i + 1

1
2
3
4
5
6
7
8
def encrypt(self, msg):
msg = self.padding(msg)
if self.r != -1:
self.r += 1
aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r))
return aes.encrypt(msg)
else:
return msg

所以加密过程中的加密流实际上是CTR + 1,CTR + 2,CTR + 3等等通过加密操作生成的

并且上一个明文(不是明文分组)加密所涉及的CTR初始值是CTR + 1,下一个明文加密所涉及的CTR初始值是CTR + 2(因为每次加密整个明文之前self.r += 1)

所以

其中mi是somgthing srong…的明文,意思是该明文在前后的加密过程中所使用的加密流是由以上CTR + i生成的

为了使得flag使用的是已知的加密流,我们使用set key操作把CTR刷新,使得
alt text
那么对mi进行对应的截取即可

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
from pwn import *
from Crypto.Util.number import *
import gmpy2
import hashlib
import string
import random
import binascii

context.log_level='debug'
io = remote("192.168.109.129","9998")
io.recvuntil(b"+")
tmp = io.recvuntil(b"\n")
part_proof = tmp.split(b") == ")[0]
result = tmp.split(b") == ")[1][:-1]
table = string.ascii_letters + string.digits
while True:
XXXX = ("".join(random.sample(table,4))).encode()
if hashlib.sha256(XXXX + part_proof).hexdigest() == result.decode():
io.recvuntil(b"\n")
io.sendline(XXXX)
break

n = int(binascii.unhexlify(io.recvline()[:-1]))
hint = int(binascii.unhexlify(io.recvline()[:-1]))
c = int(binascii.unhexlify(io.recvline()[:-1]))
mac = gmpy2.iroot(pow(c,hint,n) % n,2)[0]


io.sendline(b"set key:" + str(mac).encode())
enc = []
for _ in range(3):
io.sendline(b"I_want_flag")
time.sleep(0.5)
tmp = io.recvline()[:-1].decode()
if _ != 2:
enc.append(long_to_bytes(int(str(tmp),16))[:16])
else:
enc.append(long_to_bytes(int(str(tmp),16)))
# print(enc)
io.sendline(b"set key:" + str(mac).encode())
time.sleep(0.5)
io.sendline(b"get flag")
time.sleep(0.5)
tmp = io.recvline()
enc_flag = binascii.unhexlify(tmp[:-1])

msg = b'something wrong, check your input'
msg = msg + chr((16 - len(msg)%16)).encode() * (16 - len(msg)%16)
key_stream = b""
for i in enc:
key_stream += xor(i,msg[:len(i)])
print(xor(enc_flag,key_stream[:len(enc_flag)]))

关于AES的深入一点的题目都是交互式的,正常情况不会要求求密钥key,而是利用xor的特性以及加密器(相当于黑盒加密)来从服务器脚本处骗取可以求得flag的数值

关于交互式的题目没有头绪的时候更倾向于直接与服务端进行交互查看debug返回,而不单是查看服务器所使用的脚本

Crypto的交互式题目需要注意服务端返回的内容是什么类型的,很可能脚本会对十进制数值转字节再转十六进制,可能要进行一定的数据处理