Padding Oracle Attack

介绍

Padding Oracle Attack 攻击一般需要满足以下几个条件

  • 加密算法

    • 采用 PKCS5 Padding 的加密算法。 当然,非对称加密中 OAEP 的填充方式也有可能会受到影响。

    • 分组模式为 CBC 模式。

  • 攻击者能力

    • 攻击者可以拦截上述加密算法加密的消息。

    • 攻击者可以和 padding oracle(即服务器) 进行交互:客户端向服务器端发送密文,服务器端会以某种返回信息告知客户端 padding 是否正常。

Padding Oracle Attack 攻击可以达到的效果如下

  • 在不清楚 key 和 IV 的前提下解密任意给定的密文。

原理

Padding Oracle Attack 攻击的基本原理如下

  • 对于很长的消息一块一块解密。

  • 对于每一块消息,先解密消息的最后一个字节,然后解密倒数第二个字节,依次类推。

这里我们回顾一下 CBC 的

  • 加密

Ci=EK(PiCi1)C0=IVC_i=E_K(P_i \oplus C_{i-1})\\ C_0=IV
  • 解密

Pi=DK(Ci)Ci1C0=IVP_{i}=D_{K}(C_{i})\oplus C_{i-1}\\ C_{0}=IV

我们主要关注于解密,这里我们并不知道 IV 和 key。这里我们假设密文块的长度为 n 个字节。

假设我们截获了密文最后两个密文块 $F$ 与 $Y$ ,以获取密文块 $Y$ 的对应明文的最后一个字节为例子进行分析。为了获取 $Y$ 解密后的内容,我们首先需要伪造一块密文块 $F'$ 以便于可以修改 $Y$ 对应解密明文的最后一个字节。这是因为若我们构造密文 F'|Y ,那么解密 $Y$ 时具体为 $P'=D_K(Y)\oplus F'$ ,所以修改密文块 $F'$ 的最后一个字节 $F'_{n}$ 可以修改 Y 对应解密明文 $P'$ 的最后一个字节 $P'_n$ ,进而反推出原先的明文 $P$ 的最后一个字节。下面给出获取 $P$ 最后一个字节的过程:

  1. i=0,设置 $F'$ 的每个字节为随机字节

  2. 设置 $F'_n=i \oplus 0x01$ 。

  3. F'|Y 发送给服务器,如果服务器端没有报错,那有很大概率 $P'$ 的最后一个字节是 0x01。否则,只有 $P'$ 的最后 $P'_n \oplus i \oplus 0x01$ 字节都是 $P'_n \oplus i \oplus 0x01$ 才不会报错。而且,需要注意的是 padding 的字节只能是 1 到 n。 因此,若想要使得在 F' 随机地情况下,并且满足 padding 字节大小的约束情况下还不报错概率很小。所以在服务器端不报错的情况下,我们可以认为我们确实获取了正确的字节。这时可知 $D_k(Y)$ 的最后一个字节 $D_k(Y)_n$ 为 $P'_n \oplus F'_n = 0x01 \oplus i \oplus 0x01 = i$ ,即可知道原先的明文 $P$ 的最后一个字节 $P_n = D_k(Y)_n \oplus F_n = i \oplus F_n$ 。

  4. 在出现错误的情况下,i=i+1,跳转到 2.。

当获取了 $P$ 的最后一个字节后,我们可以继续获取 $P$ 的倒数第二个字节,此时需要设置 $F'_n=D_k(Y)n\oplus 0x02$ ,同时设置 $F{n-1}=i \oplus 0x02$ 去枚举 i。以此类推,我们可以获取 Y 所对应的明文 $P$ 的所有字节。

所以,综上所示,Padding Oracle Attack 其实在一定程度上是一种具有很大概率成功的攻击方法。

然而,需要注意的是,往往遇到的一些现实问题并不是标准的 Padding Oracle Attack 模式,我们往往需要进行一些变形。

2017 HITCON Secret Server

分析

程序中采用的加密是 AES CBC,其中采用的 padding 与 PKCS5 类似

但是,在每次 unpad 时并没有进行检测,而是直接进行 unpad。

其中,需要注意的是,每次和用户交互的函数是

  • send_msg ,接受用户的明文,使用固定的 2jpmLoSsOlQrqyqE 作为 IV,进行加密,并将加密结果输出。

  • recv_msg ,接受用户的 IV 和密文,对密文进行解密,并返回。根据返回的结果会有不同的操作

主要漏洞

这里我们再简单总结一下我们已有的部分

  • 加密

    • 加密时的 IV 是固定的而且已知。

    • 'Welcome!!' 加密后的结果。

  • 解密

    • 我们可以控制 IV。

首先,既然我们知道 Welcome!! 加密后的结果,还可以控制 recv_msg 中的 IV,那么根据解密过程

Pi=DK(Ci)Ci1C0=IVP_{i}=D_{K}(C_{i})\oplus C_{i-1}\\ C_{0}=IV

如果我们将 Welcome!! 加密后的结果输入给 recv_msg,那么直接解密后的结果便是 (Welcome!!+'\x07'*7) xor iv,如果我们恰当的控制解密过程中传递的 iv,那么我们就可以控制解密后的结果。也就是说我们可以执行上述所说的任意命令。从而,我们也就可以知道 flag 解密后的结果。

其次,在上面的基础之上,如果我们在任何密文 C 后面添加自定义的 IV 和 Welcome 加密后的结果,作为输入传递给 recv_msg,那么我们便可以控制解密之后的消息的最后一个字节,那么由于 unpad 操作,我们便可以控制解密后的消息的长度减小 0 到 255

利用思路

基本利用思路如下

  1. 绕过 proof of work

  2. 根据执行任意命令的方式获取加密后的 flag。

  3. 由于 flag 的开头是 hitcon{,一共有7个字节,所以我们任然可以通过控制 iv 来使得解密后的前 7 个字节为指定字节。这使得我们可以对于解密后的消息执行 get-md5 命令。而根据 unpad 操作,我们可以控制解密后的消息恰好在消息的第几个字节处。所以我们可以开始时将控制解密后的消息为 hitcon{x,即只保留hitcon{ 后的一个字节。这样便可以获得带一个字节哈希后的加密结果。类似地,我们也可以获得带制定个字节哈希后的加密结果。

  4. 这样的话,我们可以在本地逐字节爆破,计算对应 md5,然后再次利用任意命令执行的方式,控制解密后的明文为任意指定命令,如果控制不成功,那说明该字节不对,需要再次爆破;如果正确,那么就可以直接执行对应的命令。

具体代码如下

最后结果如下

2017 HITCON Secret Server Revenge

描述

分析

这个程序时接着上面的程序继续搞的,不过这次进行的简单的修改

  • 加密算法的 iv 未知,不过可以根据 Welcome 加密后的消息推算出来。

  • 程序多了一个 56 字节的 token。

  • 程序最多能进行 340 操作,因此上述的爆破自然不可行

程序的大概流程如下

  1. 经过 proof of work

  2. 发送 “Welcome!!” 加密后的消息

  3. 在 340 次操作中,需要猜中 token 的值,然后会自动将 flag 输出。

漏洞

当然,在上个题目中存在的漏洞,在这个题目中仍然存在,即

  1. 任意执行给定命令

  2. 长度截断

利用思路

由于 340 的次数限制,虽然我们仍然可以获得 md5(token[:i]) 加密后的值(**这里需要注意的是这部分加密后恰好是 32 个字节,前 16 个字节是 md5 后加密的值,后面的 16 个字节完全是填充的加密后的字节。**这里md5(token[:i]) 特指前16个字节。)。但是,我们不能再次为了获得一个字符去爆破 256 次了。

既然不能够爆破,那么我们有没有可能一次获取一个字节的大小呢?这里,我们再来梳理一下该程序可能可以泄漏的信息

  1. 某些消息的 md5 值加密后的值,这里我们可以获取 md5(token[:i]) 加密后的值。

  2. unpad 每次会对解密后的消息进行 unpad,这个字节是根据解密后的消息的最后一个字节来决定的。如果我们可以计算出这个字节的大小,那么我们就可能可以知道一个字节的值。

这里我们深入分析一下 unpad 的信息泄漏。如果我们将加密 IV 和 encrypt(md5(token[:i])) 放在某个密文 C 的后面,构成 C|IV|encrypt(md5(token[:i])),那么解密出来的消息的最后一个明文块就是 md5(token[:i])。进而,在 unpad 的时候就是利用 md5(token[:i]) 的最后一个字节( 0-255)进行 unpad,之后对 unpad 后的字符串执行指定的命令(比如md5)。那么,如果我们事先构造一些消息哈希后加密的样本,然后将上述执行后的结果与样本比较,如果相同,那么我们基本可以确定 md5(token[:i]) 最后一个字节。然而,如果 md5(token[:i]) 的最后一个字节小于16,那么在 unpad 时就会利用一些 md5 中的值,而这部分值,由于对于不同长度的 token[:i] 几乎都不会相同。所以可能需要特殊处理。

我们已经知道了这个问题的关键,即生成与 unpad 字节大小对应的加密结果样本,以便于查表。

具体利用思路如下

  1. 绕过 proof of work。

  2. 获取 token 加密后的结果 token_enc ,这里会在 token 前面添加 7 个字节 "token: " 。 因此加密后的长度为 64。

  3. 依次获取 encrypt(md5(token[:i])) 的结果,一共是 57 个,包括最后一个 token 的 padding。

  4. 构造与 unpad 大小对应的样本。这里我们构造密文 token_enc|padding|IV_indexi|welcome_enc。由于 IV_indexi 是为了修改最后一个明文块的最后一个字节,所以该字节处于变化之中。我们若想获取一些固定字节的哈希值,这部分自然不能添加。因此这里产生样本时 unpad 的大小范围为 17 ~ 255。如果最后测试时 md5(token[:i]) 的最后一个字节小于17的话,基本就会出现一些未知的样本。很自然的一个想法是我们直接获取 255-17+1个这么多个样本,然而,如果这样做的话,根据上面 340 的次数(255-17+1+57+56>340)限制,我们显然不能获取到 token 的所有字节。所以这里我们需要想办法复用一些内容,这里我们选择复用 encrypt(md5(token[:i])) 的结果。那么我们在补充 padding 时需要确保一方面次数够用,另一方面可以复用之前的结果。这里我们设置 unpad 的循环为 17 到 208,并使得 unpad 大于 208 时恰好 unpad 到我们可以复用的地方。这里需要注意的是,当 md5(token[:i]) 的最后一个字节为 0 时,会将所有解密后的明文 unpad 掉,因此会出现 command not found 的密文。

  5. 再次构造密文 token_enc|padding|IV|encrypt(md5(token[:i])) ,那么,解密时即使用 md5(token[:i]) 的最后一个字节进行 unpad。如果这个字节不小于17或者为0,则可以处理。如果这个字节小于17,那么显然,最后返回给用户的 md5 的结果并不在样本范围内,那么我们修改其最后一个字节的最高比特位,使其 unpad 后可以落在样本范围内。这样,我们就可以猜出 md5(token[:i]) 的最后一个字节。

  6. 在猜出 md5(token[:i]) 的最后一个字节后,我们可以在本地暴力破解 256 次,找出所有哈希值末尾为 md5(token[:i]) 的最后一个字节的字符。

  7. 但是,在第六步中,对于一个 md5(token[:i]) 可能会找出多个备选字符,因为我们只需要使得其末尾字节是给定字节即可。

  8. 那么,问题来了,如何删除一些多余的备选字符串呢?这里我就选择了一个小 trick,即在逐字节枚举时,同时枚举出 token 的 padding。由于 padding 是 0x01 是固定的,所以我们只需要过滤出所有结尾不是 0x01 的token 即可。

这里,在测试时,将代码中 sleep 注释掉了。以便于加快交互速度。利用代码如下

效果如下

Teaser Dragon CTF 2018 AES-128-TSB

这个题目还是蛮有意思的,题目描述如下

附件以及最后的 exp 自行到 ctf-challenge 仓库下寻找。

题目的基本流程为

  • 不断接收 a 和 b 两个字符串,其中 a 为明文,b 为密文,注意

    • b 在解密后需要满足尾部恰好等于 iv。

  • 如果 a 和 b 相等,那么根据

    • a 为 gimme_flag ,输出加密后的 flag。

    • 否则,输出一串随机加密的字符串。

  • 否则输出一串明文的字符串。

此外,我们还可以发现题目中的 unpad 存在问题,可以截断指定长度。

一开始,很直接的思路是 a 和 b 的长度都输入 0 ,那么可以直接绕过 a==b 检查,获取一串随机密文加密的字符串。然而似乎并没有什么作用,我们来分析一下加密的流程

不妨假设 $P_0=iv,C_0=iv$,则

$C_i=C_{i-1}\oplus E(P_{i-1} \oplus P_i)$

那么,假设消息长度为 16,与我们想要得到的gimme_flag padding 后长度类似,则

$C_1=IV\oplus E( IV \oplus P_1)$

$C_2=C_1 \oplus E(P_1 \oplus IV)$

可以很容易的发现 $C_2=IV$。

盗图arrow-up-right,下面的图片更加清晰

反过来想,如果我们向服务器发送 iv+c+iv,那么总能绕过 tsb_decrypt 的 mac 检查

那么此时,服务器解密后的消息则是

$unpad(IV \oplus D(C_1 \oplus IV))$

获取明文最后一个字节

我们可以考虑控制 D 解密的消息为常数值,比如全零,即C1=IV,那么我们就可以从 0 到 255 枚举 IV 的最后一个字节,得到 $IV \oplus D(C_1 \oplus IV)$ 的最后一个字节也是 0255。而只有是 115 的时候,unpad 操作过后,消息长度不为 0。因此,我们可以在枚举时统计究竟哪些数字导致了长度不为零,并标记为 1,其余标记为 0。

此外,我们可以在最初的时候打表获取最后一个字节所有的可能情况,记录在 xor_byte_map 中。

通过与这个表进行对比,我们就可以知道最后一个字节可能的情况。

解密任意加密块

在获取了明文最后一个字节后,我们就可以利用 unpad 的漏洞,从长度 1 枚举到长度 15 来获得对应的明文内容。

解密出指定明文

这一点比较简单,我们希望利用这一点来获取 gimme_flag 的密文

其中 plain0 和 cipher0 是我们获取的 AES 加密的明密文对,不包括之前和之后的两个异或。

解密 flag

这一点,其实就是利用解密任意加密块的功能实现的,如下

可以思考一下为什么第二块之后的密文操作会有所不同。

完整的代码参考 ctf-challenge 仓库。

参考资料

  • https://en.wikipedia.org/wiki/Padding_oracle_attack

  • http://netifera.com/research/poet/PaddingOraclesEverywhereEkoparty2010.pdf

  • https://ctftime.org/writeup/7975

  • https://ctftime.org/writeup/7974

Last updated