概述
Twisted Entanglement是来自于HTB(hackthebox.com)的一个中级密码学挑战,完成该挑战所需要掌握的知识点在于椭圆曲线算法和Python中随机数的产生机制。
题目分析
相关的任务文件包括server.py,util.py源代码和一个在线环境。
server.py内容节选如下
from util import *
assert private_key < 8748541127929402731638
p = 115792089237316195423570985008687907853269984665640564039457584007908834671663
a = 0
b = 7
E = {"a": a, "b": b, "p": p}
def main(s):
G = [
0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
]
Q = multiply(private_key, G, E)
sendMessage(s, f"Public Key: {Q}")
while True:
sendMessage(s, menu)
try:
option = receiveMessage(s, "\n> ")
if option == "1":
user_point = receiveMessage(s, "\nEnter your point x,y: ")
point = parseUserPoint(user_point)
public_key = multiply(private_key, point, E)
sendMessage(s, f"\nHere's your new Public Key: {public_key}")
elif option == "2":
user_basis = receiveMessage(
s, "\nChoose your 256 basis for the KEP: ")
basis = parseUserBasis(user_basis)
q_server_key, q_user_key = generateKeys(basis, private_key)
ciphertext = encrypt(FLAG, q_server_key)
sendMessage(s, f"\nThe Quantum key: {q_user_key}")
sendMessage(s, f"\nFlag Encrypted: {ciphertext.hex()}")
elif option == "3":
sendMessage(s, "\nQuantum Goodbye!")
break
else:
sendMessage(s, "\nInvalid option!")
except Exception as e:
sendMessage(s, f"\nOoops! something strange happen X__X: {e}")
util.py内容节选如下
import netsquid as ns
from random import seed, randint
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
O = "Origin"
ns.sim_reset()
def eea(r0, r1):
if r0 == 0:
return (r1, 0, 1)
else:
g, s, t = eea(r1 % r0, r0)
return (g, t - (r1 // r0) * s, s)
def add(P, Q, a, m):
if (P == O):
return Q
elif (Q == O):
return P
elif ((P[0] == Q[0]) and (P[1] == m - Q[1])):
return O
else:
if (P[0] == Q[0] and P[1] == Q[1]):
S = ((3 * (pow(P[0], 2)) + a) * eea(2 * P[1], m)[1]) % m
else:
S = ((Q[1] - P[1]) * eea((Q[0] - P[0]) % m, m)[1]) % m
x3 = (pow(S, 2) - P[0] - Q[0]) % m
y3 = (S * (P[0] - x3) - P[1]) % m
Q[0], Q[1] = x3, y3
return [x3, y3]
def multiply(s, P, E):
s = list(int(k) for k in "{0:b}".format(s))
a, p = E["a"], E["p"]
del s[0]
T = P.copy()
for i in range(len(s)):
T = add(T, T, a, p)
if (s[i] == 1):
T = add(P, T, a, p)
return T
def parseUserPoint(user_point):
return [int(c) for c in user_point.split(",")]
def parseUserBasis(user_basis):
if len(user_basis) != 256:
raise Exception("Input must be of length 256")
basis = []
for b in user_basis:
if b == "Z":
base = ns.Z
elif b == "X":
base = ns.X
else:
raise Exception(
"Incorrect base, must be Standard (Z) of Hadamard (X)")
basis.append(base)
return basis
def measure(q, obs):
res, _ = ns.qubits.measure(q, obs)
return res
def randomBasis():
r = randint(0, 1)
return ns.Z if r else ns.X
def bitsToHash(bits):
bit_string = ''.join([str(i) for i in bits])
blocks = bytes(
[int(bit_string[i:i + 8], 2) for i in range(0, len(bit_string), 8)])
return sha256(blocks).digest()
def bitsToHex(bits):
bit_string = ''.join([str(i) for i in bits])
blocks = bytes(
[int(bit_string[i:i + 8], 2) for i in range(0, len(bit_string), 8)])
return blocks.hex()
def generateKeys(basis, private_key):
seed(private_key)
q_server_key = []
q_user_key = []
for i in range(256):
qubits = ns.qubits.create_qubits(2)
q1, q2 = qubits[0], qubits[1]
ns.qubits.operate(q1, ns.X)
ns.qubits.operate(q1, ns.H)
ns.qubits.operate(q2, ns.X)
ns.qubits.combine_qubits([q1, q2])
ns.qubits.operate([q1, q2], ns.CX)
q_server_key.append(measure(q1, randomBasis()))
q_user_key.append(measure(q2, basis[i]))
q_server_key = bitsToHash(q_server_key)
q_user_key = bitsToHex(q_user_key)
return q_server_key, q_user_key
def encrypt(message, key):
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(message, 16))
return ciphertext
以上代码实现的加密算法包括多个部分,首先是给定的椭圆曲线private_key, 该数值被用于Python random随机数的种子以生成量子操作符(Z或X), 其次是使用netsquid来模拟实现对量子的观察,观察的结果值(0或1)被用于生成q_server_key和q_user_key, 而q_server_key的sha256哈希值则作为AES加密的密钥。
运行环境提供三个输入选项,选项1允许用户输入坐标值(x, y), 然后返回[private_key]与改点的标量积结果。选项2允许用户输入256个量子操作符,然后返回这些操作符进行量子观察的结果值,以及AES加密的密文。选项3无用。
解题过程
首先我们必须获得椭圆曲线private_key,提供的椭圆曲线代码并不检验用户输入的坐标是否在给定的曲线上,而且只使用给定的a和p曲线参数,而b并没有用到。 因此我们可以使用相同的a和p, 但不同的b来创建另一个椭圆曲线,b的选择要求是使其对应的曲线上的离散对数符合Pohlig-Hellman算法的要求。
以下代码使用b=6的曲线,并或取点的阶数,
p = 115792089237316195423570985008687907853269984665640564039457584007908834671663
a = 0
b = 6
EC = EllipticCurve(GF(p), [a, b])
Gx = 97739641136662608657079256755827419133838433889311376347497047878595450848685
Gy = 98100600220769146147883276184268394981687000350669426476581029710371895499142
G = EC(Gx, Gy)
G.order()
# 8270863516951156815969356072049136275281522608437447405948333614614684278506
对该阶数进行素因数分解,并根据代码中给出的private_key上限确认符合Pohlig-Hellman算法的要求。
>> factor(8270863516951156815969356072049136275281522608437447405948333614614684278506)
***factors found***
P1 = 2
P1 = 7
P5 = 10903
P7 = 5290657
P11 = 10833080827
P14 = 22921299619447
P41 = 41245443549316649091297836755593555342121
输入该坐标对后,获取标量积结果,然后使用Pohlig-Hellman算法就可以获得private_key
#标量积结果
Qx = 3857225661745020856873269956141698742872251158780186082433874002180145459209
Qy = 6544502763778813556431537492609375417205638644494698005245711242038528944385
Q = EC(Qx, Qy)
dlogs = []
for fac in primes:
t = int(G.order()) // int(fac)
dlog = discrete_log(t*Q, t*G, operation="+")
dlogs += [dlog]
private_key = crt(dlogs, primes)
得到private_key后,我们就可以获取生成q_server_key的量子操作符序列,其原理在于Python radom随机数产生的序列由其种子决定,使用相同的种子值,其产生的随机数序列也相同。
seed(private_key)
server_basis = ""
for i in range(256):
b = randomBasis()
if (b == ns.Z):
server_basis = server_basis + "Z"
else:
server_basis = server_basis + "X"
print("server_basis=", server_basis)
通过实验,我们可以发现在generateKeys方法中,当对q1和q2使用相同的basis调用measure方法时,其返回的结果总是相反的。由此我们可以使用选项2输入server_basis, 然后将获得的结果还原成二进制,然后逐位求反,就可以获得q_server_key
#q_user_key
hex = "a425ec07feabe32f689e7bf2322f171217a1549d2ee00f54622d99ea26dcf27d"
blocks = bytes.fromhex(hex)
bits = []
for block in blocks:
s = bin(int(block))[2:].zfill(8)
for i in s:
bits.append(int(i))
server_bits = []
for b in bits:
if (b == 0):
server_bits.append(1)
else:
server_bits.append(0)
q_server_key = bitsToHash(server_bits)
得到q_server_key后,就可以进行AES解密了。
cipher_text = bytes.fromhex("0842dbf2337a3be8b1a03ba2692ce7ed046902d537cc99613b73a372e280229a4f4f6caca4e827a952ee88426702f1dcd0f03b9fcee64d5729d46d15954bbf6a234222058295fd2c257eceab1fd9e5b0")
cipher = AES.new(q_server_key, AES.MODE_ECB)
plain_text = cipher.decrypt(cipher_text)
print(f"plain_text = {plain_text}")