Hed9eh0g

前进的路上总是孤独的

CBC Byte Flipping Attack

本文共计有7207个字

前言

CBC字节翻转攻击属于web方向的密码学领域,之所以存在这种攻击方式,是因为CBC加密方式的特点。结合我个人的理解,在此尽量把相关知识点讲清楚。

异或运算

异或运算也记为xor运算,是计算机逻辑运算的一种方式,这是因为所有字符数据在计算机底层都是由0和1所组成的二进制数据。异或运算规则可总结为:把两段二进制数字进行异或运算,两者对应的位置如果相同则结果为0,不同时结果便为1。
字符在计算机中有对应的ascii码值,因此对字符进行异或操作就是将两个字符对应的ascii码值先转化为二进制,然后再对二进制进行异或运算,最终把结果再转化为ascii码,其对应的字符即为异或结果。

规律

1、根据规则知:A xor A = 0,A xor 0 = A。
2、如果A xor B = C,那么有B xor C = B xor A xor B =A xor B xor B = A,同理可以得到:A xor C = B。也就是说,我们只需知道A、B、C中的任意两个,将这两个进行异或运算便可求出另外一个。

CBC加密

什么是CBC

CBC并不是一种加密算法,而是一种加密的模式(另外还有ECB、CFB、OFB、CTR等模式),常常结合AES或DES分组加密算法实现对明文加密。
分组加密算法就是在对明文加密前,先将明文进行分组操作,其中AES分组长度为8字节,而DES分组长度为16字节。(这样进行分组对于最后一组来说往往不满足规定的字节,因此要进行补位操作,根据补位的特点又有另外一种攻击方式padding oracle attack,我将在另一篇文章进行总结)

加密流程

《CBC Byte Flipping Attack》

图中的IV(initialization vetor)为初始向量,为了安全性IV必须确保是随机值,将明文分组后,IV与第一组相同位置的明文进行异或运算(因此IV要么是8位要么是16位),然后再对异或结果进行解密得到第一组明文的密文。
对于第二组明文,则把第一组的明文与其进行异或运算,然后再对异或结果进行解密得到第二组明文的密文……后面的操作流程以此类推即可最终得到密文的分组。

解密流程

《CBC Byte Flipping Attack》

将密文按组分开,首先让第一组密文进行解密,然后再与IV进行异或操作,异或结果即为第一组明文。
对于第二组密文,则把第二组的密文进行解密,然后再与第一组进行异或运算,异或结果得到第二组明文……后面的操作流程仍然可以以此类推,最终便可得到全部明文。

CBC字节翻转攻击

我们把目光放在解密过程上,以第一组密文的解密为例,把第一组密文经过解密之后得到的值称为中间值middle,则明文string = old_IV xor middle。此时我们希望改变明文达到攻击的目的,则根据异或运算可知我们只需要改变IV(new_IV)即可实现,也即目标明文target = new_IV xor middle。

此过程特点

1、必须知道中间值middle才可以构造出new_IV,才可以进而构造出目标明文。也即new_IV=middle xor target,只要得到了middle就可以构造出new_IV。再根据string = old_IV xor middle最后可知:只要得到old_IV和string就可以构造出new_IV。
也即:new_IV = middle xor target = old_IV xor string xor target
2、此过程只能运用于对第一组明文的攻击,对于其他组,则根据CBC结构可知,必须通过改变其前一组的密文来实现攻击。

代码实现

假设我们已知old_IV和明文string,要让string=’Hed9eh0g’转变为我们想要的目标明文target,比如让string的第一个字母H变为小写,再把倒数第二个删掉,再在最后一位添加感叹号。
以下就是python的实现方式,相关说明都在注释标明:

import os
import codecs
from Crypto.Cipher import AES
from Crypto import Random
key=codecs.encode(os.urandom(8),'hex_codec').upper()
old_IV=Random.new().read(16)   #old_IV已知
string='Hed9eh0g' #明文已知
 aes=AES.new(key,AES.MODE_CBC,old_IV)
 length=16
 count=len(string)
 padding=length-(count%length) #补位操作
 string=string + (chr(0)*padding) #根据补位规则本来应该补\x08,为防止出现乱码这里用\x00
 cipher=aes.encrypt(string.encode('utf-8')) #加密
 print("明文:",string)
 print("密钥:",key)
 print("old_IV:",old_IV)
 print("密文:",cipher)
 old_IVlist=[]
 stringlist=[]
 for i in range(0,len(codecs.encode(old_IV,'hex_codec').decode()),2):
 old_IVlist.append(int(codecs.encode(old_IV,'hex_codec').decode()[i:i+2],16))
 new_IVlist=old_IVlist
 for i in list(string):
 stringlist.append(ord(i))
 new_IVlist[0]=stringlist[0]^old_IVlist[0]^ord('h') #第一个字母H变为小写
 new_IVlist[6]=stringlist[6]^old_IVlist[6]^0 #把倒数第二位删掉
 new_IVlist[8]=stringlist[8]^old_IVlist[8]^ord('!') #在最后一位添加感叹号
 new_IV=''
 for i in new_IVlist:
     if len(hex(i)[2:])==1:
              new_IV+='0'+hex(i)[2:]
     else:
              new_IV+=hex(i)[2:]
  new_IV=codecs.decode(new_IV,'hex_codec')
  print("new_IV:",new_IV)
  aes=AES.new(key,AES.MODE_CBC,new_IV)
  target=aes.decrypt(cipher).decode()
  print("目标明文:",target)

下图为运行结果:
《CBC Byte Flipping Attack》

实例

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

<?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>

代码的意思

login函数:先对数组info进行序列化,使其成为字符串便于下一步的CBC加密(openssl_encrypt函数)。并把session的username赋值为info中的username,同时发送两个cookie:第一个为初始向量iv的base64编码,第二个为密文cipher的base64编码。
check_login函数:先看是否有发送两个cookie:第一个是cipher,第二个是iv。如果有先对其进行base64解码,然后结合cipher,iv和key三者进行oppenssl_decrypt操作,再把得到的明文进行反序列化,如果序列化成功将会得到info数组,便把info数组的username赋值给session的username,而如果无法序列化则会返回明文的base64编码。
show_homepage函数:如果session的username等于admin,那么就输出flag的值,否则便提示无法查看flag。
1、输入username和password,如果username是admin便被拒绝登录,如果不是admin则将username和password存入info这个数组中,然后执行login函数和show_homepage函数。
2、如果没有输入username和admin,则看session数组的username是否有值,如果有便执行check_login函数和show_homepage函数。

思路

想要得到flag则必须让session的username为admin,而session在login函数中是由info直接赋值而来的。如果我们直接传入username=admin(被存在info中),那么会被直接拒绝而无法对session赋值。
而另一个对session的赋值操作在于check_login函数中,在这个函数中session是由OpenSSL_decrypt函数结合两个cookie和key解密之后得到的info进行赋值,所以根据字节翻转攻击可知,我们要利用的便是:通过传入我们处理过后的两个cookie让最终的info中的username值为admin。

过程

我传的username和password分别为:Admin和12345:

《CBC Byte Flipping Attack》可以看到响应头传了两个重要的cookie:iv和cipher的base64编码,现在把info序列化后的字符串按每16个字符为一组进行分组:

a:2:{s:8:"userna
me";s:5:"Admin";
s:8:"password";s
:5:"12345";}

我们的目的就是把第二组的A转化为a(也即第9位),因此根据CBC解密结构可知我们必须通过改变密文(cookie传来的cipher)中的第一组的第9位进而间接让第二组明文的A转化为a。这里类似于我们前面分析的改变iv的值,只不过这里是第二组,其等效的iv即为第一组密文。
通过python可以实现对新cipher的构造:

import base64
cipher='WW9ncfZbMJP85gnZJcamoRWsPKCc53K07jIEJ9ATWWwDYoYzpPF19Tb+xuqyZX2tabEicvlIZK4iqNlHiLtZRQ=='.decode('base64')
old = "me\";s:5:\"Admin\";"
new = "me\";s:5:\"admin\";"
index=9
cipher = cipher[:index] + chr(ord(cipher[index]) ^ ord(old[index]) ^ ord(new[index])) + cipher[index+1:]
print cipher.encode('base64').strip()

cipher=WW9ncfZbMJP8xgnZJcamoRWsPKCc53K07jIEJ9ATWWwDYoYzpPF19Tb+xuqyZX2tabEicvlIZK4iqNlHiLtZRQ==
我们把新的cipher与原来的iv作为cookie传入,同时确保不提交username和password,也就是让后台代码执行check_login操作:

《CBC Byte Flipping Attack》我们可以得到后台回应,就是info无法反序列化并返回其base64解码值,我们可以通过解码验证:

《CBC Byte Flipping Attack》可以发现username的Admin已经被我们改为admin了,但为什么第一组会出现乱码呢?这是因为我们之前为了改变第二组明文而改变了第一组的密文,导致原本能够正常解码的第一组密文产生了乱码。
因此我们用同样的思路,即通过改变初始向量iv进而改变第一组的明文,使第一组明文恢复为:"a:2:{s:8:"userna"。类比前面的思路得出:

import base64
plain = 'WW9ncfZbMJP8xgnZJcamoRWsPKCc53K07jIEJ9ATWWwDYoYzpPF19Tb+xuqyZX2tabEicvlIZK4iqNlHiLtZRQ=='.decode('base64')
iv = 'v6qIqxiHe%2FTt6WfL5UrjUw=='.decode('base64')
old = plain[:16]
new = "a:2:{s:8:\"userna";
for i in xrange(16):
    iv = iv[:i] + chr(ord(iv[i]) ^ ord(old[i]) ^ ord(new[i])) + iv[i+1:]
print iv.encode('base64').strip()

iv=F+2mvps7HCrb827Vqgl6cA==
将新得到的iv与cipher重新发送,最终可以得到flag:

《CBC Byte Flipping Attack》

参考文章

1、CBC字节翻转攻击
2、维基百科-CBC模式

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注