CBC字节反转攻击原理
原理:
在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量。
CBC加密过程
在这里重要的一点是,CBC工作于一个固定长度的比特组,将其称之为块。在本文中,我们将使用包含16字节的块。
前一块的密文用来产生后一块的密文。
加密过程转化成文字来叙述:
- 首先将明文分组(常见的以16字节为一组),位数不足的使用特殊字符填充。
- 生成一个随机的初始化向量(IV)和一个密钥。
- 将IV和第一组明文异或。
- 用密钥对3中xor后产生的密文加密。
- 用4中产生的密文对第二组明文进行xor操作。
- 用密钥对5中产生的密文加密。
- 重复4-7,到最后一组明文。
- 将IV和加密后的密文拼接在一起,得到最终的密文。
CBC解密过程
过程转化成文字来叙述:
- 从密文中提取出IV,然后将密文分组。
- 使用密钥对第一组的密文解密,然后和IV进行xor得到明文。
- 使用密钥对第二组密文解密,然后和2中的密文xor得到明文。
- 重复2-3,直到最后一组密文。
密文-N-1是用来产生下一块明文;这就是字节翻转攻击开始发挥作用的地方。
如果我们改变密文-N-1的一个字节,然后与下一个解密后的组块异或,我们就可以得到一个不同的明文了!
注意在加密时,明文中的微小改变会导致其后的全部密文块发生改变,而在解密时,从两个邻接的密文块中即可得到一个明文块。因此,解密过程可以被并行化,而解密时,密文中一位的改变只会导致其对应的明文块完全改变和下一个明文块中对应位发生改变,不会影响到其它明文的内容。
通过损坏密文字节来改变明文字节。(注:借助CBC内部的模式)借由此可以绕过过滤器,或者改变用户权限提升至管理员,又或者改变应用程序预期明文
一个php例子
<?php
define('MY_AES_KEY', "abcdef0123456789");
function aes($data, $encrypt) {
$aes = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = "1234567891234567";
mcrypt_generic_init($aes, MY_AES_KEY, $iv);
return $encrypt ? mcrypt_generic($aes,$data) : mdecrypt_generic($aes,$data);
}
define('MY_MAC_LEN', 40);
function encrypt($data) {
return aes($data, true);
}
function decrypt($data) {
$data = rtrim(aes($data, false), "\0");
return $data;
}
$v = "a:2:{s:4:\"name\";s:6:\"w0s1np\";s:8:\"greeting\";s:20:\"echo 'Hello w0s1np!'\";}";
echo "Plaintext before attack: $v\n";
echo "<br>";
$b = array();
$enc = array();
$enc = @encrypt($v);
$enc[2] = chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));
$b = @decrypt($enc);
echo "Plaintext AFTER attack : $b\n";
查看代码,得到密文后,将密文的第三位异或6后又异或7。
然后我们查看页面输出
可以看到,第19位的值从6变为7,且第一分组全部变化
我们将明文每16位为一组,进行分组
a:2:{s:4:”name”;
s:6:”w0s1np”;s:8
:”greeting”;s:20
:”echo ‘Hello w0
s1np!’”;}
从代码中看出,异或的是$enc[2]
,即第一组的第三位。
而第一组密文的修改将影响第二组对应位置的明文发生变化。第二组的第三位原值为6,异或6后为0,0又异或7变为7。所以6会变为7输出到页面上。
然后第一组密文的修改将导致第一组明文全部变化,所以是一堆错乱的字符。
一道CTF例子
bugku Login4
<?php
define("SECRET_KEY", file_get_contents('/root/key'));
define("METHOD", "aes-128-cbc");
session_start();
function get_random_iv(){ //随机生成16位初始化向量
$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); //加密
//options:以下标记的按位或: OPENSSL_RAW_DATA 原生数据,对应数字1,不进行 base64 编码。OPENSSL_ZERO_PADDING 数据进行 base64 编码再返回,对应数字0。
$_SESSION['username'] = $info['username']; //注册SESSION全局变量
//以下两行设置cookie
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!");
}
}
}
#第二个执行,检测用户名为admin时,打印flag
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>
程序接收到POST参数(username,password)
,并且禁止admin
登陆。当用户名不是admin
的时候,首先把用户名密码放入数组,传到login
方法中。
login
方法对传入的数组进行了序列化,并且使用aes-128-cbc
对序列化进行加密。iv(初始化向量)
是随机生成的。最终把cipher
和iv
放入cookie
。
再到show_homepage()
方法,检测$_SESSION
中的username
是admin
时,打印flag
。
有$_SESSION[‘username’]
时,进入check_login()
方法。
当cookie
中存在cipher
、iv
时,对cipher
进行解密。这里是解题的关键,可以通过修改cookie
中的cipher
值,将序列化数据的用户名修改成admin
。从而绕过程序起点处禁止admin
登陆的判断。
所以我们需要把admil
修改为admin
假设:A ^ B = C,则可得
B = A ^ C
当人为修改A=A ^ C时,
A ^ B = A ^ C ^ B = B ^ B = 0
当人为修改A=A ^ C ^ x (x为任意数值)时,
A ^ B = A ^ C ^ x ^ B = B ^ B ^ x = x
举例:
密文1[4]的意思是密文1字符串第4个字节,相当于数组下标。
设:密文1[4] = A,解密(密文2)[4] = B,明文2[4] = C
因为A ^ B = C,根据结论有B = A ^ C
当人为修改A=A ^ C时,那么A ^ B = A ^ C ^ B = B ^ B = 0,这样明文2[4]的结果就为0了
当人为修改A=A ^ C ^ x (x为任意数值)时,那么
A ^ B = A ^ C ^ x ^ B = B ^ B ^ x = x,这是明文2[4] = x,这样就达到了控制明文某个字节的目的了。
<?php
header("Content-Type: text/html;charset=utf-8");
#计算cipher
/*
明文1:a:2:{s:8:"userna //r
明文2:me";s:5:"admil"; //l字母在第14个字节
明文3:s:8:"password";s
明文4::3:"123";}
*/
$cipher = base64_decode(urldecode('qX08ZL%2FJX5LG2XnrPY%2Brh7MuwT%2FpFXF8ltT8IOOY1X890xr6xwVwjyswMbQkH0hDrBLGN0PJEYUxjgHzoB3spP%2FcqO4hSGEK7nlQp%2FBzRig%3D'));
$temp = $cipher;
/*
设密文1[13]=A, 解密(密文2)[13]=B, 明文2[13]=C,
将A修改为A ^ C,则:
A ^ B = A ^ C ^ B = B ^ B = 0 = C
*/
// A C X
$cipher[13] = chr(ord($cipher[13]) ^ ord('l') ^ ord('n'));
echo urlencode(base64_encode($cipher));
?>
得到:qX08ZL%2FJX5LG2XnrPY2rh7MuwT%2FpFXF8ltT8IOOY1X890xr6xwVwjyswMbQkH0hDrBLGN0PJEYUxjgHzoB3spP%2FcqO4hSGEK7nlQp%2FBzRig%3D
但是无法反序列化
这是因为我们修改了密文1中的数据,使第一次解密出的明文数据错乱,打乱了序列化数据的格式,反序列化失败。
但是当我们把返回的base64
数据解码后,可以发现我们的username
已经修改成admin
了。
–»Îÿªw½XÂß•m{me";s:5:"admin";s:8:"password";s:15:"1SCC_2o2l_KeFuu";}
出现这种错误,根据CBC解密原理,我们只需要修改iv的值,使得iv XOR 解密(密文1) = 明文1 即可。
<?php
#计算iv
$res = base64_decode('qX08ZL%2FJX5LG2XnrPY2rh7MuwT%2FpFXF8ltT8IOOY1X890xr6xwVwjyswMbQkH0hDrBLGN0PJEYUxjgHzoB3spP%2FcqO4hSGEK7nlQp%2FBzRig%3D'); //这里放burp放回的base64数据
$iv = base64_decode(urldecode('m69Ijo1fwwMGdBx6uXXFuA%3D%3D')); //这里放cookie中的iv
$plaintext = 'a:2:{s:8:"userna';
$new_iv = '';
for ($i = 0; $i < 16; $i ++){
$new_iv = $new_iv . chr(ord($iv[$i]) ^ ord($res[$i]) ^ ord($plaintext[$i]));
}
echo urlencode(base64_encode($new_iv));
?>
得到U%2BhG0Eup3EV3TQzucPGddw%3D%3D
修改iv
的值就行