from pwn import *
import base64, time, random, string
from Crypto.Cipher import AES
from Crypto.Hash import SHA256, MD5
#context.log_level = 'debug'
p = remote('127.0.0.1', 7777)
def strxor(str1, str2):
return ''.join([chr(ord(c1) ^ ord(c2)) for c1, c2 in zip(str1, str2)])
def pad(msg):
pad_length = 16 - len(msg) % 16
return msg + chr(pad_length) * pad_length
def unpad(msg):
return msg[:-ord(msg[-1])] # remove pad
def flipplain(oldplain, newplain, iv):
"""flip oldplain to new plain, return proper iv"""
return strxor(strxor(oldplain, newplain), iv)
def bypassproof():
p.recvuntil('SHA256(XXXX+')
lastdata = p.recvuntil(')', drop=True)
p.recvuntil(' == ')
digest = p.recvuntil('\nGive me XXXX:', drop=True)
def proof(s):
return SHA256.new(s + lastdata).hexdigest() == digest
data = pwnlib.util.iters.mbruteforce(
proof, string.ascii_letters + string.digits, 4, method='fixed')
p.sendline(data)
def sendmsg(iv, cipher):
payload = iv + cipher
payload = base64.b64encode(payload)
p.sendline(payload)
def recvmsg():
data = p.recvuntil("\n", drop=True)
data = base64.b64decode(data)
return data[:16], data[16:]
def getmd5enc(i, cipher_token, cipher_welcome, iv):
"""return encrypt( md5( token[:i+1] ) )"""
## keep iv[7:] do not change, so decrypt msg[7:] won't change
get_md5_iv = flipplain("token: ".ljust(16, '\x00'), "get-md5".ljust(
16, '\x00'), iv)
payload = cipher_token
## calculate the proper last byte number
last_byte_iv = flipplain(
pad("Welcome!!"),
"a" * 15 + chr(len(cipher_token) + 16 + 16 - (7 + i + 1)), iv)
payload += last_byte_iv + cipher_welcome
sendmsg(get_md5_iv, payload)
return recvmsg()
def get_md5_token_indexi(iv_encrypt, cipher_welcome, cipher_token):
md5_token_idxi = []
for i in range(len(cipher_token) - 7):
log.info("idx i: {}".format(i))
_, md5_indexi = getmd5enc(i, cipher_token, cipher_welcome, iv_encrypt)
assert (len(md5_indexi) == 32)
# remove the last 16 byte for padding
md5_token_idxi.append(md5_indexi[:16])
return md5_token_idxi
def doin(unpadcipher, md5map, candidates, flag):
if unpadcipher in md5map:
lastbyte = md5map[unpadcipher]
else:
lastbyte = 0
if flag == 0:
lastbyte ^= 0x80
newcandidates = []
for x in candidates:
for c in range(256):
if MD5.new(x + chr(c)).digest()[-1] == chr(lastbyte):
newcandidates.append(x + chr(c))
candidates = newcandidates
print candidates
return candidates
def main():
bypassproof()
# result of encrypted Welcome!!
iv_encrypt, cipher_welcome = recvmsg()
log.info("cipher welcome is : " + cipher_welcome)
# execute get-token
get_token_iv = flipplain(pad("Welcome!!"), pad("get-token"), iv_encrypt)
sendmsg(get_token_iv, cipher_welcome)
_, cipher_token = recvmsg()
token_len = len(cipher_token)
log.info("cipher token is : " + cipher_token)
# get command not found cipher
sendmsg(iv_encrypt, cipher_welcome)
_, cipher_notfound = recvmsg()
# get encrypted(token[:i+1]),57 times
md5_token_idx_list = get_md5_token_indexi(iv_encrypt, cipher_welcome,
cipher_token)
# get md5map for each unpadsize, 209-17 times
# when upadsize>208, it will unpad ciphertoken
# then we can reuse
md5map = dict()
for unpadsize in range(17, 209):
log.info("get unpad size {} cipher".format(unpadsize))
get_md5_iv = flipplain("token: ".ljust(16, '\x00'), "get-md5".ljust(
16, '\x00'), iv_encrypt)
## padding 16*11 bytes
padding = 16 * 11 * "a"
## calculate the proper last byte number, only change the last byte
## set last_byte_iv = iv_encrypted[:15] | proper byte
last_byte_iv = flipplain(
pad("Welcome!!"),
pad("Welcome!!")[:15] + chr(unpadsize), iv_encrypt)
cipher = cipher_token + padding + last_byte_iv + cipher_welcome
sendmsg(get_md5_iv, cipher)
_, unpadcipher = recvmsg()
md5map[unpadcipher] = unpadsize
# reuse encrypted(token[:i+1])
for i in range(209, 256):
target = md5_token_idx_list[56 - (i - 209)]
md5map[target] = i
candidates = [""]
# get the byte token[i], only 56 byte
for i in range(token_len - 7):
log.info("get token[{}]".format(i))
get_md5_iv = flipplain("token: ".ljust(16, '\x00'), "get-md5".ljust(
16, '\x00'), iv_encrypt)
## padding 16*11 bytes
padding = 16 * 11 * "a"
cipher = cipher_token + padding + iv_encrypt + md5_token_idx_list[i]
sendmsg(get_md5_iv, cipher)
_, unpadcipher = recvmsg()
# already in or md5[token[:i]][-1]='\x00'
if unpadcipher in md5map or unpadcipher == cipher_notfound:
candidates = doin(unpadcipher, md5map, candidates, 1)
else:
log.info("unpad size 1-16")
# flip most significant bit of last byte to move it in a good range
cipher = cipher[:-17] + strxor(cipher[-17], '\x80') + cipher[-16:]
sendmsg(get_md5_iv, cipher)
_, unpadcipher = recvmsg()
if unpadcipher in md5map or unpadcipher == cipher_notfound:
candidates = doin(unpadcipher, md5map, candidates, 0)
else:
log.info('oh my god,,,, it must be in...')
exit()
print len(candidates)
# padding 0x01
candidates = filter(lambda x: x[-1] == chr(0x01), candidates)
# only 56 bytes
candidates = [x[:-1] for x in candidates]
print len(candidates)
assert (len(candidates[0]) == 56)
# check-token
check_token_iv = flipplain(
pad("Welcome!!"), pad("check-token"), iv_encrypt)
sendmsg(check_token_iv, cipher_welcome)
p.recvuntil("Give me the token!\n")
p.sendline(base64.b64encode(candidates[0]))
print p.recv()
p.interactive()
if __name__ == "__main__":
main()
效果如下
...
79
1
hitcon{uNp@d_M3th0D_i5_am4Z1n9!}
Teaser Dragon CTF 2018 AES-128-TSB
这个题目还是蛮有意思的,题目描述如下
Haven't you ever thought that GCM mode is overcomplicated and there must be a simpler way to achieve Authenticated Encryption? Here it is!
Server: aes-128-tsb.hackable.software 1337
server.py
附件以及最后的 exp 自行到 ctf-challenge 仓库下寻找。
题目的基本流程为
不断接收 a 和 b 两个字符串,其中 a 为明文,b 为密文,注意
b 在解密后需要满足尾部恰好等于 iv。
如果 a 和 b 相等,那么根据
a 为 gimme_flag ,输出加密后的 flag。
否则,输出一串随机加密的字符串。
否则输出一串明文的字符串。
此外,我们还可以发现题目中的 unpad 存在问题,可以截断指定长度。
def unpad(msg):
if not msg:
return ''
return msg[:-ord(msg[-1])]
一开始,很直接的思路是 a 和 b 的长度都输入 0 ,那么可以直接绕过 a==b 检查,获取一串随机密文加密的字符串。然而似乎并没有什么作用,我们来分析一下加密的流程
def tsb_encrypt(aes, msg):
msg = pad(msg)
iv = get_random_bytes(16)
prev_pt = iv
prev_ct = iv
ct = ''
for block in split_by(msg, 16) + [iv]:
ct_block = xor(block, prev_pt)
ct_block = aes.encrypt(ct_block)
ct_block = xor(ct_block, prev_ct)
ct += ct_block
prev_pt = block
prev_ct = ct_block
return iv + ct
不妨假设 $P_0=iv,C_0=iv$,则
$C_i=C_{i-1}\oplus E(P_{i-1} \oplus P_i)$
那么,假设消息长度为 16,与我们想要得到的gimme_flag padding 后长度类似,则
$C_1=IV\oplus E( IV \oplus P_1)$
$C_2=C_1 \oplus E(P_1 \oplus IV)$
可以很容易的发现 $C_2=IV$。
反过来想,如果我们向服务器发送 iv+c+iv,那么总能绕过 tsb_decrypt 的 mac 检查
def tsb_decrypt(aes, msg):
iv, msg = msg[:16], msg[16:]
prev_pt = iv
prev_ct = iv
pt = ''
for block in split_by(msg, 16):
pt_block = xor(block, prev_ct)
pt_block = aes.decrypt(pt_block)
pt_block = xor(pt_block, prev_pt)
pt += pt_block
prev_pt = pt_block
prev_ct = block
pt, mac = pt[:-16], pt[-16:]
if mac != iv:
raise CryptoError()
return unpad(pt)
def getlast_byte(iv, block):
iv_pre = iv[:15]
iv_last = ord(iv[-1])
tmp = []
print('get last byte')
for i in range(256):
send_data('')
iv = iv_pre + chr(i)
tmpblock = block[:15] + chr(i ^ ord(block[-1]) ^ iv_last)
payload = iv + tmpblock + iv
send_data(payload)
length, data = recv_data()
if 'Looks' in data:
tmp.append(1)
else:
tmp.append(0)
last_bytes = []
for i in range(256):
if tmp == xor_byte_map[i][0]:
last_bytes.append(xor_byte_map[i][1])
print('possible last byte is ' + str(last_bytes))
return last_bytes
"""
every item is a pair [a,b]
a is the xor list
b is the idx which is zero when xored
"""
xor_byte_map = []
for i in range(256):
a = []
b = 0
for j in range(256):
tmp = i ^ j
if tmp > 0 and tmp <= 15:
a.append(1)
else:
a.append(0)
if tmp == 0:
b = j
xor_byte_map.append([a, b])
def dec_block(iv, block):
last_bytes = getlast_byte(iv, block)
iv_pre = iv[:15]
iv_last = ord(iv[-1])
print('try to get plain')
plain0 = ''
for last_byte in last_bytes:
plain0 = ''
for i in range(15):
print 'idx:', i
tag = False
for j in range(256):
send_data(plain0 + chr(j))
pad_size = 15 - i
iv = iv_pre + chr(pad_size ^ last_byte)
tmpblock = block[:15] + chr(
pad_size ^ last_byte ^ ord(block[-1]) ^ iv_last
)
payload = iv + tmpblock + iv
send_data(payload)
length, data = recv_data()
if 'Looks' not in data:
# success
plain0 += chr(j)
tag = True
break
if not tag:
break
# means the last byte is ok
if plain0 != '':
break
plain0 += chr(iv_last ^ last_byte)
return plain0