jack4818
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # CryptoCTF ## Authors We're thinking of creating a CryptoHack blog post for the writeup of this CTF. If you would like to be credited as an author, please include your name below. Each solved challenge will also include the name(s) of those who first solved it. - name(s) - Robin_Jadoul - rkm0959 - TheBlueFlame121 ## Current Progress | Challenge Name | Category | Solved By | Points | | --------------- | -------- | --------- | -----: | | [Trailing Bits](#Trailing-Bits) | Bitshifting | willwam845 | 30 | [Amsterdam](#Amsterdam) | ??? | rkm0959 | 55 | [Gambler](#Gambler) | PRNG | Cryptanalyse, willwam845 | 86 | [Three Ravens](#Three-Ravens) | RSA | TheBlueFlame121 | 90 | [Model](#Model) | RSA | TheBlueFlame121, rkm0959, joachim | 112 | [One Line Crypto](#One-Line-Crypto) | Primes | UnblvR | 146 | | [Abbot](#Abbot) | ??? | rkm0959 | 194 | | [Butterfly Effect](#Butterfly-Effect) | PRNG | rkm0959, Robin_Jadoul | 209 | [Mad Hat](#Mad-Hat) | ??? | rkm0959 | 217 | [Classic](#Classic) | Classical | Tux, hyperreality | 226 | [Heaven](#Heaven) | LFSR | Q7, Robin_Jadoul | 226 | [Strip](#Strip) | ??? | pcback, rkm0959 | 285 | | [Complex to Hell](#Complex-to-Hell) | Hill Cipher | pcback, joseph, UnblvR | 300 | [Fatima](#Fatima) | Reversing | pcback | 316 | [Namura](#Namura) | Knapsack | Q7 | 375 | [Decent RSA](#Decent-RSA) | RSA | ??? | 423 | | [Gengol](#Gengol) | ??? | pcback | 450 | ## Trailing Bits ### Challenge ``` The text that includes the flag is transmitted while unfortunately both of its head and tail bits are lost :( ``` #### Solved by: Willwam ##### Points: 30 ### Solution From the challenge description, we can guess that the binary data provided has a couple bits removed from either end. Not to worry however, since the removed bits won't affect the rest of the data at all. We know that some bits have been removed, so we can just replace those with some decoy bits, and then try decoding from binary until we get readable text. ```py from Crypto.Util.number import * flag = open('output.txt', 'r').read().strip() i = 0 while True: data = long_to_bytes(int(flag,2)*2**i) if b'CCTF' in data: print(data) exit() i += 1 ``` Output: > basic unit of information in computing and digital communications. The name is a portmanteau of binary digit.[1] The bit represents a logical state with one of two possible values. These values are most commonly represented as either 0or1, but other representations such as true/false, yes/no, +/−, or on/off are common. The flag is CCTF{it5_3n0u9h_jU5T_tO_sH1ft_M3} The correspondence between these values and the physical states of the underlying storage or device is a matter of convention, and different assignments may be used even within the same device or program. It may be physically implemented with a two-st ### Flag `CCTF{it5_3n0u9h_jU5T_tO_sH1ft_M3}` --- ## Amsterdam ### Challenge #### Solved by: rkm0959 ##### Points: 55 > Is it normal to have such encoding? ```python= from Crypto.Util.number import * from functools import reduce import operator from secret import flag, n, k def comb(n, k): if k > n : return 0 k = min(k, n - k) u = reduce(operator.mul, range(n, n - k, -1), 1) d = reduce(operator.mul, range(1, k + 1), 1) return u // d def encrypt(msg, n, k): msg = bytes_to_long(msg.encode('utf-8')) if msg >= comb(n, k): return -1 m = ['1'] + ['0' for i in range(n - 1)] for i in range(1, n + 1): if msg >= comb(n - i, k): m[i-1]= '1' msg -= comb(n - i, k) k -= 1 m = int(''.join(m), 2) i, z = 0, [0 for i in range(n - 1)] c = 0 while (m > 0): if m % 4 == 1: c += 3 ** i m -= 1 elif m % 4 == 3: c += 2 * 3 ** i m += 1 m //= 2 i += 1 return c enc = encrypt(flag, n, k) print('enc =', enc) enc = 5550332817876280162274999855997378479609235817133438293571677699650886802393479724923012712512679874728166741238894341948016359931375508700911359897203801700186950730629587624939700035031277025534500760060328480444149259318830785583493 ``` ### Solution We see that there are two steps into solving this problem. First, we have to retrieve the value of $m$ from the encryption result. Then, we calculate the plaintext from $m$. The first part can be done with recursion. By dividing cases on $c \pmod 3$, we can find which 'if' statement we entered on the first 'while' iteration. This also gives our value of $m \pmod{4}$. We continue this until we have our original value of $c$. ```python= def recv(res): if res == 1: return 1 if res % 3 == 0: return 2 * recv(res//3) if res % 3 == 1: return 1 + 2 * recv(res//3) if res % 3 == 2: return -1 + 2 * recv(res//3) ## computation result m = 13037931070082386429043329808978789360911287214189289770230708339088698578551447560972351036453899271623903109387482345515668380476074788749548946464 ``` Now we calculate the plaintext. Notice that $m$ was initially a bit string, which was then converted to an integer. Therefore, we start by changing $m$ into a bit string. ```python= s = [] while m > 0: s.append(m%2) m //= 2 s = s[::-1] n = len(s) ``` It can be proved with induction that after a successful (one that does not return $-1$) call of encrypt(), the value of 'msg' must be $0$. The key idea is Pascal's Triangle. If we know the value of $k$ at the end of encrypt(), we can reverse engineer the plaintext since we know the result $m$. Brute force all possibilities for $k$, and we are done. ### Implementation ```python= def comb(n, k): if k > n : return 0 k = min(k, n - k) u = reduce(operator.mul, range(n, n - k, -1), 1) d = reduce(operator.mul, range(1, k + 1), 1) return u // d def recv(res): if res == 1: return 1 if res % 3 == 0: return 2 * recv(res//3) if res % 3 == 1: return 1 + 2 * recv(res//3) if res % 3 == 2: return -1 + 2 * recv(res//3) ## m = recv(enc) m = 13037931070082386429043329808978789360911287214189289770230708339088698578551447560972351036453899271623903109387482345515668380476074788749548946464 s = [] while m > 0: s.append(m%2) m //= 2 s = s[::-1] n = len(s) ans = 0 for k in range(0, 400): ans = 0 for i in range(0, n-1): if s[n-1-i] == 1: ans += comb(i, k) k = k + 1 print(long_to_bytes(ans)) ``` #### Flag ``` CCTF{With_Re3p3ct_for_Sch4lkwijk_dec3nt_Encoding!} ``` ## Three Ravens ### Challenge #### Solved by: TheBlueFlame ##### Points: 90 > There were three ravens sat on a tree, Downe a downe, hay downe, a downe, They were as black as they might be. ```py #!/usr/bin/python from Crypto.Util.number import * from flag import flag def keygen(nbit): while True: p, q, r = [getPrime(nbit) for _ in range(3)] if isPrime(p + q + r): pubkey = (p * q * r, p + q + r) privkey = (p, q, r) return pubkey, privkey def encrypt(msg, pubkey): enc = pow(bytes_to_long(msg.encode('utf-8')), 0x10001, pubkey[0] * pubkey[1]) return enc nbit = 512 pubkey, _ = keygen(nbit) print('pubkey =', pubkey) enc = encrypt(flag, pubkey) print('enc =', enc) ``` ### Solution The challenge encrypts the flag with a modulus $$ N = (p*q*r)*(p+q+r) $$ and gives the output $n = pqr$, $k = p+q+r$. To totally break the cryptosystem, we would want to find the totient of the modulus $$ \phi(N) = (p-1)(q-1)(r-1)(p+q+r - 1) $$ but we can simplify this when the encrypted message $m$ is small enough. If we have $m < k$, we can instead find $\phi(k) = k-1$, and find $e^{-1} \mod \phi(k)$, and solve! Observe that for any $n, l$, as long as $l | n$, any equivalence $a \equiv b \pmod n$ also holds mod $l$: $a \equiv b \pmod l$ (but note that the other way around does not necessarily hold). To fully see this, we can write \begin{align*}a &\equiv b \pmod n \\ \Leftrightarrow \\ a &= b + kn\\&=b + ktl\quad(\text{since }l|n\text{, so } n = tl)\\\Rightarrow\\a &\equiv b \pmod l\end{align*} So, since $k|n$, we can solve $m \equiv m^{1+\phi(k)} \pmod k$ as if it was a single-prime RSA problem. And because $m < k$, the residue $[m]_k$ (the rest when dividing $m$ by $k$) is exactly equal to $m$. ### Implementation ```py from Crypto.Util.number import * k = 31678428119854378475039974072165136708037257624045332601158556362844808093636775192373992510841508137996049429030654845564354209680913299308777477807442821 c = 8218052282226011897229703907763521214054254785275511886476861328067117492183790700782505297513098158712472588720489709882417825444704582655690684754154241671286925464578318013917918101067812646322286246947457171618728341255012035871158497984838460855373774074443992317662217415756100649174050915168424995132578902663081333332801110559150194633626102240977726402690504746072115659275869737559251377608054255462124427296423897051386235407536790844019875359350402011464166599355173568372087784974017638074052120442860329810932290582796092736141970287892079554841717950791910180281001178448060567492540466675577782909214 e = 0x10001 phi = k-1 d = pow(e,-1,phi) flag = pow(c,d,k) print(long_to_bytes(flag)) ``` ### Flag `CCTF{tH3_thr3E_r4V3n5_ThRe3_cR0w5}` --- ## Butterfly Effect ### Challenge #### Solved by: rkm0959, Robin_Jadoul ##### Points: 209 > Have you heard of the butterfly effect in chaos theory? We have a very clear sample! ```py def hq_prng(x, p, g): rng = 0 for _ in range(getRandomNBitInteger(14)): x = pow(g, x, p) for i in range(nbit): x = pow(g, x, p) if x < (p-1) // 2: rng += 2**i - 1 elif x > (p-1) // 2: rng -= 2**i + 1 else: rng ^= 2**(i + 1) if rng <= 0: return -rng return rng def keygen(p, g): r, s = hq_prng(getRandomNBitInteger(nbit), p, g), hq_prng(getRandomNBitInteger(nbit), p, g) u, v = gmpy2.next_prime(r**2 + s**2), gmpy2.next_prime(2*r*s) e, n = 0x10001, u * v return e, n def encrypt(msg, e, n): return pow(bytes_to_long(msg.encode('utf-8')), e, n) | encrypt(flag, e, n) = 117667582947026307482709850318214820165964980495414423711608614681075036546959172088083682928734150238100258554942348560628319669155021151214088627854571799267413754120136204715904365400299634909310436631535142237485014585244686834739846288478469145431250711508705770591654103989678439960043788163169606969942 | (p, g, n) = (68396932999729141946282927360590169590631231980913314894620521363257317833167L, 11148408907716636563689390048104567047599159688378384611325239859308138541650L, 174421003123810033381790236221837856515717326914686209725144085385038416771831243218121939343204739172142573392914539954561702192037384452671829208544052005809111211272767217997373349539621608610104198849289553199153550766664805075406312493730029215085806581713874721280621739864343621647737777392917211017939L) ``` ### Solution We start by realizing that the given value of $g$ is not actually a generator of $\mathbb{F}_p$. In reality, $g$ generates a very small subgroup, having order of $31337$. This can be easily computed with sage or brute force in Python. This implies that there are at most $31337$ possible values for the outputs for the PRNG. We calculate them all, then sort. Denote the result as $x_1 \le x_2 \le \cdots \le x_{31337}$. We now want to find a pair $(i, j)$ such that $N = \text{nxt}(x_i^2 + x_j^2) \cdot \text{nxt}(2 x_ix_j)$, where $\text{nxt}(n)$ is the smallest prime larger than $n$. This gives a solution with approximately $31337^2/2$ calls to the $\text{nxt}$ function. This is too slow to solve the problem. To optimize, we use two tricks. First, due to small prime gap, one may assume that $$ (x_i^2+x_j^2) \cdot 2x_ix_j \le N \le (x_i^2+x_j^2+1000) \cdot (2x_ix_j+1000) $$ holds true. This cuts the number of pairs to compute. Also, if we fix $x_i$, the values of $x_j$ that satisfy this inequality forms an interval. Therefore, one can use binary search to efficiently compute this interval. This is efficient enough for the task. ### Implementation ```python= from Crypto.Util.number import * p = 68396932999729141946282927360590169590631231980913314894620521363257317833167 g = 11148408907716636563689390048104567047599159688378384611325239859308138541650 N = 174421003123810033381790236221837856515717326914686209725144085385038416771831243218121939343204739172142573392914539954561702192037384452671829208544052005809111211272767217997373349539621608610104198849289553199153550766664805075406312493730029215085806581713874721280621739864343621647737777392917211017939 c = 117667582947026307482709850318214820165964980495414423711608614681075036546959172088083682928734150238100258554942348560628319669155021151214088627854571799267413754120136204715904365400299634909310436631535142237485014585244686834739846288478469145431250711508705770591654103989678439960043788163169606969942 e = 0x10001 def hq_prng(x, p, g): global rsf; rng = 0 for i in range(256): x = rsf[x] if x < (p-1) // 2: rng += 2**i - 1 elif x > (p-1) // 2: rng -= 2**i + 1 else: rng ^= 2**(i + 1) x = x % 31337 if rng <= 0: return -rng return rng rsf[0] = 1 for i in range(1, 31337): rsf[i] = (g * rsf[i-1]) % p WOW = [0] * 31337 for i in range(0, 31337): WOW[i] = hq_prng(i, p, g) WOW.sort() for i in range(0, 31337): lef = i+1 rig = 31336 mid, best = 0, 0 while lef <= rig: mid = (lef + rig) // 2 if (WOW[i]*WOW[i] + WOW[mid] * WOW[mid]) * 2 * WOW[i] * WOW[mid] >= n: best = mid rig = mid -1 else: lef = mid + 1 if best == 0: continue for j in range(best-1, min(best+30, 31337)): u = WOW[i] * WOW[i] + WOW[j] * WOW[j] v = 2 * WOW[i] * WOW[j] if u * v <= n and n <= (u+1000) * (v+1000): u = nextprime(u) v = nextprime(v) if u * v == n: break u = 13207168490744652956999406596846767472614127517045010655090178723910296606220559473477009696618646553552917605315941229263316963221556883007951846286507319 v = 13206540315287197799978983146788490475082830408392129019383447128092673850363700139125558344894148410716241976023782262109119063597770109472702331423302981 phi = (u-1)*(v-1) d = inverse_mod(e,phi) flag = pow(c,d,N) print(long_to_bytes(flag)) ``` ### Flag `CCTF{r341Ly_v3ryYyyyYY_s3cUrE___PRNG___}` --- ## Mad Hat ### Challenge #### Solved by: rkm0959 ##### Points: 217 > A dream is not reality, but who's to say which is which? ```python import random from secret import p, flag def transpose(x): result = [[x[j][i] for j in range(len(x))] for i in range(len(x[0]))] return result def multiply(A, B): if len(A[0]) != len(B): return None result = [] for i in range(len(A)): r = [] for j in range(len(B[0])): r.append(0) result.append(r) for i in range(len(A)): for j in range(len(B[0])): for k in range(len(B)): result[i][j] += A[i][k] * B[k][j] return result def sum_matrix(A, B): result = [] for i in range(len(A)): r = [] for j in range(len(A[0])): r.append(A[i][j]+B[i][j]) result.append(r) return result def keygen(p): d = random.randint(1, 2**64) if p % 4 == 1: Q = [] for i in range(p): q = [] for j in range(p): if i == j: q.append(0) elif pow((i-j), int ((p-1) // 2), p) == 1: q.append(1) else: q.append(-1) Q.append(q) Q_t = transpose(Q) H = [] r = [] r.append(0) r.extend([1 for i in range(p)]) H.append(r) for i in range(1, p + 1): r = [] for j in range(p + 1): if j == 0: r.append(1) else: r.append(Q[i-1][j-1]) H.append(r) H2 = [[0 for j in range(2*(p+1))] for i in range(2*(p+1))] for i in range(0, p+1): for j in range(0, p+1): if H[i][j] == 0: H2[i*2][j*2] = 1 H2[i*2][j*2+1] = -1 H2[i*2+1][j*2] = -1 H2[i*2+1][j*2+1] = -1 elif H[i][j] == 1: H2[i*2][j*2] = 1 H2[i*2][j*2+1] = 1 H2[i*2+1][j*2] = 1 H2[i*2+1][j*2+1] = -1 else: H2[i*2][j*2] = -1 H2[i*2][j*2+1] = -1 H2[i*2+1][j*2] = -1 H2[i*2+1][j*2+1] = +1 ID = [[(-1)**d if i == j else 0 for i in range(len(H2))] for j in range(len(H2))] H2 = multiply(ID, H2) return(H2, d) else: Q = [] for i in range(p): q = [] for j in range(p): if i == j: q.append(0) elif pow( (i-j), int ((p-1) // 2), p) == 1: q.append(1) else: q.append(-1) Q.append(q) Q_t = transpose(Q) Q_Q_t = multiply(Q, Q_t) H1 = [] H1.append([1 for i in range(p+1)]) for i in range(1, p +1): r = [] for j in range(p +1): if j == 0: r.append(-1) elif i == j: r.append(1 + Q[i-1][j-1]) else: r.append(Q[i-1][j-1]) H1.append(r) ID = [[(-1)**d if i == j else 0 for i in range(len(H1))] for j in range(len(H1))] H1 = multiply(ID, H1) return(H1, d) def encrypt(msg, key): matrix = key[0] d = key[1] m = [[ord(char) for char in msg ]] de = [[-d for i in range(len(msg))]] C = multiply(m, matrix) cipher = sum_matrix(C, de) return cipher key = keygen(p) flag = flag + (len(key[0][0]) - len(flag)) * flag[-1] cipher = encrypt(flag, key) print('cipher =', cipher) ``` ### Solution By analyzing the dimensions of the ciphertext, it's straightforward to find $p=37$. Since the matrix part of the secret key is only determined by $p$ and the parity of $d$, we have two possible matrices to consider. Also, if we fix $d$, we can compute the plaintext by solving a system of linear equations. We proceed this way. If we iterate over $d$ simply as integers, the solution of the equation may contain rational, non-integer numbers. This is slow and prone to floating-point errors, (unless we take proper care) so we will use another trick. Since all the ord values in the plaintext are between $0$ and $256$, we will take this entire problem into $\mathbb{F}_{257}$. This way, we can try $257$ different values of $d$, solve the system without worrying about floating error, and retrieve our answer. ### Implementation ```python ## keygen(d) : simply keygen with p=37, d's parity MAT0 = keygen(0) MAT1 = keygen(1) MM0 = Matrix(GF(257), MAT0) MM1 = Matrix(GF(257), MAT1) adv = [1]*76 adv = vector(GF(257), adv) AD0 = MM0.solve_right(adv) AD1 = MM1.solve_right(adv) cipher = [-3459749918754130611, -3459749918754138177, -3459749918754137803, -3459749918754138385, -3459749918754138025, -3459749918754138097, -3459749918754138073, -3459749918754138245, -3459749918754138183, -3459749918754138445, -3459749918754137991, -3459749918754138597, -3459749918754138309, -3459749918754138309, -3459749918754138279, -3459749918754138771, -3459749918754138327, -3459749918754138485, -3459749918754138233, -3459749918754138389, -3459749918754138207, -3459749918754138555, -3459749918754138141, -3459749918754138501, -3459749918754138677, -3459749918754138297, -3459749918754138563, -3459749918754138439, -3459749918754138429, -3459749918754138041, -3459749918754138611, -3459749918754138469, -3459749918754138217, -3459749918754138585, -3459749918754138403, -3459749918754138177, -3459749918754137777, -3459749918754138587, -3459749918754138231, -3459749918754138677, -3459749918754138127, -3459749918754138679, -3459749918754137789, -3459749918754138305, -3459749918754138025, -3459749918754138301, -3459749918754137941, -3459749918754138489, -3459749918754137583, -3459749918754138297, -3459749918754137949, -3459749918754138475, -3459749918754137879, -3459749918754138813, -3459749918754137981, -3459749918754138395, -3459749918754138201, -3459749918754138459, -3459749918754138195, -3459749918754138617, -3459749918754138003, -3459749918754138557, -3459749918754138429, -3459749918754138499, -3459749918754137951, -3459749918754138673, -3459749918754137975, -3459749918754138341, -3459749918754138121, -3459749918754138375, -3459749918754137869, -3459749918754138459, -3459749918754137739, -3459749918754138405, -3459749918754137921, -3459749918754138775] res = vector(GF(257), cipher) XX = MM0.solve_right(res) YY = MM1.solve_right(res) for i in range(0, 257): stX = "" stY = "" for j in range(0, 76): XX[j] += AD0[j] YY[j] += AD1[j] for j in range(0, len(XX)): if (int)(XX[j]) <= 255: stX = stX + chr((int)(XX[j])) for j in range(0, len(YY)): if (int)(YY[j]) <= 255: stY = stY + chr((int)(YY[j])) if "CCTF" in stX: print(stX) if "CCTF" in stY: print(stY) ``` ### Flag ``` CCTF{TH13_i3_Hadamard_rip_y0ung_&_bri11iant_Paley!} ``` ## Heaven ### Challenge #### Solved by: Q7, Robin_Jadoul ##### Points: 226 ```python from bitstring import BitArray from heaven import seventh_seal, oh, no, new_testament def matthew_effect(shire, rohan): gandalf = '' for every, hobbit in enumerate(shire): gandalf += oh if ord(hobbit) ^ ord(rohan[every]) == 0 else no return gandalf def born_to_die(isengard): luke = 0 for book in new_testament: luke ^= ord(isengard[book]) lizzy_grant = oh + isengard[:-1] if luke == 0 else no + isengard[:-1] return lizzy_grant david = len(seventh_seal) elf = seventh_seal lord = BitArray(bytes=bytes(open('flag.jpg', 'rb').read())).bin bilbo = len(lord) matthew = 0 princess_leia = '' destiny = bilbo // david apocalypse = bilbo % david for i in range(32): elf = born_to_die(elf) while matthew < destiny: princess_leia += matthew_effect(elf, lord[matthew * david : (matthew + 1) * david]) elf = born_to_die(elf) matthew += 1 princess_leia += matthew_effect(elf[:apocalypse], lord[matthew * david :]) res = open('flag.enc', 'wb') res.write(bytes(int(princess_leia[i : i + 8], 2) for i in range(0, bilbo, 8))) ``` ### Solution After some renaming and minor reverse engineering of the challenge logic, we see that a jpg image has been xor'ed with a keystream generated from an LFSR. Each time a key-sized block is xored, and the key is forwarded one step in the LFSR. Xor the encrypted file with a JFIF jpg file header to try to recover the current state of the LFSR. ```python from bitstring import BitArray pt = BitArray(bytes=bytes.fromhex('FFD8FFE000104A4649460001')).bin with open('flag.enc', 'rb') as f: content = f.read() ct = BitArray(bytes=content).bin key = '' for i, j in zip(pt, ct): key += str(int(i) ^ int(j)) print(key) ``` Then we can get ```python= 1100011100101011000 0110001110010101100 0011000111001010110 0001100011100101011 1000110001110010101 0 ``` Since we know from the source code that encryptions under consecutive keys share almost the entire key (`x...xa` and `bx...x`), we can recover the length of the key from this. We can observe this rotation already in the above listing of the bits, thanks to our insertion of newlines in the right positions. Finally we can brute force the polynomial to recover the origin image file. ### Implementation ```python from bitstring import BitArray key = '1100011100101011000' seventh_seal = key oh = '0' no = '1' def matthew_effect(shire, rohan): gandalf = '' for every, hobbit in enumerate(shire): gandalf += oh if ord(hobbit) ^ ord(rohan[every]) == 0 else no return gandalf new_testament = [] def born_to_die(isengard): luke = 0 for book in new_testament: luke ^= ord(isengard[book]) lizzy_grant = oh + isengard[:-1] if luke == 0 else no + isengard[:-1] return lizzy_grant a = b'1100011100101011000' b = b'0110001110010101100' c = b'0011000111001010110' d = b'0001100011100101011' e = b'1000110001110010101' for i in range(19): for j in range(i+1, 19): for k in range(j+1, 19): for l in range(k+1, 19): if a[i] ^ a[j] ^ a[k] ^ a[l] == \ b[i] ^ b[j] ^ b[k] ^ b[l] == \ c[i] ^ c[j] ^ c[k] ^ c[l] == \ e[i] ^ e[j] ^ c[k] ^ c[l] == 0: if d[i] ^ d[j] ^ d[k] ^ d[l] == 1: print(i, j, k, l) seventh_seal = '1100011100101011000' new_testament = [i, j, k, l] david = len(seventh_seal) elf = seventh_seal lord = BitArray(bytes=bytes(open('flag.enc', 'rb').read())).bin bilbo = len(lord) matthew = 0 princess_leia = '' destiny = bilbo // david apocalypse = bilbo % david # for i in range(32): # elf = born_to_die(elf) while matthew < destiny: princess_leia += matthew_effect(elf, lord[matthew * david: (matthew + 1) * david]) elf = born_to_die(elf) matthew += 1 princess_leia += matthew_effect(elf[:apocalypse], lord[matthew * david:]) res = open(f'flag_{i}-{j}-{k}-{l}.jpg', 'wb') res.write(bytes(int(princess_leia[i: i + 8], 2) for i in range(0, bilbo, 8))) res.close() ``` ### Flag `CCTF{0Ne_k3y_t0_rU1e_7hem_A11_4Nd_7o_d3crYp7_th3_fl4g!}` --- ## Model ### Challenge #### Solved by: TheBlueFlame121, rkm0959, joachim ##### Points: 112 ```py def genkey(nbit): while True: p, q = getPrime(nbit), getPrime(nbit) if gcd((p-1) // 2, (q-1) // 2) == 1: P, Q = (q-1) // 2, (p-1) // 2 r = inverse(Q, P) e = 2 * r * Q - 1 return(p, q, e) def encrypt(msg, pubkey): e, n = pubkey return pow(bytes_to_long(msg), e, n) ``` ### Solution The key to solving this challenge is that $$ e \equiv 2 Q^{-1}Q - 1 \equiv 2 - 1 \equiv 1 \pmod P $$ So encrypting a message `m` we have, for some integer $k$, $$ m^e \equiv m^{1 + \frac{k(q-1)}{2}} \equiv m \cdot m^\frac{k(q-1)}{2} \pmod q $$ Looking at the second term, it can be reduced using Fermat's Little Theorem as follows: $$ m^{\frac{k*(q-1)}{2}}\equiv (m^{(q-1)})^{\frac{k}{2}}\equiv 1^{\frac{k}{2}} \equiv 1^{\frac{1}{2}} \pmod q $$ $1$ can have only two roots here, namely $\pm 1$. Therefore the encryption becomes $$ m^e \equiv m^{1 + \frac{k(q-1)}{2}} \equiv \pm m \pmod q $$ and we can compute $q = \text{gcd}(m^e \mp m, n)$ to factorize $n$ because $m^e \mp m \equiv 0 \pmod q$ is a multiple of $q$. One last step that needs attention is that we recover $q$, not $p$, which matters for the recovery of $e$, as the primes are not interchangeable there. ### Implementation ```py from Crypto.Util.number import * import math def derive_e(p,q): P, Q = (q-1) // 2, (p-1) // 2 r = pow(Q, -1, P) e = 2 * r * Q - 1 return e N = 17790613564907955318126717576181316624843451677921227941389832111093895513875496295594784102148835715126789396535470416868485674231839509486983792844881941097589192520877472968227711640216343330193184235164710328845507199362646489303138765492026284976828397217700058854699501312701069031398507487060508966602815218264215778115331187180105972920333780067280854048113094622799996118383376340217782122945586262887450863620856214375258659362300743471229410735400189992359220551961441580630740022857304514895745174813529758766758733506538696933950282130984955594881517339093338779101106466633380921338845195921235252323721 flag_enc = 8216344743331409189205831776342200252705923796193752552649425282859227400617284746437075756157249953578189229459392338128783031841882560801175367779263048253787547952450480816724222285583987363793884961526545550108790689158473753461378651141379053427506957702375732452598640804768960184186521954448243004125900395894265450073650101942224629389391631821735998886688813393717718376391743836798122485350719355567861201466641767009303179260141365766023680788250688524528992952859061172438083227729190577738108854783536925967748199734513738782142055609101950770816942854252284355975365351013803601963990179403849614198536 m = bytes_to_long(b'0') ct = 8131881080215090371487466406674376044247120209816118806949066423689730735519795472927218783473464525260814227606067990363574576048132004742403517775620572793232598693334765641758830271460405790617624271060522834683042735967050146871067065889288923913486919193720360254923458500009885281654478144592942337767754315130844294762755237864506689552987776560881357285727629827190391683150994461127468196118126587159811046890420456603820675085450111755868116701855834309297184745623927049652098555126342100576188575279791066071616897443075423425299542959405192350563251777193668273523389978129359003036691597884885020756981 q = math.gcd(ct - m, N) assert isPrime(q) p = N // q e = derive_e(p,q) d = pow(e, -1, (p-1)*(q-1)) m = pow(flag_enc,d,N) print(long_to_bytes(m)) ``` ### Flag `CCTF{7He_mA1n_iD34_0f_pUb1iC_key_cryPto9raphy_iZ_tHa7_It_l3ts_y0u_puBli5h_4N_pUbL!c_k3y_wi7hOuT_c0mprOmi5InG_y0Ur_5ecr3T_keY}` --- ## Gambler ### Challenge In this challenge, we have access to a server with the following options ``` ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + Hi, there is a strong relation between philosophy and the gambling! + + Gamble as an ancient philosopher and find the flag :) + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | Options: | [C]ipher flag! | [E]ncryption function! | [T]ry the encryption | [Q]uit ``` Where the encrypttion function is given by ```python def encrypt(m, p, a, b): assert m < p and isPrime(p) return (m ** 3 + a * m + b) % p ``` #### Solved by: Cryptanalyse ##### Points: 87 ### Solution The goal is to decrypt the flag by recovering the hidden parameters $a,b,p$ and then solving the polynomial used in `encrypt`. We can recover all parameters quite easily with the encrypt our own message function. We can obtain from the server the value of $$ y(x) = x^3 + ax + b \mod p $$ for any input $x$. We can recover $b$ by encrypting 0 as $$ y(0) = 0^3 + a*0 + b = b \mod p $$ Where we are assuming that $a,b < p$. With the value of $b$, we can calculate $$ y(1) = 1 + a + b \mod p, \quad \Rightarrow \quad a = y(1) - 1 - b $$ Finally, with both $a,b$ recovered, we need to find the modulus $p$. If we encrypt a fairly small message, such that $y(x) > p$ we can use that $$ x^3 + ax + b = y(x) + kp, \quad \Rightarrow \quad kp = x^3 + ax + b - y(x) $$ Since we know a and b, we can compute all the terms on the right hand side of this equation and recover $k p$. All that remains is solving for $k$, which is pretty fast as $k$ is so small. With all parameters known, we can request the encrypted flag from the server and solve the cubic equation $$ x^3 + ax + b = c \mod p $$ with anything you like (probably sage). ### Implementation ```py import os os.environ["PWNLIB_NOTERM"] = "True" from pwn import * from Crypto.Util.number import long_to_bytes debug = False r.sendline('C') data = r.recvuntil(b'|\t[Q]uit\n') enc = int(data.split()[3].decode().strip()) def encrypt_int(n): r.sendline('T') r.recvuntil(' your message to encrypt:\n') r.sendline(str(n)) data = r.recvuntil(b'|\t[Q]uit\n') b = int(data.split()[3].decode().strip()) return b b = encrypt_int(0) c = encrypt_int(1) a = c - b - 1 enc_kp = encrypt_int(2) kp = (2**3 + a*2 + b) - enc_kp if debug: print(a) print(b) print(kp) p = max(f[0] for f in factor(kp)) PR.<x> = PolynomialRing(GF(p)) f = x^3 + a * x + b - enc rts = f.roots() print(rts) flag = rts[0][0] print(long_to_bytes(flag)) r.interactive() ``` ### Flag `CCTF{__Gerolamo__Cardano_4N_itaLi4N_p0lYma7H}` ## Classic ### Challenge #### Solved by: Tux, hyperreality ##### Points: 226 > Classic is Easy but Essential! ``` b7UkM iK2L0 PUVnZ Ho79I tDAf0 PUvfQ G5jHo 7GwLG wL9It vfQHo 7G5j0 PUvfQ 9Ithd JkMiK 2LU2b 0PUkM B8Nih dJK2L GwL0P UHo7U 2bK2L ... ``` ### Solution Taking trigrams from the ciphertext, this becomes a classic monoalphabetic substitution cipher. See the script for more comments. ### Implementation ```python import string from collections import Counter from cipher_solver.simple import SimpleSolver with open('enc.txt') as f: ctext = f.read().strip().replace(' ', '') def chunks(l, n): n = max(1, n) return [l[i:i+n] for i in range(0, len(l), n)] # 1. We suspect that groupings of 5 are there to confuse us. # Let's break chars into groups of different sizes and look at # the size of the set for i in range(1,10): unique = len(set(chunks(ctext, i))) print(f"{unique} unique groups when split into groups of length {i}") # 2. Breaking into groups of 3 (trigrams) gives much less unique chars chunked = chunks(ctext, 3) freq = Counter(chunked).most_common() print(freq) # 3. Build a substitution table for each trigram to a different letter # only important thing is that the most frequent trigram corresponds to a space subs = {} alphabet = " " + string.ascii_lowercase + string.ascii_uppercase cur = 0 for trigram in freq: subs[trigram[0]] = alphabet[cur] cur += 1 print(subs) # 4. Make the substitutions substituted = "".join([subs[c] for c in chunked]) print(substituted) # 5. Use any algorithm for solving substitution ciphers (quipqiup also works) s = SimpleSolver(substituted) s.solve() # 6. It's readable and the flag is visible after a couple of manual # substitutions ptext = s.plaintext() ptext = ptext.replace('z', 'T') ptext = ptext.replace('q', '_') print(ptext) ``` ### Flag `CCTF{The_main_classical_cipher_types_are_substitution_ciphers}` ## Complex to Hell ### Challenge #### Solved by: pcback, joseph, UnblvR #### Points: 300 > I Already Know I'm Going to Hell > > At This Point, It's Really Go Big Or Go Home! ```python import math import string import random from secret import flag, key mapstr = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!{}_" def multiply(A ,B): ac,ar,bc,br = len(A[0]), len(A), len(B[0]), len(B) if ac != br: return None result = [] for i in range(ar): r = [] for j in range(bc): r.append(0) result.append(r) for i in range(ar): for j in range(bc): for k in range(br): result[i][j] += A[i][k] * B[k][j] return result def comple_congruent (z): a = z.real % len(mapstr) b = z.imag % len(mapstr) return a + b * 1j def plain_to_matrix(msg ,n): p = int(math.ceil(len(msg) // (2 * n))) + 1 matrix_row_size = n matrix_col_size = p index = 0 matrix_plain = [] for i in range(matrix_row_size): col = [] for j in range(matrix_col_size): if index >= len(msg): col.append(0 + 0.j) elif index == len(msg)-1: col.append(mapstr.index(msg[index]) + 0.j) index += 1 else: col.append(mapstr.index(msg[index]) + mapstr.index(msg[index+1]) * 1.j) index += 2 matrix_plain.append(col) return matrix_plain def encrypt(flag ,key): n = len(key) p = int(math.ceil(len(flag) // (2 * n))) + 1 matrix_plain = plain_to_matrix(flag, n) key_congruent = [] for i in range(n): r = [] for j in range(n): r.append(comple_congruent(key[i][j])) key_congruent.append(r) cipher = multiply (key_congruent, matrix_plain) result = [] for i in range(n): r = [] for j in range(p): r.append(comple_congruent(cipher[i][j])) result.append(r) return result cipher = encrypt(flag, key) print("cipher = ", cipher) ``` ### Solution tldr; - Use flag format to set up system of equations. - Bruteforce the first two entries on the 2nd row. - Many keys allow decryption of the first row. - Use last entries in first row, with knowledge of padding to bruteforce the full, correct key. `plain_to_matrix(msg, n)` takes a message string as input and a row count `n`, and returns a matrix with `n` rows that has the characters of `msg` as complex numbers as entries (one complex number represents two characters). If the message doesn't fill the matrix completely, it is padded with `0`s. `encrypt(msg, key)` encrypts the given message by left multiplying the message (as a matrix) by the $2 \times 2$ key. The size of the key space is $66^8$ which is infeasible to bruteforce. We can use the flag format to reduce the amount of bruteforce required. Let the key be $$ K = \begin{bmatrix} a+bi & c+di \\ e+fi & g+hi \end{bmatrix}$$ where $a,b,c,d,e,f,g \in \mathbb{Z}/66\mathbb{Z}$ Write the plaintext flag matrix as $$M = \begin{bmatrix} m_0 + m_1i & m_2 + m_3 i & \cdots & m_{32} + m_{33} i \\ m_{34} + m_{35} i & m_{36} + m_{37} i & \cdots & m_{66} + m_{67} i \end{bmatrix}$$ and the ciphertext matrix as $$C = \begin{bmatrix} c_0 + c_1i & c_2 + c_3 i & \cdots & c_{32} + c_{33} i \\ c_{34} + c_{35} i & c_{36} + c_{37} i & \cdots & c_{66} + c_{67} i \end{bmatrix}$$ (all coefficients in $\mathbb{Z}/66\mathbb{Z}$). So $C = KM$. From this we get the equations $$\begin{aligned} c_0 + c_1 i &= (a + bi)(m_0 + m_1 i) + (c + d_i)(m_{34} + m_{35} i) \\ c_2 + c_3 i &= (a + bi)(m_2 + m_3 i) + (c + d_i)(m_{36} + m_{37} i) \\ c_{34} + c_{35} i &= (e + fi)(m_0 + m_1 i) + (g + hi)(m_{34} + m_{35} i) \\ c_{36} + c_{37} i &= (e + fi)(m_2 + m_3 i) + (g + hi)(m_{36} + m_{37} i) \end{aligned}$$ so $$\begin{aligned} c_0 &= am_0 - bm_1 + cm_{34} - dm_{35} \\ c_1 &= am_1 + bm_0 + cm_{35} + dm_{34} \\ c_2 &= am_2 - bm_3 + cm_{36} - dm_{37} \\ c_3 &= am_3 + bm_2 + cm_{37} + dm_{36} \\ c_{34} &= em_0 - fm_1 + gm_{34} - hm_{35} \\ c_{35} &= em_1 + fm_0 + gm_{35} + hm_{34} \\ c_{36} &= em_2 - fm_3 + gm_{36} - hm_{37} \\ c_{37} &= em_3 + fm_2 + gm_{37} + hm_{36} \end{aligned}$$ We'll bruteforce $66^4$ values for $m_{34}, m_{35}, m_{36}$ and $m_{37}$ and solve for the 8 key values with the 8 equations. We already know $m_0, m_1, m_2$ and $m_3$ from the flag format. Doing this quickly reveals the first row of the plaintext: `CCTF{This_0n3_Is_State_0f_th3_4rt_`. With this, we can reduce the bruteforce amount to at most $66^3$. Fortunately for us, it turns out that the last 4 characters of the plaintext are `}000`, so we have enough information to enumerate possible keys with minimal bruteforce. We can use the exact same setup as above, except instead of bruteforcing $m_{34}, m_{35}, m_{36}$ and $m_{37}$, we take them to be `}000`. Solving the system will give us a vector $\mathbf{k} = (a,b,c,d,e,f,g,h)$, but this might not be the correct key. Any vector of the form $\mathbf{k} + \mathbf{t}$ where $\mathbf{t}$ is in the kernel of the coefficients matrix, $A$, will satisfy the system. We can find all vectors in the kernel of $A$ by finding a basis for the kernel modulo each of the prime factors of $66$, and then combining them with the Chinese Remainder Theorem. In this case, the nullity of $A$ in $\mathbb{F}_3$ and $\mathbb{F}_{11}$ is $0$, and the nullity of $A$ in $\mathbb{F}_2$ is $4$. This means we'll need to enumerate at most $2^4$ possible keys. Note on `inv(key)`: I couldn't find a way to use Sage's built-ins to find the inverse of a matrix with complex entries so I just used the following theory: Suppose $K^{-1}$ exists. Write $$K^{-1} = \begin{bmatrix} a' + b'i & c' + d'i \\ e' + f'i & g' + h'i \end{bmatrix}$$ Then, by definition $$\begin{bmatrix} a + bi & c + di \\ e + fi & g + hi \end{bmatrix} \begin{bmatrix} a' + b'i & c' + d'i \\ e' + f'i & g' + h'i \end{bmatrix} = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} $$ so $$\begin{aligned} aa' - bb' + ce' - df' &= 1 \\ ab' + ba' + cf' + de' &= 0 \\ ac' - bd' + cg' - dh' &= 0 \\ ad' + bc' + ch' + dg' &= 0 \\ ea' - fb' + ge' - hf' &= 0 \\ eb' + fa' + gf' + he' &= 0 \\ ec' - fd' + gg' - hh' &= 1 \\ ed' + fc' + gh' + hg' &= 0 \end{aligned}$$ which is a system of 8 equations in 8 unknowns that can be easily solved. ### Implementation ```python from itertools import product mapstr = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!{}_" cipher = [ [(24+36j), (41+47j), (3+27j), (36+41j), (57+58j), (11+24j), (33+7j), (52+64j), (26+23j), (30+35j), (64+39j), (52+19j), (39+45j), (33+31j), (3+17j), (21+32j), (15+55j)], [(33+44j), (15+39j), (64+50j), (44+41j), (39+20j), 42j, (16+12j), (63+27j), (9+52j), (39+64j), (5+18j), (53+25j), (47+31j), (5+49j), (24+8j), (57+9j), (38+16j)] ] F = IntegerModRing(66) def multiply(A ,B): ac,ar,bc,br = len(A[0]), len(A), len(B[0]), len(B) if ac != br: return None result = [] for i in range(ar): r = [] for j in range(bc): r.append(0) result.append(r) for i in range(ar): for j in range(bc): for k in range(br): result[i][j] += A[i][k] * B[k][j] return result def inv(key): a,b,c,d,e,f,g,h = key M = [[a,-b,0,0,c,-d,0,0], [b,a,0,0,d,c,0,0], [0,0,a,-b,0,0,c,-d], [0,0,b,a,0,0,d,c], [e,-f,0,0,g,-h,0,0], [f,e,0,0,h,g,0,0,], [0,0,e,-f,0,0,g,-h], [0,0,f,e,0,0,h,g]] M = Matrix(F,M) t = vector(F,[1,0,0,0,0,0,1,0]) i = M.solve_right(t) a,b,c,d,e,f,g,h = map(ZZ, i) I = [[a+b*1j, c+d*1j],[e+f*1j, g+h*1j]] return I def cong(z): a = z.real() % 66 b = z.imag() % 66 return a + b*1j def decrypt(key): Kinv = inv(key) key = [[cong(Kinv[i][j]) for j in range(2)] for i in range(2)] M = multiply(key, cipher) res = [] flag = '' for i in range(2): for j in range(17): a = cong(M[i][j]).real() b = cong(M[i][j]).imag() flag += mapstr[int(a)] + mapstr[int(b)] return flag # first round, bruteforce m34,m35,m36,m37 # c0, c1, c2, c3 = 24, 36, 41, 17 # c34, c35, c36, c37 = 33, 44, 15, 19 # m0, m1, m2, m3 = 38, 38, 55, 41 # RECOVERS first row: CCTF{This_0n3_Is_State_0f_th3_4rt_ c0, c1, c2, c3 = 21, 32, 15, 55 c34, c35, c36, c37 = 57, 9, 38, 16 m0, m1, m2, m3 = 4, 27, 29, 65 m34, m35, m36, m37 = 64, 0, 0, 0 A = [[m0,-m1,m34,-m35,0,0,0,0], [m1, m0, m35, m34,0,0,0,0], [m2, -m3,m36,-m37,0,0,0,0], [m3,m2,m37,m36,0,0,0,0], [0,0,0,0,m0,-m1,m34,-m35], [0,0,0,0,m1,m0,m35,m34], [0,0,0,0,m2,-m3,m36,-m37], [0,0,0,0,m3,m2,m37,m36]] v = [c0,c1,c2,c3,c34,c35,c36,c37] A = Matrix(F, A) v = vector(F, v) x = A.solve_right(v) A2 = Matrix(GF(2), A) A2K = Matrix(F, A2.right_kernel_matrix()) # A3 and A11 have 0 nullity for lc in product(range(2), repeat=A2.right_nullity()): try: t2 = A2K.linear_combination_of_rows(lc) t = vector([crt([int(a2), 0, 0], [2, 3, 11]) for a2 in t2]) key = x + t flag = decrypt(key) print(flag) except ValueError: pass except KeyboardInterrupt: exit() ``` ### Flag `CCTF{This_0n3_Is_State_0f_th3_4rt_and_C0mplex_is_Truly_compl3x!!}` ## Namura ### Challenge #### Solved by: Q7 ##### Points: 375 ```python def encrypt(pubkey, msg): C = 0 for i in range(n): C += pubkey[i] * int(msg[i]) return C flag = flag.lstrip('CCTF{').rstrip('}') bflag = bin(bytes_to_long(flag.encode('utf-8')))[2:] n = len(bflag) u = n - 30 pubkey = keygen((n+1) // 3, n, u) print('pubkey =', pubkey) enc = encrypt(pubkey, bflag) print('enc =', enc) ``` ### Solution This looks like a knapsack cryptography problem, which are usually solved by lattice reduction algorithms modelling a Shortest Vector Problem (SVP). We noticed that the title of the challenge "Namura" gave away the paper describing this algorithm, Naskao and Murakami's (Knapsack Public-Key Cryptosystem Using Chinese Remainder Theorem)[https://eprint.iacr.org/2007/107.pdf]. Section 4.2 of the paper describes a lattice that can solve low density subset sum problems, and the public key in this challenge had a very low density: ```python d = len(pubkey) / log(max(pubkey), 2) print(CDF(d)) 0.5625 ``` Since the flag should be all printables, we can reduce the dimension of the pubkey by removing the corresponding number to the MSB of each character in the plaintext. ```python new = [] pubkey = [0] + pubkey for i in range(len(pubkey)): if i % 8 == 0: continue new.append(pubkey[i]) print('pubkey =', new) ``` Then we can just solve this like other knapsack problems by bruteforcing the permutation and using BKZ to find the SVP. ### Implementation ```python import re import random import multiprocessing as mp from functools import partial def check(sol, A, s): """Check whether *sol* is a solution to the subset-sum problem. """ return sum(x*a for x, a in zip(sol, A)) == s def solve(a, s, ID=None): rand = random.Random(x=ID) mat = [] for idx,val in enumerate(a): mat.append([0]*idx + [1] + [0]*(len(a)-idx-1)+[val]) mat.append([0]*len(a)+[-s]) # main loop itr = 0 start_time = cputime() while True: itr += 1 # 2. Randomly shuffle l = mat[::] shuffle(l, random=rand.random) # 3. BKZ!!! m = matrix(l) t_BKZ = cputime() l_sol = m.BKZ(block_size=25) print(f"{itr} runs. BKZ running time: {cputime(t_BKZ):.3f}s") for line in l_sol: if all([x >= 0 for x in line[:-1]]): print(line) print(check(line,a,s)) print(f"After {itr} runs. FIND SVP!!! {line}\n" f"Single core time used: {cputime(start_time):.3f}s") return True enc = 154657917005376465967753276253676484467260782425419406781078357515 pubkey = [636730424634282684150787505024636846878192530834301373045417941, 443443736056701854821550045409138156747702906153207509789193893, 4044894679347221866903041471393250783970070284064844489844729640, 2438178506188801348411667154785222321653401060527584288473058029, 1900607069477409243358471897897077706622577630696771143373974126, 4396893130381899655054557551793492148977658211100513328122993482, 2601912276825314189427819328705612999759768062840709690416685851, 849578696430489144601066711846105434868737506048858961510584478, 867634152731852110202428052792503837522496305953184128350918090, 2141949199052254673518707523413310868963934449025085556791898943, 1317724781892829476727276429613649391725697391917627197350586077, 846616203254169113248714620324777288157484807522832537271896727, 1889890413622399357217368964385384275068207071755568870885142697, 4345106754542111105556800435292359436746763182165461814839878219, 1844751943408649439970784234819027788878268101086942786334241578, 4635151867785248653584925319820032342108353583278365090165351369, 1891260029110631153447767958167471428147295737587261835048395769, 3273672699905851794278838098554938393037792687468962414002119644, 4759683391852904086863354069372064775438697384972606618058259428, 2277479715112568474291874878404028785747567257268529120464806983, 712281270914494089486011482407537474741428127403029959878626851, 4663860235979475414650446442104011820603148660069426522253772670, 3570757581386148492619721379754470899316095256123109990599128391, 4609713244848853151872498160877375734335329160891300656414838786, 1431248994688391495017629590719567118297228062918817671705412012, 2225618736576399852718197161416790353023368081178287753385225648, 4782768885432039605448539230699045953181097923357764740448690485, 1025808412433473089433862844337525335386046496037581875356716631, 2850703152833612251035169162871614900872662336925683266673455769, 4686484042664673737330267137247259184248980902553457550045106744, 3316117133062845045327521738264790714934051828005331038083037906, 1411496297445655314847983724570982636448577335114241954690062680, 2542720351620819402979547749565244924618621495731029455602801063, 4197157173419472170084161918188987699514176876278506629655813541, 775178221793495085043576729609381220589053240944436598437451103, 1341597796943613200200560889564801116846969301604051962802959921, 4724275587384586890632093268426638078191399337509178017491641396, 2254966368661541088781913210011063323766242664855534020654216185, 1559111672805843337464695743725374999443380244436636784823457268, 263060461355351726244024949311923372968467484234342136010504498, 4218489168395358789072676527116792449437414059225489587311420630, 2251347608406477583876276692162280889042972229705782250944073182, 1048197300230894759772482326800601949486880189444304544917201349, 4594309375612539584017914006965726879737368434732989117961461158, 1233526648681303204756491942769500757542366936959132748188681389, 3016611933554222534504704995395833948561521013355966057174149640, 685431642960387458833365483769661272653394129002170162343687962, 1252350578439116321952733140441764993245772656606639708501799071, 2004856906093670950398190666612521156885201358615487450361722687, 1725528220657822102510144312466698156124143365979935333948441423, 3301536380780212554033742823735941195638359575128262344444357806, 3361176781081176991336986769591969284375994647164396417238879397, 2555688594908398218735938381172552745926292876995621798813594216, 2149199142659861721027875250011594210747138266017264150889296633, 4654853318545885657451422703700711659405223637471250014707272999, 1783827755250002883819223478577480687561868815037598618999110299, 3876452588731221361888242546888347728419654382142841199604124779, 2283317070521561115970687892255141823986922119608171153201553969, 3015343638915794545630411225203386258523748035633382700837350107, 1308963799032621611684027617168973833892399982687941479751647735, 3363298156592318867171609036073104624481755649616128282937579774, 3543718722328215918394832245155182570535205536855659505934745836, 2955555006922666284454361589146164283232856958998231643765012061, 4193238914021395832431998242809775488323071053203281739810565939, 4863715450542324142694503897491361069694288484386266524822426647, 3583711168144466683911674650704848504341023445180872465082660398, 1433492863048866856968843544774985957106873111077658213115876127, 3622680772935480479302879234497984985614630209532096422962674742, 3887543917518693741822422553185837022122830870466259214366790339, 3010960827639423613523800853443011766752449479334524527050675334, 1675955542074383948970870230135814936799951109866034174734491734, 2568984843336400124353481960719548494069287783874504372170058935, 680042408260630675242336818143271325154353883745350135887078713, 1896768391347692167859873865813768792359296006947445277687988097, 537513148597668578568712785471862479586342936485511258184103046, 4338318157572996055378474172251186724034148657838505626251846209, 4509359887372553408550688030273180923282246069532844476087185588, 1961425576962957081785371096529881351777256192797473186708183898, 4562726127192998241808421239521775685020063730950933119470565151, 3197416476037069116835447572914275965582808251383336065711778098, 4509743379431751154130020063323115916220642219739358425391068150, 1737231313740527925458531852974418083735884963087687882655818328, 4723771434844173636187013002792278070911838005170476297565209636, 4021068815924596682472342770957679146819658388809493963529859273, 4786593367935490268774249574977281762592209022792374805751998882, 1706847947841349067687051586871379604391823960780007506398289654, 2092911436130136930529034363620771320336826052044341129920779847, 2386542753409262049262444109479898339116742017285966198413932291, 2575514997936878781309794857665223684996125674280321049577858392, 2526059212864002845504783002187945419965243527858703947395965701, 2077055376963690862993188737229202782309424513798741527458096967, 1947721666793448806619506886745665574368753315129031773531178573, 2321982120042809240576670901783025887795295409352093643395133004, 4191930348600938505176612143361132888157091847500134549846473180, 1279873852200144323116032749112043797286486924653552312015287694, 3934811009597203954835516432740855968621865146569217009553064951, 804570958275502176779582603101955727481164663345322968855176622, 4755601230261360181533138175300662604366870408130917516343576381, 2016264908613514961521473342929083040444069560476054659007958347, 3857121931198808981033402131835999166260880661479936388701406991, 4787908501772479625441292392638080593307265124479164945134226910, 403228266126326263488043524077179619385866145325037513940941892, 4080757802977772396554968304371742747141072297333640725823656444, 248086288384249359079536769334310714884272887049336400711180125, 1607777042247987295060365154963999272145526955355524894746933487] solve_n = partial(solve, pubkey, enc) CPU_CORE_NUM = 8 with mp.Pool(CPU_CORE_NUM) as pool: reslist = pool.imap_unordered(solve_n, range(CPU_CORE_NUM)) # terminate all processes once one process returns for res in reslist: if res: pool.terminate() break ``` Output: > After 19 runs. FIND SVP!!! (1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0) Single core time used: 643.215s ```python flag = '' svp = (1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0) for i in range(0, len(svp), 7): flag += '0' + ''.join(map(str, svp[i:i+7])) print(b'CCTF{'+long_to_bytes(int(flag, 2))+b'}') ``` ### Flag `CCTF{MuR4kam1_nA54K0}`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully