@iPhan
2018-07-06T14:26:28.000000Z
字数 6451
阅读 1436
未分类
这是一道关于对称加密、CBC、sha256的题目
有3个密钥:encrypt_key , mac_key, authen_key
encrypt_key 用来对用户名+类别做加密
mac_key用来在hash时加到信息前(即sha256(mac_key+message))
authen_key 用来加密hash
AES加密时均采用CBC,padding格式采用如下经典形式:
def pad(s):return s + (bs - len(s) % bs) * bytes((bs - len(s) % bs, ))def unpad(s):lb = s[-1]if lb > 16 or lb < 1:raise ValueErrorfor i in range(lb):if s[-1 - i] == lb:passelse:raise ValueErrorreturn s[0:-s[-1]]
用户注册时:
注:AES加密时均采用CBC
用户登录时,服务端解密cookie中的MAC部分和用户名部分,先判断用户名部分的MAC是否匹配
主要思路是拿到正常用户名 + user的hash值,然后做hash扩展攻击,得到用户名 + user + \x0000000... + admin + \x10*16的hash值 (前面的\x00要补足到整个账号名是aes块长的整数倍),然后拿到新hash值与新用户名的aes密文,即可实现攻击。
拿到正常用户名 + user的hash值(hash1)
命名第一个账号为账号1
根据程序逻辑,前128bits可以轻松得到,后128bits可以通过CBC padding oracle 攻击得到hash1。以此为基础,可以通过hash扩展攻击,得到后面加了admin的新hash值hash1_attack。
拿到新用户名的aes密文
注册账号2'旧用户名+user+\x0000....+admin+\x10',拿到新的iv、MAC、用户名密文,删掉后面含有user与padding的那个块。
拿到新hash的aes密文
因为新注册的账号2的hash值可以通过同样的办法求出,命名为hash2。sha256占aes的两个块。拿到的新MAC的形式是这样:
block1 + block2 + paddingblock
因为已知block2的明文,可以通过篡改block1的密文,使得block2的明文与hash1_attack 的后128bits相同。此时因为触发了报错信息,服务器会回显前128bits解密后的值。根据前128bits解密后的值,篡改iv,使得新MAC前128bits的值也与hash1_attack相同。
此时因为篡改了IV,将使得明文也发生改变,导致解题过程陷入死循环。我决定给MAC和用户名部分的加密使用两条不同的iv以规避这个问题。
此时可以更改细节:做MAC比较时,只比较最低256bits的值。可以穷举username,直到遇到一个hash以\x01结尾的,把hash的第二个块从第二个块移到第三个padding块,把hash的第一个块从第一个块移到第二个块。这样就能低代价地共用iv了。
此时前面还需要改动的点是,需要回显sha256的后128bits。
穷举username使得hash以\x01结尾的概率是1/256,很低
不知我上面的解决办法可以吗?以及这道题有非预期解法吗?
from Crypto.Cipher import AESfrom Crypto import Randomfrom hashlib import sha256from signal import alarmbs = 16def pad(s):return s + (bs - len(s) % bs) * bytes((bs - len(s) % bs, ))def unpad(s):lb = s[-1]if lb > 16 or lb < 1:raise ValueErrorfor i in range(lb):if s[-1 - i] == lb:passelse:raise ValueErrorreturn s[0:-s[-1]]class cipher(object):def __init__(self, key):self.key = keydef cbcPrimeEnc(self, key, iv, plain):cipher = AES.new(key, AES.MODE_CBC, iv)plain = pad(plain)return cipher.encrypt(plain)def cbcPrimeDec(self, key, iv, ciphertext):cipher = AES.new(key, AES.MODE_CBC, iv)plain = cipher.decrypt(ciphertext)return unpad(plain)def encrypt(self, plaintext):hash = sha256(mac_key + plaintext).digest()iv = Random.new().read(bs)ciphertext = iv + \self.cbcPrimeEnc(authen_key, iv, hash) + \self.cbcPrimeEnc(self.key, iv, plaintext)return ciphertext.hex()def decrypt(self, ciphertext):cookie = bytes.fromhex(ciphertext)iv = cookie[:bs]mac = cookie[bs:4 * bs]ciphertext = cookie[4 * bs:]plaintext = self.cbcPrimeDec(self.key, iv, ciphertext)hash = self.cbcPrimeDec(authen_key, iv, mac)if sha256(mac_key + plaintext).digest() == hash:return str(plaintext, 'utf-8')else:raise Exception(sha256(mac_key + plaintext).digest().hex()[:32], hash.hex()[:32])encrypt_key = Random.new().read(bs)mac_key = Random.new().read(2 * bs)authen_key = Random.new().read(bs)with open('flag', 'r') as fp:flag = fp.readline()# alarm(20)while True:c = cipher(encrypt_key)choice = input("Please [r]egister or [l]ogin :>>")if not choice:breakif choice[0] == 'r':name = input('your name is:>>').strip()name = bytes(name, 'utf-8') + b'user'if(len(name) > 64):print("username too long!")breakelse:print("Here is your cookie:")print(c.encrypt(name))elif choice[0] == 'l':data = input('your cookie:>>').strip()try:msg = c.decrypt(data)if msg[-4:] == 'user':print("Welcome %s!" % msg[:-4])elif msg[-4:] == 'admin':print(flag)except Exception as e:print('Wrong MAC! ')print('the suppposed first 128 bits of sha256 and the actual first 128 bits is:>>' + str(e))else:exit()else:print("Unknown choice!")break
part1 get ticket
part2 get flag
part3 login
from Crypto.PublicKey import RSAfrom Crypto.Cipher import PKCS1_v1_5, AESfrom Crypto.Util.number import bytes_to_long, long_to_bytesfrom os import urandombs = 16def xor(x, y):return bytes(a ^ b for a, b in zip(x, y))class Pubcipher(object):def __init__(self):self._rsa = RSA.generate(1024)self._pkcs = PKCS1_v1_5.new(self._rsa)def encrypt(self, msg):'''msg must be bytes'''return self._pkcs.encrypt(msg)def decrypt(self, ciphertext):return self._pkcs.decrypt(ciphertext, None)class Sycipher(object):def __init__(self):self.iv = urandom(AES.block_size)def _pad(self, s):return s + (AES.block_size - len(s) % AES.block_size) * bytes((AES.block_size - len(s) % AES.block_size, ))def _unpad(self, s):return s[0:-s[-1]]def encrypt(self, msg, key):self.cipher = AES.new(key, AES.MODE_OFB, self.iv)return self.cipher.encrypt(self._pad(msg))def decrypt(self, msg, key):self.cipher = AES.new(key, AES.MODE_OFB, self.iv)return self._unpad(self.cipher.decrypt(msg))def main():with open('admin.key', 'rb') as f:admin_key = f.read(bs)print('Welc.')pub = Pubcipher()sy = Sycipher()print(pub._rsa.e)print(pub._rsa.n)while True:choice = input("[t]icke or [k]ey or [f]lag :>>")if not choice:breakif choice[0] == 't':name = input('ur name:>>')if len(name) > 50:exit()name = bytes(name, 'ISO-8859-1')pw = input('ur 16bytes passwd:>>')if len(pw) != bs:exit()pw = bytes(pw, 'ISO-8859-1')tic = name + pw + urandom(bs) + admin_keytry:tic = pub.encrypt(tic)except ValueError:print('Plz input again. Remove space plz.')continueprint(tic.hex())elif choice[0] == 'k':tmp_k = input('Input the key:>>')tmp_k = bytes.fromhex(tmp_k)if tmp_k != admin_key:exit()print(admin_key.hex())elif choice[0] == 'f':name = input('ur name:>>')if len(name) > 50:exit()name = bytes(name, 'ISO-8859-1')pw = input('ur 16bytes passwd:>>')if len(pw) != bs:exit()pw = bytes(pw, 'ISO-8859-1')tic = input('ur ticket:>>')try:tic = bytes.fromhex(tic)except Exception:continuep = pub.decrypt(tic)try:_name = p[-4 * bs:-3 * bs]_pw = p[-3 * bs:-2 * bs]_admin_key = p[-bs:]seed = p[-2 * bs:-bs]if _name != name or _pw != pw or _admin_key != admin_key:raise ValueErrorexcept Exception:print('u input wrong passwd or username')continuekey = xor(seed, pw)with open('attach.bin', 'rb') as f:flag = f.read()flag = sy.encrypt(flag, key)print(flag.hex())else:exit()if __name__ == '__main__':main()
step3 PKCS#1 v1.5 CPA 漏洞
漏洞出现在106行-116行:
try:_name = p[-4 * bs:-3 * bs]_pw = p[-3 * bs:-2 * bs]_admin_key = p[-bs:]seed = p[-2 * bs:-bs]if _name != name or _pw != pw or _admin_key != admin_key:raise ValueErrorexcept Exception:print('u input wrong passwd or username')continue
因为初始定义时这样定义的:
def decrypt(self, ciphertext):return self._pkcs.decrypt(ciphertext, None)
PKCS格式不对时不会报错,只会返回一个None。将会触发try。
因此有了选择明文攻击。