AMao
小菜鸡目前对一些东西的认知,希望师傅们可以帮忙纠正!

CBC字节翻转与Padding Oracle Attack

2020-09-22 CTF Web安全 渗透测试
Word count: 2.8k | Reading time: 13min

CBC

CBC全称Cipher Block Chaining模式(密文分组链接模式),每一个分组大小一般为128bits(16字节)

  • 如果明文的长度不是16字节的整数倍,需要对最后一个分组进行填充(padding),CBC的填充规则有PKCS5和PKCS7的区别,这里使用的是PKCS7 ,即缺少N字节,就用 N 个 \xN 填充,如缺少7位则用 7 个 \x07 填充,
  • 如果刚好是整数倍时,Padding一个整组的填充值

密文:(加密后可能有不可见字符,为了方便网络传输和适应不同系统的编码方案)

  • 用 ASCII 十六进制表示,一个字节(2^8-1)用 0xMN 表示
  • base64 表示

TIPS:CBC 字节翻转和 Padding Oracle Attack 攻击与具体的加密算法无关(分组),如 AES 等

加密

  1. 分组填充
  2. 生成初始向量IV(这里的初始向量如果未特定给出则随机生成)和密钥
  3. 将初始向量与第一组明文异或生成 middle_A
  4. 用密钥加密 middle_A 得到密文 Cipher_A
  5. 重复3 将密文 Cipher_A 与第二组明文异或生成 middle_B
  6. 重复4 用密钥加密密文Cipher_B
  7. 重复3-6 直到最后一组明文
  8. 将IV和加密后的密文拼接在一起,得到最终的密文(也可以不拼接)

解密

  1. 首先从最终的密文中提取出IV (IV为加密时指定的X位) //如果加密时没有加入IV则不用提取
  2. 将密文分组
  3. 使用密钥对第一组密文A解密得到 middle_A,然后用 IV 进行异或得到第一组明文
  4. 使用密钥对第二组密文解密得到 middle_B,然后用A与B进行异或得到第二组明文
  5. 重复3-4 直到最后一组密文

TIPS:

加密:plain –xor–> middle –AES–> cipher

解密:plain <–xor– middle <–AES– cipher

加密和解密过程中的 middle 并不相同

字节翻转攻击

Flipped Ciphertext Bits,解密过程中,先进行AES解密,再用 Cipher[N-1] 异或 Middle[N] ,得到 Plain[N],如果对 Cipher[N-1] 进行修改,就可以达到修改 Plain[N] 的目的

  • 异或运算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    >>> Cipher[N-1] ^ Middle[N]
    Plain[N]

    >>> Modification_Cipher[N-1] ^ Middle[N]
    Modification_Plain[N]

    # A ^ B = C --> B ^ C = A
    >>> Middle[N] ^ Modification_Plain[N]
    Modification_Cipher[N-1]

    # Middle[N] 是中间值,不好获取,通过第一个等式可以得 Cipher[N-1] ^ Plain[N] == Middle[N]
    >>> Cipher[N-1] ^ Plain[N] ^ Modification_Plain[N]
    Modification_Cipher[N-1]

iscc2018 Web300

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
<?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 $flag;
}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>';
}
}
?>

题目逻辑:不允许admin账户登录,将 cookie 解密并且提取出username字段,为admin才能cat flag

思路:构造 admiN 登录生成,再通过修改cookie中的密文,实现CBC字节翻转,将username字段改为 admin

  • cookie分组

    1
    2
    3
    4
    a:2:{s:8:"userna
    me";s:5:"admiN";
    s:8:"password";s
    :6:"123456";}
  • 翻转关系

    1
    2
    3
    // Modification_Cipher[N-1] == Cipher[N-1] ^ Plain[N] ^ Modification_Plain[N]
    // N在分组中的第14
    $new_cipher[13] = chr(ord(13) ^ ord('N') ^ ord('n'))
  • 由于第一组的第14位修改了,所以解密之后是乱码,但是 iv 也是从 cookie 中获取,可以通过修改 iv

    1
    2
    3
    4
    5
    # 上面推导的关系式:
    Modification_Cipher[N-1] = Cipher[N-1] ^ Plain[N] ^ Modification_Plain[N]

    # 根据算法,当N=1时,Cipher[N-1] 改为 iv
    Error_Cipher = iv ^ Erroe_Plain ^ Right_Plain
  • 脚本

    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
    #-*- coding:utf8 -*-
    import base64
    import urllib
    # a:2:{s:8:"userna
    # me";s:5:"admiN";
    # s:8:"password";s
    # :6:"123456";}

    def Module1():
    ciphertext = raw_input("Please input the first round cipher:\n")
    cipher = base64.b64decode(urllib.unquote(ciphertext))
    new_cipher = cipher[:13] + chr(ord(cipher[13]) ^ ord('N') ^ ord('n')) + cipher[14:]
    print urllib.unquote(base64.b64encode(new_cipher))

    def Module2():
    errorcipher = base64.b64decode(urllib.unquote(raw_input('Please input errorcipher: \n')))
    ivtext = raw_input("Please input iv:\n")
    iv = base64.b64decode(urllib.unquote(ivtext))
    cleartext = 'a:2:{s:8:"userna'
    newiv = ''
    for i in range(16):
    newiv += chr(ord(iv[i]) ^ ord(errorcipher[i]) ^ ord(cleartext[i]))
    print urllib.unquote(base64.b64encode(newiv))

    option = raw_input("Please input option [1 or 2]:")
    if option == '1':
    Module1()
    elif option == '2':
    Module2()
    else:
    pass

Padding Oracle Attack

  • 条件

    • 攻击者能够获得密文(Ciphertext),以及附带在密文前面的IV(初始化向量)
    • 攻击者能够触发密文的解密过程,且能够知道密文的解密结果
    • 能进行二值逻辑推理(即解密正确和解密错误的返回效果不同)
  • 第一组密文的解密过程

    1
    2
    # Intermediary_Value 为中间值
    Cipher[0] --DES--> Intermediary_Value --( ^ IV)--> Plain[0]
  • padding 格式

    根据上面的分组padding 规则,正确的 padding 格式:

    1
    2
    3
    0x01
    0x02 0x02
    0x03 0x03 0x03

attack 思路

每次发送一个分组,则解密时都会用到IV

  • 爆破分组最后一个字节:

    • 根据 New_IV[-1] ^ Intermediary_Value [-1] = 0x01 爆破

      New_IV[-1] 从0x00-0xFF 进行爆破,其中只有一个值能满足与 Intermediary_Value[-1] 异或结果为 0x01,也仅有这种 padding 情况能被认为是正常解密(二值推理)

      TIPS:IV长度与分组长度相同,可能为 16 Bytes,其他位全部置零即可

    • 根据逻辑运算得到最后一字节的明文

      1
      2
      New_IV[-1]  ^ Intermediary_Value [-1] = 0x01
      => Intermediary_Value [-1] = New_IV[-1] ^ 0x01

      将 Intermediary_Value [-1] 与 IV[-1] (第一个分组)或 前一个密文分组的最后一位(其他分组)异或可以得到 Cipher[-1]

  • 爆破分组倒数第二个字节

    • 构造 New_IV[-1] ^ Intermediary_Value [-1] = 0x02

      上一步已经得到了Intermediary_Value [-1] 则

      1
      New_IV[-1]  = Intermediary_Value [-1] ^ 0x02
    • 根据 New_IV[-2] ^ Intermediary_Value [-2] = 0x02 爆破

      与爆破最后一个字节的思路相同

防御

key:

应用程序对异常的处理:当提交的加密后的数据中出现错误的填充信息时,不够健壮的应用程序解密时报错,直接抛出”填充错误”异常信息(这个错误信息在不同的应用中是不同的体现,在Web一般是报500错误)

Web 应用中将 username 和 password 进行 CBC 加密后,和 IV 拼接作为用户标识 UID

服务端的返回结果

  • 参数是一串正确的密文,分组、填充、加密都是对的(程序运行本身没出问题),包含的内容也是正确的(业务逻辑是对的),那么服务端解密、检测用户权限都没有问题,返回HTTP 200。
  • 参数是一串错误的密文(包含不正确的bit填充),程序运行本身出现致命错误,那么服务端解密时就会抛出异常,返回HTTP 500 server error
  • 参数是一串正确的密文(程序运行本身没出问题),包含的用户名是错误的(业务逻辑是错的),那么服务端解密之后检测权限不通过,但是依旧会返回HTTP 200戒者HTTP 302,而不是HTTP 500。

攻击者无需关心用户名是否正确,只需要提交错误的密文(因为这里有4中变量情况,为了构造出二值逻辑推理,我们要定住其中2个情况,即让业务逻辑恒错,对Bit Padding 的情况进行逻辑推理),根据HTTP Code即可做出攻击

Padding Oracle Attcak 并不是密码学算法本身的漏洞,但是当这种算法在实际生产环境中使用不当才会造成问题

防御思路:不让用户得知解密的结果是否符合Padding,如增加 try-catch 机制

题目

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
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/ruby -w
require 'openssl'
require 'base64'

def banner()
puts ' ____________________________________________'
puts '| |'
puts '| Welcome to our secure communication system |'
puts '| Our system is secured by AES |'
puts '| So...No key! No Message! |'
puts '|____________________________________________|'
puts ''
end

def option()
puts '1. Get the secret message.'
puts '2. Encrypt the message'
puts '3. Decrypt the message.'
puts 'Give your option:'
STDOUT.flush
op=gets
return op.to_i
end

def init()
file_key=File.new("./aeskey","r")
$key=file_key.gets
file_key.close()
end
def aes_encrypt(iv,data)
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
cipher.encrypt
cipher.key = $key
cipher.iv = iv
cipher.update(data) << cipher.final
end

def aes_decrypt(iv,data)
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
cipher.decrypt
cipher.key = $key
cipher.iv = iv
data = cipher.update(data) << cipher.final
end

def output_secret()
file_secret=File.new("./flag","r")
secret=file_secret.gets
file_secret.close
secret_enc=aes_encrypt("A"*16,secret)
secret_enc_b64=Base64.encode64(secret_enc)
puts secret_enc_b64
end

init
banner
while true do
begin
op=option
if op==1
output_secret
elsif op==2
puts "IV:"
STDOUT.flush
iv=Base64.decode64(gets)
puts "Data:"
STDOUT.flush
data=Base64.decode64(gets)
data_enc=aes_encrypt iv,data
puts Base64.encode64(data_enc)
puts "Encrytion Done"
STDOUT.flush
elsif op==3
puts "IV:"
STDOUT.flush
iv=Base64.decode64(gets)
puts "Data:"
STDOUT.flush
data=Base64.decode64(gets)
data_dec=aes_decrypt iv,data
puts data_dec
puts "Decrpytion Done"
STDOUT.flush
else
puts 'Wrong Option'
STDOUT.flush
end
rescue Exception => e
puts e.message
STDOUT.flush
retry
end
end

代码逻辑

  • 1选项:
    输出经过aes-256-cbc加密的flag
  • 2选项:
    提供你的IV和要加密的数据,返回加密后的密文
  • 3选项:
    提供你的IV和要解密的数据,返回解密明文,只返回解密成功是否

信息:

  • 加密flag所采用的IV为16个字符A
  • 不能获取到加密flag所用的密钥
  • 解密时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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *
import base64 as b64

IV = ['\x00'] * 16
secret = 'nPQctp6AezY8BcGPjlYW8Pv+Fpo15LeatsVbj47jqgE='
secret1 = b64.b64decode(secret)[0:16]
secret2 = b64.b64decode(secret)[16:]
p = remote('10.188.2.20',10010)
middle = []
pt = ''

for x in xrange(0,16):
for y in xrange(0,256):
p.recvuntil("Give your option:\n")
p.sendline('3')
p.recvuntil("IV:\n")
p.sendline(b64.b64encode(''.join(IV))) #send your IV
p.recvuntil("Data:\n")
p.sendline(b64.b64encode(secret1)) #send your Data
# p.sendline(b64.b64encode(secret2)) #send your Data
res = p.recvuntil("\n")
# print res
if 'bad decrypt' in res:
IV[15-x] = chr(y)
elif 'Decrpytion Done' in res:
print IV
IV[15-x] = chr(ord(IV[15-x]) ^ (x + 1)) #to get the correct middle, just like ---> IV[0] ^ 0x01 = middle[0]
middle.append(ord(IV[15-x])) #store the correct middle
print middle
pt += chr(ord(IV[15-x]) ^ ord('A')) #first plaint text
# pt += chr(ord(IV[15-x]) ^ ord(secret1[15-x])) #second plaint text
for z in xrange(0,x + 1):
IV[15-z] = chr(middle[z] ^ (x + 2)) #generate the next new IV
break
else:
print res
exit()
if y == 255:
print '[!] Something wrong'
print x + 1
exit()

print '[!] Final IV : '
print IV
print '[!] Get middle : ', middle
print '[!] PlaintText is : ' + pt[::-1]

参考

声明

  1. 博主初衷为分享网络安全知识,请勿利用技术做出任何危害网络安全的行为,否则后果自负,与本人无关!
  2. 部分学习内容来自网络,回馈网络,如涉及版权问题,请联系删除 orz

Author: AMao

Link: https://passenger-amao.github.io/2020/09/22/CBC/

Copyright: 本站所有文章均采用 署名-非商业性使用-相同方式共享 4.0 国际(CC BY-NC-SA 4.0) 许可协议。转载请注明出处!

< PreviousPost
Web通信协议详解(HTTP、SSL、HTTPS、HSTS)
NextPost >
预编译与SQL注入
CATALOG
  1. 1. CBC
    1. 1.1. 加密
    2. 1.2. 解密
  2. 2. 字节翻转攻击
    1. 2.1. iscc2018 Web300
  3. 3. Padding Oracle Attack
    1. 3.1. attack 思路
    2. 3.2. 防御
    3. 3.3. 题目
  4. 4. 参考
  5. 5. 声明