CBC字节反转攻击详解

时间:2024-10-26 15:56:51

前言

CBC全称为密码分组链接模式。不同于ECB模式,在CBC模式中,每个明文块先与前一个密文块儿进行异或后,在进行加密。这种加密模式很好的隐藏了明文的统计特性,但是同样也暴露出了一个很严重的缺点。本文将针对CBC模式的特点,详细讲解字节反转攻击的原理及应用。

一、CBC加密模式

在这里插入图片描述

  • Plaintext:明文

  • IV:初始化向量

  • Ciphertext:加密后的数据

  • Key:密钥

加密过程转化成文字来叙述:

  1. 先将Plaintext分组(常见以16字节为一组)
  2. 生成初始化向量IV和密钥Key(IV不需要保密,但一定要不可预测)
  3. 将IV和明文1组异或
  4. 将3中所得结果用Key进行加密,得到密文1组
  5. 将密文1组当作新IV与明文2组进行异或处理
  6. 将5中的结果用Key进行加密,得到密文2组
  7. 重复上述步骤,最后将IV和所有的密文分组拼接起来,得到最终密文

二、CBC解密模式

在这里插入图片描述
​ 解密过程转化成文字来叙述:

  1. 从密文中提取出IV,并将密文分组
  2. 用Key将密文1组解密
  3. 2中解密的结果与IV异或,获得明文1组
  4. 密文1组成为新一轮的IV
  5. 重复上述步骤,将所有明文分组拼接后,得到最终明文

三、CBC字节反转攻击原理

在这里插入图片描述

字节反转攻击原理文字描述:

​ 我们从上述图片可以看出,如果猜测出明文分组Pn,那么可以通过修改密文分组Ci-1为Ci-1XOR Pn XOR A,那么解密出来的Pn的结果是A。当然我们一般是不可能直接猜出来整个明文分组Pn,我们更多遇到的是上述图片描述的那样,仅知道Pn中部分明文。同样地,我们依旧可以发动字节反转攻击,仅需要针对正确的位置进行修改密文内容即可。

​ 这种攻击的精髓在于:通过CBC的加密特点改变明文内容,但是这种改变并不会引起其它明文块儿对应位的改变。这种攻击常用来绕过过滤器,提权(比如从guest变为admin)等。

四、CBC字节反转攻击例题

from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import base64

with open('./','r') as f:
    FLAG = f.readline().strip()
#print(FLAG)
BLOCK_SIZE=16
IV = get_random_bytes(BLOCK_SIZE)
key = get_random_bytes(BLOCK_SIZE)

pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE).encode()
unpad = lambda s: s[:-ord(s[len(s) - 1:])]

prefix = "flag="+FLAG+"&userdata="
suffix = "&user=guest"
def menu():
    print ("1. encrypt")
    print ("2. decrypt")
    return input("> ")

def encrypt():
    data = input("your data: ")
    plain = (prefix+data+suffix).encode()
    aes = AES.new(key, AES.MODE_CBC, IV)
    print (base64.b64encode(aes.encrypt(pad(plain))))


def decrypt():
    data = input("input data: ").encode()
    aes = AES.new(key, AES.MODE_CBC, IV)
    plain = unpad(aes.decrypt(base64.b64decode(data)))
    #print (b'DEBUG ====> ' + plain)
    if plain[-5:]==b"admin":
        print (plain)
    else:
        print ("you are not admin")

def main():
    for i in range(10):
        cmd = menu()
        if cmd=="1":
            encrypt()
        elif cmd=="2":
            decrypt()
        else:
            exit()

if __name__=="__main__":
    main()
  • 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

例题分析:

​ 该题目希望我们提供一个加密的字符串,如果将该字符串解密所得最后几位内容为admin,那么将直接输出明文。由题目可得,明文由prefix+data+suffix三部分组成,其中flag藏在prefix中,data由用户输入,suffix隐匿着我们要修改的字段。明文分组长度是16字节,加密算法是AES,加密模式是CBC,填充方式是PKCS。

​ 根据CBC字节反转原理,解密Pn需要Cn-1的参与。那么在已知Pn部分明文是guest时,我们可在Cn-1中寻找确定相对的guest位置,然后对该位置的数值进行修改,譬如改为Ci-1[idx] XOR ord(‘g’) XOR ord(‘a’)。

我们的目标就是将guest修改为admin!

在这里插入图片描述

​ 我们可以先随便提供一个明文,然后将密文进行修改,使得解密后的字符串最后内容是admin,我们可以通过枚举flag的长度,从而确定需要在什么位置进行修改。

例题精讲:

步骤一:根据明文的形式,爆破flag的长度

pad = 16
data = 'a'*pad
for x in range(10,100):
    BLOCK_SIZE = 16
    prefix = 'flag='+'a'*x+"&userdata="
    suffix = "&user=guest"
    plain = prefix + data + suffix
 		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
'
运行

步骤二:确定guest的首字母的index

idx = (22+x+pad) % BLOCK_SIZE + ((22+x+pad)//BLOCK_SIZE-1) * BLOCK_SIZE
  • 1

步骤三:将Cn-1对应guest位置的数值进行修改

cipher[idx+0] = chr(ord(cipher[idx+0])^ord('g')^ord('a'))
cipher[idx+1] = chr(ord(cipher[idx+1])^ord('u')^ord('d'))
cipher[idx+2] = chr(ord(cipher[idx+2])^ord('e')^ord('m'))
cipher[idx+3] = chr(ord(cipher[idx+3])^ord('s')^ord('i'))
cipher[idx+4] = chr(ord(cipher[idx+4])^ord('t')^ord('n'))
  • 1
  • 2
  • 3
  • 4
  • 5

完整exp(Python3版本)如下:

from pwn import *
import base64
import binascii

context.log_level = 'debug'
pad =16
data = b'a' * pad
#r = remote("127.0.0.1",10050)

for x in range(10,100):
	r = remote("127.0.0.1",10050)
	r.sendlineafter(b'> ', b'1')
	r.sendlineafter(b"your data:", data)
	#(0.5)
	temp = r.recvline().decode()[2:]
	#print(temp)
	cipher =list(base64.b64decode(temp))

	BLOCK_SIZE = 16
	prefix = b'flag='+b'a'*x+b'&userdata='
	suffix = b'&user=guest'
	plain = prefix + data + suffix
	
	idx = (22+x+pad)%BLOCK_SIZE + ((22+x+pad) // BLOCK_SIZE -1 ) * BLOCK_SIZE
	#print(idx)
	cipher[idx + 0] = cipher[idx + 0] ^ ord('g') ^ ord('a')
	cipher[idx + 1] = cipher[idx + 1] ^ ord('u') ^ ord('d')
	cipher[idx + 2] = cipher[idx + 2] ^ ord('e') ^ ord('m')
	cipher[idx + 3] = cipher[idx + 3] ^ ord('s') ^ ord('i')
	cipher[idx + 4] = cipher[idx + 4] ^ ord('t') ^ ord('n')
	
	#print(cipher)
	stemp = [binascii.unhexlify(hex(i)[2:].zfill(2).encode()) for i in cipher] 
	stemp1 = b''
	for i in stemp:
		stemp1 += i
	r.sendlineafter(b'> ',b'2')
	r.sendlineafter(b'input data: ', base64.b64encode(stemp1))
	msg = r.recv().decode()
	#print(msg)
	#pause()
	if 'you are not admin' not in msg:
    		print(msg)
    		break
	r.close()
  • 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

参考资料

/ctf-wiki/crypto/blockcipher/mode/cbc-zh/#_7

/2018/11/19/CBC-reverse/

/post/crypt/cbc-crack/