密码学实战 - HTB Fibopadcci

238 阅读4分钟

概述

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进行加密并返回密文,以及相应的ab值,选项1则允许用户输入密文,以及相应的b,然后返回解密是否成功。

整个加密过程可以用下图表示,而解密则是加密的逆过程, 主要的区别在于加密时ab随机生成,而解密时,用户可以选择b,但a是一个定值。

crypto_019_HTB Fibopadcci.png

解题过程

由上图可知,我们必须对各个密文块依次解密,前一个密文块以及对应明文将用于后一个密文块的解密。

解密的过程分为两步, 第一步对密文块进行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,使用相同的方法可以解出第二个明文块, 以此类推就可解出整个密文。