0%

CBC&Padding Oracle Attack

记录一下,证明我学过。

CBC 简介

CBC 密码分组链接模式(cipher block chaining Triple)。需要key,IV初始向量 (Initialization Vector) 加解密,(key、IV、c为字节类型)IV is as long as the block size ,IV一般会直接给出,大小和 block 等大,block 即加密时会将明文分成等长的模块(block) ,常见的分为8字节、16字节、32字节,所以很多时候不会恰好等分,此时需要填充。


两种常见的填充算法

两种常见的填充算法,分别是Pkcs5PaddingPkcs7Padding

Pkcs5Padding中,明确定义 Block 的大小为 8字节,一组中差几个字节就填充几,如差一字节为8字节便填充一字节:0x01 (十六进制表示)

1
2
待加密数据原长度为7字节:0x410x410x410x410x410x410x41
填充后:0x410x410x410x410x410x410x410x01

Pkcs7Padding定义中,对于块的大小是不确定的,可以在1-255之间(块长度超出255的尚待研究),填充值的算法都是一样的

1
pad = k - (l mod k)  # k=块大小,l=数据长度,如果k=8, l=9,则需要填充额外的7个byte的7
注意:

当待填充的序列恰好为8个字节时,仍需要在尾部填充8个字节的0x08,这可以让解密的数据准确无误地移除多余字节。


CBC加密过程中,第一组明文先与 IV异或,再进行cbc加密模式得到第一组密文;

1
2
3
aes = AES.new(KEY, AES.MODE_CBC, iv=IV)
plain_text = pad(plain_text.encode(), AES.block_size)
cipher = aes.encrypt(plain_text)

从第二组开始,所有的明文分组都是和前一分组加密后的密文进行异或。

CBC加密过程



CBC解密过程,第一组密文先进行CBC模式解密,再与 IV异或得到第一组明文,

1
2
aes = AES.new(KEY, AES.MODE_CBC, iv=iv)
flag = aes.decrypt(c)

第二组密文解密之后和第一组密文异或得到第二组明文。也就是说,解密一组明文需要本组和前一组的密文。

CBC解密过程

Padding Oracle Attack

概述

填充提示攻击(一说填充预言攻击)( Padding Oracle Attack ),Padding的含义是“填充”,在解密时,如果算法发现解密后得到的结果,它的填充方式不符合规则,那么表示输入数据有问题,对于解密的类库来说,往往便会抛出一个异常,提示Padding不正确。

Padding Oracle Attack针对的同样是CBC模式下的分组密码,如果满足攻击条件,那么利用Padding Oracle能够在不知道密钥的情况下,解密任意密文,或者构造出任意明文的合法密文

利用条件

  • 攻击者能够获得密文,以及密文对应的初始化向量iv
  • 攻击者能够触发密文的解密过程,并且能够知道密文的解密结果是否正确

Padding Oracle流程

我们首先来考虑一个具体的场景:

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

有以下三种异常方式:

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

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

1
2
BRIAN;12;1;0x050x050x050x050x05
# 明文11个字节,可以8字节一个,分成两组,差5个字节,填充5个5 (0x5)

以下用表格形式表示CBC加密过程:

CBC加密表格

可以看到初始向量IV0x7B 0x21 0x6A 0x63 0x49 0x51 0x17 0x0F,这时服务器发送的Cookie应该为7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6,初始向量iv被填充在加密密文之前。(上面没有详细讲iv和密文c的给出形式,代码形式如下)

1
2
3
4
5
cipher = aes.encrypt(plain_text)
return IV.hex() + cipher.hex()

iv = c[:32]
cipher = c[32:]

CBC解密表格如下:

CBC解密表格

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

Padding Oracle 解密漏洞利用

也即:(先判断密文最后一组的填充值是否正确)

  1. 密文可以正常解密并且解密结果比对正确
  2. 密文不能正常解密
  3. 密文可以正常解密但解密结果不对

这就给了我们可乘之机 —— 我们可以利用服务器的返回值判断我们提交的内容能不能正常解密,进一步讲,我们可以知道最后一组密文的填充位符不符合填充标准。

dd27f3ff24c97b7eb098864e2954142

如上图所示,明文填充了四位时,如果最后一组密文解密后的结果(Intermediary Value也就是中间值)与前一组密文(Initialization Vector也就是IV值)异或得到的最后四位是0x04,那么服务器就会返回可以正常解密。

(填充1字节,填充值为0x01;填充2字节,填充值为0x020x02,以此类推。据此判断)

所以 每组密文解密后的中间值与前一组的密文异或便可得到那一组的明文

现在我们知道所有的密文和IV,所以我们的问题就转换成了去得到中间值

如何得到中间值?

我们可以依次爆破IV(0-255 即 0x01-0xFF)的每一位值来通过交互端判断中间值是否正确(判断依据是后面是否有填充值,填充值是否符合填充规则)

最后一位iv值

如上,爆破最后一位IV值,因为只差一位,所以填充值为0x01,此时交互端判断其正

确,中间值就为 $0x01\oplus0x3C=0x3D$ , 接着爆破第二位

最后二位iv值

最后两位填充值为0x020x02,交互端判其正确, $0x02\oplus0x3F=0x3D$ ,

还有 $0x02\oplus0x24=0x26$ ,中间值就为 0x260x3D ,如此往复,得到一组中间值乃至所有中间值

此题就可解了。如未理解可以观察这五个测试

CBC字节翻转攻击&Padding Oracle Attack原理解析 - 枫のBlog



例题:

POA

附件

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
# -*- 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()

exp:

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

def proof(broke, Hash): # 爆破 sha256
assert len(broke) == 16 and len(Hash) == 64
shaTable = string.ascii_letters + string.digits
for ii in itertools.permutations(shaTable, 4):
x = ''.join(ii)
s = x + broke
if sha256(s.encode()).hexdigest() == Hash:
print(x)
return x

def Solve_Oracle():
# context.log_level = 'debug' # 调试
io = remote('124.71.177.14', 10010)
#prv
io.recvuntil(b"XXXX + ")
broke = io.recv(16).decode()
io.recvuntil(b'):')
Hash = io.recv(64).decode()

prefix = proof(broke,Hash)
io.recvuntil(b'XXXX:')
io.sendline(prefix.encode())

io.sendlineafter(b'2. decrypt the flag',b'1')
io.recvuntil(b'This is your flag: ')
c = io.recv(64).decode()
print(c)

guess_iv = [0 for _ in range(16)] # iv 初始列表
restore_midd = [0 for _ in range(16)] # 中间值初始列表
index = 1 # 填充值

for i in range(15, -1, -1): # iv长度为16字节,先爆破最后一位,所以倒序
for j in range(0, 256): # iv每一字节的所有可能性
io.sendline(b'2')
io.recvuntil(b'Please enter ciphertext:')
io.recv(1).decode() # 空格 随便接收一下
guess_iv[i] = j
mess = bytes(guess_iv).hex() + c[32:]
io.sendline(mess.encode())
result = io.recv(5).strip().decode() # 接收交互端的判断结果

if result == 'True':
print('find')
restore_midd[i] = index ^ j # 依据判断结果得到中间值
for k in range(15, i - 1, -1):
guess_iv[k] = restore_midd[k] ^ (index + 1) # 替换到iv列表里
break
index += 1

m = bytes_to_long(bytes(restore_midd)) ^ int(c[:32], 16) # iv是16字节,所以只有一组,直接异或iv即得flag
print(long_to_bytes(m))

if __name__ == '__main__':
Solve_Oracle()

# QU{0P@d4Ttk} 改为 D0g3{0P@d4Ttk}

翻出了我收藏夹里吃灰的博文:

CBC字节翻转攻击&Padding Oracle Attack原理解析 - 枫のBlog

-------------    本文结束  感谢阅读    -------------