@iPhan
2018-07-06T14:26:28.000000Z
字数 6451
阅读 1388
未分类
这是一道关于对称加密、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 ValueError
for i in range(lb):
if s[-1 - i] == lb:
pass
else:
raise ValueError
return 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 AES
from Crypto import Random
from hashlib import sha256
from signal import alarm
bs = 16
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 ValueError
for i in range(lb):
if s[-1 - i] == lb:
pass
else:
raise ValueError
return s[0:-s[-1]]
class cipher(object):
def __init__(self, key):
self.key = key
def 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:
break
if choice[0] == 'r':
name = input('your name is:>>').strip()
name = bytes(name, 'utf-8') + b'user'
if(len(name) > 64):
print("username too long!")
break
else:
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 RSA
from Crypto.Cipher import PKCS1_v1_5, AES
from Crypto.Util.number import bytes_to_long, long_to_bytes
from os import urandom
bs = 16
def 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:
break
if 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_key
try:
tic = pub.encrypt(tic)
except ValueError:
print('Plz input again. Remove space plz.')
continue
print(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:
continue
p = 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 ValueError
except Exception:
print('u input wrong passwd or username')
continue
key = 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 ValueError
except Exception:
print('u input wrong passwd or username')
continue
因为初始定义时这样定义的:
def decrypt(self, ciphertext):
return self._pkcs.decrypt(ciphertext, None)
PKCS格式不对时不会报错,只会返回一个None。将会触发try。
因此有了选择明文攻击。