密码学实战 - HTB Composition

358 阅读19分钟

概述

Composition是来自于HTB(hackthebox.com)的一个中级密码学挑战,完成该挑战所需要掌握的知识点在于椭圆曲线算法。

题目分析

相关的任务文件包括ecc.pyserver.py源代码以及一个在线运行环境。

ecc.py内容如下

from collections import namedtuple
from functools import reduce
from operator import mul
from Crypto.Util.number import inverse
import random

Point = namedtuple("Point","x y")

def moddiv(x,y,p):
    return (x * inverse(y,p)) % p

def crt(*args):
    # Takes a bunch of lists in form [value,modulus]
    values = [row[0] for row in args]
    ns = [row[1] for row in args]
    N = reduce(mul,ns)
    _sum = 0
    for i in range(len(args)):
        yi = N // ns[i]
        zi = inverse(yi,ns[i])
        _sum += values[i]*yi*zi
    return _sum % N

def composite_square_root(num,p,q):
    # Only works if num is a quadratic residue mod p and q AND p and q are 3 mod 4
    n = p * q
    root1 = pow(num,(p+1)//4,p)
    root2 = pow(num,(q+1)//4,q)
    ans = crt([root1,p],[root2,q])
    assert pow(ans,2,n) == (num % n)
    return ans

class EllipticCurve:
    INF = Point(0,0)
    def __init__(self, a, b, p):
        self.a = a
        self.b = b
        self.p = p
    def add(self,P,Q):
        if P == self.INF:
            return Q
        elif Q == self.INF:
            return P

        if P.x == Q.x and P.y == (-Q.y % self.p):
            return self.INF
        if P != Q:
            Lambda = moddiv(Q.y - P.y, Q.x - P.x, self.p)
        else:
            Lambda = moddiv(3 * P.x**2 + self.a,2 * P.y , self.p)
        Rx = (Lambda**2 - P.x - Q.x) % self.p
        Ry = (Lambda * (P.x - Rx) - P.y) % self.p
        return Point(Rx,Ry)
        
    def multiply(self,P,n):
        n %= self.p
        if n != abs(n):
            ans = self.multiply(P,abs(n))
            return Point(ans.x, -ans.y % p)
        R = self.INF
        while n > 0:
            if n % 2 == 1:
                R = self.add(R,P)
            P = self.add(P,P)
            n = n // 2
        return R
        
    def lift_x(self,x,p,q):
        expr = x**3 + self.a*x + self.b
        y = composite_square_root(expr,p,q)
        return Point(x,y)

server.py内容如下

from Crypto.Util.number import isPrime, getPrime, GCD, long_to_bytes, bytes_to_long
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from secret import flag
from ecc import EllipticCurve
from hashlib import md5
import os
import random

print("Welcome to the ECRSA test center. Your encrypted data will be sent soon.")
print("Please check the logs for the parameters.")

legendre = lambda x,p: pow(x,(p-1)//2,p)

def next_prime(num):
    if num % 2 == 0:
        num += 1
    else:
        num += 2
    while not isPrime(num):
        num += 2
    return num

def getrandpoint(ec,p,q):
    num = random.randint(1,p*q)
    while legendre(expr(num),p) != 1 or legendre(expr(num),q) != 1:
        num = random.randint(1,p*q)
    return ec.lift_x(num,p,q)
    
    
# Calculate discriminant(ensures elliptic curve is non-singular)
calc_discrim = lambda a,b,n: (-16 * (4 * a**3 + 27 * b**2)) % n

def keygen(bits):
    # Returns RSA key in form ((e,n),(p,q))
    p = getPrime(bits // 2)
    while p % 4 == 1:
        p = next_prime(p)
    e = next_prime(p >> (bits // 4))
    q = next_prime(p)
    for i in range(50):
        q = next_prime(q)
    while q % 4 == 1:
        q = next_prime(q)
    n = p * q
    if n.bit_length() != bits:
        return keygen(bits)
    return (e,n),(p,q)
print("Generating your key...")
key = keygen(512)
e,n = key[0]
p,q = key[1]
print("Creating ECC params")
# Gotta make sure the params are valid
a,b = random.getrandbits(128),random.getrandbits(128)
discrim = calc_discrim(a,b,n)
expr = lambda x: x**3 + a*x + b

while not discrim:
    a,b = random.getrandbits(128),random.getrandbits(128)
    discrim = calc_discrim(a,b,n)

ec = EllipticCurve(a,b,n)

g = getrandpoint(ec,p,q)

A = ec.multiply(g,e)

# Use key that has been shared with ECRSA
key = md5(str(g.x).encode()).digest()
iv = os.urandom(16)
cipher = AES.new(key,AES.MODE_CBC,iv)
data = cipher.encrypt(pad(flag,16))
print(f"Encrypted flag: {data.hex()}")
print(f"IV: {iv.hex()}")
print(f"N: {n}")
print(f"ECRSA Ciphertext: {A}")
print("Would you like to test the ECRSA curve?")
if input("[y/n]> ") == 'n':
    exit()
print("Generating random point...")
print(getrandpoint(ec,p,q))
print("Thanks for testing!")

我们可以看到以上加密的过程分为两个步骤,第一步使用椭圆曲线算法,第二步则使用AES。

使用提供的在线环境,得到如下数据

$ nc 68.183.47.198 32373
Welcome to the ECRSA test center. Your encrypted data will be sent soon.
Please check the logs for the parameters.
Generating your key...
Creating ECC params
Encrypted flag: 6b6ca22f36821e1efb8f06e6be0e63a58a0a68a55a550bc305b7d3ade42d37a9cb7243e79d1178f66e55ee6c2c6c028132a7f5077432baa7114ab4377c922adb
IV: 052a0c940bbe594a3acd6ee8745af468
N: 7418047617976484380023089967316816047896842312988655390245270648452053035583660338516845941624984957634389917172279787944191962057776787412971209247542641
ECRSA Ciphertext: Point(x=5999780899979132298815917806277551189943825849887566166396844815382144235347171867102064885353503179104030913138549347680037270490038101582462241332658640, y=2305678117374164998171166708386620999748529710219556400508078962332536807765806990521540910960846933571839427629115557610389876992635360223253611860325660)
Would you like to test the ECRSA curve?

Generating random point...
Point(x=4737232813526999245732335265217373607509494495030656658865172408587706156513540608669983319290795334107073584160668092692732198765752725431342728924803866, y=5589167295257706199115799655863257053008058760356255386704758935066536423572797569504381575097484798889484032483448174719446951334738339027291970937805999)

解题过程

首先,通过椭圆曲线上给定的两个点,计算该曲线的ab系数

from Crypto.Util.number import inverse

N = 7418047617976484380023089967316816047896842312988655390245270648452053035583660338516845941624984957634389917172279787944191962057776787412971209247542641

Px = 5999780899979132298815917806277551189943825849887566166396844815382144235347171867102064885353503179104030913138549347680037270490038101582462241332658640

Py = 2305678117374164998171166708386620999748529710219556400508078962332536807765806990521540910960846933571839427629115557610389876992635360223253611860325660

Qx = 4737232813526999245732335265217373607509494495030656658865172408587706156513540608669983319290795334107073584160668092692732198765752725431342728924803866

Qy = 5589167295257706199115799655863257053008058760356255386704758935066536423572797569504381575097484798889484032483448174719446951334738339027291970937805999

a = ((Py ** 2 - Px ** 3 - Qy ** 2 + Qx ** 3) * inverse(Px-Qx, N)) % N
b  = (Py ** 2 - Px ** 3 - a * Px) % N
assert (Px ** 3 + a * Px + b) % N == Py ** 2 % N
assert (Qx ** 3 + a * Qx + b) % N == Qy ** 2 % N

print("a=", a)
print("b=", b)

#a= 55132848128243528383228523718629442569
#b= 66745471766449167142898250203962326746

其次,我们看到该椭圆曲线使用的模数N是两个素数pq的乘积, 因此需要对其进行分解, 使用YAFU可以获得如下素因子。

===============================================================
======= Welcome to YAFU (Yet Another Factoring Utility) =======
=======             bbuhrow@gmail.com                   =======
=======     Type help at any time, or quit to quit      =======
===============================================================
cached 78498 primes. pmax = 999983


>> factor(7418047617976484380023089967316816047896842312988655390245270648452053035583660338516845941624984957634389917172279787944191962057776787412971209247542641)

fac: factoring 7418047617976484380023089967316816047896842312988655390245270648452053035583660338516845941624984957634389917172279787944191962057776787412971209247542641
fac: using pretesting plan: normal
fac: no tune info: using qs/gnfs crossover of 95 digits
div: primes less than 10000
fmt: 1000000 iterations
Total factoring time = 0.2670 seconds


***factors found***

P77 = 86128088437956663964356496898715198865502936958224674910641165021904434760951
P77 = 86128088437956663964356496898715198865502936958224674910641165021904434751191

ans = 1

>>

使用分解结果,我们可以得到p, qe

p = min(86128088437956663964356496898715198865502936958224674910641165021904434760951, 86128088437956663964356496898715198865502936958224674910641165021904434751191)

# 86128088437956663964356496898715198865502936958224674910641165021904434751191

q = max(86128088437956663964356496898715198865502936958224674910641165021904434760951, 86128088437956663964356496898715198865502936958224674910641165021904434751191)

# 86128088437956663964356496898715198865502936958224674910641165021904434760951

bits = 512
e = next_prime(p >> (bits // 4))

# 253107703514851088106769997566192881757

N = p * q的情况下, 该曲线可以看成是另外两条曲线的组合, 这两条曲线使用相同的AB系数,其模数则一条为p,另一条则为q

由于pq都是素数,我们可以使用常规的椭圆曲线方法来分别计算g在这两条曲线上的坐标。

a= 55132848128243528383228523718629442569
b= 66745471766449167142898250203962326746

p = 86128088437956663964356496898715198865502936958224674910641165021904434751191

e = 253107703514851088106769997566192881757
 
F_p = GF(p)

EC_P = EllipticCurve(F_p, [a, b])

inverse_e_p = inverse_mod(e, EC_P.order())

A_on_EC_P = EC_P(5999780899979132298815917806277551189943825849887566166396844815382144235347171867102064885353503179104030913138549347680037270490038101582462241332658640, 2305678117374164998171166708386620999748529710219556400508078962332536807765806990521540910960846933571839427629115557610389876992635360223253611860325660)

G_on_EC_P = inverse_e_p * A_on_EC_P

# (25650306284800938489823344339763469413733349250212875793302994571060072430870 : 63867768177300002714240081397818297393115996637584599269405725601492257607762 : 1)

q = 86128088437956663964356496898715198865502936958224674910641165021904434760951

F_q = GF(q)

EC_Q = EllipticCurve(F_q, [a, b])

inverse_e_q = inverse_mod(e, EC_Q.order())

A_on_EC_Q = EC_Q(5999780899979132298815917806277551189943825849887566166396844815382144235347171867102064885353503179104030913138549347680037270490038101582462241332658640, 2305678117374164998171166708386620999748529710219556400508078962332536807765806990521540910960846933571839427629115557610389876992635360223253611860325660)

G_on_EC_Q = inverse_e_q * A_on_EC_Q

# (16874957431056929761238054751995542286749901127078133836509079012619344412177 : 32670506373989604729615288814763972988067742908957147704021562573890049282237 : 1)

随后可以使用中国剩余定理得出g在以N为模数的曲线上的坐标。

x = crt([25650306284800938489823344339763469413733349250212875793302994571060072430870, 16874957431056929761238054751995542286749901127078133836509079012619344412177], [p, q])

# 7172630243326458225399616552182847194469376787623665204840030600859174461209606641318281772742328304884585896402488082826958997203347574962359211934614468

最后,使用g的x坐标生成AES密钥对密文进行解密

from Crypto.Cipher import AES
from hashlib import md5

gx = 7172630243326458225399616552182847194469376787623665204840030600859174461209606641318281772742328304884585896402488082826958997203347574962359211934614468
key = md5(str(gx).encode()).digest()
iv = bytes.fromhex("052a0c940bbe594a3acd6ee8745af468")
cipher = AES.new(key,AES.MODE_CBC,iv)
cipher_text = bytes.fromhex("6b6ca22f36821e1efb8f06e6be0e63a58a0a68a55a550bc305b7d3ade42d37a9cb7243e79d1178f66e55ee6c2c6c028132a7f5077432baa7114ab4377c922adb")
flag = cipher.decrypt(cipher_text)
print("flag =", flag)