写这篇文章呢我会去网上找一下资料,把自己的理解写下来,然后找一些实例来加深。

ECB模式(Electronic codebook,ECB)

ECB模式是先对明文进行分组,然后对每组进行加密,加密之后再将所有的密文连接起来,所有的明文分组使用相同的密钥加密,也就是说如果明文相同,那么加密之后的密文也相同。

这里直接拿网上一个师傅写的例子来说

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
<?php
function AES($data){
$privateKey = "12345678123456781234567812345678";
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $privateKey, $data, MCRYPT_MODE_ECB);
$encryptedData = (base64_encode($encrypted));
return $encryptedData;
}
function DE__AES($data){
$privateKey = "12345678123456781234567812345678";
$encryptedData = base64_decode($data);
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $privateKey, $encryptedData, MCRYPT_MODE_ECB);
$decrypted = rtrim($decrypted, "") ;
return $decrypted;
}
if (@$_GET['a']=='reg'){
setcookie('uid', AES('9'));
setcookie('username', AES($_POST['username']));
header("Location: http://127.0.0.1/mima/ecb.php");
exit();
}
if (@!isset($_COOKIE['uid'])||@!isset($_COOKIE['username'])){
echo '<form method="post" action="ecb.php?a=reg">
Username:<br>
<input type="text" name="username">
<br>
Password:<br>
<input type="text" name="password" >
<br><br>
<input type="submit" value="注册">
</form> ';
}
else{
$uid = DE__AES($_COOKIE['uid']);
if ( $uid != '4'){
echo 'uid:' .$uid .'<br/>';
echo 'Hi ' . DE__AES($_COOKIE['username']) .'<br/>';
echo 'You are not administrotor!!';
}
else {
echo "Hi you are administrotor!!" .'<br/>';
echo 'Flag is 360 become better';
}
}
?>

首先可以看到注册的账号和密码都是用ECB模式加密的,然后权限的判断是根据Cookie里面,Cookie里面有个验证用户的和验证用户权限的uid,这里呢用户的明文是我们可以操控的,并且可以拿到加密之后的密文,也就是说我们可以先fuzz出分组的字节,然后构造个多出的字节,那么多出来的字节也就是会单独加密,然后长度拼接,那么我们就可以在不知道密钥的情况下拿到对应内容的加密结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*-coding:utf-8-*-
import requests
import time
import urllib
import base64
url = "http://127.0.0.1/mima/ecb.php?a=reg"
a = "a"
for i in range(1,33):
data = {
"username":a*i,
"password":"aa"
}
Cookies = requests.post(url=url,data=data, allow_redirects=False).cookies
print len(base64.b64decode(urllib.unquote(Cookies['username']))),i

这样可以先fuzz出长度

1
2
3
4
5
16 14
16 15
16 16
32 17
32 18

可以知道分组是以16个字节为一组

构造16*a+"4"那么我们取后16位加密内容去替换UID的值就可以成为4权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#-*-coding:utf-8-*-
import requests
import time
import urllib
import base64
url = "http://127.0.0.1/mima/ecb.php?a=reg"
a = "a"
for i in range(16,17):
data = {
"username":a*i+"4",
"password":"aa"
}
Cookies = requests.post(url=url,data=data, allow_redirects=False).cookies
print base64.b64encode(base64.b64decode(urllib.unquote(Cookies['username']))[:16])
print base64.b64encode(base64.b64decode(urllib.unquote(Cookies['username']))[16:])

CBC模式及攻击

密码分组链接(CBC,Cipher-block chaining)模式。在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有密文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量。

  • 也就是说首先明文分组,常见以16字节128位为一组,位数不足的使用特殊字符填充,填充规则一般是PKCS#7或PKCS#5格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PKCS #7 填充字符串由一个字节序列组成,每个字节填充该填充字节序列的长度。
假定块长度为 8,数据长度为 9,
数据: FF FF FF FF FF FF FF FF FF
PKCS7 填充: FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07
简单地说, PKCS5, PKCS7和SSL3, 以及CMS(Cryptographic Message Syntax)
有如下相同的特点:
1)填充的字节都是一个相同的字节
2)该字节的值,就是要填充的字节的个数
如果要填充8个字节,那么填充的字节的值就是0×8;
要填充7个字节,那么填入的值就是0×7;
如果只填充1个字节,那么填入的值就是0×1;
这种填充方法也叫PKCS5, 恰好8个字节时还要补8个字节的0×08
  • 然后生成一个随机的初始化向量(IV)和一个密钥key。
  • 将IV和第一组明文异或,然后用密钥对其加密。
  • 将上一步产生的密文与第二组进行异或,然后用密钥对其加密
  • 重复操作,直到最后一组明文
  • 将IV和加密后的密文拼接起来,得到最终的密文。

步骤如下:

1
2
Ciphertext[0] = Encrypt(Plaintext xor IV)
Ciphertext[1] = Encrypt(Plaintext xor Ciphertext[0])

解密的话是一个相反的过程

  • 首先从密文中提取出IV,然后对密文进行分组。
  • 使用密钥对第一组密文解密,然后解密结果与IV异或。
  • 使用密钥对第二组密文解密,然后解密结果与上一步的结果进行异或。
  • 依次进行。
1
2
Plaintext[0] = Decrypt(Ciphertext[0]) xor IV
Plaintext[1] = Decrypt(Ciphertext[1]) xor Ciphertext[0]

用我及其烂的字画了画,直接想的话可能不好想,如果画出来就很明显了。

例题

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
#coding=utf_8
import flag
from Crypto.Cipher import AES
def depadding(message):
padLength = ord(message[-1])
message = message[:-padLength]
return message
def decrypt(message):
key = flag.flag[5:-1]
iv = key
aes = AES.new(key, AES.MODE_CBC, iv)
plain = aes.decrypt(message)
# plain = depadding(plain)
return plain
def hasHighASCII(plain):
for i in range(len(plain)):
if ord(plain[i]) > 127:
return True
return False
def runTheServer():
message = raw_input()
try:
message = message.decode("base_64")
except:
print "ERROR!!!\nThe message should be encoded in bases64!"
return
if len(message) % 16 != 0:
print "ERROR!!!\nLength of the cipher text should be times of 16!"
return
plain = decrypt(message)
if hasHighASCII(plain):
print "ERROR!!!\nThere are some high ASCII characters in the plain text. The plain text is encode in base_64 is:\n" + plain.encode("base_64")
elif "I_enjoy_cryptography" in plain:
print "Good! You did it!"
else:
print "Hint: Try to get the key, encrypt a message that contains \"I_enjoy_cryptography\" with it, and send the ciphertext to me!"
if __name__ == "__main__":
#运行命令行
#socat tcp4-listen:5678,fork exec:"python server.py",pty,stderr
runTheServer()

这道题目的flag是作为IV参与运算的

发送任意一组密文,得到明文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
λ nc 192.168.190.128 12358
MTIzNDU2NzgxMjM0NTY3OA==
ERROR!!!
There are some high ASCII characters in the plain text. The plain text is encode in base_64 is:
6TKusViZN7iXxvZWEEnUug==
λ nc 192.168.190.128 12358
ODc2NTQzMjE4NzY1NDMyMTEyMzQ1Njc4MTIzNDU2Nzg=
ERROR!!!
There are some high ASCII characters in the plain text. The plain text is encode in base_64 is:
swIRqgHgf0GLQDn/TXvh4rhzpe8J01rg3K6kAkodg/k=
c1 = MTIzNDU2NzgxMjM0NTY3OA==
p1 = 6TKusViZN7iXxvZWEEnUug==
c2 = "a"*16 + c2 = ODc2NTQzMjE4NzY1NDMyMTEyMzQ1Njc4MTIzNDU2Nzg=
p2 = swIRqgHgf0GLQDn/TXvh4rhzpe8J01rg3K6kAkodg/k=

这样的话看解密过程,

1
2
3
4
p2 = c1 xor (与key运算后的)
p1 = IV xor (与key运算后的)
c1 xor p2 = p1 xor IV
IV = c1 xor p2 xor p1

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
import base64
p2 = "swIRqgHgf0GLQDn/TXvh4rhzpe8J01rg3K6kAkodg/k="
p1 = "6TKusViZN7iXxvZWEEnUug=="
c1 = "ODc2NTQzMjE4NzY1NDMyMQ=="
tmp2 = base64.b64decode(p2)[16:]
tmp1 = base64.b64decode(p1)
c1 = base64.b64decode(c1)
flag = ''
for i in range(16):
flag += chr(ord(tmp1[i]) ^ ord(tmp2[i]) ^ ord(c1[i]))
print flag
out:iv=key_is_danger

例题

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Login Form</title>
<link href="static/css/style.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="static/js/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$(".username").focus(function() {
$(".user-icon").css("left","-48px");
});
$(".username").blur(function() {
$(".user-icon").css("left","0px");
});
$(".password").focus(function() {
$(".pass-icon").css("left","-48px");
});
$(".password").blur(function() {
$(".pass-icon").css("left","0px");
});
});
</script>
</head>
<?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 SKCTF{CBC_wEB_cryptography_6646dfgdg6}</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>

这道题呢攻击iv和第一组密文,通过解密过程可以知道要是想修改第二组明文的话,只需要修改第一组密文即可

1
2
3
p2 = c1 xor dec(key,c2)
p2_new = c1_new xor dec(key,c2)
c1_new = p2_new xor p2 xor c1

编写脚本

1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*-
import base64
cipher = '2sbYqzixdgeMUHGgchooFv+9KB0gzxsEj2fvZD+zxRuI8RTraW09SHTwDfm1SI0R8sSwTlKZ0Hdn06bXsoG7aw=='.decode('base64')
old = "me\";s:5:\"admi0\";"
new = "me\";s:5:\"admin\";"
tmp = ''
for i in range(16):
tmp += chr(ord(old[i]) ^ ord(new[i]) ^ ord(cipher[i]))
tmp = tmp + cipher[16:]
print tmp.encode("base64")
#2sbYqzixdgeMUHGgckQoFv+9KB0gzxsEj2fvZD+zxRuI8RTraW09SHTwDfm1SI0R8sSwTlKZ0Hdn06bXsoG7aw==

但是如果修改了第一组密文的话就会破坏第一组的解密明文,相同的道理

1
2
3
p1 = IV xor dec(key,c1)
p1_new = IV_new xor dec(key,c1)
IV_new = p1 xor p1_new xor IV

利用脚本计算新的IV

1
2
3
4
5
6
7
iv = "WvYz6LHl3Xl9y9uyUKtYtw==".decode("base64")
old = "a:2:{s:8:\"userna"
new = "ckjW586ZbpTrFRqquX5Tl21lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjI6ImZmIjt9".decode("base64")
IV = ''
for i in range(16):
IV += chr(ord(new[i]) ^ ord(old[i]) ^ ord(iv[i]))
print IV.encode("base64")

替换后即可。

Padding orcale

填充规则

加密

解密

解密之后是有填充的,如果解密之后的填充不符合要求,那么在WEB中一般会500,如果符合要求,则会是200,那么可以伪造IV的话通过爆破IV,让IV与中间值异或

1
2
3
Plain = IV xor 中间值
中间值 = IV xor Plain

我们已知明文的填充规则,那么爆破IV的话就可以计算出中间值,那么拿到中间值以后

1
真实Plain = 真实IV xor 中间值

也就是说在尝试爆破最后一位的话

比如说我们已知

1
IV = '1234567890abcdef'

通过爆破最后一位得到

1
2
0x6e xor middle = 0x01
middle = 0x01 xor 0x6e = 0x6f

当我们爆破倒数第二位的时候,也就是data 0x02 0x02

1
新IV最后一位 = 0x02 xor middle = 0x02 xor 0x6f = 0x6d

编写脚本如下

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
import requests
import base64
url = "http://127.0.0.1/test.php"
N = 16
def inject_cookie(token):
header = {"Cookie": "PHPSESSID=" + phpsession + ";token=" + token}
result = requests.post(url, headers = header).text
return result
def pad(string, N):
l = len(string)
if l != N:
return string + chr(N - l) * (N - l)
def xor(a,b):
print a.encode("hex"), b.encode("hex")
return "".join([chr(ord(a[i]) ^ ord(b[i % len(b)])) for i in xrange(len(a))])
session = requests.get(url).headers['Set-Cookie'].split(',')
phpsession = session[0].split(";")[0][10:]
print phpsession
token = session[1][6:].replace("%3D", '=').replace("%2F", '/').replace("%2B", '+').decode('base64')
print token
m = ''
for j in range(1,17):
padding = xor(m,chr(j)*(j-1))
for i in range(256):
tt = (chr(0)*(16-j) + chr(i)) + padding
print tt.encode("hex")
content = inject_cookie(base64.b64encode(tt))
if "Error" not in content:
m = chr(i ^ j) + m
print tt.encode("hex")
break
print len(m)
middle1 = m

但是这样只能爆破后15位,因为当爆破第16位的时候是16个0x10,解密的全是padding的plaintext,即NULL,但是我们可以爆破第一位,现在我们已经拿到了后15位的middle

1
2
3
4
5
6
middle xor token = plaintext
new_plaintext = new_token xor middle
middle = new_plaintext xor new_token = token xor plaintext
new_token = token xor plaintext xor new_plaintext

然后通过传入新的token也就是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
47
48
49
50
51
52
import requests
import base64
url = "http://127.0.0.1/test.php"
N = 16
def inject_cookie(token):
header = {"Cookie": "PHPSESSID=" + phpsession + ";token=" + token}
result = requests.post(url, headers = header).text
return result
def pad(string, N):
l = len(string)
if l != N:
return string + chr(N - l) * (N - l)
def xor(a,b):
print a.encode("hex"), b.encode("hex")
return "".join([chr(ord(a[i]) ^ ord(b[i % len(b)])) for i in xrange(len(a))])
session = requests.get(url).headers['Set-Cookie'].split(',')
phpsession = session[0].split(";")[0][10:]
print phpsession
token = session[1][6:].replace("%3D", '=').replace("%2F", '/').replace("%2B", '+').decode('base64')
print token
m = ''
for j in range(1,17):
padding = xor(m,chr(j)*(j-1))
for i in range(256):
tt = (chr(0)*(16-j) + chr(i)) + padding
print tt.encode("hex")
content = inject_cookie(base64.b64encode(tt))
if "Error" not in content:
m = chr(i ^ j) + m
print tt.encode("hex")
break
print len(m)
middle1 = m
print "middle:",middle1
if len(middle1) == 15:
for i in xrange(0, 256):
middle = chr(i) + middle1
print "token:" + token
print "middle:" + middle
plaintext = xor(middle,token)
print "plaintext:" + plaintext
des = pad('admin', N)
tmp = ""
print des.encode("base64")
for i in xrange(16):
tmp += chr(ord(token[i]) ^ ord(plaintext[i]) ^ ord(des[i]))
print tmp.encode('base64')
result = inject_cookie(base64.b64encode(tmp))
if "You are admin!" in result:
print result
print "success"
exit()