概述
Fibopadcci是来自于HTB(hackthebox.com)的一个中级密码学挑战,完成该挑战所需要掌握的知识点在于针对AES算法的扩展(padding)攻击。
题目分析
相关的任务文件包括一个server.py源代码以及一个在线环境, 实现了一个基于AES算法的加解密系统, 代码节选如下
from Crypto.Cipher import AES
import os
from secret import flag, key
fib = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 121, 98, 219, 61]
menu_msg = """\n
-------------------------
| Menu |
-------------------------
|[0] Encrypt flag. |
|[1] Send me a message! |
-------------------------
"""[1:]
def xor(a, b):
return bytes([_a ^ _b for _a, _b in zip(a, b)])
def pad(data): #Custom padding, should be fine!
c = 0
while len(data) % 16:
pad = str(hex(fib[c] % 255))[2:]
data += unhex("0" * (2-len(pad)) + pad)
c += 1
return data
def checkpad(data):
if len(data) % 16 != 0:
return 0
char = data[-1]
try:
start = fib.index(char)
except ValueError:
return 0
newfib = fib[:start][::-1]
for i in range(len(newfib)):
char = data[-(i+2)]
if char != newfib[i]:
return 0
return 1
def unhex(data):
return bytes.fromhex(data)
class SuperSecureEncryption: # This should be unbreakable!
def __init__(self, key):
self.cipher = AES.new(key, AES.MODE_ECB)
def encrypt(self, data):
data = pad(data)
a = os.urandom(16).replace(b'\x00', b'\xff')
b = os.urandom(16).replace(b'\x00', b'\xff')
lb_plain = a
lb_cipher = b
output = b''
data = [data[i:i+16] for i in range(0, len(data), 16)]
for block in data:
enc = self.cipher.encrypt(xor(lb_cipher, block))
enc = xor(enc, lb_plain)
output += enc
lb_plain = block
lb_cipher = enc
return output, a.hex(), b.hex()
def decrypt(self, data, a, b):
lb_plain = a
lb_cipher = b
output = b''
data = [data[i:i+16] for i in range(0, len(data), 16)]
for block in data:
dec = self.cipher.decrypt(xor(block, lb_plain))
dec = xor(dec, lb_cipher)
output += dec
lb_plain = dec
lb_cipher = block
if checkpad(output):
return output
else:
return None
def encryptFlag():
encrypted, a, b = SuperSecureEncryption(key).encrypt(flag)
return f'encrypted_flag: {encrypted.hex()}\na: {a}\nb: {b}'
def sendMessage(ct, a, b):
if len(ct) % 16:
return "Error: Ciphertext length must be a multiple of the block length (16)!"
if len(a) != 16 or len(b) != 16:
return "Error: a and b must have lengths of 16 bytes!"
decrypted = SuperSecureEncryption(key).decrypt(ct, a, b)
if decrypted != None:
return "Message successfully sent!"
else:
return "Error: Message padding incorrect, not sent."
def handle(self):
self.write(wlc_msg)
while True:
self.write(menu_msg)
option = self.query("Your option: ")
if option == "0":
self.write(encryptFlag())
elif option == "1":
try:
ct = unhex(self.query("Enter your ciphertext in hex: "))
b = unhex(self.query("Enter the B used during encryption in hex: "))
a = b'HTB{th3_s3crt_A}' # My secret A! Only admins know it, and plus, other people won't be able to work out my key anyway!
self.write(sendMessage(ct,a,b))
except ValueError as e:
self.write("Provided input is not hex!")
else:
self.write("Invalid input, please try again.")
...
以上代码提供两个选项,选项0调用encryptFlag方法对flag进行加密并返回密文,以及相应的a和b值,选项1则允许用户输入密文,以及相应的b,然后返回解密是否成功。
整个加密过程可以用下图表示,而解密则是加密的逆过程, 主要的区别在于加密时a和b随机生成,而解密时,用户可以选择b,但a是一个定值。
解题过程
由上图可知,我们必须对各个密文块依次解密,前一个密文块以及对应明文将用于后一个密文块的解密。
解密的过程分为两步, 第一步对密文块进行xor操作,使之对应于内置的a,第二步则是使用系统提供的选项1来对各个可能的字符进行枚举并判断是否是明文字符,首先解出第16个字符,然后解第15个...,直到第1个字符, 其利用的漏洞在于该算法中使用的扩展方法,以第16个字符为例,当该字符是fib[0]时,选项1会返回成功信息,而对第15个和16个字符为例,则 必须是(fib[0],fib[1])。
代码如下
PRINTABLE_CHARS = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
conn = remote('167.99.206.87', 31348)
#选项1测试
def oracle(ct, b):
conn.recvuntil('Your option:')
conn.sendline("1")
conn.recvuntil('Enter your ciphertext in hex:')
conn.sendline(ct)
conn.recvuntil('Enter the B used during encryption in hex:')
conn.sendline(b)
r = conn.recvline()
return (b"Error" not in r)
#内置的a
A = b'HTB{th3_s3crt_A}'
#选项0返回的a和b
a = bytes.fromhex("146b5fe0ff8aede66698a369e48c3ab5")
b = bytes.fromhex("63cfef0856c029312ff06f6a88eee7b4")
#选项0返回的密文
encrypted_flag = bytes.fromhex("f4e8ec75aa383006932b52cf07982f57c092dc7ea9ba89ce0e48ac9f3e45185ec7095fa116f928ab7a57d7d9db2cff00")
#第一个密文块
c1 = encrypted_flag[:16]
# 对应于A的密文块 = c1 xor a xor A
c1 = xor(c1, a)
c1 = xor(c1, A)
#第一个明文块
flag = bytearray(16)
#以解出的明文字符数量
numberOfKnownChars = 0
while (numberOfKnownChars < 16):
print("numberOfKnownChars = ", numberOfKnownChars)
#解密所用的b, xor的结果符合扩展检查
bx = bytearray(b)
for i in range(numberOfKnownChars):
bx[15 - i] = fib[numberOfKnownChars - i] ^ flag[15 - i] ^ b[15 - i]
#枚举可能的字符
for c in PRINTABLE_CHARS:
bx[15 - numberOfKnownChars] = fib[0] ^ ord(c) ^ b[15 - numberOfKnownChars]
#使用选项1以测试当前字符是否是解
if (oracle(c1.hex(), bx.hex())):
print("Found ", c)
flag[15 - numberOfKnownChars] = ord(c)
print("FLAG = ", flag.hex(), "," , flag)
break
numberOfKnownChars = numberOfKnownChars + 1
使用以上代码可以解出第一个明文块, 然后可以用第一个明文块作为a,第一个密文块作为b,使用相同的方法可以解出第二个明文块, 以此类推就可解出整个密文。