分組密碼CBC加密缺陷



title: 分組密碼CBC加密缺陷
date: 2017-05-15 10:04:47
tags: ["密碼學"]

關於密碼學的種種漏洞以及利用網上也有不少,但是比較零散,有關介紹比較局限,導致一些東西晦澀難懂不易理解,這里是一個有關於CBC分組加密的一個講解

CBC加密模式

首先上圖

這里文字描述不如看圖直觀,還是大致描述一下,CBC模式的加密方式是通過一個初始向量(IV)先和明文分組第一組異或后使用秘鑰K加密,作為第一組密文,同時又與后一分組的明文異或后進行加密產生下一組密文,依次重復。

其解密和加密是對稱的,密文先解密,再異或。

關於這個初始向量IV的完整性要比其保密性更為重要。在CBC模式下,最好是每發一個消息,都改變IV,比如將其值加一。

這里說說有關於CBC的錯誤傳播,有利於之后字節翻轉攻擊的理解

其特點如下:

  1. 明文有一組中有錯,會使以后的密文組都受影響,但經解密后的恢復結果,除原有誤的一組外,其后各組明文都正確地恢復。
  2. 解密時,有一組秘鑰錯誤,該錯誤會影響下一分組相同位置的解密
  3. 若在傳送過程中,某組密文組出錯時,則該組恢復的明文和下一組恢復數據出錯。再后面的組將不會受中錯誤比特的影響。

字節翻轉攻擊

概述

有關於這里的攻擊,沒有下面那種方式刺激,但是效果也還是可以

這里最大的效果就是:在不知道Key(秘鑰)的情況下篡改明文

通過上面的錯誤傳播,我們可以想到,解密時通過修改前一個密文分組可以影響后一個的解密后的明文分組

這里修改可以將前一個密文中的任意比特進行修改(0,1進行翻轉)

詳解

這里舉個例子:

明文是"lee-jayy:12345678$,ohh he is very rich!"

這里使用DES算法,秘鑰key = "leejleej" 初始向量iv='thisisiv'

加密結果為:04f2e7d245cec18f6c1769c66f0e30ccc30a378c58597b15409a0a8c296df6a66a10679b55ddbabb

第一步將消息分組,其des算法的分組長度是64bit,一個字符是8bit故8個字符一組,如下

第一組:lee-jayy

第二組::1234567

第三組:8$,ohh h

第四組: eis ver

第五組: y rich!

這里我想把第二組的第2個字符“1”改為“9”,這里該如何操作?

由上面的特點我們知道,修改第n分組的秘文,其第n+1分組的明文會被竄改,這里我們待修改的字符在第二分組,我們可以修改第一分組的密文來控制第二分組。

為了方便我把密文也分組一下:

04f2e7d245cec18f

6c1769c66f0e30cc

c30a378c58597b15

409a0a8c296df6a6

6a10679b55ddbabb

由於這里需要修改的字符“1”位於第二分組的第二個字符,所以我們只需修改第一分組相同的位置的密文

,一個字符是16bit兩個十六進制位,故我們需要修改第一分組密文的“f2”,這里改如何改呢?

在改之前我們先了解一個基本知識,有關於異或:

異或的規則是相同為0,不同為1

於是有1 xor 1 =0,1 xor 0 =1,0 xor 0 = 0

可以看出這么一個規則,A xor B = C <=> A xor C = B

這時候可以回看一下上面的解密的圖

我們知道的東西有:

1、第二組des算法加密后要和第一組的密文異或得到第二組的密文

2、字符1的ascii碼16進制是31

3、字符1經過cbc后的密文要和f2進行異或

這里我們並不曉得其秘鑰key,這里我們假設第二組des算法加密后16進制是ABCDEFGHIGKLMNOP,故字符1經過DES加密的結果是AB

第一步、我們根據上面的算法知道 AB xor f2 = 31,這里04是字符1的密文,我們需要解出AB,只需要f2 xor 31既可得出1經過des算法加密后是C3

第二步、我們利用上一步計算出的DES對字符1加密的結果35異或待修改的字符“9”(其ascii碼為39)。

C3 xor 39 = FA

第三步、修改密文中第一分組開始的f2為FA,修改后的密文是:

04FAe7d245cec18f6c1769c66f0e30ccc30a378c58597b15409a0a8c296df6a66a10679b55ddbabb

可以對比一下輸出:

修改前:lee-jayy:12345678$,ohh he is very rich!

修改后:4糉L柖292345678$,ohh he is very rich!

看出對應的1的確變為了9

利用同樣方法我把1234567修改為了9999999,秘文為04FAECD848C2CE816c1769c66f0e30ccc30a378c58597b15409a0a8c296df6a66a10679b55ddbabb

結果:d&φΤ5ς:99999998$,ohh he is very rich!

加密腳笨如下,有興趣可以自己試一試:

<?php

function jiami_DES($input = "",$key = "leejleej",$iv='thisisiv')
{
$td = mcrypt_module_open(MCRYPT_DES, '', MCRYPT_MODE_CBC, '');
mcrypt_generic_init($td, $key, $iv);

$encrypted_data = mcrypt_generic($td, $input);

mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return bin2hex($encrypted_data);
}

function jiemi_DES($input = "",$key = "leejleej",$iv='thisisiv')
{
$td = mcrypt_module_open(MCRYPT_DES, '', MCRYPT_MODE_CBC, '');
mcrypt_generic_init($td, $key, $iv);
$mdecrypted_data = mdecrypt_generic($td,hex2bin($input));//$encrypted_data);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $mdecrypted_data;
}

echo jiami_DES('lee-jayy:12345678$,ohh he is very rich!');
echo '<br />';
echo jiemi_DES($_GET['key']);
?>

CTF實例

from twisted.internet import reactor, protocol
from Crypto.Cipher import AES
import os
import random
from secret import KEY,KEYSIZE,IV,FLAG

PORT = 6666

def pad(instr, length):
        if(length == None):
                print "Supply a length to pad to"
        elif(len(instr) % length == 0):
                print "No Padding Needed"
                return instr
        else:
                return instr + '\x04' * (length - (len(instr) % length ))

def encrypt_block(key, plaintext):
        encobj = AES.new(key, AES.MODE_ECB)
        return encobj.encrypt(plaintext).encode('hex')

def decrypt_block(key, ctxt):
        decobj = AES.new(key, AES.MODE_ECB)
        return decobj.decrypt(ctxt).encode('hex')

def xor_block(first,second):
        if(len(first) != len(second)):
                print "Blocks need to be the same length!"
                return -1

        first = list(first)
        second = list(second)
        for i in range(0,len(first)):
                first[i] = chr(ord(first[i]) ^ ord(second[i]))
        return ''.join(first)

def encrypt_cbc(key,IV, plaintext):
        if(len(plaintext) % len(key) != 0): #加密文本的長度必須能整除Key,否則在后面加x40
                plaintext = pad(plaintext,len(key))
        blocks = [plaintext[x:x+len(key)] for x in range(0,len(plaintext),len(key))]
        for i in range(0,len(blocks)):
                if (i == 0):
                        ctxt = xor_block(blocks[i],IV)
                        ctxt = encrypt_block(key,ctxt)
                else:
                        tmp = xor_block(blocks[i],ctxt[-1 * (len(key) * 2):].decode('hex')) 
                        ctxt = ctxt + encrypt_block(key,tmp)
        return ctxt

def decrypt_cbc(key,IV,ctxt):
        ctxt = ctxt.decode('hex')
        if(len(ctxt) % len(key) != 0):
                print "Invalid Key."
                return -1
        blocks = [ctxt[x:x+len(key)] for x in range(0,len(ctxt),len(key))]
        for i in range(0,len(blocks)):
                #print blocks[0].encode('hex')
                if (i == 0):
                        ptxt = decrypt_block(key,blocks[i])
                        ptxt = xor_block(ptxt.decode('hex'),IV)
                        #print ptxt.encode('hex')
                else:
                        tmp = decrypt_block(key,blocks[i])
                        tmp = xor_block(tmp.decode('hex'),blocks[i-1])
                        ptxt = ptxt + tmp
        return ptxt


def mkprofile(email):
	if((";" in email)):
		return -1
	prefix = "comment1=wowsuch%20CBC;userdata="
	suffix = ";coment2=%20suchsafe%20very%20encryptwowww"
	
	ptxt = prefix + email + suffix #連接字符串
	print ptxt
	return encrypt_cbc(KEY,IV,ptxt)	


def parse_profile(data):
	print "DATA:"
	print data	
	ptxt = decrypt_cbc(KEY,IV,data.encode('hex'))
	ptxt = ptxt.replace("\x04","")
	print ptxt
	if ";admin=true" in ptxt:
		return 1
	return 0


class MyServer(protocol.Protocol):
    def dataReceived(self,data):
	if(len(data) > 512):
		self.transport.write("Data too long.\n")
		self.transport.loseConnection()
		return

	if(data.startswith("mkprof:")):
		data = data[7:]
		resp = mkprofile(data)
		if (resp == -1):
			self.transport.write("No Cheating!\n")
		else:
			self.transport.write(resp + '\n')
	elif(data.startswith("parse:")):
		self.transport.write("Parsing Profile...")
		data = data[6:].decode('hex')
		if (len(data) % KEYSIZE != 0):
			self.transport.write("Invalid Ciphertext <length>\n")
			self.transport.loseConnection()
			return
		
		if(parse_profile(data) == 1):
			self.transport.write("Congratulations!\nThe FLAG is: ")
			self.transport.write(FLAG)
			self.transport.loseConnection()
		
		else:
			self.transport.write("You are a normal user.\n")
	
	else:
		self.transport.write("Syntax Error")
		self.transport.loseConnection()


class MyServerFactory(protocol.Factory):
    protocol = MyServer



factory = MyServerFactory()
reactor.listenTCP(PORT, factory)
reactor.run()

wp:

from pwn import *
sh = remote('133.130.52.128',6666)
target = ";admin=true"
email = '0000000000000000000000'
prefix = "comment1=wowsuch%20CBC;userdata="
suffix = ";coment2=%20suchsafe%20very%20encryptwowww"
ptxt = prefix + email + suffix

sh.send('mkprof:' + email)
s = sh.recv(1024)[0:len(ptxt)*2]
s = list(s.decode('hex'))
for i in range(len(target)):
    s[32+i] = p8( u8(s[32+i]) ^ u8(target[i]) ^ u8(ptxt[48+i]) )
s = ''.join(s).encode('hex')
sh.send('parse:' + s)
print sh.recv(1024)

Padding Oracle Attack

概述

有關於這種方式的攻擊,實在是巧妙!

首先說一下使用條件,以及可以達到的效果。

使用條件:

一、我們可以修改iv(在我的理解其實這里如果數據分組大於1組其實不需要iv也可以進行攻擊)

二、我們知道密文

三、我們可以利用服務端進行解密

效果:獲取明文!(如果分組大於1組,沒有iv的情況這里可以獲取到部分明文)

詳解

首先介紹一下對於分組加密過程中數據填充常用的方式(PKCS#5)

這種方式其實就是缺少多少字節就補充多少字節,其補充的數值就是填充的字節的數量比如數據差2字節,我們就補充兩個0x02即可,就是差幾個補幾個幾,如下圖。

下面我們回顧一下關於cbc模式的解密方式

這里對每一分組進行一下細分,

可以看出末尾的數值為四個0×04,即為填充,這里如果填充的數值不對或者沒有進行填充,解密過程往往不會進行,同時程序會報異常。

有了這個特性,我們就有了利用點,在這一點有些類似於在SQL注入中的盲注的思想。

下面具體分析一下:

在sql注入中,盲注我們通常是只有兩種狀態,truefalse

這里又是如何區分的呢?這里先具體看看具體原理分析

之前說到的使用條件是,我們可以修改iv、知道密文同時我們可以利用服務端進行解密

這里構造一個例子,如果服務端如果給用戶設置了這樣的cookie:key=6d367076036e2239|f851d6cc68fc9537

我們推測出‘|’之前的是iv,其后面的是加密結果,這個時候如果這里我將iv設置為0000000000000000

即為key=0000000000000000|f851d6cc68fc9537,發現這個時候頁面出錯了

看一下這樣子的解密過程

這里之前提到的有關填充,可以知道不管消息多長,最后肯定是有填充的,其中填充數值范圍和分組的大小有關,這里如果是DES算法8字節一組的話那么最后一位的范圍一定是在0x01~0x08之間的,這里0x3D不是這個范圍故出錯。

這時候使程序不出錯只有最后一位是0x01才可以

我們開始修正iv,發現當iv為000000000000003C的時候程序沒有報錯了,這個時候分析一下解密過程

這個這個時候0x3D異或0x3C為0x01,程序以為這里的填充位為一位,之前的全是數據,沒毛病,開始解密

我們在這里0x3c可以窮舉得出,中間值0x3d可以利用0x01異或0x3c計算出,同時我們還知道一個原始的iv=6d367076036e2239。

由於明文 = 中間值 xor IV,我們列兩個式子

0x01 = 中間值 xor 0x3c

待求明文 = 中間值 xor 0x39

這里一個二元一次方程,不難解出待求明文。

這時候繼續向前一位進行攻擊,這時候需要后兩位為0x02

第八位已經計算出,這里只需要重復之前的方法計算第七位即可

最開始看到這里會覺得有些疑問,為什么我們不能直接多位一起計算,這里可能會出現這么一個情況,假設最后一位是一個合理的填充(假設八字節分組0x01~0x08)第七位是個錯誤的值時候,就會出現非預期的錯誤,比如中間值的后兩位為0x00和0x02,我們構造的初始向量的后兩位為0x02和0x00,這時候程序並不會報錯。

以此類推我們便可以獲取到明文。

如果這里我們並不知道初始iv,在這里我們依然可以進行攻擊,只不過第一組的數據無法獲取到

仔細回顧一下上述的加密方式,我們的初始iv只是加密第一組數據中用得到,加密第二組數據的時候其實就是用的第一組的密文充當iv的角色繼續加密,我們這里只需要不斷修改分組上一分組的密文即可得到下一組的明文。

實例

有關的例子:

<?php 
$type = "aes-128-cbc";	//加密類型,即分組大小為16
$P = "aaaaaaa";	//明文
$Key = "aAbBcCdDeE";	//加密要用到的Key
$IV = "thisivthisivthis";	//初始化向量,因為有一個異或的過程,所以它的大小和分組大小要一樣
$C = openssl_encrypt($P, $type, $Key, OPENSSL_RAW_DATA, $IV);
//滿足padding oracle attack前提條件1
print "iv: ".bin2hex($IV)."<br>";
print "c: ".bin2hex($C)."<br>";	//可能存在不可顯示的字符,加個base64的編碼

if(isset($_GET['s']) && isset($_GET['iv'])){
	$s = hex2bin($_GET['s']);
	$iv = hex2bin($_GET['iv']);
	if(($n = openssl_decrypt($s, $type, $Key, OPENSSL_RAW_DATA, $iv)) !== false){		//解密失敗會返回false
		//bit flipping attack
		echo $n;
		if($n === "admin"){
			print "well done!";
		}
	}else{
		//滿足padding oracle attack前提條件2
		die("Fail!");
	}
}
?>

腳本:

import requests
import base64
url = 'http://192.168.248.1/test/demo.php'
N = 16
l = [0] * N
iv = '74686973697674686973697674686973'.decode('hex')
tmp_iv = ''
out = [0] * N
s = ''
for i in range(1, N+1):
	for c in range(0,256):
		l[N-i] = c
		tmp_iv = ''
		for m in l:
			tmp_iv += chr(m)
		print tmp_iv.encode('hex')
		payload = "?s=e211ffa0baa91627a5827f3867a0cff1&iv=" + tmp_iv.encode('hex')
		#print payload
		data = requests.get(url+payload).content
		if 'Fail!' not in data:
			out[N-i] = c ^ i
			for y in range(i):
				l[N-y-1] = out[N-y-1] ^ (i+1)
			break
for i in range(N):
	out[i] = out[i] ^ ord(iv[i])
for c in out:
	s += chr(c)
print s

python有一個關於此方式利用的庫,項目地址

參考

http://k1n9.me/2017/03/16/attack-in-cbc/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM