CBC字节反转攻击

作者: 分类: Security,Programming 时间: 2017-03-26 评论: 暂无评论

CBC加解密原理

原理

先来看下CBC(AES为例)是如何工作的:

加密流程如下:

cbc_encrypt.png

  1. 首先将明文分组(常见的以16字节为一组),位数不足的使用特殊字符填充。
  2. 生成一个随机的初始化向量(IV)和一个密钥。
  3. 将IV和第一组明文异或。
  4. 用密钥对3中xor后产生的密文加密。
  5. 用4中产生的密文对第二组明文进行xor操作。
  6. 用密钥对5中产生的密文加密。
  7. 重复4-7,到最后一组明文。
  8. 将IV和加密后的密文拼接在一起,得到最终的密文。

加密公式:

Ciphertext-0 = Encrypt(Plaintext XOR IV)—只用于第一个组块
Ciphertext-N = Encrypt(Plaintext XOR Ciphertext-N-1)—用于第二及剩下的组块

解密流程如下:

cbc_decrypt.png

  1. 从密文中提取出IV,然后将密文分组。
  2. 使用密钥对第一组的密文解密,然后和IV进行xor得到明文。
  3. 使用密钥对第二组密文解密,然后和2中的密文xor得到明文。
  4. 重复2-3,直到最后一组密文。

解密公式:

Plaintext-0 = Decrypt(Ciphertext) XOR IV—只用于第一个组块
Plaintext-N = Decrypt(Ciphertext) XOR Ciphertext-N-1—用于第二及剩下的组块

这里可以注意到Ciphertext-N-1用来产生下一块明文,如果我们改变Ciphertext-N-1中的一个字节,然后和下一块解密后的密文xor,就可以得到一个不同的明文,而这个明文是我们可以控制的。利用这一点,我们就欺骗服务端或者绕过过滤器。

下图展示了整个攻击过程:

attack_on_cbc.jpg

解析

xor.png

根据解密流程,我们假设A为明文,B为前一组密文,C为密文经过AES解密后的字串:

A = Plaintext[0] = 11
B = (Ciphertext-N-1)[0] = 13
C = Decrypt(Ciphertext)[0] = 6

我们可以知道,解密过程中是C与B异或得到A,即A xor B = C

那么关键来了

C xor C = 0 (任何数与自己异或都为0)
等价于
A xor B xor C = 0

由于任何数与0异或都为自己本身,所以

A xor B xor C xor 3 = 3

那么此时我们其实可以这样来看:

A = A = 11
B = B xor 3 = 13 xor 3 = 14
C = C = 6

现在我们修改密文对应的位让B = 14,那么当密文解密后,会发现,明文A会变成3,通过这种方法我们可以控制任何一位明文。

实战

CBC字节反转攻击的实例

下面这个是一个CBC字节反转攻击的实例

'''
This server is a modified version of the previous one.
    
CBC mode is used instead of ECB mode.
You must supply a ciphertext that will contain the string ;admin=true.
    
Ciphertexts are sent back and forth as ASCII Encoded Hex Strings. 0xFF will be sent as
"FF" (2 Bytes), not as "\xff" (1 Byte).
    
You can use python's string.encode('hex') and string.decode('hex') to quickly convert between
raw data and string representation if you need/want to.
    
Email biernp@rpi.edu with questions/comments :)
    
-Patrick Biernat
'''
    
from twisted.internet import reactor, protocol
from Crypto.Cipher import AES
import os
import random
    
PORT = 9002
    
KEYSIZE = 16
KEY = "AAA" + "BBB" + "CCC" + '\x01' + "\x80" * 6
IV = "\x00" * KEYSIZE
SECRET = "flag{fl!ppd_b!tz_synk_cyb3r_sh!pz}"
    
    
class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'
    CLEAR = '\x1b[2J\x1b[1;1H'
    
    
BANNER = bcolors.OKBLUE + """
.--------------------------------------------.
|+ [ BLACKBOX ]  PUBLIC API DOCS v1.33.7   + |
'--------------------------------------------'""" + bcolors.ENDC
    
DOCS = """
|                                            |
|                                            |
|getapikey:              Get an Account      |
|getflag:<admin_apikey>  Get a Flag!!!!      |
|                                            |
|                                            |
'--------------------------------------------'
"""
    
    
def pad(instr, length):
    if(length is None):
        print "Supply a length to pad to"
    elif(len(instr) % length == 0):
        print "No Padding Needed"
        return instr
    else:
        return instr + ' ' * (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):
    '''
    Return a string containing a XOR of bytes in first with 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):
    '''
    High Level Function to encrypt things in AES CBC Mode.
    1: Pad plaintext if necessary.
    2: Split plaintext into blocks of length <keysize>
    3: XOR Block 1 w/ IV
    4: Encrypt Blocks, XOR-ing them w/ the previous block.
    '''
    if(len(plaintext) % len(key) != 0):
        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:
            # len(key) * 2 because ctxt is an ASCII string that we convert to
            # "raw" binary.
            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):
    '''
    High Level function to decrypt thins in AES CBC mode.
    1: Split Ciphertext into blocks of len(Key)
    2: Decrypt block.
    3: For the first block, xor w/ IV.
       For the others, xor with last ciphertext block.
    '''
    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)):
        if (i == 0):
            ptxt = decrypt_block(key, blocks[i])
            ptxt = xor_block(ptxt.decode('hex'), IV)
        else:
            tmp = decrypt_block(key, blocks[i])
            tmp = xor_block(tmp.decode('hex'), blocks[i - 1])
            ptxt = ptxt + tmp
    return ptxt
    
    
class MyServer(protocol.Protocol):
    
    def mkprofile(self, email):
        if((";" in email)):
            return -1
        prefix = "comment1=wowsuch%20CBC;userdata="
        suffix = ";coment2=%20suchsafe%20very%20encryptwowww"
        ptxt = prefix + email + suffix
        return encrypt_cbc(self.key, self.iv, ptxt)
    
    def parse_profile(self, data):
        ptxt = decrypt_cbc(self.key, self.iv, data.encode('hex'))
        ptxt = ptxt.replace(" ", "")
        print ptxt
        if ";admin=true" in ptxt:
            return 1
        return 0
    
    def print_docs(self):
        self.transport.write(BANNER)
        self.transport.write(DOCS)
    
    def dataReceived(self, data):
        if(len(data) > 512):
            self.transport.write("Data too long.\n")
            self.transport.loseConnection()
            return
    
# Make Profile From "Email"
        if(data.startswith("getapikey:")):
            data = data.strip()
            print "%r" % data   # debug
            data = data[10:]
            resp = self.mkprofile(data)
            if (resp == -1):
                self.transport.write("No Cheating!\n")
            else:
                self.transport.write(resp + "\n")
    
# Decrypt Ciphertext and "parse" into Profile
        elif(data.startswith("getflag:")):
            data = data.strip()
            print "%r" % data   # debug
            self.transport.write("Parsing Profile...\n")
            data = data[8:].decode('hex')
            if (len(data) % 32 != 0):
                self.transport.write(
                    "[BLACKBOX] Invalid Length for API Endpoint\n")
                self.transport.loseConnection()
                return
    
            if (self.parse_profile(data) == 1):
                self.transport.write("Congratulations!\nThe Secret is: ")
                self.transport.write(SECRET)
                self.transport.loseConnection()
    
            else:
                self.transport.write("[BLACKBOX] You are a normal user.\n")
    
        else:
            self.transport.write("Syntax Error")
            self.transport.loseConnection()
    
    def connectionMade(self):
        self.key = os.urandom(16)
        self.iv = os.urandom(16)
        self.print_docs()
    
    
class MyServerFactory(protocol.Factory):
    protocol = MyServer
    
    
factory = MyServerFactory()
reactor.listenTCP(PORT, factory)
reactor.run()

程序跑起来后,结合源码,尝一下就知道了程序的逻辑了,我们所要做的就是输入一串字符,然后通过返回的密文,构造新的密文让解密出来的字串里面包含;admin=true

cbc2.png

这里为了便捷,构造*admin=true进去,接受返回的密文,因为是按照16位分组的,所以如下:

comment1=wowsuch
%20CBC;userdata=
*admin=true;come
nt2=%20suchsafe%
20very%20encrypt
wowww

我们必须改变第16位的值(即%),才能使得32位的*能变成我们想要的;,代码如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date    : 2017-03-26 15:36:44
# @Author  : iFurySt (hk326459@163.com)
# @Link    : http://ifuryst.com/
    
    
from pwn import *
    
# context.log_level = True
    
# comment1=wowsuch
# %20CBC;userdata=
# *admin=true;come
# nt2=%20suchsafe%
# 20very%20encrypt
# wowww
    
r = remote("192.168.160.200", 9002)
r.recv(1024)
r.sendline("getapikey:*admin=true")
sleep(1)
data = r.recv(1024)
print data
data = data.strip()
data = data.decode("hex")
data = list(data)
data[16] = chr(ord("*") ^ ord(";") ^ ord(data[16]))
data = "".join(data)
data = data.encode("hex")
print data
r.sendline("getflag:" + data)
sleep(1)
print r.recv(1024)

结果如下,成功了:

getflag.png

Hash长度扩展攻击实例

上周0CTF里遇到的一个Crypto,是一个有点不太一样的CBC字节反转攻击,或者叫Hash长度扩展攻击,代码如下:

integrity.py

#!/usr/bin/python -u
    
from Crypto.Cipher import AES
from hashlib import md5
from Crypto import Random
from signal import alarm
    
BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 
unpad = lambda s : s[0:-ord(s[-1])]
    
    
class Scheme:
    def __init__(self,key):
        self.key = key
    
    def encrypt(self,raw):
        raw = pad(raw)
        raw = md5(raw).digest() + raw
    
        iv = Random.new().read(BS)
        cipher = AES.new(self.key,AES.MODE_CBC,iv)
    
        return ( iv + cipher.encrypt(raw) ).encode("hex")
    
    def decrypt(self,enc):
        enc = enc.decode("hex")
    
        iv = enc[:BS]
        enc = enc[BS:]
    
        cipher = AES.new(self.key,AES.MODE_CBC,iv)
        blob = cipher.decrypt(enc)
    
        checksum = blob[:BS]
        data = blob[BS:]
    
        if md5(data).digest() == checksum:
            return unpad(data)
        else:
            return
    
key = Random.new().read(BS)
scheme = Scheme(key)
    
flag = open("flag",'r').readline()
alarm(30)
    
print "Welcome to 0CTF encryption service!"
while True:
    print "Please [r]egister or [l]ogin"
    cmd = raw_input()
    
    if not cmd:
        break
    
    if cmd[0]=='r' :
        name = raw_input().strip()
    
        if(len(name) > 32):
            print "username too long!"
            break
        # pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 
        if pad(name) == pad("admin"):
            print "You cannot use this name!"
            break
        else:
            print "Here is your secret:"
            print scheme.encrypt(name)
    
    
    elif cmd[0]=='l':
        data = raw_input().strip()
        name = scheme.decrypt(data)
    
        if name == "admin":
            print "Welcome admin!"
            print flag
        else:
            print "Welcome %s!" % name
    else:
        print "Unknown cmd!"
        break

这边比较不一样的地方是返回的密文是这样的结构:

IV + AES[md5(plaintext) + plaintext]

多了一个MD5,而且解密后还会校对plaintextMD5是否和前面的一样。这个我一开始一直想错,一直想着怎么同时去修改MD5plaintext,实验证明这种想法真的是进入了一个误区。

正确的想法是,构造出正确的plaintext,然后再去改IV以得到想要的MD5

我们构造如下:

admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0biFurySt

后面iFurySt只是为了使得产生两组16位的密文,到时候我们留下第一组密文,也就是

admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b

这样就得到正确的明文了,此时再去反转IV的比特位,使得我们能得到我们想要的MD5即可。

exp如下:

from pwn import *
from hashlib import md5
    
# context.log_level = True
    
BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
    
    
def crack(enc):
    enc = enc.decode('hex')
    enc = list(enc)
    for i in range(BS):
        enc[i] = chr(
            ord(enc[i]) ^ ord(name_pad_md5[i]) ^ ord(admin_pad_md5[i]))
    enc = ''.join(enc)
    return enc.encode('hex')
    
    
name = 'admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0biFurySt'
name_pad = pad(name)
name_pad_md5 = md5(name_pad).digest()
    
admin = 'admin'
admin_pad = pad(admin)
admin_pad_md5 = md5(admin_pad).digest()
    
r = remote("192.168.160.200", 1234)
sleep(1)
r.recv(1024)
r.sendline("r")
r.sendline(name)
sleep(1)
enc = r.recv(1024)
enc = enc[enc.index("secret:\n") + 8:enc.index("\nPlease")]
fake_enc = crack(enc)
r.sendline('l')
r.sendline(fake_enc[:96])
sleep(1)
print r.recv(1024)
r.close()

getflag2.png

参考文章:

CBC字节反转攻击

基于CBC模式模式的密文攻击

标签: PythonCBC

声明:文章基本原创,允许转载,但转载时必须以超链接的形式标明文章原始出处及作者信息。

添加新评论