CBC相关攻击

CBC简介

CBC,密码分组链接模式,对应的其他加密模式还有ECB(电子编码簿模式),CFB(密文反馈模式),OFB(输出反馈模式),CTR(计数器模式)

  • Plaintext:待加密的数据。
  • IV:用于随机化加密的比特块,保证即使对相同明文多次加密,也可以得到不同的密文。
  • Key:被一些如AES的对称加密算法使用。
  • Ciphertext:加密后的数据。

    CBC比特翻转攻击

我们知道,CBC的解密模式是密文分组与前一部分的密文分组进行异或得到明文,从第一步开始看,也就是密文分组1解密后再与初始化向量IV进行异或才得到最后的明文分组1,通过修改初始化向量IV,很容易操纵明文分组1的某位的比特值.

假如我们不能控制初始化向量,而是可以控制密文分组1,那么我们就可以控制明文分组2的任意位的比特值,但是,同时,为了修改明文分组2的某位的比特值,我们得修改密文分组1的相应位的比特值,这样解密后,得到的明文分组1就会发生变化(一般就变成乱码),也就是会导致明文分组1的结果和正常的解密结果不一样.

值得注意的是,AES的密钥扩展部分的处理就是CBC模式的加解密,所以这种攻击手段一般都是针对AES加密的.

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(本组密文) ^ 想要的字符 )

例题:bugku login4

进入网站,是个可以任意登录的页面,无需注册,提示需要admin才能看到flag,但是我们并不能以admin进行登录.扫目录有swp源码泄露.

核心代码部分

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
<?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{
echo '<body class="login-body">
...............................

}
}
?>

审计代码,发现login函数对于信息的处理是将array('username'=>$username,'password'=>$password)进行序列化后进行AES加密,并且本题的初始化向量iv和密文cipher是可控的

我们先本地简单的写个代码得到username为admia,password为12345的序列化的值,
a:2:{s:8:"username";s:5:"admia";s:8:"password";s:5:"12345";}

我们需要想办法让我们的admia在解密后反序列化为admin,达到username为admin的效果,所以我们需要对序列化的值进行修改.因为返回的iv变量长度为16(base64解码后),所以以16字节为一组将我们的序列化的值进行分组,这也就是对应上文的明文1,2,3,4.

1
2
3
4
a:2:{s:8:"userna
me";s:5:"admia";
s:8:"password";s
:5:"12345";}

抓包后得到对应的iv的base64为CLESSZwEk8IokrFbgJUyiw%3D%3D

密文cipher的base64为m2PGbo%2Bq2CRq%2F3vSmrsIw54vwbgt18Jr%2FWzq0h4KXpfeiF3RlGnUhCmW%2FCh%2BrPpiDVllfBrjZL9dsfIea2xskg%3D%3D

为了达到目的,我们想要将第二组的第14个字节,也就是a修改为n,这样就变成了admin,我们就得对第一组的第14个字节进行修改,根据上文的公式:想要的字符 = 上一组密文 ^ 本组明文 ^ Decrypt(本组密文) ^ 想要的字符

核心思路就是通过修改第一组的第14个字符,然后与an异或,得到新的字符,待这新的字符和第二组的密文进行异或,就变成了我们想要的字符n

代码为

1
2
3
4
5
6
7
8
import base64

cipher="Tq1AMQT1SBoyuPCm3PRMWZ346XjZPWaAVkMa0taiwsUldH5lji6G8CI08ZV1q71kPPDcsRhoQWAEoH9hEToT8Q=="
plain=base64.b64decode(cipher)
result=plain[0:13]+chr(ord(plain[13])^ord("n")^ord("a"))+plain[14:]
print base64.b64encode(result)

#Tq1AMQT1SBoyuPCm3PtMWZ346XjZPWaAVkMa0taiwsUldH5lji6G8CI08ZV1q71kPPDcsRhoQWAEoH9hEToT8Q==

将得到的新的cipher发过去,给了can’t unserialize,当然不能正常的反序列化,因为按照上文的介绍,我们动了第一组密文的内容,自然就影响到了第一组明文的内容,导致整个序列化的字符串都不能正确的被解析,因此我们需要控制iv来保证第一串的明文也都是我们想要的,也就是想要a:2:{s:8:"userna

审计代码,发现返回的恰好是对应的明文的base64

UAhQWFwNaxUXzG8f5cpNNm1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6IjEyMzQ1Ijt9

我们解码看看

PPX\kÌoåÊM6me";s:5:"admin";s:8:"password";s:5:"12345";}

果然,前面部分已经乱码了.但是关键的,admia以及变成了admin,说明我们对第二组的明文修改成功,接下来的事情就是让前面部分的明文恢复正常

再次根据上文的规则,通过iv异或a:2:{s:8:"userna异或第一组明文,可以伪造出能够正常反序列化的加密字符串,第一组的明文当然就可以从返回的明文base64中提取

1
2
3
4
5
6
7
8
9
10
import base64
plain = 'UAhQWFwNaxUXzG8f5cpNNm1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6IjEyMzQ1Ijt9'
plain = base64.b64decode(plain)
oldiv = base64.b64decode("gR/IiGHpKTwzfWt9zznnXw==")
one = 'a:2:{s:8:"userna'
iv = ''
for i in range(0,16):
iv=iv+chr(ord(one[i])^ord(plain[i])^ord(oldiv[i]))
print base64.b64encode(iv)
#sC2q6kaXeBEek3ERT4HECA==

最后将伪造的iv以及刚才伪造的cipher发送过去即可得到flag

CBC选择密文攻击

通过选择密文攻击,也就是能够控制密文,那么就可以恢复出IV

1
2
3
4
5
6
待解密的密文:C|C  ,M1为明文组1,M2为明文组2
Decrypt(C)^C=M1
Decrypt(C)^IV=M2
Decrypt(C)^C^IV=M1^M2
最终可以推出
IV=M1^M2^C

例题:jarvis oj xcc

源码

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
class Unbuffered(object):
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
import sys
sys.stdout = Unbuffered(sys.stdout)
#import signal
#signal.alarm(600)

import random
import time
flag=open("/root/xcc/flag","r").read()

from Crypto.Cipher import AES
import os

def aes_cbc(key,iv,m):
handler=AES.new(key,AES.MODE_CBC,iv)
return handler.encrypt(m).encode("hex")
def aes_cbc_dec(key,iv,c):
handler=AES.new(key,AES.MODE_CBC,iv)
return handler.decrypt(c.decode("hex"))

key=os.urandom(16)
iv=flag

for i in range(10):
c=raw_input("c:")
print aes_cbc_dec(key,iv,c).encode("hex")

nc目标服务器,我们需要输入C,然后就会返回C对应的解密后的明文,所以我们可控密文C,根据上文的公式,我们想要恢复IV,就得有一个明文组,然后将这个明文组分成两个,

参考exp:

1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Util.number import long_to_bytes,bytes_to_long

plain = ("A"*32).encode('hex')
print (plain)
#4141414141414141414141414141414141414141414141414141414141414141
p = 'a1ec53caa0e9ab0d3fa76cf04145a2f089db4de292f78e2d108148c35f25c290'
p0 = p[:32]
p1 = p[32:]
iv = bytes_to_long(p0.decode('hex'))^bytes_to_long(p1.decode('hex'))^bytes_to_long("A"*16)
print (p0.decode('hex'))
print (bytes_to_long(p0.decode('hex')))
print (long_to_bytes(iv))

padding oracle攻击

  • 常出现在WEB题目中
  • 分组密码CBC模式的Padding oracle攻击需要满足以下特定条件:
    • 加密时采用了PKCS5的填充;(填充的数值是填充的字符个数)
    • 攻击者可以和服务器进行交互,可以提交密文,服务器会以某种返回信息告知客户端的padding是否正常。
  • 攻击效果是在不清楚key和IV的时候解密任意给定的密文
  • Padding oracle攻击的原理主要是利用服务器再对padding进行检查的时候不同回显进行的。这是一种侧信道攻击。利用服务器对padding的检查,可以从末位开始逐位爆破明文。

oracle
• 对某一个block C2的解密
• M2=Dec(C2)^C1
• 可以在C2前拼接一个我们构造的F,向服务器发送F|C2解密,爆破最后一位明文的流程如下:

- 枚举M2的最后一位x;
- 构造F的最后一位为x^1;
- 发送并观察padding的判断结果是否正确,错误返回1 

参考链接

https://mochazz.github.io/2018/05/06/CBC%E5%AD%97%E8%8A%82%E7%BF%BB%E8%BD%AC%E6%94%BB%E5%87%BB/#CBC%E5%8E%9F%E7%90%86

http://www.bubuko.com/infodetail-2782706.html