|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "[CryptoVerse 2022] A Tale of Two Systems" |
| 4 | +author: vEvergarden |
| 5 | +--- |
| 6 | + |
| 7 | +*Cross-posted @ [kevinliu.me/posts/cryptoverse/](https://kevinliu.me/posts/cryptoverse/)* |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +Another win for Maple Bacon! |
| 12 | + |
| 13 | +# A Tale of Two Systems |
| 14 | + |
| 15 | +> It's impossible to get a secure cryptosystem by combining two insecure cryptosystems. |
| 16 | +
|
| 17 | +This challenge is split into two stages independent from each other. |
| 18 | + |
| 19 | +## Stage 1 |
| 20 | + |
| 21 | +```python |
| 22 | +NBITS = 1024 |
| 23 | + |
| 24 | +def system_one(m: bytes): |
| 25 | + a, b = [getRandomNBitInteger(NBITS) for _ in range(2)] |
| 26 | + p = getPrime(2 * NBITS) |
| 27 | + q = getPrime(NBITS // 2) |
| 28 | + g = inverse(a, p) * q % p |
| 29 | + |
| 30 | + ct = (b * g + bytes_to_long(m)) % p |
| 31 | + |
| 32 | + print(f"p = {p}") |
| 33 | + print(f"g = {g}") |
| 34 | + print(f"ct = {ct}\n") |
| 35 | +``` |
| 36 | + |
| 37 | +At first, this system doesn't seem solvable because $a$ and $b$ are randomly generated and used in the encryption process. However, since we know the relative sizes of the parameters, we can apply some ✨ lattice magic ✨ to recover $a$ and $b$, and subsequently $m$. |
| 38 | + |
| 39 | +First, the sizes of the internal variables are bounded by: |
| 40 | +- $a$ - 1024 bits |
| 41 | +- $b$ - 1024 bits |
| 42 | +- $p$ - 2048 bits |
| 43 | +- $q$ - 512 bits |
| 44 | +- $g$ - 2048 bits |
| 45 | +- $ct$ - 2048 bits |
| 46 | + |
| 47 | +From the encryption process, we have that: |
| 48 | + |
| 49 | +$$g \equiv a^{-1}q \mod p$$ |
| 50 | + |
| 51 | +$$ga \equiv q \mod p$$ |
| 52 | + |
| 53 | +Thus, there exists some integer $k$ such that $ga = pk + q$. |
| 54 | + |
| 55 | +Since $ga$ is 3072 bits and $p$ is 2048 bits, $k$ will be around 1024 bits. |
| 56 | + |
| 57 | +Now, consider the lattice: |
| 58 | + |
| 59 | +$$\begin{bmatrix} |
| 60 | +p & 1 \\ |
| 61 | +g & 1 |
| 62 | +\end{bmatrix}$$ |
| 63 | + |
| 64 | +Notice that we can express the vector $(q, a-k)$ as a linear combination of the rows of the lattice: |
| 65 | + |
| 66 | +$$(q, a-k) = a(g, 1) + -k(p, 1)$$ |
| 67 | + |
| 68 | +Since $q$ is 512 bits while $a$ and $k$ are 1024 bits, this vector is "small" compared to the original rows with 2048 bit numbers. So, we can apply LLL and hope that the small basis vector is $(q, a-k)$: |
| 69 | + |
| 70 | +```python |
| 71 | +m = Matrix([[p, 1], [g, 1]]) |
| 72 | +print(m.LLL()) |
| 73 | +``` |
| 74 | + |
| 75 | +Now that we have $q$ and $a$ from the lattice reduction, we can decrypt the message. |
| 76 | + |
| 77 | +Observe that: |
| 78 | + |
| 79 | +$$a \cdot ct \equiv abg + am \equiv (g^{-1}q)bg + am \equiv bq + am \mod p$$ |
| 80 | + |
| 81 | +However, since $p$ is 2048-bit and $bg$ and $am$ are both around ~1500 bits, |
| 82 | + |
| 83 | +$$bq + am < p$$ |
| 84 | + |
| 85 | +From this, recovering $m$ is easy: |
| 86 | + |
| 87 | +$$m \equiv (bq + am)a^{-1} \mod q$$ |
| 88 | + |
| 89 | +```python |
| 90 | +m = Matrix([[p, 1], [g, 1]]) |
| 91 | +m = m.LLL() |
| 92 | +q = m[0][0] |
| 93 | +a = inverse(g, p) * q % p |
| 94 | +temp = a * ct % p |
| 95 | +m = temp * inverse(a, q) % q |
| 96 | +print(long_to_bytes(m)) |
| 97 | +``` |
| 98 | + |
| 99 | +Flag: `cvctf{n0_On3_1S_u53l355_1n_7h15_w0r1d_...` |
| 100 | + |
| 101 | +## Stage 2 |
| 102 | + |
| 103 | +```python |
| 104 | +def system_two(m: bytes): |
| 105 | + p, q = [getPrime(NBITS // 2) for _ in range(2)] |
| 106 | + n = p * q |
| 107 | + e = 0x10001 |
| 108 | + ct = pow(bytes_to_long(m), e, n) |
| 109 | + |
| 110 | + print(f"n = {n}") |
| 111 | + print(f"e = {e}") |
| 112 | + print(f"ct = {ct}") |
| 113 | + |
| 114 | + # what if q is reversed? |
| 115 | + q = int('0b' + ''.join(reversed(bin(q)[2:])), 2) |
| 116 | + hint = p + q - 2 * (p & q) |
| 117 | + print(f"hint = {hint}") |
| 118 | +``` |
| 119 | + |
| 120 | +First, let's make sense of the hint: what does `x + y - 2 * (x & y)` represent for two arbitrary integers $x$ and $y$? |
| 121 | + |
| 122 | +Let's say $a_i$ represents the $i$th bit of a number $a$. Working in binary, $(x \\& y)_i$ is 1 if and only if $x_i = y_i = 1$. When we add $x$ and $y$ in those positions, the sum of $1+1$ in binary will carry over to the next position. But this is perfectly cancelled out when we subtract $(x \\& y)$ shifted to the left by 1. |
| 123 | + |
| 124 | +In other words, if $x_i = y_i = 0$ or if $x_i=y_i=1$, then the corresponding bit $(x \\& y)_i$ is 0. This is just the XOR operation between $x$ and $y$! |
| 125 | + |
| 126 | +Our goal is to factor $n$, and we currently know: |
| 127 | +- $n = pq$ |
| 128 | +- $p \oplus rev(q)$ |
| 129 | + |
| 130 | +### Similar: XORSA |
| 131 | +A similar problem appeared in [PlaidCTF](https://ctftime.org/task/15578), where $p \oplus q$ was given. The central idea of one solution is to find the bits of $p$ and $q$ one by one. |
| 132 | + |
| 133 | +Starting from the lowest bit, there are always two possibilities for $(p, q)$ since we know $(p \oplus q)$. We can try both of these possibilities recursively, for a naive solution of $2^k$ where $k$ is the number of bits in $n$. But, we can prune the majority of the search space by checking that |
| 134 | +$$p_0 \cdot q_0 \equiv n \mod 2^l$$ |
| 135 | + |
| 136 | +for our current $p_0$ and $q_0$ (lower bits of $p$ and $q$). This optimization is enough to solve the XORSA challenge. |
| 137 | + |
| 138 | +### Back on track |
| 139 | +Let's apply a similar idea to our problem, but instead of starting only from the lower bits, we recurse over the lower and higher bits simultaneously. |
| 140 | + |
| 141 | +Specifically, we can guess the highest bits of $rev(q)$ and the highest bits of $p$. At the same time, we can guess the lowest bits of $rev(q)$ and the lowest bits of $p$. Note that the highest bits of $rev(q)$ are the lowest bits of $q$, and vice versa. |
| 142 | + |
| 143 | +To optimize this brute force, at each step we prune over the lower bits by checking that: |
| 144 | + |
| 145 | +$$p_{low} \cdot q_{low} \equiv n \mod 2^l$$ |
| 146 | + |
| 147 | +Additionally, we prune over the top bits: |
| 148 | +- If we set the remaining bits of $p_{high}$ and $q_{high}$ to $1$, the product $p_{high}q_{high}$ must be greater than $n$ |
| 149 | +- If we set the remaining bits of $p_{high}$ and $q_{high}$ to $0$, the product $p_{high}q_{high}$ must be less than $n$. |
| 150 | + |
| 151 | +The final solve script: |
| 152 | +```python |
| 153 | +n = 153342396916538105228389... |
| 154 | +e = 65537 |
| 155 | +ct = 1073382308863262771844706024... |
| 156 | +# p xor (reverse q) |
| 157 | +xor = 35510848380770904338319... |
| 158 | +NBITS = 512 |
| 159 | + |
| 160 | +# q = highQ || lowQ and p = highP || lowP, where all high/low have idx bits |
| 161 | +def find(idx, lowP, highP, lowQ, highQ): |
| 162 | + |
| 163 | + if idx == NBITS: |
| 164 | + assert highP * highQ == n |
| 165 | + print("FOUND!") |
| 166 | + print(highP) |
| 167 | + print(highQ) |
| 168 | + exit() |
| 169 | + |
| 170 | + |
| 171 | + highX = (xor >> (NBITS - 1 -idx)) & 1 |
| 172 | + lowX = (xor >> idx) & 1 |
| 173 | + |
| 174 | + possibleLow = [] |
| 175 | + possibleHigh = [] |
| 176 | + |
| 177 | + # find possible (highP, lowQ) pairs from the MSB of the XOR |
| 178 | + if highX == 1: |
| 179 | + possibleHigh.append(((highP << 1) | 1, lowQ)) |
| 180 | + possibleHigh.append((highP << 1, lowQ + (1 << idx))) |
| 181 | + else: |
| 182 | + possibleHigh.append((highP << 1, lowQ)) |
| 183 | + possibleHigh.append(((highP << 1) | 1, lowQ + (1 << idx))) |
| 184 | + |
| 185 | + # find possible (lowP, highQ) pairs from the LSB of the XOR |
| 186 | + if lowX == 1: |
| 187 | + possibleLow.append((lowP, (highQ << 1) | 1)) |
| 188 | + possibleLow.append((lowP + (1 << idx), highQ << 1)) |
| 189 | + else: |
| 190 | + possibleLow.append((lowP, highQ << 1)) |
| 191 | + possibleLow.append((lowP + (1 << idx), (highQ << 1) | 1)) |
| 192 | + |
| 193 | + |
| 194 | + for highP, lowQ in possibleHigh: |
| 195 | + for lowP, highQ in possibleLow: |
| 196 | + # prune lower bits |
| 197 | + if lowP * lowQ % (1 << (idx + 1)) != n % (1 << (idx + 1)): |
| 198 | + continue |
| 199 | + |
| 200 | + pad = NBITS-1-idx |
| 201 | + |
| 202 | + # check upper bit bounds |
| 203 | + if (highP << pad) * (highQ << pad) > n: |
| 204 | + continue |
| 205 | + |
| 206 | + if ((highP << pad) + (1 << pad) - 1) * ((highQ << pad) + (1 << pad) - 1) < n: |
| 207 | + continue |
| 208 | + |
| 209 | + find(idx+1, lowP, highP, lowQ, highQ) |
| 210 | + |
| 211 | +find(0, 0, 0, 0, 0) |
| 212 | +``` |
| 213 | + |
| 214 | +Partial flag: `_wH0_l16h73N5_tHe_BurD3nS_0F_4n07h3R.-_-}` |
| 215 | + |
| 216 | +Final flag: `cvctf{n0_On3_1S_u53l355_1n_7h15_w0r1d_..._wH0_l16h73N5_tHe_BurD3nS_0F_4n07h3R.-_-}` |
0 commit comments