From d615a88a9068a6d0f26a9755792725b548650744 Mon Sep 17 00:00:00 2001 From: aditya-adiraju Date: Sat, 15 Jul 2023 01:55:31 -0700 Subject: [PATCH 1/2] Fix Custom 404 page (fixes #125) --- 404.html | 3 ++- _layouts/error.html | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/404.html b/404.html index f626b2d..8e7e468 100644 --- a/404.html +++ b/404.html @@ -1,8 +1,9 @@ --- layout: error +permalink: /404.html ---

404

Page not found :(

-

The requested page could not be found.

+

The requested page could not be found.

\ No newline at end of file diff --git a/_layouts/error.html b/_layouts/error.html index ff32824..7bbe5ae 100644 --- a/_layouts/error.html +++ b/_layouts/error.html @@ -24,8 +24,24 @@
- {{ content }} - +
+ +
+ +
+ + CTF Team at the University of British Columbia + +
+
+
+
+ {{ content }} +
{% include footer.html %}
From eab6d8b36ae9b6cb82d35fce453c5e135f2e7f84 Mon Sep 17 00:00:00 2001 From: aditya-adiraju Date: Mon, 20 May 2024 02:06:49 -0700 Subject: [PATCH 2/2] Added SDCTF 2024 writeup: ReallyComplexProblem --- .../2024-05-19-sdctf-reallycomplexproblem.md | 425 ++++++++++++++++++ .../sdctf-2024/reallycomplexproblem/CRSA.py | 106 +++++ .../sdctf-2024/reallycomplexproblem/LEAK.png | Bin 0 -> 598567 bytes .../reallycomplexproblem/3x3_lattice.png | Bin 0 -> 86520 bytes .../reallycomplexproblem/coefficient_vec.png | Bin 0 -> 118843 bytes .../reallycomplexproblem/imaginary_vec.png | Bin 0 -> 152536 bytes 6 files changed, 531 insertions(+) create mode 100644 _posts/2024-05-19-sdctf-reallycomplexproblem.md create mode 100644 assets/code/sdctf-2024/reallycomplexproblem/CRSA.py create mode 100644 assets/code/sdctf-2024/reallycomplexproblem/LEAK.png create mode 100644 assets/images/sdctf-2024/reallycomplexproblem/3x3_lattice.png create mode 100644 assets/images/sdctf-2024/reallycomplexproblem/coefficient_vec.png create mode 100644 assets/images/sdctf-2024/reallycomplexproblem/imaginary_vec.png diff --git a/_posts/2024-05-19-sdctf-reallycomplexproblem.md b/_posts/2024-05-19-sdctf-reallycomplexproblem.md new file mode 100644 index 0000000..6ba53f8 --- /dev/null +++ b/_posts/2024-05-19-sdctf-reallycomplexproblem.md @@ -0,0 +1,425 @@ +--- +layout: post +title: "[SDCTF 2024] ReallyComplexProblem" +author: hiswui +--- + +## Problem Description + +We have a ciphertext that we have to decrypt in 48 hours. Luckily, one of our guys at the NSA was able to take a +screenshot of the computer as it was performing the encryption, unfortunately it only captured part of the screen. Can +you help us break the cipher? + +- Difficulty: Hard +- Tags: Crypto +- author: 18lauey2 + +#### Attachments + +[CRSA.py](/assets/code/sdctf-2024/reallycomplexproblem/CRSA.py) +[LEAK.png](/assets/code/sdctf-2024/reallycomplexproblem/LEAK.png) + +## TL;DR +Modified coppersmith method that converts the complex valued matrix into a real matrix through a canonical embedding and +solve it like normal. + +## Introduction, audience, and pre-requisites + +This writeup, like most of my writeups, is geared towards people with an elementary understanding of Math. Additionally, +this writeup focuses on the logic behind the solution as opposed to *just* the solution. + +The pre-requisites that would be nice to know before reading this are: +- The RSA encryption and decryption scheme Basic modular arithmetic Matrix algebra (vectors, linear combinations, and +- matrices) An elementary understanding of complex numbers + + Alright then. Sit tight and buckle up because we are in for a doozy! + +## Challenge Overview and Inspecting the Code + +The challenge performs RSA with complex integers (Gaussian Integers: $\mathbb{Z}[i]$) as opposed to regular Integers +$\mathbb{Z}$. A Complex integer is a complex number $a + bi$ such that $a, b \in \mathbb{Z}$. + +Fortunately, the logic behind the algorithm, Complex-RSA (CRSA), remains fairly familiar with a few caveats: +- Since there's no real notion of a prime over the Complex Plane, we instead say that a Gaussian integer $w$ is prime if + and only if its norm is prime. + - "What's a norm?" In this case, consider a norm to be defined as $Re(w)^2 + Im(w)^2$. (This can be interpreted, +- geometrically, as the square of the point's distance from the origin) Once we generate our primes `p` and `q`, the + rest of the process is the same as regular RSA. (I'm skipping over + details for modular exponentiation because it's not relevant to the challenge) + + +The second part of the challenge involves our LEAKed picture which features a terminal with output that reads the values +of `N`, `ciphertext`, and a some portion of `p`. Interestingly enough, we see about two-thirds of both the real and the +imaginary part of `p` with the rest covered by the beautiful hand-drawn raccoon + +## But we're missing bits! Now what? + +You're right. There is still a bit of work to do if we would like to decrypt our message. Alright, let's take a deep +breath and work step-by-step. What information do we need to retrieve the original plaintext `m`. + +To decrypt a message we need `d` which is defined as `e^-1 (mod (norm(p)-1)*(norm(q)-1))`. To find `d`, we need `p` and +`q` which in turn require us to factor `N = pq`. To factorize `N`, we would need "recover" `p` from the information that +was leaked and divide `N` by `p`. + +**Oh boy.** That's a lot. All these steps are fairly straightforward with the exception of recovering `p`. So, our goal +is to recover this value. + +After some painful counting and testing, There's roughly about 155 digits for both the real and imaginary parts. we have +about 85 and 87 of these digits respectively. (Okay, maybe it wasn't two-thirds...) + + +Retrieving these missing bits seems hard. Let's consider a simpler problem: What if this was regular RSA and we had +about 60% of p. As it turns out, someone has solved this problem before. + +## A Copper sword crafted by the kingdom's finest blackSmith + +Enter the Coppersmith method. In a nutshell, the method finds small integer roots of polynomials modulo a given integer. +To clarify, this means that if we have a polynomial of the form $F(x) = x^n + a_{n-1}x^{n-1} + ... + a_1x + a_0$ where +$a_i \in \mathbb{Z \text{ (mod N)}}$, and we know that there exists some integer $x_0$ such that $F(x_0) \equiv 0 +\text{ (mod N)}$ and $|x_0|$ is less than $N^{\frac{1}{n}}$, we can find $x_0$. + +You might be wondering, "cool fact. What does this have to do with us?" The answer is *everything*. Let me take you +through this step-by-step. + +1. Recall that we have knowledge of `N`, the fact that `N = p * q`, and a fair chunk of `p` (let's say about + 110 digits of 155 digits). +2. We can express `p` as follows `p = the_known_part + the_unknown_part`. Mathematically, $p = a + r$ + where a and r are the known and unknown parts of p respectively. + - For example if `p = 382xx`, we would express it as $p = 38200 + r$. +3. We also have that $r$ is less than $10^{45}$ since $r$ has 45 digits. Thus, we get an upper bound $R = 10^{45}$. +4. Let's create a polynomial $f(x)$ modulo $p$. We will define $f(x) = a + x$ where $a$ is a constant which represents + the known part of $p$. + - In the definition of the method above, $n = 1$ (aka the degree of the polynomial we must solve) +5. Now, $f(r) = a + r = p \equiv 0 \text{ (mod p)}$. In other words, $r$ is our small ineger root $x_0$ from the + definition above. +6. Note, that $r$ is less than $R$ which is less than $p^{\frac{1}{1}} = p^1$ which is less than $N$. +7. YAY! This is literally what the Coppersmith method needs to work. + +![oh yeah, it's all coming together](https://media1.tenor.com/m/sqYV7D2euF8AAAAC/kronk-oh-yeah-its-all-coming-together.gif) + + +## The Coppersmith Attack is truly one of the attacks of all time + +Now that we have the pieces, let's apply the coppersmith method to find our $x_0$ ($r$). Firstly, it's good to +understand a bit of our motivation here. It is very difficult to find the roots of an integer polynomial over some +modulo N. However, it is extremely trivial (relatively) to find the roots of the same polynomial over the integers. The +method takes in our polynomial $f(x)$ performs a bit of magic and in combination with the Howgrave-Graham theorem it +converts our polynomial modulo N to a simple polynomial with the same small roots over the integers (no modulo). + +### The Howgrave-Graham Theorem +Okay, so the (extremely abridged version of) Howgrave-Graham Theorem states that for a polynomial $g(x)$, if: +- $g(x_0) \equiv 0 \text{ (mod }b^k\text{)}$ for some $b, k$ +- $abs(x_0) \le R$ Where R is the upper bound we discussed earlier +- The length of the coefficient vector of $g(R \cdot x)$ is small. (The coefficient vector refers to the vector containing the + coefficients of each term in our polynomial $[a_n, a_{n-1}, ..., a_1, a_0]$.) + - Small is once again defined as being less than some bound based on $b, k$ and the degree of $g(x)$. However, it's + not relevant to us because we will fulfill it at the end. (Haha! this might be forshadowing) + +then $g(x_0) = 0$ over the integers too. That is, $x_0$ is an integer root. + +Great! let's use this on $f(x)$. Well... we can't use it just yet because the coefficients of the polynomial $f(R\cdot x)$ are +**huge**. In particular, the constant term $a$ is the same number of digits of $p$. This fails the third condition in +the Howgrave-Graham theorem which wants a small coefficient vector. Fortunately, there's a way to fix this. + +### Reducing the Size of our Massive Polynomials + +At first glance, it seems difficult to do reduce the size of our coefficients. However, all we need is a small cameo +from our good old friend: linear combinations. + +Suppose I had two polynomials $a(x)$ and $b(x)$ such that $a(x_0) \equiv b(x_0) \equiv 0 \text{ (mod m)}$ for some +integers $m$ and $x_0$. Note that $a(x_0)$ doesn't neccessarily equal $b(x_0)$. Now, we have that $a(x_0) + b(x_0) +\equiv 0 \text{ (mod m)}$. Trivially, we also have that $l \cdot a(x_0) \equiv 0 \text{ mod(m)}$ for any integer $l$. +Thus, for any integers $l$ and $k$, we get $l \cdot a(x_0) + k \cdot b(x_0) \equiv 0 \text{ mod(m)}$. So + +In summary, we just showed that any *integer* linear combination of two polynomials preserves (or has the same) the root +$x_0$ over our modulus $m$. +So, this means that if we can find other polynomials which has the same root, x_0, as $f(Rx)$ (and $f(x)$) modulo $p$, then we can +craft an integer linear combination between them to reduce the size of our coefficients. (Note: this is similar to the +idea of row reductions in matrix algebra). + +### A Trick to Create Unlimited Polynomials + +Our long chain of dominos continues as we search for polynomials with the same root $x_0$ as $f(x)$ over our modulus p. +For convenience, I will call this set of polynomials $F$. The problem is we don't know $p$, so we can't make polynomials +like $g(x) = px^2 + 4px + p^3$ which will always be 0 for all values of $x$. (They're not particularly useful either). +Let's use some clever tricks instead. + +- Firstly, we know $N$ which is a multiple of $p$ so $g(x) = N \equiv 0 \text{ (mod p)}$ for all $x$ including $x_0$. + Let's add it to $F$. +- Next, we have that $f(x_0) \equiv 0 \text{ (mod p)}$. We could square both sides and get: $[f(x_0)]^2 \equiv 0 \text{ + (mod p)}. Nice! Let's add $[f(Rx)]^2$ to $F$. +- Why stop there? We can just continue raising $f(Rx)$ to various ineger powers and have the same outcome as above. We + can thus add all the powers of $f(Rx)$ to $F$. + +Now, we have a long list of polynomials to choose from. An alternative to this method would be to simply multiply $f(Rx)$ +by different powers of $x$. However, the downside to this method is that we would lose our constant term in the elements +of $F$. The powers of $f(Rx)$ is much more elegant in the sense that due to the binomial theorem, we are bound to have +constant terms. + +### The Magical Mysteries of the Lattice and LLL + +We have a list of polynomials with the same root $x_0$ whose coefficients we seek to reduce through their integer linear +combinations. It remains to be asked: "How do we determine the most efficient integer linear combinations". It's time to +introduce Lattices and LLL. + +---- +### Introducing our New Show: DeComplexify This! + +Today, we will be learning what a Lattice and how LLL might help our little predicament. Recall that if we were working +with the Real numbers, we could simply use a matrix to reduce the size of a basis and make it orthogonal using the +gram schmidt method. However, we are working over the Integers where the same strategy cannot be used. + +Introducing the Lattice. No, not the lattice from Organic Chemistry. An n x n (integer) lattice is essenitally just like a +n x n matrix with two exceptions: +- All the elemwents in our lattice are integers +- The Span of our vectors refers to just the *integer* linear combinations. (Instead of real coefficients for matrices). + +Like a matrix, we can put express our polynomial f(Rx) in the form of a row vector. In fact, you've already seen this +before in the form of our coefficient vectors. + +![](/assets/images/sdctf-2024/reallycomplexproblem/coefficient_vec.png) + + +We can create a matrix using some of our polynomials in $F$ where each row is a polynomial and each column is represents +the coefficients of a power of $x$. We can create a matrix using the polynomials $g(x) = N$, $f(Rx)$, $[f(Rx)]^2$. + +![](/assets/images/sdctf-2024/reallycomplexproblem/3x3_lattice.png) + + +Now that we have constructed our lattice, let me introduce the LLL algorithm (Lenstra-Lenstra-Lovász). I won't be going +over the nitty-gritty details of this algorithm and will instead treat this as a black box. This algorithm takes in a +lattice basis (Basis has the same meaning as in matrix algebra) and outputs a lattice with a more orthogonal and smaller +basis. You can read about it more in this wonderful [tutorial](https://eprint.iacr.org/2023/032.pdf). A fun exercise is +justifying to yourself that our row vectors are linearly independent to each other. + +---- + +Once we apply the LLL algorithm on this lattice, our rows, representing polynomials, will now have smaller coefficients. +Since the length of our coefficient vector is smaller (by definition of LLL), we can apply Howgrave-Graham's theorem in +order to find $x_0$ by finding the roots of $h(x)$ over the integers. Note that the resulting row vectors will be of the +form $h(Rx)$. We simply divide each coefficient by $R$ to retrieve $h(x)$. + +We have succesfully found $r$ (our $x_0$) and we can reconstruct $p$ by $a + r$. Victory! We solved our simpler RSA +problem. Now, to deal with something more complex. (literally) + + +## The Complexities of Complex Numbers + +The question now is: "Can we do the same for our complex integers?" The answer is ***mostly***. While most of the +theorems extends out to the Complex Integers, LLL only operates over the regular integers. To understand how we overcome +this problem, let's first go through our solution till our roadblock. + +1. Write down `N` and the known part of `p` +```py +N = -117299665605343495500066013555546076891571528636736883265983243281045565874069282036132569271343532425435403925990694272204217691971976685920273893973797616802516331406709922157786766589075886459162920695874603236839806916925542657466542953678792969287219257233403203242858179791740250326198622797423733569670 + 617172569155876114160249979318183957086418478036314203819815011219450427773053947820677575617572314219592171759604357329173777288097332855501264419608220917546700717670558690359302077360008042395300149918398522094125315589513372914540059665197629643888216132356902179279651187843326175381385350379751159740993*I +a = 1671911043329305519973004484847472037065973037107329742284724545409541682312778072234 * 10^70 + 193097758392744599866999513352336709963617764800771451559221624428090414152709219472155 * 10^68 * I +``` +2. At the same time as finding `a`, we can define our upper bound $R$ as $R_r$ and $R_i$ for the bound of the real and + imaginary part of `r`. Since the primes will always have about 155 digits (this could be verified with a bit of + testing/bruteforcing other limits). +3. Our $f(x) = a + x$. Instead of this, we can choose to be more verbose and write it as $a + bi + x + i \cdot x$. Here, + we treat $i$ similar to a variable and all the coefficients (like $a$ and $b$) are real integers. +4. We do the same process as before to generate different powers of $f((R_r + R_i \cdot i)x)$ modulo p. (refer to the + challenge code to see how you can take the modulo under a complex number) +5. Now, we hit our roadblock of representing our polynomials as row vectors of integers. Well, we could simply double + the columns (adding an imaginary part to each power of $x$). This looks like... +![](/assets/images/sdctf-2024/reallycomplexproblem/imaginary_vec.png) + - one more note is that we can double our set from before by adding the imaginary multipe of $f$ such as $-i\cdot f(Rx)$ +6. Construct a matrix with a lot of these row vectors and perform LLL. + - The reason we need a lot of polynomials has to do with the Howgrave-Graham theorem which essenitally ends up + equating to us requiring more rows to have a greater chance of finding our root. +7. find the root of the reduced polynomial over the Complex Integers. +8. Retrieve `r` and thus find $p$ +9. Use $p$ to find $q$ and then find $d$ and use $d$ to decrypt our ciphertext given by: +``` +e = 65537 +ciphertext = 49273345737246996726590603353583355178086800698760969592130868354337851978351471620667942269644899697191123465795949428583500297970396171368191380368221413824213319974264518589870025675552877945771766939806196622646891697942424667182133501533291103995066016684839583945343041150542055544031158418413191646229 - 258624816670939796343917171898007336047104253546023541021805133600172647188279270782668737543819875707355397458629869509819636079018227591566061982865881273727207354775997401017597055968919568730868113094991808052722711447543117755613371129719806669399182197476597667418343491111520020195254569779326204447367 * I +``` + +Wow, we did it! oh no... It did not work :( + + +## WHY DOESNT IT WORK!! + +The short answer is that we need to modify our choice of polynomials because it still fails the conditions for the +Howgrave-Graham Theorem. Recall that the Howgrave theorem limits us on our choices of $b$, $k$, and the degree of the +polynomial. For the theorem, we need $f(x_0) \equiv 0 \text{ (mod }b^k\text{)}$. Previously, we just set $b^k = p$ and +called it a day. However. However, through a long series of proofs that are very well highlighted on this [blog](https://www.klwu.co/maths-in-crypto/lattice-2/#howgrave-grahams-formulation), +this can be very inefficient and makes it such that the maximum upper bound for $R$ ends up being very small. The +maximum bound is usually defined by some relation $X \approx N^\frac{1}{c(d)}$ where $c(d)$ is a function that depends +on the degree, $d$, of our polynomial. Understanding this, our goal would be to reduce the the growth of $c(d)$ as much +as possible. We will be using two techniques to do this (from the same blog post). + +---- +### The First Technique: + +Rather than considering $b^k = p$, we could instead try $b = p$. This would ultimately help increase our upper bound (as +described in the blog if you are curious). What changes? Well, unfortunately $f(x_0) \equiv 0 \text{ (mod }p^k\text{)}$ +is no longer true. However, this might actually be useful. + +I will leave this as an exercise to the curious readers, but it's trivial to observe that if an integer $a$ divides $b$, +then $a^k$ divides $b^k$. Also, if $a$ divides $c$, then $a^k$ divides $c^{i}b^{k - i}$ for some $i \in \mathbb{Z}$ that +is less than $K$ and greater than zero. This implies that if we have two polynomials $a(x)$ and $b(x)$ such that $a(x_0) +\equiv b(x_0) \equiv 0 \text{ (mod m)}$ for some integer $m$, then $[a(x_0)]^i[b(x_0)]^{k - i} \equiv 0 \text{ (mod +}m^k\text{)}$. + +So, let's use the two polynomials we know are divisible by $p$ at $x_0$: $N$ and $f(Rx)$. (yes, N is a polynomial that +equates to a constant.) Now, instead of using powers of $f(x)$, we can instead add polynomials of the form +$[f(Rx)]^i[N]^{k - i}$ for each integer $i \in [0, k - 1]$ to our set $F$. + +Note that for our complex integers, whenever I add a polynomial $g(x)$ to $F$, I'm also adding its imaginary multiple +$-i\cdot g(x)$ to the set. This simply helps with the lattice reduction by giving the LLL algorithm more options to +reduce our polynomials by. + +### Technique Numero Dos: + +The second technique, which was discussed directly in the blog, involves multiplying $[f(Rx)]^k$ with various powers of +$x$. Recall that $[f(x_0)]^k \equiv 0 \text{ (mod }p^k\text{)}$.So, we add polynomials of the form $[N]^i[f(Rx)]^k $ for +each integer $i \in [0, k - 1]$ to our set $F$ (along with its imaginary multiples. Note that there's nothing really +stopping us from taking a different number of polynomials for the second technique, rather than $k - 1$ polynomials we +can take $5$ or $4000$. Though I'm not sure what those bounds would be. + +---- + +## Back to business + +Now that we have created a better lattice, we can finally solve our challenge. Nevermind! There's a lot of sage-specific +bugs that had to be squashed. + +*hours later*, we can finally use our script to reverse the encryption and encoding to get our flag. + + +## The solve script (finally) + +```py + +from CRSA import GaussianRational, decrypt +from fractions import Fraction +from Crypto.Util.number import long_to_bytes + +ciphertext = 49273345737246996726590603353583355178086800698760969592130868354337851978351471620667942269644899697191123465795949428583500297970396171368191380368221413824213319974264518589870025675552877945771766939806196622646891697942424667182133501533291103995066016684839583945343041150542055544031158418413191646229 - 258624816670939796343917171898007336047104253546023541021805133600172647188279270782668737543819875707355397458629869509819636079018227591566061982865881273727207354775997401017597055968919568730868113094991808052722711447543117755613371129719806669399182197476597667418343491111520020195254569779326204447367 * I +N = -117299665605343495500066013555546076891571528636736883265983243281045565874069282036132569271343532425435403925990694272204217691971976685920273893973797616802516331406709922157786766589075886459162920695874603236839806916925542657466542953678792969287219257233403203242858179791740250326198622797423733569670 + 617172569155876114160249979318183957086418478036314203819815011219450427773053947820677575617572314219592171759604357329173777288097332855501264419608220917546700717670558690359302077360008042395300149918398522094125315589513372914540059665197629643888216132356902179279651187843326175381385350379751159740993*I +a = 1671911043329305519973004484847472037065973037107329742284724545409541682312778072234 * 10^70 + 193097758392744599866999513352336709963617764800771451559221624428090414152709219472155 * 10^68 * I + + +# This function takes in our polynomial and returns two rows +# The first row is the coefficient vector, scaled by the uppper bounds, of the regular polynomial +# The second row is the coefficient vector, scaled by the upper bounds, of its imaginary multiple +def get_coefficients(f, R_r, R_i): + regular = [] + imag_multiple = [] + coeffs = f.list() + + for i, c in enumerate(coeffs): + regular.extend([c.real() * R_r^i, c.imag() * R_i^i]) + + for i, c in enumerate(coeffs): + imag_multiple.extend([-1 * c.imag() * R_r^i, c.real() * R_i^i]) + + return [regular, imag_multiple] + +# since our row vectors have different lengths, we need to pad them with zeros +# Note that the solve script reverses the columns. The leftmost column is the constant while +# the rightmost column is the coefficient of the highest degree of x +def rpad(lst, length): + result = [] + for l in lst: + result.append(l + [0 for i in range(length - len(l))]) + return result + + +def coppersmith(f, R_r, R_i, N, k): + # This was the maximum number of columns/entries a row vector has. + max_cols = 4 * k + # polynomial row vectors + polynomial_rows = [] + x = f.parent().gen(0) # apparently helps sage do its thing + + # Add polynomials from our first technique + for i in range(k): + poly_rows = get_coefficients(f^i * N^(k-i), R_r, R_i) + poly_rows = rpad(poly_rows, max_cols) + polynomial_rows.extend(poly_rows) + + # Add polynomials from our second technique + for i in range(k): + poly_rows = get_coefficients(f^k * x^i, R_r, R_i) + poly_rows = rpad(poly_rows, max_cols) + polynomial_rows.extend(poly_rows) + + # We perform LLL on our lattice + M = matrix(polynomial_rows) + B = M.LLL() + + # v is the first polynomial from our reduced lattice + v = B[0] + + # This section was lifted from the official solve, but just cleans up our polynomial + Q = 0 + for (s, i) in enumerate(list(range(0, len(v), 2))): + z = v[i] / (R_r^s) + v[i+1] / (R_i^s) * I + Q += z * x^s + + return Q + +R. = PolynomialRing(I.parent(), "x") # sage once again doing its thing +f = x + a # our beloved polynomial + +# 10 seemed to be the sweet spot +Q = coppersmith(f, 10^70, 10^68, N, k=10) + +# r = x_0 = Q.roots()[0][0] +p = a + Q.roots()[0][0] + + +# Now we cast the values we calculated to GaussianRationals and find q +p = GaussianRational(Fraction(int(p.real())), Fraction(int(p.imag()))) +N = GaussianRational(Fraction(int(N.real())), Fraction(int(N.imag()))) +ciphertext = GaussianRational(Fraction(int(ciphertext.real())), Fraction(int(ciphertext.imag()))) +q = N / p + +# calculate the value of d from p and q +p_norm = int(p.real*p.real + p.imag*p.imag) +q_norm = int(q.real*q.real + q.imag*q.imag) +tot = (p_norm - 1) * (q_norm - 1) +e = 65537 +d = pow(e, -1, tot) + +# decrypt our ciphertext +m = decrypt(ciphertext, (N, d)) + +# decode the message +print(long_to_bytes(int(m.real)) + long_to_bytes(int((m.imag)))) + +``` + +## Flage + +`SDCTF{lll_15_k1ng_45879340409310}` Indeed it is king. + + +## Final Thoughts + +This was a really hard challenge. I spent over 30 hours straight running in circles with various techniques like complex +LLL and Algebraic LLL. However, I did not solve this challenge at the end of the CTF. In fact, this challenge went +unsolved by anyone. After discussing with the author, I realized that one of my earlier ideas of converting the complex +integers to a real matrix to do LLL was actually the intended solution. However, I didn't quite understand how to +complete the solve path which was doing a canonical embedding. An embedding is similar to what we did with using +different columns for the real and imaginary part of the powers of $x$ and using the imaginary multiples. + +I'm glad I was able to solve it regardless because it's better late than never. More importantly, I hope that this guide +can give you some understanding behind the complexities of the coppersmith method often needed for RSA challenges. In +this vein, I have another section with resources I found useful for this challenge. + +Finally, shoutout to 18lauey2 for making such a cool challenge. + +## Resources to help my dumb dumb brain + +- A bunch of lectures from Tanja Lange on Coppersmith and RSA as part of 2MMMC10 at Eindhoven University of Technology + [https://www.youtube.com/@tanjalangecryptology783/videos](https://www.youtube.com/@tanjalangecryptology783/videos) +- The blog written by Cousin Wu Ka Lok from `blackb6a` [https://www.klwu.co/maths-in-crypto/lattice-2/#second-idea](https://www.klwu.co/maths-in-crypto/lattice-2/#second-idea) +- The paper the challenge was inspired by [Ideal forms of Coppersmith’s theorem and Guruswami-Sudan list decoding](https://ia803007.us.archive.org/2/items/arxiv-1008.1284/1008.1284.pdf) +- A wonderful paper that summarizes the various attacks on RSA. [Recovering cryptographic keys from partial information, by example](https://eprint.iacr.org/2020/1506.pdf) + +That's all folks. + diff --git a/assets/code/sdctf-2024/reallycomplexproblem/CRSA.py b/assets/code/sdctf-2024/reallycomplexproblem/CRSA.py new file mode 100644 index 0000000..9528de4 --- /dev/null +++ b/assets/code/sdctf-2024/reallycomplexproblem/CRSA.py @@ -0,0 +1,106 @@ +from math import floor, ceil +from secrets import randbits +from Crypto.Util.number import isPrime +from fractions import Fraction +from binascii import hexlify + +class GaussianRational: + def __init__(self, real: Fraction, imag: Fraction): + assert(type(real) == Fraction) + assert(type(imag) == Fraction) + self.real = real + self.imag = imag + + def conjugate(self): + return GaussianRational(self.real, self.imag * -1) + + def __add__(self, other): + return GaussianRational(self.real + other.real, self.imag + other.imag) + + def __sub__(self, other): + return GaussianRational(self.real - other.real, self.imag - other.imag) + + def __mul__(self, other): + return GaussianRational(self.real * other.real - self.imag * other.imag, self.real * other.imag + self.imag * other.real) + + def __truediv__(self, other): + divisor = (other.conjugate() * other).real + dividend = other.conjugate() * self + return GaussianRational(dividend.real / divisor, dividend.imag / divisor) + + # credit to https://stackoverflow.com/questions/54553489/how-to-calculate-a-modulo-of-complex-numbers + def __mod__(self, other): + x = self/other + y = GaussianRational(Fraction(round(x.real)), Fraction(round(x.imag))) + z = y*other + return self - z + + # note: does not work for negative exponents + # exponent is (non-negative) integer, modulus is a Gaussian rational + def __pow__(self, exponent, modulo): + shifted_exponent = exponent + powers = self + result = GaussianRational(Fraction(1), Fraction(0)) + while (shifted_exponent > 0): + if (shifted_exponent & 1 == 1): + result = (result * powers) % modulo + shifted_exponent >>= 1 + powers = (powers * powers) % modulo + return result + + def __eq__(self, other): + if type(other) != GaussianRational: return False + return self.imag == other.imag and self.real == other.real + + def __repr__(self): + return f"{self.real}\n+ {self.imag}i" + +# gets a Gaussian prime with real/imaginary component being n bits each +def get_gaussian_prime(nbits): + while True: + candidate_real = randbits(nbits-1) + (1 << nbits) + candidate_imag = randbits(nbits-1) + (1 << nbits) + if isPrime(candidate_real*candidate_real + candidate_imag*candidate_imag): + candidate = GaussianRational(Fraction(candidate_real), Fraction(candidate_imag)) + return candidate + +def generate_keys(nbits, e=65537): + p = get_gaussian_prime(nbits) + q = get_gaussian_prime(nbits) + N = p*q + p_norm = int(p.real*p.real + p.imag*p.imag) + q_norm = int(q.real*q.real + q.imag*q.imag) + tot = (p_norm - 1) * (q_norm - 1) + d = pow(e, -1, tot) + return ((N, e), (N, d), (p, q)) # (N, e) is public key, (N, d) is private key + +def encrypt(message, public_key): + (N, e) = public_key + return pow(message, e, N) + +def decrypt(message, private_key): + (N, d) = private_key + return pow(message, d, N) + +if __name__ == "__main__": + flag = None + with open("flag.txt", "r") as f: + flag = f.read() + (public_key, private_key, primes) = generate_keys(512) + (p, q) = primes + (N, e) = public_key + print(f"N = {N}") + print(f"e = {e}") + flag1 = flag[:len(flag) // 2].encode() + flag2 = flag[len(flag) // 2:].encode() + real = int(hexlify(flag1).decode(), 16) + imag = int(hexlify(flag2).decode(), 16) + message = GaussianRational(Fraction(real), Fraction(imag)) + print(f"original: ", message) + ciphertext = encrypt(message, public_key) + message = decrypt(ciphertext, private_key) + print(f"decrypt", message) + print(f"ciphertext = {ciphertext}") + print(f"\n-- THE FOLLOWING IS YOUR SECRET KEY. DO NOT SHOW THIS TO ANYONE ELSE --") + print(f"p = {p}") + print(f"q = {q}") diff --git a/assets/code/sdctf-2024/reallycomplexproblem/LEAK.png b/assets/code/sdctf-2024/reallycomplexproblem/LEAK.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1db0d93dd4161bd3f18624d28cec67faba16c4 GIT binary patch literal 598567 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