从零学习Padding Oracle攻击和CBC字节翻转攻击

CBC模式(密码分组链接)是四种分组加密模式中的一种


CBC加解密过程

Plaintext是明文块,IV是初始向量,Key是密钥,Ciphertext是密文块
注意:在链接模式中,初始化IV的长度要和对称加密算法的分组长度一致。原因是链接模式中的异或操作是等长操作。

加密

给的是每块8字节,16字节同理

其中的Encryption,是特定加密算法。
初始化向量IV与明文(第一组明文)XOR(异或)后,再经过运算得到的结果作为新的IV,用于下一分组(分组2),迭代下去。

解密

解密过程是加密过程的逆过程:

其中的Decryption,是特定解密算法。

Padding Oracle

Padding Oracle攻击就是跳过了上图中的Encryption/Decryption。

分组密码中的padding(填充)

常用的对称加密算法,如DES和AES,在用密钥加密数据时,只能加密和密钥长度相同的数据。对于超长数据,我们需要将其切分成块。这就带来一个问题,可能最后一个块,无法和密钥“对齐”(当然,这也包括原始数据本来就比密钥短的情况)。这就需要一些数据去填充最后的几位。常用的填充算法即PKCS#5,在数据填充中,使用缺失的位数长度来统一填充

上图每块是8字节的,缺5个,就用0×05填充;缺2个,就用0×02填充;如果刚刚好,还要扩展出一个块,全用0×08填充。
16字节的块同理,如缺9字节补9个0x9。

关注点

其实加密解密过程中前几个分组的解密结果对我们都没有意义,我们重点关注的是最后一个分组的解密结果

注意到最后一个分组的末尾的数值为0×04,即表示填充了4个Padding。如果最后的Padding不正确(值和数量不一致),则解密程序往往会抛出异常(Padding Error)。而利用应用的错误回显,我们就可以判断出Paddig是否正确。
在Padding Oracle Attack攻击中,攻击者输入的参数是IV+Cipher,我们要通过对IV的”穷举”来请求服务器端对我们指定的Cipher进行解密,并对返回的结果进行判断。

攻击前提条件

攻击者能够获得密文(Ciphertext),以及附带在密文前面的IV(初始化向量)
攻击者能够触发密文的解密过程,且能够知道密文的解密结果
如果解密过程没有问题,明文验证(如用户名密码验证)也通过,则会返回正常 HTTP 200
如果解密过程没有问题,但是明文验证出错(如用户名密码验证),则还是会返回 HTTP 200,只是内容上是提示用户用户名密码错误
如果解密过程出问题了,比如Padding规则核对不上,则会爆出 HTTP 500错误。

攻击注意点

先给篇文章
比如从最后一个字节开始爆破
爆破出iv最后一个字节为0x3c
爆破倒数第二个字节时,先要将 0x01 xor 0x3c 得到0x3d(临时中间值的最后一个字节)
因此可以预测当初始化向量最后一个字节是0x3f的时候,padding的最后一个字节会是0x02。
0x02 xor 0x3d = 0x3f
现在将初始化向量的最后一个字节设置为0x3f

例子

然后上自己魔改pwnhub中的一个测试代码及exp
php代码

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
<?php
define("SECRET_KEY", "*******************");
define("METHOD", "aes-128-cbc");
function get_random_token()
{
$random_token='';
for($i=0;$i<16;$i++)
{
$random_token.=chr(rand(1,255));
}
return $random_token;
}
function en($id)
{
$token = get_random_token();
$c = openssl_encrypt((string)$id, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token);
$retid = base64_encode(base64_encode($token.'|'.$c));
return $retid;
}
function de($id)
{
if($c = base64_decode(base64_decode($id)))
{
if($iv = substr($c,0,16))
{
if($pass = substr($c,17))
{
if($u = openssl_decrypt($pass, METHOD, SECRET_KEY, OPENSSL_RAW_DATA,$iv))
{
return "Successssssssssssssss!!!!!!!!!!!~~~~~~~~~~~ </br>".$u;
}
else
die("error");
}
else
return 123;
}
else
return 12;
}
else
return 1;
}
// echo en(1)."\n</br>\n";
echo de($_GET['test']);
?>

先得到1加密后的值TldLYzRSMmZmZlArU0RkcmpMOGlRM3gxWkhIRXM5dlZiMk82dnpUb0tmaEs=
test=TldLYzRSMmZmZlArU0RkcmpMOGlRM3gxWkhIRXM5dlZiMk82dnpUb0tmaEs= 可以正常访问得到1
分解得到
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
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import requests
import base64
ll = "0123456789abcdef"
s = requests.session()
iv = ""
url = "http://10.211.55.4/pwnhub/cbc.php?test="
base = "TldLYzRSMmZmZlArU0RkcmpMOGlRM3gxWkhIRXM5dlZiMk82dnpUb0tmaEs="
base_dec = base64.b64decode(base64.b64decode(base))
base_list = list(base_dec)
# print len(base_list)
en_hex = ""
for i in base_list:
en_hex = en_hex + hex(ord(i))[2:]
#en_hex = "35629ce11d9f7df3fe48376b8cbf22437c756471c4b3dbd56f63babf34e829f84a"
def hexstr_to_hex(str):
cc = ""
for k in range(0,len(str)):
if k % 2 == 0:
cc = cc + chr(int(str[k:k+2],16))
return cc
print len(hexstr_to_hex("629ce11d9f7df3fe48376b8cbf2243"))
def test():
index = "index:"
mid = ""
test = "000000000000000000000000000000007c756471c4b3dbd56f63babf34e829f84a"
for j in range(0,31):
if j % 2 == 0:
head = test[:-j+30]
tail = test[32:]
for i in range(0,256):
if j == 30:
mid = "629ce11d9f7df3fe48376b8cbf2243"
for m in range(0,256):
full = hex(m)[2:].zfill(2) + mid + tail
print "full:"+full
base_en = base64.b64encode(base64.b64encode(hexstr_to_hex(full)))
r = s.get(url + base_en)
if "1" in r.text:
print r.text,hex(m)
iv = hex(m)[2:].zfill(2) + mid
# print iv
return iv
# continue
full = head + (hex(i)[2:]).zfill(2) + mid + tail
print full ,i,j
base_en = base64.b64encode(base64.b64encode(hexstr_to_hex(full)))
r = s.get(url + base_en)
if "sss" in r.text:
print base_en,hex(i)
cccc = ""
cc = ""
mid_ahead = mid
for l in range(0,len(mid)):
if l % 2 == 0:
cccc = (int(mid[l:l+2],16))
cc = cc + hex(cccc ^ j/2+1 ^ j/2+2)[2:]
mid = hex(i ^ j/2+1 ^ j/2+2)[2:] + cc
print "mid:" + mid
break
# print test()
# iv = 0x35629ce11d9f7df3fe48376b8cbf2243
print hex(0x35629ce11d9f7df3fe48376b8cbf2243 ^ 0x310f0f0f0f0f0f0f0f0f0f0f0f0f0f0f)
enc = 0x046d93ee129072fcf147386483b02d4c
t = hex(enc ^ 0x330f0f0f0f0f0f0f0f0f0f0f0f0f0f0f)
print str(t)[2:-1].zfill(32)
tt = str(t)[2:-1].zfill(32)
ttt = tt + "7c756471c4b3dbd56f63babf34e829f84a"
print base64.b64encode(base64.b64encode(hexstr_to_hex(ttt)))

当执行到最后一字节时,取前一个mid值,先xor出中间值,在指定初始化向量,便可任意伪造
当要构造多个块时
例如构造第二个块,将第二块密文设置为id为1的密文
知道id为1的中间值,通过异或得到第一组密文
再通过padding oracle获得第一块中间值即可

结论

Padding Oracle攻击并没有破解掉加密算法的密钥,也没有能力对任意密文做逆向解密,只是可以利用一个有效密文,生成一个解密后得到任意指定内容明文的伪造密文。

字节翻转攻击

此攻击方法的精髓在于:通过损坏密文字节来改变明文字节

加密伪公式

Plaintext-0 = Decrypt(Ciphertext) XOR IV 只用于第一个组块
Plaintext-N= Decrypt(Ciphertext) XOR Ciphertext-N-1 用于第二及剩下的组块

关注点

Ciphertext-N-1(密文-N-1)是用来产生下一块明文;这就是字节翻转攻击开始发挥作用的地方。如果我们改变Ciphertext-N-1(密文-N-1)的一个字节,然后与下一个解密后的组块异或,我们就可以得到一个不同的明文了

例子

如最近的pwnhub,其实也可以通过cbc字节翻转来控制id
只有一个块,所需要的就是iv,如果有多个块,如目标字符位于块2,这意味着我们需要改变块1的密文来改变第二块的明文
将1改为2的脚本如下(已知iv和加密后1的值分别为iv和enc)

1
2
3
4
5
6
iv = "%]E\xe10\xe8\xb7\x8eR\xcb5\x94\x1f\xdd\xbb\x8a"
enc = "\xa0&\xea\xb8\\'\x0e\x06C\xd1@l\r\xdcI~"
iv = list(iv)
iv[0] = chr(ord(iv[0]) ^ ord('1') ^ ord('2'))
iv = ''.join(iv)
print (iv + '|' + enc).encode("base64").encode("base64")

Contents
  1. 1. CBC加解密过程
    1. 1.1. 加密
    2. 1.2. 解密
  2. 2. Padding Oracle
    1. 2.1. 分组密码中的padding(填充)
    2. 2.2. 关注点
    3. 2.3. 攻击前提条件
    4. 2.4. 攻击注意点
    5. 2.5. 例子
    6. 2.6. 结论
  3. 3. 字节翻转攻击
    1. 3.1. 加密伪公式
    2. 3.2. 关注点
    3. 3.3. 例子