AES Padding Oracle Attack

AES Padding Oracle Attack

先回顾一下AES的CBC模式

加密:

Ci = Ek(Pi ⊕ Ci-1)
C0 = IV

解密:
Pi = Dk(Ci ⊕ Ci-1)
C0 = IV

CBC字节翻转攻击

对于CBC模式的解密算法,每一组明文进行分组算法解密之后,需要和前一组的密文异或才能得到明文。第一组则是和初始向量IV进行异或。

CBC字节翻转攻击的核心原理是通过破坏一个比特的密文来篡改一个比特的明文。

假设现在我们有第 N-1 组密文某一位的值 A ,以及第 N 组密文相同位置经过分组解密后的值 B ,于是我们能够很容易得到第 N 组该位置上的明文 C 。

A ⊕ B = C

如果我们破坏第 N-1 组的密文 A ,将其与明文 C 进行异或运算,由异或的性质可以得到下式:

A ⊕ C ⊕ B = C ⊕ C = 0

可以看见,现在计算出的明文变成0了,现在我们可以将明文随意更改成我们想要的字符。只需要在上一组的密文异或我们想要的字符即可,假设我们想将明文 C 更改为 X ,可以由下式得出:

A ⊕ C ⊕ X ⊕ B = C ⊕ C ⊕ X = X

此时我们已经通过破坏密文将明文更改成我们想要的字符,具体攻击流程可以参考下图

CBC字节翻转攻击流程

用Python模拟一下攻击流程

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
from Crypto.Cipher import AES
import uuid
import binascii


BS=AES.block_size #分组长度
key=b'test' #密钥
iv=uuid.uuid4().bytes #随机初始向量
pad=lambda s: s+((BS-len(s)%BS)*chr(BS-len(s)%BS)).encode() #Pkcs5Padding
data=b'1234567890abcdefabcdef1234567890' #明文M

#加密
def enc(data):
aes=AES.new(pad(key),AES.MODE_CBC,iv)
ciphertext=aes.encrypt(pad(data))
ciphertext=binascii.b2a_hex(ciphertext)
return ciphertext

#解密
def dec(c):
c=binascii.a2b_hex(c)
aes=AES.new(pad(key),AES.MODE_CBC,iv)
data=aes.decrypt(c)
return data

#测试CBC翻转
def CBC_test(c):
c=bytearray(binascii.a2b_hex(c))
c[0]=c[0]^ord('a')^ord('A') #c[0]为第一组的密文字符,a为第二组相应位置的明文字符,A是我们想要的明文字符
c=binascii.b2a_hex(c)
return c

print("ciphertext:",enc(data))
print("data:",dec(enc(data)))
print("CBC Attack:",dec(CBC_test(enc(data))))

运行结果

1
2
3
ciphertext: b'ffa645d1b5e40afbbae47de053a66f978fa0a824e99864a7e8baf38ceccda613c304883f11fc0857c1bb7603f859798e'
data: b'1234567890abcdefabcdef1234567890\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
CBC Attack: b':8O<\xe7\x04\xd8v\xe8Q\xfe\xa5I\xc9c]Abcdef1234567890\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'

可以看到第二组密文解密之后已经被我们更改成了A,而由于我们更改了第一组的密文,所以第一组解密的明文变成了乱码。如果我们想要更改第一组的明文,则需要修改初始向量IV的值。

通过CBC字节翻转攻击,假如我们能够触发加解密过程,并且能够获得每次加密后的密文。那么我们就能够在不知道key的情况下,通过修改密文或IV,来控制输出明文为自己想要的内容,而且只能从最后一组开始修改,并且每改完一组,都需要重新获取一次解密后的数据,要根据解密后的数据来修改前一组密文的值。

通过修改明文修改IV(不会改变对应密文)

1
2
3
def flipplain(oldplain, newplain, iv):  # 定义一个函数用于进行CBC字节翻转攻击
"""flip oldplain to new plain, return proper iv"""
return strxor(strxor(oldplain, newplain), iv)

介绍

Padding Oracle Attack 攻击一般需要满足以下几个条件:

  • 加密算法:
    • 采用 PKCS5或者PKCS7 Padding 的加密算法。 当然,非对称加密中 OAEP 的填充方式也有可能会受到影响。
    • 分组模式为 CBC 模式。
  • 攻击者能力:
    • 攻击者可以拦截上述加密算法加密的消息。
    • 攻击者可以和 padding oracle(即服务器) 进行交互:客户端向服务器端发送密文,服务器端会以某种返回信息告知客户端 padding 是否正常。

此时Padding Oracle Attack就可以在不清楚 key 和 IV 的前提下解密任意给定的密文。

原理

假设现在有一个真实场景:

某程序使用Cookie来加密传递用户的加密用户名、公司 ID 和角色 ID。该Cookie使用CBC Mode加密,每个Cookie使用一个唯一的初始化向量iv,该向量位于密文之前。

当应用程序收到一个加密Cookie时,它有以下三种响应方式:

  • 当收到一个有效的密文(一个被正确填充并包含有效数据的密文)时,应用程序正常响应(200 OK)
  • 当收到无效的密文时(解密时填充错误的密文),应用程序会抛出加密异常(500 内部服务器错误)
  • 当收到一个有效密文(解密时正确填充的密文)但解密为无效值时,应用程序会显示自定义错误消息 (200 OK)

上面描述的场景是一个经典的Padding Oracle,因为我们可以使用程序的行为来确定提供的加密值是否被正确填充。

假设有一个员工,其信息分别为:用户名:BRIAN、公司ID:12、角色ID:1。各信息之间用分号分隔,于是可以表示成以下形式:BRAIN;12;1

假设我们使用PKCS5填充,可以转换为以下形式:

1
2
BRIAN;12;1;0x050x050x050x050x05
#16字节,符合Pkcs5分组长度,可以将以上字符串分为两组,每组8字节

下面是CBC的加密过程

CBC加密

这里设置的初始向量iv为0x7B 0x21 0x6A 0x63 0x49 0x51 0x17 0x0F,这时服务器发送的Cookie应该为7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6,初始向量iv被填充在加密密文之前。

CBC解密过程如下

CBC解密

密文进行分组解密之后会产生中间值Intermediary Value,这些中间值再和前一组密文异或便会得到本组明文。解密出的明文后面会有正确的填充块。当然在客户端,我们无法得知这些中间值是什么。

攻击原理一

首先我们可以更改Cookie 中的初始向量iv,将iv的值改为全零,并且只发送第一个加密块。此时Cookie变为
Cookie:0000000000000000F851D6CC68FC9537

服务器端的解密过程如下

攻击原理一

可以看到,如果按照我们发送的错误数据来解密的话,最后一位数据是0x3D。而正常的解密数据最后是包含填充数据的。对于分组长度为8字节来说,这些填充数据的值位于0x01-0x08,而0x3D并不在其中。所以服务器会检测到填充块错误,返回500异常。

攻击原理二

我们再次更改Cookie中的初始向量iv,和上次不同的是,我们将iv最后一位的值改为0x01,此时Cookie的值如下
Cookie:0000000000000001F851D6CC68FC9537

我们再次将Cookie发送给服务器端解密

攻击原理二

可以看见,此时明文最后一位变成了0x3C,这仍然不是正确的填充数据,服务器同样会返回500异常。但是与上次不一样的是,明文最后一位的值从0x3D变为了0x3C。也就是说,我们可以通过控制初始向量iv的值来控制明文输出。

如果我们重复发出相同的请求并每次只更改iv中同一个字节的值(最多至0xFF),我们肯定能够碰到一个值,使明文符合填充规律。

攻击原理三

假设我们通过不断发送类似的Cookie给服务器端解密,找到了一个使明文符合填充规律的Cookie,此时服务器端的解密过程如下

攻击原理三

解密后的明文最后一位为0x01,符合填充规律,但是明文结果并不正确。此时服务器会返回200OK自定义页面。由于填充规律是固定的,我们只更改了一位iv的值,所以解密明文肯定是一位填充数据,值为0x01。此时我们可以根据以下公式得到一位加密的中间值0x3D

0x3D = 0x3C ⊕ 0x01

攻击原理四

类似地,我们可以爆破iv值的每一位,直到解密出来的每一位明文数据都变成全部符合填充规律的0x08,解密过程如下

攻击原理四

至此,我们可以利用Padding Oracle Attack来爆破出每一组加密中间值。然后再使用第一组的中间值和服务器端初始的iv异或,便可以得到第一组明文。继续使用CBC Mode解密,可以依次得到所有明文分组。

对于每一组,至多需要尝试256*8次也就是2048次,便可爆破出加密中间值。这样我们就可以绕过加密,从而直接获得密文的明文。

攻击原理五

在利用测试四中,我们已经能够利用Padding Oracle Attack爆破出所有中间值和明文了,那么我们该怎么构造出任意明文的合法密文呢?

首先我们先看单一分组的情况

单一分组

由于我们已经爆破出了intermediary,所以我们可以很容易地构造出iv来生成任意明文。

那么对于多组明文,情况又是怎样的呢?

多组情况

先从最后一组开始,爆破最后一组的intermediary并构造出iv,然后将本组的iv当作前一组的密文,以此类推。由此我们可以得到构造密文的步骤

  1. 从最后一组开始,爆破出该组的intermediary并构造出iv,然后将本组的iv当作前一组的密文
  2. 爆破前一组的intermediary并构造出iv,然后将本组的iv当作前一组的密文
  3. 最后会得到第一组的iv,至此我们已经构造出了所有合法密文以及iv

基础脚本

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
from pwn import *
from copy import deepcopy
from tqdm import trange
from Crypto.Cipher import AES
import os
import random


def AES_CBC_enc(m, key, iv):
aes = AES.new(key, AES.MODE_CBC, iv)
return aes.encrypt(m)


def AES_CBC_dec(c, key, iv):
aes = AES.new(key, AES.MODE_CBC, iv)
return aes.decrypt(c)


def padding_to_16(msg):
padding = 16 - (len(msg) % 16)
return msg + bytes([padding]) * padding


# unpadding and check padding characters
def unpadding(msg):
padding = msg[-1]
if padding == 0:
return msg, False
for i in range(padding):
if (msg[-i-1] != padding):
return msg, False
return msg[:-padding], True


def padding_oracle_attack(msg_enc, key, iv):
middle = [0] * 16
for i in trange(16):
my_iv = deepcopy(middle)
if i != 0:
my_iv[-i:] = xor(my_iv[-i:], [i+1]*i)
print(my_iv)
for j in range(256):
my_iv[-i-1] = j

msg_after_padding = AES_CBC_dec(msg_enc, key, bytes(my_iv))
flag = unpadding(msg_after_padding)[1]
if flag == True:
middle[-i-1] = j ^ (i+1)
break

msg_after_padding = xor(middle, bytearray(iv))
print(msg_after_padding)
msg, flag = unpadding(bytes(msg_after_padding))
if flag == False:
return None
return msg


msg = os.urandom(random.randint(1,15))
key = os.urandom(16)
iv = os.urandom(16)
print(msg)

# padding msg
msg_after_padding = padding_to_16(msg)
msg_enc = AES_CBC_enc(msg_after_padding, key, iv)

# padding_oracle_attack recover msg
msg = padding_oracle_attack(msg_enc, key, iv)
print(msg)

例题

2023 第六届安洵杯 Cry2

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
# -*- coding:utf-8 -*-
from Crypto.Util.number import isPrime, long_to_bytes, getStrongPrime, bytes_to_long
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os
import binascii
import random
import string
import hashlib
import socketserver

FLAG = '**********'
KEY = b'****************'
IV = b'****************'


def cbc_decrypt(c, iv):
aes = AES.new(KEY, AES.MODE_CBC, iv=iv)
return aes.decrypt(c)


def encrypt():
plain_text = ''.join([random.choice(string.ascii_letters) for _ in range(2)]) + FLAG
aes = AES.new(KEY, AES.MODE_CBC, iv=IV)
plain_text = pad(plain_text.encode(), AES.block_size)
cipher = aes.encrypt(plain_text)
return IV.hex() + cipher.hex()


def asserts(pt: bytes):
num = pt[-1]
if len(pt) == 16:
result = pt[::-1]
count = 0
for i in result:
if i == num:
count += 1
else:
break
if count == num:
return True
else:
return False
else:
return False


def decrypt(c):
iv = c[:32]
cipher = c[32:]
plain_text = cbc_decrypt(binascii.unhexlify(cipher), binascii.unhexlify(iv))
if asserts(plain_text):
return True
else:
return False


class MyServer(socketserver.BaseRequestHandler):
def proof(self):
random.seed(os.urandom(8))
random_str = ''.join([random.choice(string.ascii_letters + string.digits) for _ in range(20)])
str_sha256 = hashlib.sha256(random_str.encode()).hexdigest()
self.request.sendall(('SHA256(XXXX + %s):%s\n' % (random_str[4:], str_sha256)).encode())
self.request.sendall('Give Me XXXX:\n'.encode())
XXXX = self.request.recv(2048).strip()

if hashlib.sha256((XXXX + random_str[4:].encode())).hexdigest() != str_sha256:
return False

return True

def handle(self):
if not self.proof():
self.request.sendall(b'Error Hash!')
return
cipher = encrypt()
self.request.sendall('Welcome to AES System, please choose the following options:\n1. encrypt the flag\n2. decrypt the flag\n'.encode())
n = 0
while n < 65536:
options = self.request.recv(512).strip().decode()
if options == '1':
self.request.sendall(('This is your flag: %s\n' % cipher).encode())
elif options == '2':
self.request.sendall('Please enter ciphertext:\n'.encode())
recv_cipher = self.request.recv(512).strip().decode()
if decrypt(recv_cipher):
self.request.sendall('True\n'.encode())
else:
self.request.sendall('False\n'.encode())
else:
self.request.sendall('Input wrong! Please re-enter\n'.encode())
n += 1
return


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

if __name__ == '__main__':
sever = socketserver.ThreadingTCPServer(('0.0.0.0', 10010), MyServer)
ThreadedTCPServer.allow_reuse_address = True
ThreadedTCPServer.allow_reuse_port = True
sever.serve_forever()

题目分析

这一题的核心思路就是利用Padding Oracle Attack爆破出middle,但是这个攻击是有条件的,我们先分析一下题目的关键代码

flag通过PKCS7的方式padding

1
2
3
4
from Crypto.Util.Padding import pad

plain_text = ''.join([random.choice(string.ascii_letters) for _ in range(2)]) + FLAG
plain_text = pad(plain_text.encode(), AES.block_size)

已知iv和ciphertext

1
2
if options == '1':
self.request.sendall(('This is your flag: %s\n' % cipher).encode())

服务端会用key以及我们发过去的iv和ciphertext解密得到一个plaintext,然后用asserts函数检查该plaintext的padding字符,如果符合pkcs7会输出”True”,不符合则输出”False”

1
2
3
4
5
6
7
elif options == '2':
self.request.sendall('Please enter ciphertext:\n'.encode())
recv_cipher = self.request.recv(512).strip().decode()
if decrypt(recv_cipher):
self.request.sendall('True\n'.encode())
else:
self.request.sendall('False\n'.encode())

总结一下,我们现在已知

  1. plaintext经过pad
  2. 已知iv和ciphertext(其实只要有iv就足够了)
  3. 解密的iv和ciphertext可控,解密得到plaintext,检查其padding是否正确并输出检查结果

符合这些条件就可以实现Padding Oracle Attack来恢复plaintext了

解题代码

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
from pwn import *
import itertools
from copy import deepcopy
from tqdm import trange
from string import ascii_letters, digits
import hashlib
import binascii


# context.log_level = "debug"
p = remote("127.0.0.1", 10010)


def get_proof():
p.recvuntil(b"SHA256(XXXX + ")
last = p.recvuntil(b"):", drop=True)
shav = p.recvline()[:-1]
print(f"last = {last}")
print(f"shav = {shav}")
for cont in itertools.product(ascii_letters + digits, repeat=4):
cont = ''.join(cont).encode()
if hashlib.new("sha256", cont + last).hexdigest() == shav.decode():
print(cont)
break
p.sendlineafter(b"Give Me XXXX:\n", cont)


# unpadding and check padding characters
def unpadding(msg):
padding = msg[-1]
if padding == 0:
return msg, False
for i in range(padding):
if (msg[-i-1] != padding):
return msg, False
return msg[:-padding], True


def padding_oracle_attack(iv, c):
solved_dec = [0] * 16
for i in trange(16):
new_iv = deepcopy(solved_dec)
if i != 0:
new_iv[-i:] = xor(new_iv[-i:], [i+1]*i)
for j in range(256):
new_iv[-i-1] = j

p.sendline(b"2")
p.sendlineafter(b"Please enter ciphertext:\n", (bytes(new_iv).hex() + c.hex()).encode())
if p.recvline() != b"False\n":
solved_dec[-i-1] = j ^ (i+1)
break

msg_after_padding = xor(solved_dec, bytearray(iv))
print(msg_after_padding)
msg, flag = unpadding(bytes(msg_after_padding))
if flag == False:
return None
return msg


get_proof()

p.sendlineafter(b"1. encrypt the flag\n2. decrypt the flag\n", b"1")
iv_and_c = binascii.unhexlify(p.recvline().decode()[19:-1])
iv, c = iv_and_c[:16], iv_and_c[16:]
print(f"iv = {iv}")
print(f"c = {c}")

msg = padding_oracle_attack(iv, c)
print(msg)

"""
last = b'OfBypjLi4BizYvHW'
shav = b'c0786d83da7177ab64ac113343ad157e4a7784998fb2be1ff84ab1e378499375'
b'OTnk'
iv = b'`\xb9\x9c\xa7K>SfC|\xabz\x8b*\x00`'
c = b'#\xad\xfd\x84m\x16F\x17\xc3\xc9s\xce\x02\x86\x04\x88'
100%|██████████| 16/16 [00:00<00:00, 51.35it/s]
b'Obflag{test}\x04\x04\x04\x04'
b'Obflag{test}'
"""

2017 HITCON Secret Server

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
import os, base64, time, random, string 
from Crypto.Cipher import AES
from Crypto.Hash import *

key = os.urandom(16)


def pad(msg):
pad_length = 16-len(msg)%16
return msg+chr(pad_length)*pad_length


def unpad(msg):
return msg[:-ord(msg[-1])]


def encrypt(iv,msg):
msg = pad(msg)
cipher = AES.new(key,AES.MODE_CBC,iv)
encrypted = cipher.encrypt(msg)
return encrypted


def decrypt(iv,msg):
cipher = AES.new(key,AES.MODE_CBC,iv)
decrypted = cipher.decrypt(msg)
decrypted = unpad(decrypted)
return decrypted


def send_msg(msg):
iv = '2jpmLoSsOlQrqyqE'
encrypted = encrypt(iv,msg)
msg = iv+encrypted
msg = base64.b64encode(msg)
print msg
return

# 定义一个函数,用于接收消息
def recv_msg():
msg = raw_input()
try:
msg = base64.b64decode(msg) # 对接收到的消息进行Base64解码
assert len(msg)<500
decrypted = decrypt(msg[:16],msg[16:])
return decrypted
except:
print 'Error' # 如果在解码或解密过程中出现错误,打印错误信息并退出程序
exit(0)

# 定义一个函数,用于进行工作量证明(Proof of Work)
def proof_of_work():
proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in xrange(20)]) # 生成一个随机的20字符的字符串
digest = SHA256.new(proof).hexdigest() # 对生成的字符串进行SHA256哈希
print "SHA256(XXXX+%s) == %s" % (proof[4:],digest) # 打印哈希值
x = raw_input('Give me XXXX:') # 接收用户输入的前4个字符
if len(x)!=4 or SHA256.new(x+proof[4:]).hexdigest() != digest: # 验证用户输入的前4个字符是否正确
exit(0) # 如果验证失败,退出程序
print "Done!" # 如果验证成功,打印成功信息
return


if __name__ == '__main__':
proof_of_work() # 执行工作量证明
with open('flag.txt') as f:
flag = f.read().strip()
assert flag.startswith('hitcon{') and flag.endswith('}')
send_msg('Welcome!!')
while True:
try:
msg = recv_msg().strip() # 接收并去除消息两端的空白字符
if msg.startswith('exit-here'): # 如果消息以'exit-here'开头,退出程序
exit(0)
elif msg.startswith('get-flag'): # 如果消息以'get-flag'开头,发送flag
send_msg(flag)
elif msg.startswith('get-md5'): # 如果消息以'get-md5'开头,发送消息的MD5哈希值
send_msg(MD5.new(msg[7:]).digest())
elif msg.startswith('get-time'): # 如果消息以'get-time'开头,发送当前时间
send_msg(str(time.time()))
elif msg.startswith('get-sha1'): # 如果消息以'get-sha1'开头,发送消息的SHA1哈希值
send_msg(SHA.new(msg[8:]).digest())
elif msg.startswith('get-sha256'): # 如果消息以'get-sha256'开头,发送消息的SHA256哈希值
send_msg(SHA256.new(msg[10:]).digest())
elif msg.startswith('get-hmac'): # 如果消息以'get-hmac'开头,发送消息的HMAC哈希值
send_msg(HMAC.new(msg[8:]).digest())
else: # 如果消息不符合以上任何一种格式,发送'command not found'
send_msg('command not found')
except:
exit(0) # 如果在接收消息或处理消息过程中出现错误,退出程序

题目分析

程序中采用的加密是 AES CBC,其中采用的 padding 与 PKCS5 类似

1
2
3
4
5
6
def pad(msg):
pad_length = 16-len(msg)%16
return msg+chr(pad_length)*pad_length

def unpad(msg):
return msg[:-ord(msg[-1])]

但是,在每次 unpad 时并没有进行检测,而是直接进行 unpad。

其中,需要注意的是,每次和用户交互的函数是

  • send_msg ,接受用户的明文,使用固定的 2jpmLoSsOlQrqyqE 作为 IV,进行加密,并将加密结果输出。
  • recv_msg ,接受用户的 IV 和密文,对密文进行解密,并返回。根据返回的结果会有不同的操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
        msg = recv_msg().strip()  # 接收并去除消息两端的空白字符
    if msg.startswith('exit-here'): # 如果消息以'exit-here'开头,退出程序
    exit(0)
    elif msg.startswith('get-flag'): # 如果消息以'get-flag'开头,发送flag
    send_msg(flag)
    elif msg.startswith('get-md5'): # 如果消息以'get-md5'开头,发送消息的MD5哈希值
    send_msg(MD5.new(msg[7:]).digest())
    elif msg.startswith('get-time'): # 如果消息以'get-time'开头,发送当前时间
    send_msg(str(time.time()))
    elif msg.startswith('get-sha1'): # 如果消息以'get-sha1'开头,发送消息的SHA1哈希值
    send_msg(SHA.new(msg[8:]).digest())
    elif msg.startswith('get-sha256'): # 如果消息以'get-sha256'开头,发送消息的SHA256哈希值
    send_msg(SHA256.new(msg[10:]).digest())
    elif msg.startswith('get-hmac'): # 如果消息以'get-hmac'开头,发送消息的HMAC哈希值
    send_msg(HMAC.new(msg[8:]).digest())
    else: # 如果消息不符合以上任何一种格式,发送'command not found'
    send_msg('command not found')
    except:
    exit(0) # 如果在接收消息或处理消息过程中出现错误,退出程序

主要漏洞

我们已有的部分

  • 加密时IV是固定的且已知
  • Welcome!! 加密后的结果
  • 我们可以控制IV

首先,既然我们知道 Welcome!! 加密后的结果,还可以控制 recv_msg 中的 IV,那么根据解密过程

Pi = Dk(Ci ⊕ Ci-1)
C0 = IV

如果我们将 Welcome!! 加密后的结果输入给 recv_msg,那么直接解密后的结果便是 (Welcome!!+'\x07'*7) xor iv,如果我们恰当的控制解密过程中传递的 iv,那么我们就可以控制解密后的结果。也就是说我们可以执行上述所说的任意命令。从而,我们也就可以知道 flag 解密后的结果。

其次,在上面的基础之上,如果我们在任何密文 C 后面添加自定义的 IV 和 Welcome 加密后的结果,作为输入传递给 recv_msg,那么我们便可以控制解密之后的消息的最后一个字节,那么由于 unpad 操作,我们便可以控制解密后的消息的长度减小 0 到 255。

利用思路

  1. 绕过 proof of work
  2. 根据执行任意命令的方式获取加密后的 flag
  3. 由于 flag 的开头是 hitcon{,一共有 7 个字节,所以我们任然可以通过控制 iv 来使得解密后的前 7 个字节为指定字节。这使得我们可以对于解密后的消息执行 get-md5 命令。而根据 unpad 操作,我们可以控制解密后的消息恰好在消息的第几个字节处。所以我们可以开始时将控制解密后的消息为 hitcon{x,即只保留 hitcon{ 后的一个字节。这样便可以获得带一个字节哈希后的加密结果。类似地,我们也可以获得带制定个字节哈希后的加密结果。
  4. 这样的话,我们可以在本地逐字节爆破,计算对应 md5,然后再次利用任意命令执行的方式,控制解密后的明文为任意指定命令,如果控制不成功,那说明该字节不对,需要再次爆破;如果正确,那么就可以直接执行对应的命令。

解题代码

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
#coding=utf-8
from pwn import *
import base64, time, random, string
from Crypto.Cipher import AES
from Crypto.Hash import SHA256, MD5
#context.log_level = 'debug'
if args['REMOTE']:
p = remote('52.193.157.19', 9999)
else:
p = remote('127.0.0.1', 7777)


def strxor(str1, str2):
return ''.join([chr(ord(c1) ^ ord(c2)) for c1, c2 in zip(str1, str2)])


def pad(msg):
pad_length = 16 - len(msg) % 16
return msg + chr(pad_length) * pad_length


def unpad(msg):
return msg[:-ord(msg[-1])]


def flipplain(oldplain, newplain, iv): # 定义一个函数用于进行CBC字节翻转攻击
"""flip oldplain to new plain, return proper iv"""
return strxor(strxor(oldplain, newplain), iv)


def bypassproof(): # 定义一个函数用于绕过服务器的工作量证明
p.recvuntil('SHA256(XXXX+') # 读取服务器发来的信息,直到遇到'SHA256(XXXX+'为止
lastdata = p.recvuntil(')', drop=True) # 读取'SHA256(XXXX+'后面的字符,直到遇到')'为止,得到proof的后16个字符
p.recvuntil(' == ') # 继续读取服务器发来的信息,直到遇到' == '为止
digest = p.recvuntil('\nGive me XXXX:', drop=True) # 读取' == '后面的字符,直到遇到'\nGive me XXXX:'为止,得到SHA256哈希值

def proof(s): # 定义一个函数,用于检查给定的s是否满足条件
return SHA256.new(s + lastdata).hexdigest() == digest # 计算s加上lastdata的SHA256哈希值,然后检查计算出的哈希值是否等于digest

# 使用暴力破解的方法,尝试所有可能的s值,直到找到一个使得proof函数返回True的s值
data = pwnlib.util.iters.mbruteforce(
proof, string.ascii_letters + string.digits, 4, method='fixed')
p.sendline(data) # 将找到的s值发送给服务器
p.recvuntil('Done!\n') # 读取服务器的回应,确认工作量证明已经完成

iv_encrypt = '2jpmLoSsOlQrqyqE'

def getmd5enc(i, cipher_flag, cipher_welcome): # 定义一个函数用于获取flag的前i个字符的MD5哈希值的加密结果
"""return encrypt( md5( flag[7:7+i] ) )"""

# 通过修改iv的前7个字节,使得解密后的明文块的前7个字节变为'get-md5',而后面的字节不变
new_iv = flipplain("hitcon{".ljust(16, '\x00'), "get-md5".ljust(
16, '\x00'), iv_encrypt)
payload = new_iv + cipher_flag # 将修改后的iv和cipher_flag连接起来,作为新的payload

# 计算最后一个字节的值,使得解密后的明文块的最后一个字节为len(cipher_flag) + 16 + 16 - (7 + i + 1)
# 这样可以使得服务器在解密后的明文上调用md5函数,计算出flag的前i个字符的MD5哈希值
last_byte_iv = flipplain(
pad("Welcome!!"),
"a" * 15 + chr(len(cipher_flag) + 16 + 16 - (7 + i + 1)), iv_encrypt)
payload += last_byte_iv + cipher_welcome # 将计算出的最后一个字节和cipher_welcome添加到payload的末尾

p.sendline(base64.b64encode(payload)) # 将payload进行base64编码,然后发送给服务器
return p.recvuntil("\n", drop=True) # 读取服务器的回应,获取flag的前i个字符的MD5哈希值的加密结果

def main(): # 定义主函数
bypassproof() # 首先绕过服务器的工作量证明

# 获取加密后的"Welcome!!"的密文
cipher = p.recvuntil('\n', drop=True)
cipher_welcome = base64.b64decode(cipher)[16:]
log.info("cipher welcome is : " + cipher_welcome)

# 执行get-flag命令,获取加密后的flag的密文
get_flag_iv = flipplain(pad("Welcome!!"), pad("get-flag"), iv_encrypt)
payload = base64.b64encode(get_flag_iv + cipher_welcome)
p.sendline(payload)
cipher = p.recvuntil('\n', drop=True)
cipher_flag = base64.b64decode(cipher)[16:]
flaglen = len(cipher_flag)
log.info("cipher flag is : " + cipher_flag)

# 获取"command not found"的密文
p.sendline(base64.b64encode(iv_encrypt + cipher_welcome))
cipher_notfound = p.recvuntil('\n', drop=True)

flag = ""
# 对于flag的每一个字节,使用暴力破解的方法进行猜测
for i in range(flaglen - 7):
md5_indexi = getmd5enc(i, cipher_flag, cipher_welcome)
md5_indexi = base64.b64decode(md5_indexi)[16:]
log.info("get encrypt(md5(flag[7:7+i])): " + md5_indexi)
for guess in range(256):
# 计算猜测的字节的MD5哈希值
guess_md5 = MD5.new(flag + chr(guess)).digest()
# 通过修改前一个密文块,使得解密后的明文块的值为'get-time',然后发送给服务器
payload = flipplain(guess_md5, 'get-time'.ljust(16, '\x01'),
iv_encrypt)
payload += md5_indexi
p.sendline(base64.b64encode(payload))
res = p.recvuntil("\n", drop=True)
# 如果收到的回应是'command not found'的密文,说明猜测的字节是错误的
if res == cipher_notfound:
print 'Guess {} is wrong.'.format(guess)
# 如果收到的回应不是'command not found'的密文,说明猜测的字节是正确的
else:
print 'Found!'
flag += chr(guess)
print 'Flag so far:', flag
break

if __name__ == "__main__":
main()

#Flag so far: Paddin9_15_ve3y_h4rd__!!}\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10

2017 HITCON Secret Server Revenge

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
import os
import base64
import time
import sys
import random
import string
from Crypto.Cipher import AES
from Crypto.Hash import *

key = os.urandom(16)
iv = os.urandom(16)


def pad(msg):
pad_length = 16 - len(msg) % 16
return msg + chr(pad_length) * pad_length

def unpad(msg):
return msg[:-ord(msg[-1])]


def encrypt(iv, msg):
msg = pad(msg)
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(msg)
return encrypted

def decrypt(iv, msg):
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(msg)
decrypted = unpad(decrypted)
return decrypted

def send_msg(msg):
encrypted = encrypt(iv, msg)
msg = iv + encrypted
msg = base64.b64encode(msg)
print msg

def recv_msg():
msg = raw_input() # 从用户输入中获取消息
try:
msg = base64.b64decode(msg)
assert len(msg)<500 # 检查解码后的消息长度是否小于500
decrypted = decrypt(msg[:16], msg[16:])
return decrypted
except:
print 'Error'
exit(0)

def check_token(token):
print 'Give me the token!'
msg = raw_input() # 从用户输入中获取消息
msg = base64.b64decode(msg)
return msg == token

# 定义一个函数用来进行工作证明
def proof_of_work():
proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in xrange(20)]) # 生成一个20字符的随机字符串
digest = SHA256.new(proof).hexdigest() # 计算随机字符串的SHA256哈希
print "SHA256(XXXX+%s) == %s" % (proof[4:],digest)
x = raw_input('Give me XXXX:') # 从用户输入中获取响应
# 检查响应的长度是否为4,以及响应和随机字符串的后16个字符连接后的SHA256哈希是否等于原哈希
if len(x)!=4 or SHA256.new(x+proof[4:]).hexdigest() != digest:
exit(0)
return

def main():
proof_of_work()
token = os.urandom(56)
with open('flag.txt') as f:
flag = f.read().strip()
past = 0
send_msg('Welcome!!')
for i in xrange(340):
try:
cur = time.time()
# 如果当前时间和past之差小于1,那么等待一段时间,以确保每次循环至少需要1秒
if cur-past < 1: time.sleep(1-cur+past)
# 更新past为当前时间
past = cur
# 接收消息
msg = recv_msg()
# 如果消息以'exit-here'开头,那么退出程序
if msg.startswith('exit-here'):
exit(0)
# 如果消息以'get-md5'开头,那么计算消息的MD5哈希并发送回去
elif msg.startswith('get-md5'):
send_msg(MD5.new(msg[7:]).digest())
# 如果消息以'get-time'开头,那么发送当前时间
elif msg.startswith('get-time'):
send_msg(str(time.time()))
# 如果消息以'get-sha1'开头,那么计算消息的SHA1哈希并发送回去
elif msg.startswith('get-sha1'):
send_msg(SHA.new(msg[8:]).digest())
# 如果消息以'get-token'开头,那么发送token
elif msg.startswith('get-token'):
send_msg('token: ' + token)
# 如果消息以'check-token'开头,那么检查token,如果token匹配,打印flag
elif msg.startswith('check-token'):
if check_token(token):
print flag
# 无论token是否匹配,都退出程序
exit(0)
# 如果消息不符合上述任何一种格式,那么发送'command not found'
else:
send_msg('command not found')
# 如果在上述过程中出现任何错误,退出程序
except:
exit(0)

if __name__ == '__main__':
main()

题目分析

这个就是接着上面的来的,不过这次简单的修改了题目

  • 加密算法的 iv 未知,不过可以根据 Welcome!! 加密后的消息推算出来
  • 程序多了一个 56 字节的 token
  • 程序最多能进行 340 操作,因此上述的爆破自然不可行

程序的大概流程如下

  1. 经过 proof of work
  2. 发送 Welcome!! 加密后的消息
  3. 在 340 次操作中,需要猜中 token 的值,然后会自动将 flag 输出

主要漏洞

当然,在上个题目存在的漏洞在这里也有

  1. 任意执行给定命令
  2. 长度截断

利用思路

由于 340 的次数限制,虽然我们仍然可以获得 md5(token[:i]) 加密后的值(这里需要注意的是这部分加密后恰好是 32 个字节,前 16 个字节是 md5 后加密的值,后面的 16 个字节完全是填充的加密后的字节。这里 md5(token[:i]) 特指前 16 个字节)但是,我们不能再次为了获得一个字符去爆破 256 次了。

既然不能够爆破,那么我们有没有可能一次获取一个字节的大小呢?这里,我们再来梳理一下该程序可能可以泄漏的信息

  1. 某些消息的 md5 值加密后的值,这里我们可以获取 md5(token[:i]) 加密后的值
  2. unpad 每次会对解密后的消息进行 unpad,这个字节是根据解密后的消息的最后一个字节来决定的。如果我们可以计算出这个字节的大小,那么我们就可能可以知道一个字节的值

这里我们深入分析一下 unpad 的信息泄漏。如果我们将加密 IV 和 encrypt(md5(token[:i])) 放在某个密文 C 的后面,构成 C|IV|encrypt(md5(token[:i])) ,那么解密出来的消息的最后一个明文块就是 md5(token[:i]) 。进而,在 unpad 的时候就是利用 md5(token[:i]) 的最后一个字节( 0-255)进行 unpad ,之后对 unpad 后的字符串执行指定的命令(比如 md5)。那么,如果我们事先构造一些消息哈希后加密的样本,然后将上述执行后的结果与样本比较,如果相同,那么我们基本可以确定 md5(token[:i]) 的最后一个字节。然而,如果 md5(token[:i]) 的最后一个字节小于 16,那么在 unpad 时就会利用一些 md5 中的值,而这部分值,由于对于不同长度的 token[:i] 几乎都不会相同。所以可能需要特殊处理。

我们已经知道了这个问题的关键,即生成与 unpad 字节大小对应的加密结果样本,以便于查表

具体利用思路如下

  1. 绕过 proof of work
  2. 获取 token 加密后的结果 token_enc ,这里会在 token 前面添加 7 个字节 "token: " 。 因此加密后的长度为 64
  3. 依次获取 encrypt(md5(token[:i])) 的结果,一共是 57 个,包括最后一个 token 的 padding
  4. 构造与 unpad 大小对应的样本。这里我们构造密文 token_enc|padding|IV_indexi|welcome_enc 。由于 IV_indexi 是为了修改最后一个明文块的最后一个字节,所以该字节处于变化之中。我们若想获取一些固定字节的哈希值,这部分自然不能添加。因此这里产生样本时 unpad 的大小范围为 17 ~ 255。如果最后测试时 md5(token[:i]) 的最后一个字节小于 17 的话,基本就会出现一些未知的样本。很自然的一个想法是我们直接获取 255-17+1 个这么多个样本,然而,如果这样做的话,根据上面 340 的次数(255-17+1+57+56>340)限制,我们显然不能获取到 token 的所有字节。所以这里我们需要想办法复用一些内容,这里我们选择复用 encrypt(md5(token[:i])) 的结果。那么我们在补充 padding 时需要确保一方面次数够用,另一方面可以复用之前的结果。这里我们设置 unpad 的循环为 17 到 208,并使得 unpad 大于 208 时恰好 unpad 到我们可以复用的地方。这里需要注意的是,当 md5(token[:i]) 的最后一个字节为 0 时,会将所有解密后的明文 unpad 掉,因此会出现 command not found 的密文。
  5. 再次构造密文 token_enc|padding|IV|encrypt(md5(token[:i])) ,那么,解密时即使用 md5(token[:i]) 的最后一个字节进行 unpad 。如果这个字节不小于 17 或者为 0,则可以处理。如果这个字节小于 17,那么显然,最后返回给用户的 md5 的结果并不在样本范围内,那么我们修改其最后一个字节的最高比特位,使其 unpad 后可以落在样本范围内。这样,我们就可以猜出 md5(token[:i]) 的最后一个字节。
  6. 在猜出 md5(token[:i]) 的最后一个字节后,我们可以在本地暴力破解 256 次,找出所有哈希值末尾为 md5(token[:i]) 的最后一个字节的字符。
  7. 但是,在第六步中,对于一个 md5(token[:i]) 可能会找出多个备选字符,因为我们只需要使得其末尾字节是给定字节即可。
  8. 那么,问题来了,如何删除一些多余的备选字符串呢?这里我就选择了一个小 trick,即在逐字节枚举时,同时枚举出 token 的 padding。由于 padding 是 0x01 是固定的,所以我们只需要过滤出所有结尾不是 0x01 的 token 即可。

解题代码

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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
from pwn import *
import base64, time, random, string
from Crypto.Cipher import AES
from Crypto.Hash import SHA256, MD5
#context.log_level = 'debug'

p = remote('127.0.0.1', 7777)


def strxor(str1, str2):
return ''.join([chr(ord(c1) ^ ord(c2)) for c1, c2 in zip(str1, str2)])


def pad(msg):
pad_length = 16 - len(msg) % 16
return msg + chr(pad_length) * pad_length


def unpad(msg):
return msg[:-ord(msg[-1])] # remove pad


def flipplain(oldplain, newplain, iv):
"""flip oldplain to new plain, return proper iv"""
return strxor(strxor(oldplain, newplain), iv)


def bypassproof():
p.recvuntil('SHA256(XXXX+')
lastdata = p.recvuntil(')', drop=True)
p.recvuntil(' == ')
digest = p.recvuntil('\nGive me XXXX:', drop=True)

def proof(s):
return SHA256.new(s + lastdata).hexdigest() == digest

data = pwnlib.util.iters.mbruteforce(
proof, string.ascii_letters + string.digits, 4, method='fixed')
p.sendline(data)


def sendmsg(iv, cipher):
payload = iv + cipher
payload = base64.b64encode(payload)
p.sendline(payload)


def recvmsg():
data = p.recvuntil("\n", drop=True)
data = base64.b64decode(data)
return data[:16], data[16:]


def getmd5enc(i, cipher_token, cipher_welcome, iv):
"""return encrypt( md5( token[:i+1] ) )"""
## keep iv[7:] do not change, so decrypt msg[7:] won't change
get_md5_iv = flipplain("token: ".ljust(16, '\x00'), "get-md5".ljust(
16, '\x00'), iv)
payload = cipher_token
## calculate the proper last byte number
last_byte_iv = flipplain(
pad("Welcome!!"),
"a" * 15 + chr(len(cipher_token) + 16 + 16 - (7 + i + 1)), iv)
payload += last_byte_iv + cipher_welcome
sendmsg(get_md5_iv, payload)
return recvmsg()


def get_md5_token_indexi(iv_encrypt, cipher_welcome, cipher_token):
md5_token_idxi = []
for i in range(len(cipher_token) - 7):
log.info("idx i: {}".format(i))
_, md5_indexi = getmd5enc(i, cipher_token, cipher_welcome, iv_encrypt)
assert (len(md5_indexi) == 32)
# remove the last 16 byte for padding
md5_token_idxi.append(md5_indexi[:16])
return md5_token_idxi


def doin(unpadcipher, md5map, candidates, flag):
if unpadcipher in md5map:
lastbyte = md5map[unpadcipher]
else:
lastbyte = 0
if flag == 0:
lastbyte ^= 0x80
newcandidates = []
for x in candidates:
for c in range(256):
if MD5.new(x + chr(c)).digest()[-1] == chr(lastbyte):
newcandidates.append(x + chr(c))
candidates = newcandidates
print candidates
return candidates


def main():
bypassproof()

# result of encrypted Welcome!!
iv_encrypt, cipher_welcome = recvmsg()
log.info("cipher welcome is : " + cipher_welcome)

# execute get-token
get_token_iv = flipplain(pad("Welcome!!"), pad("get-token"), iv_encrypt)
sendmsg(get_token_iv, cipher_welcome)
_, cipher_token = recvmsg()
token_len = len(cipher_token)
log.info("cipher token is : " + cipher_token)

# get command not found cipher
sendmsg(iv_encrypt, cipher_welcome)
_, cipher_notfound = recvmsg()

# get encrypted(token[:i+1]),57 times
md5_token_idx_list = get_md5_token_indexi(iv_encrypt, cipher_welcome,
cipher_token)
# get md5map for each unpadsize, 209-17 times
# when upadsize>208, it will unpad ciphertoken
# then we can reuse
md5map = dict()
for unpadsize in range(17, 209):
log.info("get unpad size {} cipher".format(unpadsize))
get_md5_iv = flipplain("token: ".ljust(16, '\x00'), "get-md5".ljust(
16, '\x00'), iv_encrypt)
## padding 16*11 bytes
padding = 16 * 11 * "a"
## calculate the proper last byte number, only change the last byte
## set last_byte_iv = iv_encrypted[:15] | proper byte
last_byte_iv = flipplain(
pad("Welcome!!"),
pad("Welcome!!")[:15] + chr(unpadsize), iv_encrypt)
cipher = cipher_token + padding + last_byte_iv + cipher_welcome
sendmsg(get_md5_iv, cipher)
_, unpadcipher = recvmsg()
md5map[unpadcipher] = unpadsize

# reuse encrypted(token[:i+1])
for i in range(209, 256):
target = md5_token_idx_list[56 - (i - 209)]
md5map[target] = i

candidates = [""]
# get the byte token[i], only 56 byte
for i in range(token_len - 7):
log.info("get token[{}]".format(i))
get_md5_iv = flipplain("token: ".ljust(16, '\x00'), "get-md5".ljust(
16, '\x00'), iv_encrypt)
## padding 16*11 bytes
padding = 16 * 11 * "a"
cipher = cipher_token + padding + iv_encrypt + md5_token_idx_list[i]
sendmsg(get_md5_iv, cipher)
_, unpadcipher = recvmsg()
# already in or md5[token[:i]][-1]='\x00'
if unpadcipher in md5map or unpadcipher == cipher_notfound:
candidates = doin(unpadcipher, md5map, candidates, 1)
else:
log.info("unpad size 1-16")
# flip most significant bit of last byte to move it in a good range
cipher = cipher[:-17] + strxor(cipher[-17], '\x80') + cipher[-16:]
sendmsg(get_md5_iv, cipher)
_, unpadcipher = recvmsg()
if unpadcipher in md5map or unpadcipher == cipher_notfound:
candidates = doin(unpadcipher, md5map, candidates, 0)
else:
log.info('oh my god,,,, it must be in...')
exit()
print len(candidates)
# padding 0x01
candidates = filter(lambda x: x[-1] == chr(0x01), candidates)
# only 56 bytes
candidates = [x[:-1] for x in candidates]
print len(candidates)
assert (len(candidates[0]) == 56)

# check-token
check_token_iv = flipplain(
pad("Welcome!!"), pad("check-token"), iv_encrypt)
sendmsg(check_token_iv, cipher_welcome)
p.recvuntil("Give me the token!\n")
p.sendline(base64.b64encode(candidates[0]))
print p.recv()

p.interactive()


if __name__ == "__main__":
main()

参考:CBC字节翻转攻击&Padding Oracle Attack原理解析
          一文搞明白 Padding Oracle Attack
          CTF Wiki
          skateXu的博客


AES Padding Oracle Attack
http://example.com/2024/03/02/AES Padding Oracle Attack/
作者
John Doe
发布于
2024年3月2日
更新于
2024年9月8日
许可协议