Snails - CryptoCTF - writeup

This challenge implements ECDSA over a custom elliptic curve. The nonces that it generates for ECDSA are 512 bits long, while the order of the curve is 521 bits long. Therefore, the generated nonces are “small” compared to the order of the curve. We will use LLL to exploit this and recover the key. This is the code of the challenge: def sign(msg, skey): h = bytes_to_long(sha512(msg).digest()) k = getRandomNBitInteger(h.bit_length()) P = k * G r = int(P.xy()[0]) % n s = pow(k, -1, n) * (h + r * skey) % n return (r, s) def main(): border = "┃" pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓") pr(border, " Hey! To solve the Snails cryptography challenges one often needs", border) pr(border, " to perform meticulous bit by bit analysis to uncover loopholes ", border) pr(border, " and ultimately extract the high value flag! Good luck ;) 🐌🐌🐌 ", border) pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛") global flag, G, n skey = bytes_to_long(flag) p = 0x013835f64744f5f06c88c8d7ebfb55e127d790e5a7a58b7172f033db4afad4aca1ae1cdb891338cf963b30ff08d6af71327770d00c472c52290a60fb43f1d070025b a = 0x0109ec0177a5a57e7b7890993e11ba1bc7ba63c1f2afd904a1df35d1fda7363ea8e83f3291e25b69dac26d046dc5ba9a42ff74cd7e52c9df5dbe8d4d02755d26b111 b = 0x0037c84047a6cc14e36d180f9b688fe9959cb63f4ac37b22eb24559e83cfc658ff0ab753540b8ab8d85a62dd67aa92f79dec20d28e453d4663ef2882c7b031ddc0b9 n = 0x013835f64744f5f06c88c8d7ebfb55e127d790e5a7a58b7172f033db4afad4aca1aad8763fe2401b5189d1c449547a6b5295586ce30c94852845d468d52445548739 x = 0x00339495fdbeba9a9f695d6e93effeb937609ce2e628958cd59ba307eb3a43c4c3a54b9b951cd593c876df93a9b0ed7d64df641af94668cb594b6a636ae386e1ac1b y = 0x00038389f29ad8c87e79a8b854e78310b72febb6b1840e360b0a43733933529ee6a04f6d7ea0d91104eb83d1162d55c410eca1c7b45829925fb2a9bf9c1232c32972 E = EllipticCurve(GF(p), [a, b]) G = E(x, y) m = '✔✔✔ My signature is the priority'.encode() while True: pr(f"{border} Options: \n{border}\t[S]ign message! \n{border}\t[Q]uit") ans = sc().decode().strip().lower() if ans == 's': pr(border, f'Please send your message: ') msg = sc().strip() if m in msg and len(msg) == 40: r, s = sign(msg, skey) pr(border, f'{r = }') pr(border, f'{s = }') else: die(border, 'Not valid message! Bye!!') elif ans == 'q': die(border, "Quitting...") else: die(border, "Bye...") We note that the order of the curve is 521 bits long:

ASIS Primes - CryptoCTF - writeup

This challenge is a RSA-style challenge, in which we can provide the prime numbers used for encryption as long as they satisfy specific conditions. First, standard parameters are generated: global flag nbit = 512 p, q = [getPrime(nbit) for _ in range(2)] e = 65537 Then, the main challenge’s logic goes into this loop: while True: pr(f"{border} Options: \n{border}\t[E]ncrypted the flag! \n{border}\t[S]ubmit primes! \n{border}\t[Q]uit") ans = sc().decode().strip().lower() if ans == 'e': m = bytes_to_long(flag) c = pow(m, e ^ 1, p * q) pr(f'{c = }') elif ans == 's': pinit = f'CCTF{{7H!S_iZ_th3_f1RSt_pRim3__P_f0R_oUr_{nbit}-bit_m0DulU5_{rand_str(randint(5, 40))}'.encode() qinit = f'CCTF{{7H!S_iZ_th3_s3c0Nd_pRim3_Q_f0R_oUr_{nbit}-bit_m0DulU5_{rand_str(randint(5, 40))}'.encode() pr(border, f'the condition for the first prime is: {pinit}') pr(border, f'the condition for the second prime is: {qinit}') pr(border, f'Please submit the primes p, q: ') inp = sc().decode().strip() try: _p, _q = [int(_) for _ in inp.split(',')] _pbytes, _qbytes = [long_to_bytes(_) for _ in (_p, _q)] if ( isPrime(_p) and isPrime(_q) and _pbytes.startswith(pinit) and _qbytes.startswith(qinit) and _pbytes.endswith(b'}') and _qbytes.endswith(b'}') and is_valid(_pbytes) and is_valid(_qbytes) and (9 * _p * _q).bit_length() == 2 * nbit ): p, q = _p, _q except: pr(border, f'The input you provided is not valid! Try again!!') nbit += 1¡ And the is_valid and rand_str functions are:

Mitram - CryptoCTF - writeup

This is a bit weird challenge in my opinion. It constructs some exotic matrices, mix them up a little bit and expects us to recover the flag from the output. This is the code: q, v, m = 256, 40, 14 _F = GF(q) def makeup(M, n): for i in range(n): for j in range(i, n): M[i, j] += M[j, i] M[j, i] = 0 return M def mitramap(): _M = [] for s in range(m): M = zero_matrix(_F, v + m, v + m) for i in range(0, v): M[i, (i + s + 1) % v] = _F.random_element() M[i, (i + s) % m + v] = _F.random_element() M = makeup(M, v + m) _M.append(M) return _M def n2F(n): x = _F.gen() e = sum(((n >> _) & 1) * x ** _ for _ in range(8)) return e def F2n(e): x = _F.gen() n = 0 for _ in range(8): n |= (int(e.coefficient(x, _)) << _) return n def embed_secret(msg, v, m): M = random_matrix(_F, v, m) for _ in range(v): M[_, 0] = n2F(msg[_]) return M def maketrig(): return block_matrix([[identity_matrix(_F, v), embed_secret(flag, v, m)], [zero_matrix(_F, m, v), identity_matrix(_F, m)]]) def makepub(F, S): S = S.submatrix(0, v, v, m) Z = zero_matrix(_F, m, v) return [ block_matrix([ [G := M.submatrix(0, 0, v, v), (G + G.transpose()) * S + (H := M.submatrix(0, v, v, m))], [Z, makeup(S.transpose() * G * S + S.transpose() * H, m)] ]) for M in F[:m] ] F, S = mitramap(), maketrig() P = makepub(F, S) print(f'{dumps(F) = }') print(f'{dumps(P) = }') Let’s forget about the specifics of the mitramap and makeup functions, as I won’t be using them in this solution. Note that the maketrig function creates a random matrix whose first column is the flag. Our goal is to recover this matrix $S$.

Toffee - CryptoCTF - writeup

In this challenge, we are given the file Toffee.sage, which implements a custom signature scheme using elliptic curves. First, it generates some parameters: global flag, u, v, k, _n, G skey = bytes_to_long(flag) p = 0xaeaf714c13bfbff63dd6c4f07dd366674ebe93f6ec6ea51ac8584d9982c41882ebea6f6e7b0e959d2c36ba5e27705daffacd9a49b39d5beedc74976b30a260c9 a, b = -7, 0xd3f1356a42265cb4aec98a80b713fb724f44e747fe73d907bdc598557e0d96c5 _n = 0xaeaf714c13bfbff63dd6c4f07dd366674ebe93f6ec6ea51ac8584d9982c41881d942f0dddae61b0641e2a2cf144534c42bf8a9c3cb7bdc2a4392fcb2cc01ef87 x = 0xa0e29c8968e02582d98219ce07dd043270b27e06568cb309131701b3b61c5c374d0dda5ad341baa9d533c17c8a8227df3f7e613447f01e17abbc2645fe5465b0 y = 0x5ee57d33874773dd18f22f9a81b615976a9687222c392801ed9ad96aa6ed364e973edda16c6a3b64760ca74390bb44088bf7156595f5b39bfee3c5cef31c45e1 F = FiniteField(p) E = EllipticCurve(F, [a, b]) G = E(x, y) u, v, k = [randint(1, _n) for _ in ';-)'] Then, we have the main challenge’s loop: while True: pr(f"{border} Options: \n{border}\t[G]et toffee! \n{border}\t[S]ign message! \n{border}\t[Q]uit") ans = sc().decode().strip().lower() if ans == 'g': pr(border, f'Please let me know your seed: ') _k = sc().decode().strip() try: _k = int(_k) except: die(border, 'Your seed is not valid! Bye!!') pr(f'{toffee(u, v, _k) = }') elif ans == 's': pr(border, f'Please send your message: ') msg = sc().strip() r, s = sign(msg, skey) pr(border, f'{r = }') pr(border, f'{s = }') elif ans == 'q': die(border, "Quitting...") else: die(border, "Bye...") The secret key used to sign the messages is the flag. We can: