The “H1” Challenge
We are given two files.
A python3 script (chall.py
):
Python Script (click to expand)
|
|
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 \( y^2 = x^3 + \text{a} x + \text{b} \) over the 512-bit prime field \( \mathbb{Z}_\text{mod} \) with \( \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 \) and \( G \) is a valid point on the curve (which has prime order).
The script mocks a key exchange between parties Alice and Bob:
First it picks random secret keys for Alice (
da
) / Bob (db
) usingRNG
(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)
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
andkb
are the resulting shared secret (andka = kb
).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).
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')))
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:
- One from Alice to Bob: “Hello Bob.”
- One from Bob to Alice: “Hello Alice.”
- One from Alice to Bob: “Dinner tonight? What about Tapioca? Btw, here is the flag: {flag}”
- 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 \) using the private key \( d \in \mathbb{Z}_n \).
- Hash the message: \( h \gets \texttt{SHA512}(m) \in \mathbb{Z}_{n} \)
- Sample a random nonce: \( k \overset$\leftarrow \mathbb{Z}_{n} \)
- Exponentiate by the nonce: \( (x_1, y_1) \gets k \times G \in \mathbb{E}[\mathbb{Z}_p] \)
- Reduce the x-coordinate mod the group order: \( r \gets x_1 \mod n \)
- Complete the signature: \( s \gets k^{-1} (h + r d) \mod n \)
- Signature is: \( \sigma = (r, s) \)
Verifying
Here is how to verify a signature \( (r, s) \) against the message \( m \) and public key \( Q = d \times G \):
- Hash the message: \( h \gets \texttt{SHA512}(m) \in \mathbb{Z}_{n} \)
- Calculate \( u_1 \gets h s^{-1} \mod n \), \( u_2 \gets r s^{-1} \mod n \)
- Compute \( (x_1, y_1) \gets u_1 \times G + u_2 \times Q \)
- Output \( r \overset?= x_1 \mod n \)
The Role Of k
When you look at how \( s \) is computed, it is pretty clear that \( k \) ( \( k^{-1} \) really) acts as a kind of “mask”: given \( h \) and \( r \) it is the only thing hiding \( d \) (the private key). In ECDSA nomenclature \( 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:
- The nonce is predictable – you are toast.
- Nonces from different signatures share a linear relation – you are toast.
- Some part of the nonces is predictable – you are toast.
- 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 \) is computed as: \[ 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 = \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 \) can be eliminated, as it is a multiple of \( 2^\text{nbits} \).
\[ 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 \( 2^\text{nbits} - 1 \), we get: \[ 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 \( B_i < \texttt{0x100} \), the \( B_i \cdot \texttt{0x1010101} \) operation, repeats the same byte 4 times to fill a 32-bit word. The \( 2^{32 * i} \) multiplication is a right shift by \( i \) 32-bit words. In other words we expect \( k \) to look something like this:
0x0x9f9f9f9fb6b6b6b667676767c5c5c5c5...omitted...eaeaeaea9a9a9a9a2929292915151515d0d0d0d0
Where \( \text{nbits} / 32 = 512 / 32 = 16 \) bytes are each repeated 4 times and concated to form \( k \). In other words: \( k \) can be written as a linear combination \( k = \sum_{i} B_i V_i \) of integers \( V_i = \texttt{0x01010101} \cdot 2^{32 \cdot i} \) where the \( 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:
\[ h_1 = \texttt{SHA512}(\text{“Hello Alice”}) \] \[ h_2 = \texttt{SHA512}(\text{“Dinner sounds good. Thanks for the flag.”}) \]
Recall (from the description of ECDSA) that:
\[ s_1 = k_1^{-1} \cdot (h_1 + r_1 \cdot d) \] \[ s_2 = k_2^{-1} \cdot (h_2 + r_2 \cdot d) \]
If we isolate \( k_1 \) the \( d \) (secret key) terms cancel:
\[ s_2 \cdot k_2 - h_2 = r_2 \cdot d, s_1 \cdot k_1 - h_1 = r_1 \cdot d \] \[ (s_2 \cdot k_2 - h_2) / (s_1 \cdot k_1 - h_1) = r_2 / r_1 \] \[ (s_2 \cdot k_2 - h_2) \cdot r_1 = (s_1 \cdot k_1 - h_1) \cdot r_2 \] \[ (s_2 \cdot k_2 - h_2) \cdot r_1 / (s_1 \cdot r_2) = k_1 - h_1 / s_1 \] \[ (s_2 \cdot k_2 - h_2) \cdot r_1 / (s_1 \cdot r_2) + h_1 / s_1 = k_1 \] \[ (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 = - 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 = k_1 + t k_2 + u \mod n \] Using our knowlege of how \( k_1 \) and \( k_2 \) are generated: \[ 0 = \sum_{i = 0}^{15} V_i B_i + \sum_{i = 0}^{15} t V_i B_i’ + u \mod n \]
\[ -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 \]
\[ -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 \] \[ -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 \), over the integers. Now consider the following matrix:
\[ \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 \) in the row span of the matrix above with L-infinity norm at most \( 2^8 \), i.e. where every entry \( W_i \) has \( | W_i | \leq 2^8 \). Why? Because \( ( -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:
\[ \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 \( k_1 \) and \( k_2 \) (as done in the script). Since \( s_1 = k_1^{-1} (h_1 + r d) \) (from the ECDSA description) and having discovered \( k_1 \) it is easy to recover the Bob’s secret key from \( s_1, h_1, r_1 \) as:
\[ 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:
\[ u_1 = h s^{-1} \mod n, \ \ \ \ u_2 = r s^{-1} \mod n \]
\[ (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 \]
\[ \text{Check: } x_1 = r \mod n \]
Where \( Q_a \) is Alice’s public key. Now, the field in which \( x_1 \) resides happens to be larger than \( n \), which means that \( x_1 \in \{ r, r + n \} \). So we enumerate all possible points \( R = (x_1, y_1) \in \mathbb{E}[D] \) and for each we compute \( 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 \) 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