title: 分组密码CBC加密缺陷
date: 2017-05-15 10:04:47
tags: ["密码学"]
---
关于密码学的种种漏洞以及利用网上也有不少,但是比较零散,有关介绍比较局限,导致一些东西晦涩难懂不易理解,这里是一个有关于CBC分组加密的一个讲解
CBC加密模式
首先上图
这里文字描述不如看图直观,还是大致描述一下,CBC模式的加密方式是通过一个初始向量(IV)先和明文分组第一组异或后使用秘钥K加密,作为第一组密文,同时又与后一分组的明文异或后进行加密产生下一组密文,依次重复。
其解密和加密是对称的,密文先解密,再异或。
关于这个初始向量IV的完整性要比其保密性更为重要。在CBC模式下,最好是每发一个消息,都改变IV,比如将其值加一。
这里说说有关于CBC的错误传播,有利于之后字节翻转攻击的理解
其特点如下:
- 明文有一组中有错,会使以后的密文组都受影响,但经解密后的恢复结果,除原有误的一组外,其后各组明文都正确地恢复。
- 解密时,有一组秘钥错误,该错误会影响下一分组相同位置的解密
- 若在传送过程中,某组密文组出错时,则该组恢复的明文和下一组恢复数据出错。再后面的组将不会受中错误比特的影响。
字节翻转攻击
概述
有关于这里的攻击,没有下面那种方式刺激,但是效果也还是可以
这里最大的效果就是:在不知道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注入中,盲注我们通常是只有两种状态,true
和false
这里又是如何区分的呢?这里先具体看看具体原理分析
之前说到的使用条件是,我们可以修改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有一个关于此方式利用的库,项目地址