CBC Padding Oracle Attack

原理:

https://blog.gdssecurity.com/labs/2010/9/14/automated-padding-oracle-attacks-with-padbuster.html

https://www.jianshu.com/p/1851f778e579

https://www.cnblogs.com/LittleHann/p/3391393.html

总结:

在加密解密中,为了针对不同长度的明文都能进行加密,经常会有分组与填充的情况。对于CBC模式,经常使用的模式是PKCS#5,即:最后缺少x字节,就填充x字节的x。

同时,在解密的过程中,一旦解密出来的结果不符合PKCS#5的填充规则,那么会给出与正确解密不一致的提示信息,这就给了攻击者暴力破解的可乘之机。


题目链接:

https://github.com/sonickun/ctf-crypto-writeups/tree/master/2016/hack.lu-ctf/cryptolocker

先分析题目的关键代码

class AESCipher(object):
    def __init__(self, key):
        self.bs = 32
        self.key = key
    def encrypt(self, raw):
        raw = self._pad(AESCipher.str_to_bytes(raw))
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return iv + cipher.encrypt(raw)
    def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * AESCipher.str_to_bytes(chr(self.bs - len(s) % self.bs))

AES加密,16个字节一组,以PKCS#5方式填充,CBC模式

    user_input = sys.argv[2].encode('utf-8')
    assert len(user_input) == 8
    i = len(user_input) // 4
    keys = [ # Four times 256 is 1024 Bit strength!! Unbreakable!!
        hashlib.sha256(user_input[0:i]).digest(),
        hashlib.sha256(user_input[i:2*i]).digest(),
        hashlib.sha256(user_input[2*i:3*i]).digest(),
        hashlib.sha256(user_input[3*i:4*i]).digest(),
    ]
    s = SecureEncryption(keys)

自己的密钥为8位,分成4组,不够的程序自动填充


所以,满足攻击条件,我们只需要暴力密钥,然后对明文解密,根据padding的情况是不是符合格式来判断密钥是否正确即可。暴力成功后,直接解密得到flag

import sys
import hashlib
from AESCipher import *
import string
import itertools

class SecureEncryption(object):
    def __init__(self, keys):
        #assert len(keys) == 4
        self.keys = keys
        self.ciphers = []
        for i in range(len(keys)):
            self.ciphers.append(AESCipher(keys[i]))

    def enc(self, plaintext): # Because one encryption is not secure enough
        one        = self.ciphers[0].encrypt(plaintext)
        two        = self.ciphers[1].encrypt(one)
        three      = self.ciphers[2].encrypt(two)
        ciphertext = self.ciphers[3].encrypt(three)
        return ciphertext

    def dec(self, ciphertext):
        three      = AESCipher._unpad(self.ciphers[3].decrypt(ciphertext))
        two        = AESCipher._unpad(self.ciphers[2].decrypt(three))
        one        = AESCipher._unpad(self.ciphers[1].decrypt(two))
        plaintext  = AESCipher._unpad(self.ciphers[0].decrypt(one))
        return plaintext

    def mydec(self, ciphertext):
        tmp = ciphertext
        for i in range(len(self.keys)-1):
            tmp = AESCipher._unpad(self.ciphers[len(self.keys)-i-1].decrypt(tmp))
        plaintext = self.ciphers[0].decrypt(tmp)
        return plaintext


def checkPadding1(plain):
    padlen = ord(plain[-1])
    pad = chr(padlen)*padlen
    if plain[-padlen:] != pad or padlen > 32 or padlen == 1:
        return False
    else:
        return True

def checkPadding2(plain):
    pad = chr(16)*16
    if len(plain) > 1 and plain[-16:] == pad:
        return True
    else:
        return False

cipher = open("flag.encrypted", "rb").read()

keys = []
password = ""
charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

for i in range(4):
    for c in itertools.product(charset, repeat=2):
        user_input = "".join(c)
        tmp_keys = keys[:]
        tmp_keys.insert(0, hashlib.sha256(user_input).digest())
        s = SecureEncryption(tmp_keys)
        plain = s.mydec(cipher)

        if i == 3:
            if checkPadding1(plain):
                keys = tmp_keys[:]
                password = user_input + password
                print "[+] found password:", password
                open("flag.odt", "wb").write(AESCipher._unpad(plain))
                break
        else:
            if checkPadding2(plain):
                keys = tmp_keys[:]
                password = user_input + password
                print "[+] found password:", password
                break

代码即为链接中的solver.py

学到了一个新的暴力姿势:

charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
for c in itertools.product(charset, repeat=2):
    user_input = "".join(c)