ctf中web题目一般会有一个或多个包含密码学的题目,这块一直是弱项,这几天恶补了一波,总结一下,方便之后复习、寻找思路。
写到最后发现有点多了,做个目录。。

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
<?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, "\0");
return $decrypted;
}

if (@$_GET['a'] == 'reg'){
setcookie('uid', AES('9'));
setcookie('username', AES($_POST['username']));
header("Location: ecb.php");
exit();
}

if (@!isset($_COOKIE['uid'])||@!isset($_COOKIE['username'])){
echo '<form method="post" action="ecb.php?a=reg"> Username:<br>';
echo '<input type="text" name="username"> <br>';
echo 'Password:<br> <input type="text" name="password" > <br><br>';
echo '<input type="submit" value="register">';
echo '</form>';
} else {
$uid = DE__AES($_COOKIE['uid']);
if ( $uid != '4'){
echo 'uid:' .$uid .'<br/>';
echo '' . DE__AES($_COOKIE['username']) .'<br/>';
echo 'You are not administrotor!!';
} else {
echo "Hi you are administrotor!!" .'<br/>';
echo 'Flag is 360 become better';
}
}
?>

简单分析一下,只要uid == 4,就可以得到flag。而在前面将uid设置为9并加密,而且将注册的username也进行了加密,两者都使用了ebc加密。username是可控的,可以通过注册一个特殊的用户名来获得uid的密文进行替换。
首先我们通过fuzz来获取分组的长度,代码如下:

1
2
3
4
5
6
7
import requests

url = 'http://10.10.10.1/test/ecb.php?a=reg'
for i in range(1, 31):
cookies = requests.post(url, data={'username': 'a'*i, 'password':'1'}, allow_redirects=False).cookies
username = base64.b64decode(urllib.unquote(cookies['username']))
print len('a'*i), len(username)

通过查看username即密文的长度即可判断分组的长度

1
2
3
4
5
6
7
......
13 16
14 16
15 16
16 16
17 32
.......

可以看到分组的长度为16,根据分析写出利用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import base64
import urllib

url = "http://10.10.10.1/test/ecb.php"

cookies = requests.post(url+'?a=reg', data={'username': 'aaaaaaaaaaaaaaaa4', 'password': '123456'}, allow_redirects=False).cookies
#分组长度为16,所以注册一个长度为17的用户名。
#得到32位的密文,前16位为aaaaaaaaaaaaaaaa的密文,后16位为4的密文
username = base64.b64decode(urllib.unquote(cookies['username']))
uid = urllib.quote_plus(base64.b64encode(username[16:]))

r = requests.get(url, cookies={'uid': uid, 'username': cookies['username']})
print r.text

CBC

参考:分组密码模式: CBC模式(密码分组链接模式)

cbc加密方式同样是先进行分组,区别在于将上一组的密文先与下一组的明文进行XOR,然后在进行加密,解密过程类似。
cbc字节翻转攻击(http://www.tuicool.com/articles/vEVFZz)在ctf中非常常见,一般有两种攻击方式:

  1. iv向量,影响第一个明文分组
  2. 第n个密文分组,影响第n+1个明文分组

其中第二个尤为常见,网上有很多题目,这里就不举例子了。放一个比较特殊的,上面两个攻击点都有利用,代码如下:

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
<!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>
</head>

<?php
define("SECRET_KEY", 'kH3LH3sk45HLsd3n');
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>

大体流程就是注册一个用户,将数据序列化后加密,保存到cookie中。注册的用户不能是admin,但只有admin才能看到flag。iv和cipher都放在cookie中,就是可控的,我们可以注册一个1dmin用户,然后通过修改密文进行字节翻转将1翻转为a。但是这样做必然会将前一组数据破坏掉,导致解密出来的数据乱码。注意看check_login这个函数中的这行代码

1
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");

如果数据被破坏,unserialize是无法执行成功的,我们就拿不到flag。还记得cookie里的iv吗?它并不是没有用的,我们需要再对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
# -*- coding: utf-8 -*-
import requests
import base64
import urllib
import re

url = 'http://10.10.10.1/test/cbc/index.php'

def getCookies():
global url
data = {
'username': '1dmin',
'password': '12345',
}
cookies = requests.post(url, data=data).cookies
return cookies

def exploit(cookies):
global url

cipher = base64.b64decode(urllib.unquote(cookies['cipher']))
pos = 9 #偏移量为9
cipher = cipher[0:pos] + chr(ord(cipher[pos]) ^ ord('1') ^ ord('a')) + cipher[pos+1:]
cipher = urllib.quote_plus(base64.b64encode(cipher))
cookies = {
'PHPSESSID': cookies['PHPSESSID'],
'iv' : cookies['iv'],
'cipher' : cipher,
}

r = requests.get(url, cookies=cookies)
#print r.text
plain = base64.b64decode(re.findall(r'base64.decode\((.*?)\)', r.text)[0])
iv = base64.b64decode(urllib.unquote(cookies['iv']))

old = plain[0:16]
new = 'a:2:{s:8:"userna'
newiv = ''.join([chr(ord(iv[i]) ^ ord(old[i]) ^ ord(new[i])) for i in xrange(16)])
newiv = urllib.quote_plus(base64.b64encode(newiv))

cookies = {
'PHPSESSID': cookies['PHPSESSID'],
'iv' : newiv,
'cipher' : cipher,
}

r = requests.get(url, cookies=cookies)
print (r.text)

if __name__ == '__main__':
cookies = getCookies()
exploit(cookies)

cbc就介绍这么多

padding oracle attack

padding oracle attack其实也是对cbc模式加密的一种攻击,具体分析下面这篇文章介绍的很详细,不再赘述。
我对Padding Oracle攻击的分析和思考(详细)
这里截取一些重要的部分(划个重点~)

攻击成立的两个重要条件
1. 攻击者能够获得密文(Ciphertext),以及附带在密文前面的IV(初始化向量)
2. 攻击者能够触发密文的解密过程,且能够知道密文的解密结果

padding只有可能在0x01~0x08之间

还有一些我们看着测试代码来分析,代码取自下面两位大佬的blog。
padding oracle攻击原理分析(附带rsa资料)
从DES加密到Padding Oracle Attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$string = 'bertram';
$key = '123456789';
$cipher = MCRYPT_DES;
$modes = MCRYPT_MODE_CBC;

if(isset($_GET['iv']) && isset($_GET['str'])){
$iv = hex2bin($_GET['iv']);
#echo $iv;
$str = hex2bin($_GET['str']);
$decode = mcrypt_decrypt($cipher, $key, $str, $modes, $iv);
#echo bin2hex($iv)."br>";
#echo bin2hex($encode)."br>";
echo bin2hex($decode);
$temp = bin2hex($decode);
} else {
$iv = hex2bin("08fe1649311fd298");
$encode = mcrypt_encrypt($cipher, $key, $string, $modes, $iv);
echo "the iv is <h2>".bin2hex($iv)."</h2><br>";
echo "the cipher is <h2>".bin2hex($encode)."</h2><br>";
}
?>

如果代码运行不了的话应该是没有安装或php没有开启Mcrypt扩展。
首先什么参数也不加,获取iv及密文,满足攻击所需的两个条件。

接着我们将密文取出,并将iv置为0000000000000000

下面就是不断调整iv直到算出正确的padding

这里个人认为还是要把这个图解释一下,图中的Intermediary Value是中间值,Initialization Vector才是初始化的iv。
以测试代码为例,padding 0x01的结果为

1
2
3
中间值 ^ 0x99 = 0x01
中间值 = 0x99 ^ 0x01 = 0x98
明文 = 中间值 ^ 原始iv = 0x98 ^ 0x98 = 0x00

同理padding 0x02,需要再次调整iv(0x98 ^ 0x02 = 0x9a),这地方可能有点绕,靠个人理解吧。。。
利用脚本如下:

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
import requests

#方便起见直接将iv和密文写死
#需要获得这两个值才能进行padding oracle attack攻击
iv = '08fe1649311fd298'
cipher = '262b4647cc70d363'

url = 'http://seaii-blog.com/test/padding.php'
tmp_iv = '0000000000000000'

tail = ''
result = ''

for i in xrange(1, 9): #iv长度为16,8个分组
for j in xrange(256): #0x00~0xff进行穷举
#两位为一组,不满两位补0
test = ('0' + str(hex(j)[2:])) if j < 16 else str(hex(j)[2:])
#构造测试iv
test_iv = tmp_iv[i*2:] + test + tail
#print test_iv

r = requests.get(url, params={'iv': test_iv, 'str': cipher})
tmp = r.text
if tmp[(8-i)*2:(9-i)*2] == '0'+str(i):
#计算中间值
mid_val = eval('0x'+test) ^ eval('0x'+str(i))
#计算明文(中间值 ^ 原iv)
result = chr(mid_val ^ eval('0x'+iv[(8-i)*2:(9-i)*2])) + result
#修改iv进行下一轮攻击
tail = hex(mid_val ^ eval('0x'+str(i+1)))[2:] + tail
print test_iv
break

print result

只测试了第一轮,运行结果如下:

如果想测试下一分组,根据cbc加密的原理,只要把iv换为第一分组的密文即可。

利用工具:

实战: Padding Oracle Attack 笔记

CFB

参考:分组密码模式: CFB模式(密文反馈模式)

cfb的题目比较少,在这里找到了一个Hitcon 2015的题目
https://nusgreyhats.org/write-ups/HITCONCTF-Quals-2015-Simple-(Crypto-100)/
原题是用ruby写的,这里转化为php。

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
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<?php
//error_reporting(0);
define("SECRET_KEY", 'kH3LH3sk45HLsd3n');
define("METHOD", "aes-128-cfb");

function get_random_iv() {
$random_iv = '';
for($i = 0; $i < 16; $i++){
$random_iv .= chr(rand(1, 255));
}
return $random_iv;
}

$username = @$_POST['user'];
$password = @$_POST['pass'];

if(isset($username) && isset($password)) {
$iv = get_random_iv();

$info = array(
'username' => $username,
'password' => $password,
);
$info = json_encode($info);
$cipher = openssl_encrypt($info, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);

setcookie('iv', $iv);
setcookie('auth', $cipher);
header('Location: cfb.php');
} else {
if(isset($_COOKIE['auth']) && isset($_COOKIE['iv'])){
$cipher = $_COOKIE['auth'];
$iv = $_COOKIE['iv'];
$info = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
$info = (array)json_decode($info);

if($info['admin'] === true){
exit('flag{123456}');
} else {
echo 'Hello '.$info['username'];
}
} else {
echo '<body>
<form action="" method="post">
<input type="text" name="user"><br>
<input type="password" name="pass"><br>
<input type="submit" value="register">
</form>
</body>';
}
}
?>
</html>

套路和上面的都差不多,注册一个用户,将usernaem和password json一下,然后加密存到cookie中,json_decode之后info['admin'] === true即可。但是正常情况下应该只有info[‘username’]和info[‘password’]。
利用脚本如下:

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
import requests
import base64
import urllib

url = 'http://10.10.10.1/test/cfb.php'
cookies = requests.post(url, data={'user': '1', 'pass': '123456'}, allow_redirects=False).cookies

old_iv = urllib.unquote(cookies['iv']).encode('hex')
cipher = urllib.unquote(cookies['auth']).encode('hex')

first_block = cipher[0:32] #取第一个密文分组
plain = '{"username":"1",' #与上面对应的明文分组
#注意看上面的加密过程,iv是先经过加密之后再与明文进行xor得到密文
#这个加密过程我们不知道,可以通过密文 ^ 明文得到加密后的iv
encrypted_iv = hex(int(plain.encode('hex'), 16) ^ int(first_block, 16))[2:-1]

#修改明文获取新的密文
new_plain = '{"admin": true }'
new_cipher = hex(int(encrypted_iv, 16) ^ int(new_plain.encode('hex'), 16))[2:-1]

new_cookies = {
'auth': urllib.quote_plus(new_cipher.decode('hex')),
'iv' : cookies['iv'],
}
r = requests.get(url, cookies=new_cookies)
print r.text

具体分析代码中写的很详细了,但是这个题目没有体现cfb最典型的攻击——重放攻击,关于这种攻击的一个题目见:cfb重放攻击

hash长度扩展攻击

详细介绍见另一篇博文:hash长度扩展攻击研究

参考资料