The “H1” Challenge

We are given two files. A python3 script (chall.py):

Python Script (click to expand)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/python3
import os
import hashlib
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

flag = open('flag.txt').read()
INF = (1, 1, 0)

mod = 8948962207650232551656602815159153422162609644098354511344597187200057010413552439917934304191956942765446530386427345937963894309923928536070534607816947
a = 6294860557973063227666421306476379324074715770622746227136910445450301914281276098027990968407983962691151853678563877834221834027439718238065725844264138
b = 3245789008328967059274849584342077916531909009637501918328323668736179176583263496463525128488282611559800773506973771797764811498834995234341530862286627
n = 8948962207650232551656602815159153422162609644098354511344597187200057010413418528378981730643524959857451398370029280583094215613882043973354392115544169
G = (5139617820728399941653175323358137352238277428061991823713659546881441331696699723004749024403291797641521696406798421624364096550661311227399430098134141,
     1798860115416690485862271986832828064808333512613833729548071279524320966991708554765227095605106785724406691559310536469721469398449016850588110200884962,
     5042518522433577951395875294780962682755843408950010956510838422057522452845550974098236475624683438351211176927595173916071040272153903968536756498306512)

def Double(p):
    x, y, z = p
    if z == 0 or y == 0:
        return INF
    ysqr = y * y % mod
    zsqr = z * z % mod
    s = 4 * x * ysqr % mod
    m = (3 * x * x + a * zsqr * zsqr) % mod
    x2 = (m * m - 2 * s) % mod
    y2 = (m * (s - x2) - 8 * ysqr * ysqr) % mod
    z2 = 2 * y * z % mod
    return x2, y2, z2

def Add(p, q):
    if p[2] == 0:
        return q
    if q[2] == 0:
        return p
    x1, y1, z1 = p
    x2, y2, z2 = q
    z1sqr = z1 * z1 % mod
    z2sqr = z2 * z2 % mod
    u1 = x1 * z2sqr % mod
    u2 = x2 * z1sqr % mod
    s1 = y1 * z2 * z2sqr % mod
    s2 = y2 * z1 * z1sqr % mod
    if u1 == u2:
        if s1 != s2:
            return INF
        else:
            return Double(p)
    h = u2 - u1 % mod
    hsqr = h * h % mod
    hcube = hsqr * h % mod
    r = s2 - s1 % mod
    t = u1 * hsqr % mod
    x3 = (r * r - hcube - 2 * t) % mod
    y3 = (r * (t - x3) - s1 * hcube) % mod
    z3 = h * z1 * z2 % mod
    return x3, y3, z3

def Multiply(p, x):
    if p == INF:
        return p
    res = INF
    while x:
        x, r = divmod(x, 2)
        if r:
            res = Add(res, p)
        p = Double(p)
    return res

def Transform(m, l):
    z = m
    shift = l - n.bit_length()
    if shift > 0:
        z >>= shift
    return z

def RNG(nbits, a, b):
    nbytes = nbits // 8
    B = os.urandom(nbytes)
    return a * sum([B[i] * b ** i for i in range(len(B))]) % 2**nbits

def Sign(msg, d):
    h = hashlib.sha512(msg)
    z = Transform(int.from_bytes(h.digest(), 'big'), h.digest_size*8)
    k = RNG(n.bit_length(), 16843009, 4294967296)
    x1, y1, z1 = Multiply(G, k)
    r = (x1 * pow(z1, -2, mod) % mod) % n
    s = pow(k, -1, n) * (z + r * d) % n
    assert int(s) == pow(k, -1, n) * (z + r * d) % n
    return r, s

def Verify(msg, Q, r, s):
    h = hashlib.sha512(msg)
    z = Transform(int.from_bytes(h.digest(), 'big'), h.digest_size*8)
    u1 = z*pow(s, -1, n) % n
    u2 = r*pow(s, -1, n) % n
    x1, y1, z1 = Add(Multiply(G, u1), Multiply(Q, u2))
    return r == (x1 * pow(z1, -2, mod) % mod) % n

def Encrypt(plaintext, x):
    key = hashlib.sha256(str(x).encode()).digest()
    aes = algorithms.AES(key)
    encryptor = Cipher(aes, modes.ECB(), default_backend()).encryptor()
    padder = padding.PKCS7(aes.block_size).padder()
    padded_data = padder.update(plaintext) + padder.finalize()
    ciphertext = encryptor.update(padded_data) + encryptor.finalize()
    return ciphertext

def Decrypt(ciphertext, x):
    key = hashlib.sha256(str(x).encode()).digest()
    aes = algorithms.AES(key)
    decryptor = Cipher(aes, modes.ECB(), default_backend()).decryptor()
    unpadder = padding.PKCS7(aes.block_size).unpadder()
    decrypted_data = decryptor.update(ciphertext) + decryptor.finalize()
    plaintext = unpadder.update(decrypted_data) + unpadder.finalize()
    return plaintext

# Alice and Bob have their keys:
da = RNG(n.bit_length(), 1, 256)
Qa = Multiply(G, da)
db = RNG(n.bit_length(), 1, 256)
Qb = Multiply(G, db)
x1a, y1a, z1a = Multiply(Qb, da)
ka = x1a * pow(z1a, -2, mod) % mod
x1b, y1b, z1b = Multiply(Qa, db)
kb = x1b * pow(z1b, -2, mod) % mod

# Alice sends message to Bob:
msga = b'Hello Bob.'
ra, sa = Sign(msga, da)
ca = Encrypt(msga, ka)
print('Alice -> Bob:', (ra, sa, int.from_bytes(ca, 'big')))

# Bob receives and verifies message:
recv_msg = Decrypt(ca, kb)
assert Verify(recv_msg, Qa, ra, sa)

# Bob sends message to Alice:
msgb = b'Hello Alice.'
rb, sb = Sign(msgb, db)
cb = Encrypt(msgb, kb)
print('Bob -> Alice:', (rb, sb, int.from_bytes(cb, 'big')))

# Alice receives and verifies message:
recv_msg = Decrypt(cb, ka)
assert Verify(recv_msg, Qb, rb, sb)

# Alice sends message to Bob:
msga = (f'Dinner tonight? What about Tapioca? Btw, here is the flag: {flag}').encode()
ra, sa = Sign(msga, da)
ca = Encrypt(msga, ka)
print('Alice -> Bob:', (ra, sa, int.from_bytes(ca, 'big')))

# Bob receives and verifies message:
recv_msg = Decrypt(ca, kb)
assert Verify(recv_msg, Qa, ra, sa)

# Bob sends message to Alice:
msgb = b'Dinner sounds good. Thanks for the flag.'
rb, sb = Sign(msgb, db)
cb = Encrypt(msgb, kb)
print('Bob -> Alice:', (rb, sb, int.from_bytes(cb, 'big')))

# Alice receives and verifies message:
recv_msg = Decrypt(cb, ka)
assert Verify(recv_msg, Qb, rb, sb)

And the following output (stdout from the python script):

Alice -> Bob: (8618416354247009865173783322782283385800726568519779763790691157278063798628048418532907783021806238103423515210146966468025964847364086792099622893845216, 2932674107137731789093617068375500084388905453653468925392946088867116597531950960271857205235755778202380084260003117176704579423285955014316540314931750, 27865871384804321325511205140263204607)
Bob -> Alice: (6706720197123832142768727143395528571627385686729472279085077699672602636953568596628477227511870037092766007177588966333093607595831000050978265637877796, 1073779379108240410856990657565545229209771903946426639922087094813786902448520023335130808991515066506036954730763452318418336144389425011731474342543709, 111403492170712993917428321974111102656)
Alice -> Bob: (8832295267397231051293216564016639537146222596144354850230682204978731311879255662259663270183445827348338041752369314181111940713714991119349376636404112, 8683784208731634307361157916911868656279723101808163939313971801256736484458199874570532609285522391139002296248059424750941962344918156540408403221858292, 105398535464409171419472607677747462033030589690350997911381059472020486557672504778060748058626707326992258591478040500759349352824508941100030623708235493999018571171774658661651532338275358740821547158517615704187173346885098836066743736788259192831313414309775979590033581301910426314601982482556670097620)
Bob -> Alice: (7616464676048536081690041693308621105395807976530049374449777558721544903144139995398352331701243976154631946836252022822278420653297509476320989403738186, 7381293847317597354132365685036776332763847784351725370466906894121224725876600151244989563125805807680044723478850091886778158534219876869209592138625256, 7994736246642278834331127451449673561762900804586058657648578638831731501930073150317394505661139656896430884711113)

The python script implements the operation (Add/Double/Exponentiation) of the curve y2=x3+ax+b y^2 = x^3 + \text{a} x + \text{b} over the 512-bit prime field Zmod \mathbb{Z}_\text{mod} with mod,a,b \text{mod}, \text{a}, \text{b} as defined in the script. All operations are done using Jacobian coordinates. The order of this curve is n n and G G is a valid point on the curve (which has prime order).

The script mocks a key exchange between parties Alice and Bob:

  1. First it picks random secret keys for Alice (da) / Bob (db) using RNG (more on that later) and computes their public key (Qa = G^da, Qb = G^db):

     da = RNG(n.bit_length(), 1, 256)
     Qa = Multiply(G, da)
     db = RNG(n.bit_length(), 1, 256)
     Qb = Multiply(G, db)
    
  2. It then does a Diffie-Hellman key exchange:

     x1a, y1a, z1a = Multiply(Qb, da)
     ka = x1a * pow(z1a, -2, mod) % mod
     x1b, y1b, z1b = Multiply(Qa, db)
     kb = x1b * pow(z1b, -2, mod) % mod
    

    Remembering to convert the x-coordinate from Jacobian coordinates to affine (such that the representation is unique). ka and kb are the resulting shared secret (and ka = kb).

  3. Alice then signs a message:

     # Alice sends message to Bob:
     msga = b'Hello Bob.'
     ra, sa = Sign(msga, da)
    

    Using ECDSA (much more on that later).

  4. Alice then encrypts the message using the shared secret and sents the encrypted message to Bob:

     ca = Encrypt(msga, ka)
     print('Alice -> Bob:', (ra, sa, int.from_bytes(ca, 'big')))
    
  5. Bob receives the message, decrypts it using the shared secret and verifies the ECDSA signature on the decrypted message.

     # Bob receives and verifies message:
     recv_msg = Decrypt(ca, kb)
     assert Verify(recv_msg, Qa, ra, sa)
    

Steps 2-5 is repeated for 4 different message:

  1. One from Alice to Bob: “Hello Bob.”
  2. One from Bob to Alice: “Hello Alice.”
  3. One from Alice to Bob: “Dinner tonight? What about Tapioca? Btw, here is the flag: {flag}”
  4. One from Bob to Alice: “Dinner sounds good. Thanks for the flag.”

Obviously our goal is to decrypt the third message…

About ECDSA and Its “Nonces”

Let us do a brief recap on ECDSA (a depressingly prolific signature scheme without a security proof):

Signing

Here is how to sign m m using the private key dZn d \in \mathbb{Z}_n .

  1. Hash the message: hSHA512(m)Zn h \gets \texttt{SHA512}(m) \in \mathbb{Z}_{n}
  2. Sample a random nonce: k$Zn k \sample \mathbb{Z}_{n}
  3. Exponentiate by the nonce: (x1,y1)k×GE[Zp] (x_1, y_1) \gets k \times G \in \mathbb{E}[\mathbb{Z}_p]
  4. Reduce the x-coordinate mod the group order: rx1modn r \gets x_1 \mod n
  5. Complete the signature: sk1(h+rd)modn s \gets k^{-1} (h + r d) \mod n
  6. Signature is: σ=(r,s) \sigma = (r, s)

Verifying

Here is how to verify a signature (r,s) (r, s) against the message m m and public key Q=d×G Q = d \times G :

  1. Hash the message: hSHA512(m)Zn h \gets \texttt{SHA512}(m) \in \mathbb{Z}_{n}
  2. Calculate u1hs1modn u_1 \gets h s^{-1} \mod n , u2rs1modn u_2 \gets r s^{-1} \mod n
  3. Compute (x1,y1)u1×G+u2×Q (x_1, y_1) \gets u_1 \times G + u_2 \times Q
  4. Output r=?x1modn r \overset?= x_1 \mod n

The Role of k

When you look at how s s is computed, it is pretty clear that k k ( k1 k^{-1} really) acts as a kind of “mask”: given h h and r r it is the only thing hiding d d (the private key). In ECDSA nomenclature k k is often called a “nonce”, however the word “nonce” in relation to ECDSA is a huge misnomer: if you reuse an ECDSA nonce, you are toast – as expected. However, even if:

  1. The nonce is predictable – you are toast.
  2. Nonces from different signatures share a linear relation – you are toast.
  3. Some part of the nonces is predictable – you are toast.
  4. The distribution of any bit in the nonce is not perfectly uniform – you are toast.

Therefore, when looking for vulnerabilities in an ECDSA implementation it is an obvious thing to check for. If you are in interested in more details (beyond this blog post) take a look at Recovering cryptographic keys from partial information, by example. How let us take a look at how the H1 challenge implements ECDSA:

def Sign(msg, d):
    h = hashlib.sha512(msg)
    z = Transform(int.from_bytes(h.digest(), 'big'), h.digest_size*8)
    k = RNG(n.bit_length(), 16843009, 4294967296)
    x1, y1, z1 = Multiply(G, k)
    r = (x1 * pow(z1, -2, mod) % mod) % n
    s = pow(k, -1, n) * (z + r * d) % n
    assert int(s) == pow(k, -1, n) * (z + r * d) % n
    return r, s

Vis-à-vis the previous section, the RNG call is going to be interesting…

RNG Shenanigans

The RNG function looks like this:

def RNG(nbits, a, b):
    nbytes = nbits // 8
    B = os.urandom(nbytes)
    return a * sum([B[i] * b ** i for i in range(len(B))]) % 2**nbits

In other words, the output o o is computed as:

o=ai=0nbytes1Bibimod2nbits,   i:Bi[0,255] o = a \cdot \sum_{i=0}^{\text{nbytes}-1} B_i \cdot b^i \mod 2^\text{nbits}, \ \ \ \forall i: B_i \in [0, 255]

Recall that in Sign, the ECDSA nonce is generated as k = RNG(n.bit_length(), 16843009, 4294967296). In other words: a = 16843009 = 0x1010101, b = 4294967296 = 0x100000000 = 2^32. Plugging this in to the equation for RNG, means that the nonce k is generated as:

k=0x1010101i=0nbytes1Bi232imod2nbits k = \texttt{0x1010101} \cdot \sum_{i=0}^{\text{nbytes}-1} B_i \cdot 2^{32 \cdot i} \mod 2^\text{nbits}

Unpacking this expression a bit, we see that any term beyond i=nbits/32 i = nbits / 32 can be eliminated, as it is a multiple of 2nbits 2^\text{nbits} .

k=(0x1010101i=0(nbits/32)1Bi232i)+2nbits(0x1010101i=nbits/32bytesBi232inbits)mod2nbits k = \left(\texttt{0x1010101} \cdot \sum_{i=0}^{(\text{nbits} / 32) - 1} B_i \cdot 2^{32 \cdot i}\right) + 2^\text{nbits} \left(\texttt{0x1010101} \cdot \sum_{i = \text{nbits} / 32}^{\text{bytes}} B_{i} \cdot 2^{32 \cdot i - \text{nbits}}\right) \mod 2^\text{nbits}

Also, by observing that the left side cannot be larger than 2nbits1 2^\text{nbits} - 1 , we get:

k=0x1010101i=0(nbits/32)1Bi232i=i=0(nbits/32)1(Bi0x1010101)232i k = \texttt{0x1010101} \cdot \sum_{i=0}^{(\text{nbits} / 32) - 1} B_i \cdot 2^{32 \cdot i} = \sum_{i=0}^{(\text{nbits} / 32) - 1} (B_i \cdot \texttt{0x1010101}) \cdot 2^{32 \cdot i}

In the integers. Since Bi<0x100 B_i < \texttt{0x100} , the Bi0x1010101 B_i \cdot \texttt{0x1010101} operation, repeats the same byte 4 times to fill a 32-bit word. The 232i 2^{32 * i} multiplication is a right shift by i i 32-bit words. In other words we expect k k to look something like this:

0x0x9f9f9f9fb6b6b6b667676767c5c5c5c5...omitted...eaeaeaea9a9a9a9a2929292915151515d0d0d0d0

Where nbits/32=512/32=16 \text{nbits} / 32 = 512 / 32 = 16 bytes are each repeated 4 times and concated to form k k . In other words: k k can be written as a linear combination k=iBiVi k = \sum_{i} B_i V_i of integers Vi=0x01010101232i V_i = \texttt{0x01010101} \cdot 2^{32 \cdot i} where the Bis B_i's are short (they are bytes). This has all the markings of a lattice attack.

Back to ECDSA (and LLL)

We take the two signatures by Bob (for which we know the messages):

r1, s1, _ = (6706720197123832142768727143395528571627385686729472279085077699672602636953568596628477227511870037092766007177588966333093607595831000050978265637877796, 1073779379108240410856990657565545229209771903946426639922087094813786902448520023335130808991515066506036954730763452318418336144389425011731474342543709, 111403492170712993917428321974111102656)
r2, s2, _ = (7616464676048536081690041693308621105395807976530049374449777558721544903144139995398352331701243976154631946836252022822278420653297509476320989403738186, 7381293847317597354132365685036776332763847784351725370466906894121224725876600151244989563125805807680044723478850091886778158534219876869209592138625256, 7994736246642278834331127451449673561762900804586058657648578638831731501930073150317394505661139656896430884711113)

And we can compute:

h1=SHA512("Hello Alice") h_1 = \texttt{SHA512}(\text{"Hello Alice"})

h2=SHA512("Dinner sounds good. Thanks for the flag.") h_2 = \texttt{SHA512}(\text{"Dinner sounds good. Thanks for the flag."})

Recall (from the description of ECDSA) that:

s1=k11(h1+r1d) s_1 = k_1^{-1} \cdot (h_1 + r_1 \cdot d)

s2=k21(h2+r2d) s_2 = k_2^{-1} \cdot (h_2 + r_2 \cdot d)

If we isolate k1 k_1 the d d (secret key) terms cancel:

s2k2h2=r2d,s1k1h1=r1d s_2 \cdot k_2 - h_2 = r_2 \cdot d, s_1 \cdot k_1 - h_1 = r_1 \cdot d

(s2k2h2)/(s1k1h1)=r2/r1 (s_2 \cdot k_2 - h_2) / (s_1 \cdot k_1 - h_1) = r_2 / r_1

(s2k2h2)r1=(s1k1h1)r2 (s_2 \cdot k_2 - h_2) \cdot r_1 = (s_1 \cdot k_1 - h_1) \cdot r_2

(s2k2h2)r1/(s1r2)=k1h1/s1 (s_2 \cdot k_2 - h_2) \cdot r_1 / (s_1 \cdot r_2) = k_1 - h_1 / s_1

(s2k2h2)r1/(s1r2)+h1/s1=k1 (s_2 \cdot k_2 - h_2) \cdot r_1 / (s_1 \cdot r_2) + h_1 / s_1 = k_1

(s2r1/(s1r2))k2+(h2r1/(s1r2)+h1/s1)=k1 (s_2 \cdot r_1 / (s_1 \cdot r_2)) \cdot k_2 + (- h_2 \cdot r_1 / (s_1 \cdot r_2) + h_1 / s_1) = k_1

Note that the terms:

t=s2r1/(s1r2),u=h2r1/(s1r2)h1/s1 t = - s_2 \cdot r_1 / (s_1 \cdot r_2), u = h_2 \cdot r_1 / (s_1 \cdot r_2) - h_1 / s_1

Can be computed directly from the signature, rewriting:

0=k1+tk2+umodn 0 = k_1 + t k_2 + u \mod n

Using our knowlege of how k1 k_1 and k2 k_2 are generated:

0=i=015ViBi+i=015tViBi+umodn 0 = \sum_{i = 0}^{15} V_i B_i + \sum_{i = 0}^{15} t V_i B_i' + u \mod n B0V0=B00x01010101=i=115ViBi+i=015tViBi+umodn -B_0 V_0 = -B_0 \cdot \texttt{0x01010101} = \sum_{i = 1}^{15} V_i B_i + \sum_{i = 0}^{15} t V_i B_i' + u \mod n B0=i=115ViBi/0x01010101+i=015tViBi/0x01010101+u/0x01010101modn -B_0 = \sum_{i = 1}^{15} V_i B_i / \texttt{0x01010101} + \sum_{i = 0}^{15} t V_i B_i' / \texttt{0x01010101} + u / \texttt{0x01010101} \mod n

B0=i=115ViBi/0x01010101+i=015tViBi/0x01010101+u/0x01010101+qn -B_0 = \sum_{i = 1}^{15} V_i B_i / \texttt{0x01010101} + \sum_{i = 0}^{15} t V_i B_i' / \texttt{0x01010101} + u / \texttt{0x01010101} + q n

For some q q , over the integers. Now consider the following matrix:

[n00000000000000000000000000000000V1/0x0101010110000000000000000000000000000000V2/0x0101010101000000000000000000000000000000V3/0x0101010100100000000000000000000000000000V4/0x0101010100010000000000000000000000000000V5/0x0101010100001000000000000000000000000000V6/0x0101010100000100000000000000000000000000V7/0x0101010100000010000000000000000000000000V8/0x0101010100000001000000000000000000000000V9/0x0101010100000000100000000000000000000000V10/0x0101010100000000010000000000000000000000V11/0x0101010100000000001000000000000000000000V12/0x0101010100000000000100000000000000000000V13/0x0101010100000000000010000000000000000000V14/0x0101010100000000000001000000000000000000V15/0x0101010100000000000000100000000000000000tV0/0x0101010100000000000000010000000000000000tV1/0x0101010100000000000000001000000000000000tV2/0x0101010100000000000000000100000000000000tV3/0x0101010100000000000000000010000000000000tV4/0x0101010100000000000000000001000000000000tV5/0x0101010100000000000000000000100000000000tV6/0x0101010100000000000000000000010000000000tV7/0x0101010100000000000000000000001000000000tV8/0x0101010100000000000000000000000100000000tV9/0x0101010100000000000000000000000010000000tV10/0x0101010100000000000000000000000001000000tV11/0x0101010100000000000000000000000000100000tV12/0x0101010100000000000000000000000000010000tV13/0x0101010100000000000000000000000000001000tV14/0x0101010100000000000000000000000000000100tV15/0x0101010100000000000000000000000000000010u/0x01010101000000000000000000000000000000028] \scriptstyle \begin{bmatrix} n & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{1} / \texttt{0x01010101} & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{2} / \texttt{0x01010101} & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{3} / \texttt{0x01010101} & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{4} / \texttt{0x01010101} & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{5} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{6} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{7} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{8} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{9} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{10} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{11} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{12} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{13} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{14} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ V_{15} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{0} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{1} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{2} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{3} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{4} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{5} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{6} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{7} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{8} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{9} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{10} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 & 0 \\ t V_{11} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 & 0 \\ t V_{12} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 & 0 \\ t V_{13} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 & 0 \\ t V_{14} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 & 0 \\ t V_{15} / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & \textbf{1} & 0 \\ u / \texttt{0x01010101} & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 2^8 \\ \end{bmatrix}

We happen to know that there exists a vector W W in the row span of the matrix above with L-infinity norm at most 28 2^8 , i.e. where every entry Wi W_i has Wi28 | W_i | \leq 2^8 . Why? Because (B0,B1,B2,,B15,B0,,B15,28) ( -B_0, B_1, B_2, \ldots, B_{15}, B_0, \ldots, B_{15}, 2^8) is one such vector, per the equality above! To solve this we feed the matrix to LLL. This looks something like this in SageMath:

def lll_solve(t, u):

    cols = (L // B) * 2 + 1

    K = 2^8

    M = []
    M.append([n] + (cols - 1) * [0])

    inv = F(0x01010101)^-1

    ci = [0x01010101 * 2^(B * i) for i in range(L / B)]
    ti = ci + [ci[i] * t for i in range(L / B)]
    ti = [int(v * inv) for v in ti]

    for i, v in enumerate(ti[1:]):
        M.append([v] + [0] * i + [1] + [0] * (cols - i - 2))

    M.append([int(u * inv)] + [0] * (cols - 2) + [K])

    for row in M:
        print(row)

    M = Matrix(M)

    for row in M.LLL():
        print(row)
        break

    # flip
    row[0] = -row[0]

    k1 = int(''.join([4 * ('%02x' % v) for v in row[:16]][::-1]), 16)
    k2 = int(''.join([4 * ('%02x' % v) for v in row[16:32]][::-1]), 16)

    return k1, k2

The solution we find is:

(162,239,161,163,166,102,49,47,165,156,72,217,230,37,141,125,234,51,157,242,222,239,162,66,145,133,171,141,172,29,221,51,256) \scriptstyle (-162, 239, 161, 163, 166, 102, 49, 47, 165, 156, 72, 217, 230, 37, 141, 125, 234, 51, 157, 242, 222, 239, 162, 66, 145, 133, 171, 141, 172, 29, 221, 51, 256)

From this is it straightforward to recover k1 k_1 and k2 k_2 (as done in the script). Since s1=k11(h1+rd) s_1 = k_1^{-1} (h_1 + r d) (from the ECDSA description) and having discovered k1 k_1 it is easy to recover the Bob’s secret key from s1,h1,r1 s_1, h_1, r_1 as:

d=(k1s1h1)/r1modn d = (k_1 \cdot s_1 - h_1) / r_1 \mod n

Recovering Alice Public Key

We are almost done. However to complete the Diffie-Hellman exchange, we also need Alice’s public key (which we are not given explicitly). This is easy to extract from a signature as long as we know which message is signed. Luckly we have one such message (“Hello Bob.”).

Recall that during verification:

u1=hs1modn,    u2=rs1modn u_1 = h s^{-1} \mod n, \ \ \ \ u_2 = r s^{-1} \mod n (x1,y1)=R=u1×G+u2×Qa=(hs1)×G+(rs1)×Qa (x_1, y_1) = R = u_1 \times G + u_2 \times Q_a = (h s^{-1}) \times G + (r s^{-1}) \times Q_a Check: x1=rmodn \text{Check: } x_1 = r \mod n

Where Qa Q_a is Alice’s public key. Now, the field in which x1 x_1 resides happens to be larger than n n , which means that x1{r,r+n} x_1 \in \{ r, r + n \} . So we enumerate all possible points R=(x1,y1)E[D] R = (x_1, y_1) \in \mathbb{E}[D] and for each we compute Qa=r1×(s×Rh×G) Q_a = r^{-1} \times (s \times R - h \times G) . It looks something like this in SageMath:

def recover_key(r, s, h):

    # compute possible x-coordinates
    xs = [D(int(r))]
    if D.order() > F.order():
        xs.append(D(int(r) + n))

    pks = []
    for x in xs:

        # find curve point with the given x-coordinate
        try:
            R = E.lift_x(x)
        except ValueError:
            # not on the curve
            continue

        # it is either R or -R (for which the x-coordinate is the same)
        pks.append(int(r^-1) * (int(s) * R - int(h) * G))
        pks.append(int(r^-1) * (int(s) * (-R) - int(h) * G))

    return pks

Wrapping Up

Finally we recover the possible shared secrets (depending on whether we guessed R R correctly) as:

# Alice -> Bob: "Hello Bob."
r, s, _ = (8618416354247009865173783322782283385800726568519779763790691157278063798628048418532907783021806238103423515210146966468025964847364086792099622893845216, 2932674107137731789093617068375500084388905453653468925392946088867116597531950960271857205235755778202380084260003117176704579423285955014316540314931750, 27865871384804321325511205140263204607)

msga = b'Hello Bob.'

r = F(r)
s = F(s)

for pk in recover_key(r, s, h = msg_to_h(msga)):
    ss = int((int(d) * pk)[0])
    print('ss = 0x%x' % ss)

Lastly we decrypt the message from Alice which contains the flag:

_, _, ca = (8832295267397231051293216564016639537146222596144354850230682204978731311879255662259663270183445827348338041752369314181111940713714991119349376636404112, 8683784208731634307361157916911868656279723101808163939313971801256736484458199874570532609285522391139002296248059424750941962344918156540408403221858292, 105398535464409171419472607677747462033030589690350997911381059472020486557672504778060748058626707326992258591478040500759349352824508941100030623708235493999018571171774658661651532338275358740821547158517615704187173346885098836066743736788259192831313414309775979590033581301910426314601982482556670097620)

ka = [
    0x8a6e81a10c229af504772b51c502638820811034faa62b8dafa019210347918419b71d0638c89b59026b7611edc6a14b2c1c1fb1092a352adfffb7e114f4f385,
    0x3f837315a1fb46097f5eb680697901d75758b859846d37cad33d3f464efb84ace1e85fc60f4e445a031b5ca0e4965e0b081bd4a6e8efea1d3ba07aad51a70cd
]
ca = ca.to_bytes(byteorder='big',length=(ca.bit_length() + 7) // 8)

for k in ka:
    try:
        recv_msg = Decrypt(ca, k)
        print(recv_msg)
    except:
        pass

And recover the flag:

CTF{But_in_real_life_devs_would_never_use_such_a_buggy_RNG_right?}

The code and challenge can be found on Github