密码学实战 - HTB Twisted Entanglement

1,315 阅读7分钟

概述

Twisted Entanglement是来自于HTB(hackthebox.com)的一个中级密码学挑战,完成该挑战所需要掌握的知识点在于椭圆曲线算法和Python中随机数的产生机制。

题目分析

相关的任务文件包括server.pyutil.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_keyq_user_key, 而q_server_key的sha256哈希值则作为AES加密的密钥。

运行环境提供三个输入选项,选项1允许用户输入坐标值(x, y), 然后返回[private_key]与改点的标量积结果。选项2允许用户输入256个量子操作符,然后返回这些操作符进行量子观察的结果值,以及AES加密的密文。选项3无用。

解题过程

首先我们必须获得椭圆曲线private_key,提供的椭圆曲线代码并不检验用户输入的坐标是否在给定的曲线上,而且只使用给定的ap曲线参数,而b并没有用到。 因此我们可以使用相同的ap, 但不同的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方法中,当对q1q2使用相同的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}")