细致分析Padding Oracle渗透测试全解析

2017-10-18 411100人围观 ,发现 1 个不明物体 数据安全

*本文原创作者:Sherkey,本文属FreeBuf原创奖励计划,未经许可禁止转载

最近在研究Padding Oracle渗透测试,发现网上没有详细讲原理的文章。因此自己整理了这样一篇,希望哪怕是没有密码学基础的朋友也能看完后完全理解攻击的原理。

一、基础知识介绍

本节针对无密码学基础的读者,若均理解可直接跳过。

1.1 什么是分组密码?

以下定义来自维基百科:

密码学中,分组加密(英语:Block cipher),又称分块加密块密码,是一种对称密钥算法。它将明文分成多个等长的模块(block),使用确定的算法和 对称密钥对每组分别加密解密。

简单来说,就是将明文进行分组,每组分别加密,最后再连在一起形成密文。AES, DES等加密方法均属于分组加密。

1.2 什么是PKCS#5?

正如1.1中所说,分组密码中,需要对明文进行分组。

但是分组要求每个块的大小都要相同。

那么问题就来了,如果说我的明文是‘testabc’,而我用的分组密码是五个一组。这样第一组内容是‘testa’,那么第二组只剩下了‘bc’,不够五个,应该怎么办呢?

这是就要用到一种填充方式,用来把最后空出来的几位填满。

而PKCS#5,就是一种由RSA信息安全公司设计的填充标准。

对于PKCS#5标准来说,一般缺少几位,就填充几位那个数字。

比方说,在上面的例子里,我们有三位空缺,那么就要在空缺处都填上3 。这样,第二组的内容就变成了‘bc333’ 。

这里需要注意的是,比如说,如果每个分组是8字节,我的明文是‘testabcd’,这样恰好是8字节了。但是按照PKCS#5的标准,我们仍然需要在后面添加一个块,块里的内容全部填充为8 (因为一个块大小为8字节,而第二个块全部为空,因此有8位需要填充)。

1.3 CBC模式

在密码学中,分组密码有许多的工作模式。

可能有人会问,我们直接分组以后加密不就好了吗,为什么需要设计模式呢?

没错,其实分组后直接加密也是一种设计模式,名为电子密码本(Electronic codebook,ECB)模式。

如图所示,其实就是每个分组的明文均利用相同的密钥进行加密。

但是这种模式明显有个缺点,那就是所有的密钥都相同,导致相同的明文,一定会被加密成相同的密文。

这样很显然是一种不安全的模式。因为它不能提供严格的数据保密性。可以联想一下最简单的凯撒密码,相同的明文加密后得到相同的密文,导致这种方式难以阻挡频率攻击。

那么为了让我们加密后的信息更加难以破解,在1976年,IBM发明了密码分组链接(CBC,Cipher-block chaining)模式。

如下图所示。

我们可以看到,这里加入了一个初始向量(IV, Initialization Vector)。在第一块明文进行加密之前,需要先与初始向量进行异或。而产生的密文,将与下一组明文进行异或。在这种方法中,每个密文块都依赖于它前面的所有明文块。

这种模式使原本独立的分组密码加密过程形成迭代,使每次加密的结果影响到下一次加密。这样无疑比ECB模式要安全了许多。

上图分别是CBC模式下加密和解密的过程。我们可以看到在中间写着Block Cipher Encryption 的方块那里即是加密算法,本文中就用AES来举例。

而Initiallzation Vector(IV)即是初始向量,也是本攻击方式所重点利用的地方。

1.4 异或

在密码学里,异或是一种很重要的运算方式。

具体表现在,一个数字连着异或两次另一个数字后,得到的值还是它本身。

比方说,65 ^ 66 = 3

3 ^ 66 = 65

在下面我们会利用到这个重要的运算。

二、破解过程详解

文章开头说了,这是一种针对CBC模式的攻击。与你具体选择的是哪种加密方式是没有关系的。

也就是说,在上面CBC加密模式的图里,不管你在‘Block Cipher Decryption’那个框里选的是AES加密,还是DES加密,都无所谓,只要服务器配置不正确,我们都有机会破解。

那么这是如何做到的呢?在说明攻击方式之前,我们先来了解一下正常的解密方式是如何的。

2.1 正常解密过程

我们先来分析一下正常状态下,服务器的解密过程。

首先,CBC模式下AES的解密需要知道IV值与密钥。这个应该很好理解。‘Block Cipher Decryption’肯定需要知道密钥来解密,而CBC模式需要IV值来解密。

而服务器端仅仅保留了密钥,没有保留IV值。因为默认情况下加密后的文件会把IV值附在文件开头,因此服务器会直接使用传给它的文件开头8或16位(视加密方式而定),作为IV值。

在此我们停一下,先举个例子,详细解释一下CBC模式下AES加密过的密文如何正常解密得到明文。

比方说,我们有密文’9F0B13944841A832B2421B9EAF6D9836813EC9D944A5C8347A7CA69AA34D8DC0DF70E343C4000A2AE35874CE75E64C31′ 。这段密文用CBC模式下AES加密,并且已知key和IV值。

接下来我们进行解密。

首先,AES是一种分组密码,每组由16字节组成。密文是用十六进制表示的,也就是相邻两位数字合在一起组成一个十六进制数,用来表示一字节。密文长度为96,按照32一组进行分组,则恰好可以分为三组。我们把这三组密文存到一个数组C里,以后用C[0], C[1], C[2]来表示这三组密文。

然后我们看一下解密用的图。

如上图所示,我已经在图中标出了C[0], C[1], C[2]的位置。我们正常解密时,先用C[0]与Key解密AES,然后得到一个中间值,这个中间值与IV值进行异或,得到第一段明文。而C[0]作为新的IV值,与C[1]在AES解密后的中间值进行异或,从而得到第二段明文。第三段以此类推。这就是一个正常解密的过程。

下图中把“中间值”这个概念用红圈表示了出来。千万千万要好好看懂解密过程以及“中间值”究竟代表什么,这是本攻击的关键。

而Padding Oracle 攻击的重点,就在于上一段提到的中间值。

我们继续聊服务器端正常情况下如何解密。其实和我们上面分析的差不多。区别在于,我们解密的时候是从左往右,也就是先从C[0]开始解密;而服务器是从右往左,也就是先解密C[2]。

那么问题又来了:服务器如何知道自己解密后得到的结果是否正确呢?

因为服务器无法判断解密后明文是否有具体含义,因此它也就不从含义上去判断。它判断的方式很简单粗暴,就是利用Padding值来进行判断。

在1.2处我们了解了PKCS#5标准下的填充方式。我们已经知道,明文加密之前肯定会在末尾附上一段Padding值用来填充。

那么接下来,服务器从C[2]进行解密。比如说,一开始明文最后Padding值为0×05,也就是最后五位应该均为5 。而服务器解密后,发现也确实如此,那么它就认为这次解密时正确的。而如果服务器发现解密后Padding值为0×05 ,但是只有最后四位的值为0×05 ;或者说它解密后得到的Padding值为0×04 ,但是最后五位都是0×04 ,那么就造成了Padding的值与数目不相符,服务器就会认为解密失败,从而报错。

我们可以从其他参考文章的描述中看到:

  1. 如果解密过程没有问题,明文验证(如用户名密码验证)也通过,则会返回正常 HTTP 200

  2. 如果解密过程没有问题,但是明文验证出错(如用户名密码验证),则还是会返回 HTTP 200,只是内容上是提示用户用户名密码错误

  3. 如果解密过程出问题了,比如Padding规则核对不上,则会爆出 HTTP 500错误。

其实就是我们刚才说的判断方式的更具体应用。

而知道服务器的判断方式以后,我们就可以在这里做文章了。

2.2 攻击过程

本次攻击的关键,集中在我用红圈圈起来的地方。

也就是C[0]. C[1]. C[2]分别用key对AES进行解密,但是还没有与IV值异或的那个值。本文把那个值称为中间值。

上图是正常解密的结果。而本次攻击的关键,就在被绿色标注的那一行。

本文把绿色标注那一行的值称为中间值。

根据上面的CBC模式解密的图,我们可以知道,解密会先使用AES解密得到一个中间值,然后在通过中间值与IV值的异或,最后得到明文。

而padding oracle攻击的本质,其实就是通过传递密文以及自己猜测的IV值,通过观察服务器对padding判断的结果,进而反推出中间值,最后用中间值与正确的IV异或得到明文。

也就是这个攻击直接跳过了AES,而针对CBC进行攻击。

需要注意的是,自己传递的IV值与正确的IV值不能混淆。鉴于图中自己传递的IV值为黄色,因此下称自己传递的IV值为黄IV,正确的IV值为原IV。

我们再来想想服务器是怎么解密的:从右往左。

再看看CBC模式解密的那张图,从右往左,就是说先解密C[2]。而解密时,就会用C[1]当做IV值,来与C[2]解密后得到的中间值进行异或,进而得到明文。

而我们首先需要自己构造C[1]中的值,也即在上面图片中的黄IV值,构造以后与C[2]解密得到的中间值进行异或。

并且根据上面服务器判断解密过程是否正确的条件来看,只要最后padding值与个数相对应即可。

因此,我们可以一位一位的构造,第一次先通过改变黄IV值,产生padding结果为1的情况,即可倒推出中间值。

那么我们开始进行破解。

2.3 第一次循环

还记得我们前面提到的服务器判断方式吗?就是只需要padding的值与个数相符即可。

而在第一次循环里,我们只需要构造C[1],使得与C[2]产生的中间值异或以后,得到的最后一位为0×01即可。

C[1]是一个十六位的数组(相邻两位算作一个十六进制数)。我们可以使它前15位均为随机数。因为如果是随机数的话,与中间值异或后的结果也是一个随机数,不太可能恰好是0×01,因此就避免了你最后一位得到0×01,而前一位恰好也是0×01导致个数不符,服务器判断为错的尴尬。C[1]最后一位从00到ff进行尝试。根据上面所说,会有一个值恰好与C[2]解密后的中间值异或后会得到0×01这个结果。这时服务器返回显示正确。

返回正确时,意味着如下公式成立:

C1 ^ 中间值的最后一位 = 0×01

那么按照异或运算的性质,我们不难得到:

中间值的最后一位 = C1 ^ 0×01

这样我们就成功得到了中间值的最后一位。

2.4 第二次至第N次循环

Padding值为1的情况结束后,再用同样方法尝试2 。但是要注意,为了让服务器判断正确,我们需要使最后两位结果都为2 。那么,倒数第二位是我们本次循环的尝试位,倒数第一位如何确定呢?

因为我们在padding值为1的时候,已经通过尝试得到了正确的中间值。因此,我们只需要将中间值与0×02进行异或的结果放到此处即可。

参考下式会更容易理解:

C1 = 上一步得到的中间值最后一位 ^ 0×02

这里可能有人会问,为什么这一步的C1和上一步的不一样了?

这是因为我们是在通过穷举法来破解。我们在穷举的时候,是把整个C[1]都当做了可以改变的对象,目的就是通过改变C[1]来解除C[2]的中间值。因此上一步我们求出来了中间值以后,C1就可以被我们继续改变了。

而在这一次,我们是为了让padding值为2,这就意味着最后两位都需要为0×02。而倒数第二位是我们这次穷举需要改变的值,那么我们就需要让C1与最后一位的中间值异或以后,能得到0×02。

而最后一位中间值我们已经求得了。因此在这一轮循环里,我们要让C1固定,这样异或后的结果为0×02,我们也就可以愉快的继续穷举倒数第二位了。

之后的操作也与此相同。第n轮循环的时候,只要把C[1]倒数第n-1一直到倒数第一位的值全部固定,使得他们与得到的中间值异或后得到n就可以了。

在整个C[2]破解结束后,我们会得到这个块中所有的中间值。这时,我们只需要掏出未经改变的C[1] ,也就是原IV,与中间值异或,就可以得到C[2]块的全部明文了。

之后我们再用相同的方法破解C[1],就能得到全部明文了。

*本文原创作者:Sherkey,本文属FreeBuf原创奖励计划,未经许可禁止转载

发表评论

已有 1 条评论

取消
Loading...
css.php