概述
Broken Decryptor是来自于HTB(hackthebox.com)的一个中级密码学挑战,完成该挑战所需要掌握的知识点在于使用CTR模式的AES算法。
题目分析
相关的任务文件包括一个challenge.py源代码以及一个在线运行环境。
challenge.py代码节选如下
import socketserver
from Crypto.Cipher import AES
from Crypto.Util import Counter
import os
key = os.urandom(0x10).replace(b'\x00', b'\xff')
iv = os.urandom(0x10).replace(b'\x00', b'\xff')
def xor(a, b):
return bytes([_a ^ _b for _a, _b in zip(a, b)])
def unhex(msg):
return bytes.fromhex(msg)
def encrypt(data):
ctr = Counter.new(128, initial_value=int(iv.hex(), 16))
crypto = AES.new(key, AES.MODE_CTR, counter=ctr)
if type(data) != bytes:
data = data.encode()
otp = os.urandom(len(data)).replace(b'\x00', b'\xff')
return xor(crypto.encrypt(data), otp)
def decrypt(data):
ctr = Counter.new(128, initial_value=int(iv.hex(), 16))
crypto = AES.new(key, AES.MODE_CTR, counter=ctr)
return crypto.decrypt(data.encode())
def get_flag():
flag = open('flag.txt', 'r').read().strip()
return encrypt(flag)
def send_msg(s, msg):
s.send(msg.encode())
def get_input(s, msg):
send_msg(s, msg)
data = b''
while (recv := s.recv(0x1000)) != b'':
data += recv
if data.endswith(b'\n'):
break
data = data.strip()
return data.decode()
def main(s):
while True:
send_msg(s, '1) Get flag\n')
send_msg(s, '2) Encrypt Message\n')
send_msg(s, '3) Decrypt Message\n')
try:
opt = get_input(s, 'Your option: ')
if opt == '1':
send_msg(s, get_flag().hex()+'\n')
elif opt == '2':
pt = get_input(s, 'Enter plaintext: ')
send_msg(s, encrypt(unhex(pt)).hex()+'\n')
elif opt == '3':
ct = get_input(s, 'Enter ciphertext: ')
send_msg(s, decrypt(unhex(ct)).hex()+'\n')
else:
send_msg(s, 'Invalid option!\n')
except:
send_msg(s, 'An error occured.')
return
以上代码提供了三个输入选项,选项1对flag进行加密并返回密文,选项2运行用户输入明文并返回相应的密文,选项3运行用户输入密文并返回相应的明文。 加密算法是使用CTR模式的AES算法。
运行以上代码可以发现选项3的实现有问题,始终换回错误信息。 另外可以可以看到CTR模式所用的key和IV都是定值。但其加密输出又与一个随机生成的OTP进行异或后生成最终的密文。
解题过程
整个解题过程分为两个步骤。
第一步在于获得flag的AES加密的输出t,也就是与OTP进行异或的输入。 我们知道t与OTP异或的结果c, 但OTP未知,因此我们只能采取暴力穷举。 对于OTP中的第j个字节而言,其可能的数值为1到255之间的某个数 (0被替换成255),因此,我们可以对c[j]与每个可能的数值进行异或,以得到所有可能的t[j], 使用选项1重复运行该步骤,可以减少t[j]的可能值数量,当t中的每一个字节都只有一个可能值时,我们就得到了整个t。
获得了c之后,第二步就可以获得flag的明文。 其原理在于CTR模式下当key和IV是定值时,将密文再次加密就可以获得明文。因此使用与第一步相同的方法,但使用选项2并输入t, 穷举完成之后,就得到完整的明文。
代码如下:
#!/usr/bin/python3
from Crypto.Cipher import AES
from Crypto.Util import Counter
import os
from Crypto.Util.number import bytes_to_long, long_to_bytes
from pwn import remote
flag_length = 15
def encrypt1():
conn.recvuntil("Your option:")
conn.sendline("1")
line = conn.recvline()
line = line.decode().strip()
return bytes.fromhex(line)
def encrypt2(data):
conn.recvuntil("Your option:")
conn.sendline("2")
conn.recvuntil("Enter plaintext:")
conn.sendline(data)
line = conn.recvline()
line = line.decode().strip()
return bytes.fromhex(line)
def checkBruteForceResults(results):
for i in range(flag_length):
if (sum(results[i]) > 1):
print("check byte #", i, " sum = ", sum(results[i]))
return False
return True
def bruteForceWithOTP(option, data = None):
brute_force_results = []
for _ in range(flag_length):
brute_force_results.append([1] * 256)
done = False
while (not done):
if (option == 1):
c = encrypt1()
else:
c = encrypt2(data)
for i in range(flag_length):
possible_bytes = [0] * 256
#穷举所有可能的OTP值
for j in range(0, 256):
if (j == 0):
j = 0xff
possible_byte = c[i] ^ j
possible_bytes[possible_byte] = 1
for j in range(256):
if (possible_bytes[j] == 0):
#剔除不可能的数值
brute_force_results[i][j] = 0
done = checkBruteForceResults(brute_force_results)
#穷举完成, 获取结果
result = bytearray(flag_length)
for i in range(flag_length):
for j in range(256):
if (brute_force_results[i][j] == 1):
result[i] = j
break
return result.hex()
conn = remote('139.59.172.163', 30827, level = 'error')
t = bruteForceWithOTP(1)
print("t =", t)
flag = bruteForceWithOTP(2, t)
print("flag =", bytes.fromhex(flag))