CBC字节反转攻击深入思考

温故而知新。

理论知识

记得密码学讲过这个,然鹅。。

异或操作

a, b= 10, 15
c = a ^ b # c=5
a = c ^ b
b = a ^ c
# 意思就是说这三个数可以相互转化

a = 10
a ^ a = 0
# 异或自己等于0

a = 10
a ^ 0 = 10
# 和0异或等于自身

CBC加密过程:

20170525149569300467639.jpg

CBC解密过程:

20170525149569301911892.jpg

有以下几个要点:

  • Plaintext:待加密的数据。

  • IV:用于随机化加密的字符串,保证即使对相同明文多次加密,也可以得到不同的密文。

  • Key:被一些如AES的对称加密算法使用。

  • Ciphertext:加密后的数据。

  • 加密过程:

    Ciphertext-0 = Encrypt(Plaintext XOR IV) 只用于第一个组块
    Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1) 用于第二及剩下的组块
  • 解密过程

    Plaintext-0 = Decrypt(Ciphertext) XOR IV 只用于第一个组块
    Plaintext-N= Decrypt(Ciphertext) XOR Ciphertext-N-1 用于第二及剩下的组块

CBC攻击:

20170525149569330192390.jpg

CBC字节翻转攻击发生在解密的过程中,实质就是通过更改上一个块的内容,来间接修改明文中的内容。有一条经验法则是(结合上图),你在密文中改变的字节,会影响到在下一明文当中,具有相同偏移量的字节。因此,进行CBC攻击仅影响到两个块。CBC反转攻击的用途主要体现在在不知道加密密钥的情况下,通过修改密文,可以间接修改明文。

举个栗子

比方说,在加密算法为AES-128-CBC时,我们有这样的明文序列:

a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";},并且我们已经知道了该明文对应的CBC密文

我们的目标是将s:6当中的数字6转换成数字“7”。我们需要做的第一件事就是把明文分成16个字节的块(因为AES-128以128为为一块,正好16字节):

  • Block 1:a:2:{s:4:"name";
  • Block 2:s:6:"sdsdsd";s:8
  • Block 3::"greeting";s:20
  • Block 4::"echo 'Hello sd
  • Block 5:sdsd!'";}

我们的目标字符位于块2,这意味着我们需要改变块1的密文来改变第二块的明文。

A = Ciphertext-N-1 , B = Decrypt(Ciphertext), 这里的A对应着CBC攻击图中左边标红的那块,A已知,B未知,由图示:

s:6:"sdsdsd";s:8 = A xor B , 左边卫Block2的明文,设为C

C也是已知,我们可以应用异或的小技巧得到B:

B = A xor C

我们的目的是得到修改后的密文,可以用这段代码实现目的:

newCipher 
= C' xor B
= C' xor (A xor C)
= C' xor A xor C

那么此处的例子对应payload就是:

newCipher[2] = chr(ord("7") ^ ord(oldCipher[2]) ^ ord("6"))

总的来看,过程差不多是这样:

<?php
define("SECRET_KEY", "You don't know this key");
define("METHOD", "aes-128-cbc");

# this should a random string
define("IV", "abcdefghabcdefgh");

$plain = 'a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo \'Hello sdsdsd!\'";}';
echo $plain, "\n";
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, IV);
$cipher[2] = chr(ord($cipher[2]) ^ ord("6") ^ ord ("7"));
echo openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, IV);
?>

如果想练手可以用这个题:

<!-- please login as uid=1!--> 
<?php
include("AES.php");
highlight_file('index.php');
$v = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890auid=9;123123123123";
$b = array();
$enc = @encrypt($v);
$b = isset($_COOKIE[user])?@decrypt(base64_decode($_COOKIE[user])):$enc;
$uid = substr($b,strpos($b,"uid")+4,1);
echo 'uid:'.$uid.'<br/>';
if ($uid == 1){
echo $flag;
}
else {
echo "Hello Client!";
}
setcookie("user",base64_encode($enc));
?>
uid:9
Hello Client!

深入探讨

自己试过的童鞋应该会发现,上面的例子运行后是不完美的,

20170525149569776035048.png

可以发现尽管我们确实更改了第二块中的字符,但第一块明文由于我们对密文的修改而丢失,这样的明文在大多数情况下就是去了意义,比如unserialize操作就会不成功:

var_dump(unserialize($newplain)); # bool(false)

回想刚才的图,可以发现,如果我们知道了IV,就可以通过对IV做类似的操作,来实现对第一块明文的修改。这种题目也确实在大型CTF比赛中出现过,我前几日做的山科邀请赛也出现了,贴一下py,过程很清楚了:

先是通过修改密文第一块来修改明文第二块中的数据:

import base64 
cipher = 'yrCdSYR2spWrkuMOma06DOtlmYlJKJ3c3O7XW2qXMfZrYyNprtJYL32j6U7a7Qkr2Mk2V59N0k3AINMeygEqeQ=='
# old = 'me";s:5:"1dmin";'
# new = 'me";s:5:"admin";'

cipher = base64.b64decode(cipher)
cipher = cipher[:9] + chr(ord(cipher[9]) ^ ord('1') ^ ord('a')) + cipher[10:]

print(base64.b64encode(cipher))

在通过修改IV来修改明文第一块的数据:

import base64

iv = base64.b64decode("9epuhbk1gWcOAbyOLPufFw==")
plain = base64.b64decode("miyoHqnRiYHdtrONi7RSdm1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6ImFkbWluIjt9")
want = 'a:2:{s:8:"userna'

first_16 = ''
for i in range(16):
first_16 = chr(ord(plain[i]) ^ ord(iv[i]) ^ ord(want[i])

new_iv = first_16 + iv[16:]
print(base64.b64encode(new_iv))

这题题目核心我也贴出来吧,感兴趣的可以尝试下:


<?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>';
}
}
?>
</html>

参考:

http://wooyun.tangscan.cn/static/drops/tips-7828.html

https://zh.wikipedia.org/wiki/%E5%88%86%E7%BB%84%E5%AF%86%E7%A0%81%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%BC%8F

加载评论框需要翻墙