├── README.md ├── a_tale_of_two_keys.md ├── it_is_over_9000!.md ├── loki's_vault.md ├── lost_funds.md ├── operation_zk_rescue.md ├── safe_bn254.md ├── the_day_of_sagittarius_iv.md ├── the_lost_relic.md ├── the_power_of_integers.md ├── umculo.md ├── zk_rescue_part_2.md ├── zokclub.md └── zokclub_-_as_it_should've_been.md /README.md: -------------------------------------------------------------------------------- 1 | # ZK CTF - May '23 2 | 3 | ![lion-hero-square (1)](https://github.com/ingonyama-zk/zkctf-2023-writeups/assets/122266060/b6b3a95a-8046-42f5-a414-d72f9bacd613) 4 | 5 | 6 | 7 | ## Event 8 | For the full event details and summary click [here](https://medium.com/@ingonyama/recap-zk-capture-the-flag-cdf3ffef8186) 9 | 10 | Below you will find write-ups submitted by some of the participants 11 | 12 | ## Challenge Write-ups 13 | 14 | 1. [The Day of Sagittarius IV](the_day_of_sagittarius_iv.md) 15 | 2. [It is over 9000!](it_is_over_9000!.md) 16 | 3. [Safe bn254](safe_bn254.md) 17 | 4. [Zokclub](zokclub.md) 18 | 5. [Zokclub - as it should've been](zokclub_-_as_it_should've_been.md) 19 | 6. [A Tale of two keys](a_tale_of_two_keys.md) 20 | 7. [Loki's Vault](loki's_vault.md) 21 | 8. [Operation ZK Rescue](operation_zk_rescue.md) 22 | 9. [ZK Rescue Part 2](zk_rescue_part_2.md) 23 | 10. [Lost Funds](lost_funds.md) 24 | 11. [The Lost Relic](the_lost_relic.md) 25 | 12. [The Power of iNTEgers](the_power_of_integers.md) 26 | 13. [Umculo](umculo.md) 27 | 28 | 29 | ## Contributions 30 | 31 | Some challenges are still missing a proper write-up and for some there is more than one solution. Feel Free to add your write up to the list! 32 | 33 | 34 | ## Winners 35 | **1st place:** [ChainLight](https://twitter.com/chainlight_io) 36 | 37 | **2nd place:** rbtree fan club 38 | 39 | **3rd place:** LDGR 40 | 41 | **4th place:** baby step forward, giant step backward 42 | 43 | **5th place:** King of the Jungle 44 | 45 | **Write-up winners:** 46 | 47 | * Ayush from King of the Jungle - Full write-up [here](https://hackmd.io/@shuklaayush/SkWizdyBh) 48 | * Vladimir from [zkbob](https://twitter.com/zkBob_) - Full write-ups here: [Loki's Vault](https://hackmd.io/@mNhjpIg3TJ2CXuL_n4g55g/B1DwUA1r2) & [It is over 9000!](https://hackmd.io/XB0gc_8eTNOxq8d-CSU52Q?utm_source=preview-mode&utm_medium=rec) 49 | 50 | -------------------------------------------------------------------------------- /a_tale_of_two_keys.md: -------------------------------------------------------------------------------- 1 | # A Tale of Two Keys 2 | ## Challenge 3 | > Alice deployed a Groth16 based system, and to convince everyone her system is secure and the secrets used in the setup are not exposed, she thought of a clever way - she would publish a few different circuits that share the same secrets, that don't have a valid solution. In this way, the only way malicious prover could create proofs would be by using the exposed secret. she put a large bounty in one of these to incentivize hackers to look at it. 4 | > 5 | > See more details on Github: https://github.com/ingonyama-zk/TaleOfTwoKeys 6 | ## Comments 7 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 8 | 9 | (This challenge was updated during the CTF. I only solved the initial unmodified challenge.) 10 | 11 | In the initial version, the problem posed was to find the square root of 15 and 17. Given that 15 is a quadratic residue in the BLS12-377 scalar field, calculating the square root was trivial. Thus, by simply generating a valid proof for the square root of 15 and submitting it, I was able to get the flag. 12 | 13 | The revised challenge tweaked the parameters. The value 15 was replaced with 11, which is a non-residue. This turned the challenge into a seemingly more complex problem that I didn't have time to look into. 14 | -------------------------------------------------------------------------------- /it_is_over_9000!.md: -------------------------------------------------------------------------------- 1 | # It is over 9000! 2 | 3 | ## Challenge 4 | 5 | > The Saiyans have landed on planet earth. Our great defenders Krillin, Piccolo, Tien and Gohan have to hold on till Goku arrives on the scene. 6 | > 7 | > Vegeta and Nappa have scouters that indicate our heroes power levels and sadly we are not doing too well. 8 | > 9 | > Somehow, Gohan has raised his power level to `p_4(X) = 9000`, but it is not good enough. Piccolo `p_3(X)` can help but he is still regenerating, and Krillin `p_2(X)` and Tien `p_1(X)` are in bad shape. The total power of the team is computed as 10 | > ``` 11 | > P = p_1(X) * 0 + p_2(X) * 0 + p_3(X) * 0 + p_4(X) 12 | > ``` 13 | > At the current moment, the X is equal to `42`. 14 | > 15 | > Suddenly Gohan, and Piccolo recieve a message from Bulma that the scouters verify the sensed power level of individual enemies using KZG and for multiple enemies with batched KZG method. Vegeta knows for sure that the power level of Gohan is `p_4(X) = 9000`, so he will know if we change that. If only the team had a way to trick their opponents to believe that their total power level is `P > 9000` - then the enemies will surely flee. 16 | > 17 | > To run 18 | > ```bash 19 | > cargo run --release 20 | > ``` 21 | > Verification 22 | > Vegeta is running the scouters from `52.7.211.188:8000`. 23 | > 24 | > https://github.com/ingonyama-zk/ctf-over-9000 25 | 26 | ## Solution 27 | * By [Vladimir](https://github.com/ingonyama-zk/zkctf-2023-writeups/blob/main/it_is_over_9000!.md#by-vladimir) 28 | * By [Ayush Shukla](https://github.com/ingonyama-zk/zkctf-2023-writeups/blob/main/it_is_over_9000!.md#by-ayush-shukla) 29 | 30 | --- 31 | 32 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 33 | 34 | The key to solving this challenge is understanding how batching works in KZG commitments. Looking into the code reveals two important functions, `open_batch` and `verify_batch`. The crucial insight comes from realizing that the `upsilon` parameter has to be random. Otherwise, it can be used to make the verification ignore some of the polynomials. 35 | 36 | ```rust 37 | fn open_batch( 38 | &self, 39 | x: &FieldElement, 40 | ys: &[FieldElement], 41 | polynomials: &[Polynomial>], 42 | upsilon: &FieldElement, 43 | ) -> Self::Commitment { 44 | let acc_polynomial = polynomials 45 | .iter() 46 | .rev() 47 | .fold(Polynomial::zero(), |acc, polynomial| { 48 | acc * upsilon.to_owned() + polynomial 49 | }); 50 | 51 | let acc_y = ys 52 | .iter() 53 | .rev() 54 | .fold(FieldElement::zero(), |acc, y| acc * upsilon.to_owned() + y); 55 | 56 | self.open(x, &acc_y, &acc_polynomial) 57 | } 58 | 59 | fn verify_batch( 60 | &self, 61 | x: &FieldElement, 62 | ys: &[FieldElement], 63 | p_commitments: &[Self::Commitment], 64 | proof: &Self::Commitment, 65 | upsilon: &FieldElement, 66 | ) -> bool { 67 | let acc_commitment = 68 | p_commitments 69 | .iter() 70 | .rev() 71 | .fold(P::G1Point::neutral_element(), |acc, point| { 72 | acc.operate_with_self(upsilon.to_owned().representative()) 73 | .operate_with(point) 74 | }); 75 | 76 | let acc_y = ys 77 | .iter() 78 | .rev() 79 | .fold(FieldElement::zero(), |acc, y| acc * upsilon.to_owned() + y); 80 | self.verify(x, &acc_y, &acc_commitment, proof) 81 | } 82 | ``` 83 | 84 | In the code, `u` (`upsilon`) can be selected by the prover. By hardcoding it to `0` instead of a random value, only `p1` is checked. Now since `p4` isn't checked, you can keep the polynomial the same so that Vegeta can verify the commitment to it, and confirm Gohan's power level. But you can change the `y4` value, which would never be verified in the `batch_verify` function. As a result, the sum of power levels appears to be over 9000, fooling the villains. 85 | 86 | --- 87 | ### By [Vladimir](https://twitter.com/zkBob_) 88 | 89 | #### Notation 90 | * $G_1$, $G_2$ - multiplicative group generators on BLS12-381 91 | * $[x]_1$ is a point $x*G_1$ as opposed to $[x]_2$ which is $x*G_2$. It can be thought as encryption of $x$ that is additively homomorphic: $[x]_1$ + $[y]_1$ = $[x+y]_1$, but you can't get $x$ from $[x]_1$ or $[x]_2$, and you can't get $[x]_1$ from $[x]_2$ either 92 | 93 | #### Batched KZG protocol 94 | The prover is trying to assure the Verifier that a set of polynomials have certain evaluations at some point. Below we look into more details how batching is performed and how it can be broken. 95 | 96 | An individual proof for KZG10 is calculated as following: 97 | 98 | $$\pi_i = [\frac {p_i(\tau) - y_i}{\tau - X}]_1$$ 99 | 100 | where 101 | * $p_i$ is an individual polynomial in the set 102 | * $\tau$ is a secret point from $SRS$ 103 | * $X$ is a point where the evaluation is being proven 104 | * $y_i$ is an individual evaluation being proven 105 | 106 | The fragile part of this protocol lies of course in the batching mechanism. Batch proof is just a proof for a new polynomial that is composed from individual polynomials: 107 | 108 | $$P(x) = \sum_{i=0}^n \upsilon^{i-1} *p_i(x)$$ 109 | The evaluation is calculated correspondingly: 110 | $$Y = \sum_{i=0}^n \upsilon^{i-1}*y_i(X)\tag 1$$ 111 | and then the batch proof is 112 | $$\pi_{batch} = [\frac {P(\tau) - Y}{\tau - X}]_1$$ 113 | 114 | So the Prover sends the following: 115 | * batch proof $\pi_{batch}$ 116 | * individual commitments $p(\tau)$ 117 | * individual evaluations $y_i$ 118 | * challenge $\upsilon$ 119 | 120 | And the verifier checks that 121 | $$ e(\pi_{batch}, [\tau-X]_2) = e([P(\tau)-Y]_1,[1]_2)\tag {2}$$ 122 | 123 | The challenge requires us to change an individual component $y_3$ with $y_3'=y_3+\delta$ where $\delta$ is some positive number and still make batch proof pass the check $(2)$ 124 | 125 | The suggested protocol could be fine if only it is interactive whereas $\upsilon$ is provided by the verifier or there is a some random oracle to provide it. It could also be fixed by implementing Fiat-Shamir heuristic in a proper way, where all the public inputs are included in the hash calculation. 126 | For more details on the topic you can see [zk hack challenge](https://zkhack.dev/events/puzzle5.html) and [plonk vulnerability description](https://blog.trailofbits.com/2022/04/18/the-frozen-heart-vulnerability-in-plonk/) 127 | 128 | Since in this particular case $\upsilon$ is chosen arbitrarily by the Prover, an easy way to cheat is to change both $y_3$ to $y_3'$ and $\upsilon$ to $\upsilon'$ for the same proof before sending both of them to the Verifier. 129 | In order to preserve consistency we have to make $Y$ the same: 130 | 131 | $$Y = \upsilon^3*y_3+\upsilon^2*y_2 + \upsilon*y_1 + y_0 = \upsilon'^3*(y_3+1)+\upsilon'^2*y_2 + \upsilon'*y_1 + y_0$$ 132 | $$\upsilon^3*y_3+\upsilon^2*y_2 + \upsilon*y_1 = \upsilon'^3*(y_3+1)+\upsilon'^2*y_2 + \upsilon'*y_1$$ 133 | 134 | In general this is a cubic equation with respect to $\upsilon'$, but since we can set $\upsilon$ to whatever we want, we can just use $0$ for both of them and that would make the equation hold. So the first way is to find such a $\upsilon$ for any desired $y'$ so that the proof is the same 135 | 136 | Alternatively we could leave $\upsilon$ as it is and mess with individual $y_i$ so that the sum is the same, but $y_3$ is changed to $y_3+1$. 137 | 138 | $$Y' = \upsilon^3*(y_3+1)+\upsilon^2*y_2 + \upsilon*y_1 + y_0 = \upsilon^3*y_3+\upsilon^3+\upsilon^2*y_2 + \upsilon*y_1 + y_0 = Y + \upsilon^3$$ 139 | 140 | So the only thing that we need to do is to make one of other polynomials contribution to evaluation to be equal exactly $\upsilon^3$ and than exclude it from the list of evaluatiions by zeroing it. 141 | It doesn't matter which other polynomial we use, for example we could take $y_2$ and set it's only coefficient to be $\upsilon$. 142 | $\upsilon^3*y_3+\upsilon^2*y_2 = \upsilon^3*y_3+\upsilon^2*\upsilon = \upsilon^3*(y_3+1) + 0 = \upsilon^3*(y_3+1) + y_2'$ 143 | 144 | So that gives us exactly what we wanter if we pass a fake $y_2$ evaluation $y_2'=0$ 145 | We could do the same with $y_1$ or $y_0$ changing a constant coefficient to $\upsilon^2$ and $\upsilon^3$ respectively 146 | #### Code 147 | 148 | The solution for $\upsilon$ trivial , we just set it to zero 149 | ```rust 150 | let u = FieldElement::zero(); 151 | ``` 152 | and now we can create a new set of evaluations, that includes a fake value 153 | ```rust 154 | let y4_fake = y4.clone() + FieldElement::one(); 155 | let ys_fake = [y1.clone(), y2.clone(), y3.clone(), y4_fake.clone()]; 156 | let total_power = u32::from_str_radix(&ys_fake[3].to_string()[2..], 16).unwrap(); 157 | println!("total power: {}", total_power); 158 | assert!(total_power > 9000); 159 | assert!(kzg.verify_batch(&x, &ys_fake, &ps_c, &proof, &FieldElement::zero())); 160 | ``` 161 | Now we have Gohan power 9001 that still statisfies batch proof check 162 | 163 | For alternative solution we change coefficient for an individual polynomial: 164 | ```rust! 165 | // Sample random u 166 | let u = FieldElement::from(rand::random::()); 167 | let p1_coeffs = [u.pow(3 as u32)]; 168 | ``` 169 | This is the case for $y_0$ (indexes in code start from 1 for some reason, so it corresponds to `y1` variable and `ys[0]` element in evaluations array). Of course for $y_1$ and $y_2$ everything is the same 170 | After that you remove $y_1$ contribution to the batch, so that $y_3$ would take all the credit 171 | ```rust 172 | let ys_fake = [FieldElement::zero(), y2, y3, y4 + FieldElement::one()]; 173 | ``` 174 | And you're good to go! 175 | 176 | ```rust 177 | let total_power = u32::from_str_radix(&ys_fake[3].to_string()[2..], 16).unwrap(); 178 | println!("total power: {}", total_power); 179 | assert!(total_power > 9000); 180 | assert!(kzg.verify_batch(&x, &ys_fake, &ps_c, &proof, &u)); 181 | ``` 182 | You will see that $y_3$ (`y4` variable, `y[3]` element in evaluations ) is greater than 9000 and the batch proof is again successfully checked. 183 | 184 | #### Overview 185 | 186 | This challenge was comparatively easy if you get familiar with KZG. From the very first sight batch proof seems suspicious. Furthermore vulnerabilities that are caused by bad Fiat-Shamir are very common, so this should be one of the first things to check. 187 | To fix this protocol we would have to either check individual proofs, which defeats the purpose of batching, or we need to make sure, that the Prover can't mess with the challenge, because otherwise everything can be just solved backwards for any specific result the Prover chooses. 188 | 189 | -------------------------------------------------------------------------------- /loki's_vault.md: -------------------------------------------------------------------------------- 1 | # Loki's Vault 2 | 3 | ## Challenge 4 | > After years of careful investigation, you have reached the gate to Loki's vault in the icy mountains of Norway, where it is said that many great treasures and powerful weapons are hidden. The gate seems unbreakable, but you spot some ancient machinery with inscriptions in old runes. 5 | > 6 | > Read more: https://github.com/ingonyama-zk/breaking_into_vault_of_loki 7 | 8 | ## Solution 9 | * By [Vladimir](https://github.com/ingonyama-zk/zkctf-2023-writeups/blob/main/loki's_vault.md#by-vladimir) 10 | * By [Ayush Shukla](https://github.com/ingonyama-zk/zkctf-2023-writeups/blob/main/loki's_vault.md#by-ayush-shukla) 11 | 12 | --- 13 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 14 | 15 | This challenge was one of the toughest and needed a good understanding of how KZG commitments work. We are provided with the following polynomial 16 | 17 | $$ 18 | \begin{aligned} 19 | p(x) &= 69 +78x + 32x^2 + 65x^3 + 82x^4 + 71x^5 + 69x^6 + 78x^7 + 84x^8 + 73x^9 \newline &+78x^{10} + 65x^{11} + 32x^{12} + 78x^{13} + 65x^{14}+ 67x^{15} + 73x^{16} + 32x^{17} \newline 20 | &+ 84x^{18} + 73x^{19} + 69x^{20} + 82x^{21} + 82x^{22} + 65 x^{23} 21 | \end{aligned} 22 | $$ 23 | 24 | Our objective is to generate a KZG proof that the polynomial equals $3$ at $x = 1$ within the BLS12-381 scalar field. 25 | 26 | Given that the polynomial's value is clearly not $3$ at $x = 1$, it is clear that forging a proof is our only option. So I started looking into KZG commitments in detail. [Dankrad's article](https://dankradfeist.de/ethereum/2020/06/16/kate-polynomial-commitments.html) was a great resource in understanding how KZG commitments work. Essentially, a person can create a fraudulent proof if they know the secret "toxic waste" $s$ from the trusted setup. I looked into how trusted setups work by reading [Vitalik's article](https://vitalik.ca/general/2022/03/14/trustedsetup.html). The SRS string derived from the trusted setup has the following form: 27 | 28 | $$ 29 | [G_1, G_1 * s, G_1 * s^2 ... G_1 * s^{n_1-1}]\\ 30 | [G_2, G_2 * s, G_2 * s^2 ... G_2 * s^{n_2-1}]\\ 31 | $$ 32 | 33 | The given SRS had $n_1 = 1024$ and $n_2 = 2$ with $G_1$ and $G_2$ representing the generators of the main and secondary group of the elliptic curve. 34 | 35 | Analyzing the powers in the main group, I realized that they were repeating with a frequency of 64 i.e. $s^64 = 1\mod p$. This implied that $s$ is a 64th root of unity. To determine $s$, I wrote a simple Sage script to calculate the 64 roots of unity and find the correct one: 36 | 37 | ```python 38 | from sage.all import * 39 | 40 | p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab 41 | K = GF(p) 42 | a = K(0x00) 43 | b = K(0x04) 44 | E = EllipticCurve(K, (a, b)) 45 | G = E(0x17F1D3A73197D7942695638C4FA9AC0FC3688C4F9774B905A14E3A3F171BAC586C55E83FF97A1AEFFB3AF00ADB22C6BB, 0x08B3F481E3AAA0F1A09E30ED741D8AE4FCF5E095D5D00AF600DB18CB2C04B3EDD03CC744A2888AE40CAA232946C5E7E1) 46 | E.set_order(0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001 * 0x396C8C005555E1568C00AAAB0000AAAB) 47 | 48 | sG = E(0xb45e08705bc9f96ddef642f24f7e6d326c5e450aefb21363fd8c6788591afca990680a8f862e8d43609430f54aca45f, 0x63e8c7cd26cee9463932fd15ddaac016f42d598bd1abedfdfc37bfeb9f326cd80e36ab003b5b4a79bb25c5695e291b) 49 | 50 | q = G.order() 51 | L = GF(q) 52 | g = L.multiplicative_generator() 53 | 54 | n = 64 55 | s = 0 56 | for i in range(n): 57 | s = pow(g, i * (q - 1) // n) 58 | if s*G == sG: 59 | print(f"Found s: {s}") 60 | break 61 | ``` 62 | 63 | Once $s$ is known, we can construct a fake proof using the formula: 64 | 65 | $$ 66 | \pi_{fake} = \frac{1}{s - y} (C - yG) 67 | $$ 68 | 69 | Here, $s$ is the secret, $C$ is the commitment, and $y$ is the intended fake value for which the proof is to be generated. 70 | 71 | Below is another Sage script to compute this: 72 | 73 | ```python 74 | x = 1 75 | ynew = 3 76 | 77 | k = pow(L(s - x), -1) 78 | C = E(0x1167e707d11074bef0ee02040d38c06e32d829341246af1ba03572a61a3d2052d687d5ebb5de356ff089006e6318bb8b, 0x1537d55fffdd6d31d1b831bb8ac2e24142084f122c830cddc117d31505d49becd3854df61ce8b7f7ca14aa8f3a0eb0c) 79 | 80 | proof = k*(C - ynew*G) 81 | print([hex(z) for z in proof]) 82 | ``` 83 | 84 | As stated in the problem description, the $x$ coordinate of the fake proof is the solution. 85 | 86 | --- 87 | ### By [Vladimir](https://twitter.com/zkBob_) 88 | 89 | 90 | #### Notation 91 | * $G_1$, $G_2$ - multiplicative group generators on BLS12-381 92 | * $[x]_1$ is a point $x*G_1$ as opposed to $[x]_2$ which is $x*G_2$. It can be thought as encryption of $x$ that is additively homomorphic: $[x]_1$ + $[y]_1$ = $[x+y]_1$, but you can't get $x$ from $[x]_1$ or $[x]_2$, and you can't get $[x]_1$ from $[x]_2$ either 93 | #### KZG recap 94 | 95 | The idea behind KZG polynomial commitment scheme is to provide an ability for the prover to assure verifier that a certain operation (which could have been agreed upon beforehand and expressed as a polynomial) has been applied to some input. This is a very useful thing for protocols where computational integrity is important because you can encode pretty much any computational instructions as a polynomial. We can think of it as a hash function for some lambda function that captures how it affects the input parameters and therefore it's result can be effectively checked by the verifier without having to redo everything. 96 | 97 | **The statement to be proved:** an $n$-degree polynomial $p(x)=\sum_{i=0}^n c_i*x^i$ evaluates to $y$ at a field element $X$ 98 | 1. Setup $\tau \leftarrow random, SRS= \{[\tau^i]_1\}, [\tau]_2$. It's very important that $\tau$ is selected at random, we'll see later why. . The powers of $\tau$ must be generated using a trusted setup ceremony which is a complicated topic that we skip. The most important is that neither Prover, nor Verifier can find $\tau$ 99 | 2. Prover commits to $p$ by evaluaing it at $\tau$ using powers of $\tau$ from $SRS$ : $C=[p(\tau)]_1$ = $sum_i^n c_i*[\tau^i]_1$ 100 | 3. Prover evaluates $p$ at $x$, generates a proof $\pi$ that $p(x)=y$. Let's look into more details for this step, since it's important to understand how we can break it. The statement $p(X)=y$ means that a polynomial $p(x)-y$ must have a root at $X$ so it can be formulated as following: 101 | $p(x)$ evaluates to $y$ at $X$ if 102 | * $x-X$ divides $p(x) - y$ without a remainder 103 | * there exists such h(x) so that $p(x)-y=h(x)*(x-X)$ 104 | 105 | This property of polynomial $p(x) - y$ must hold everywhere, including a secret point $\tau$, so the prover calculates $h(x)$ in polynomial form 106 | $$ h(x) = \frac{p(x)-y}{x-X}$$ 107 | and then provides commitment to evaluation at $\tau$ using $SRS$ 108 | So the proof is $\pi = [h(\tau)]_1$ 109 | It also could have been expressed in a scalar form 110 | $$\pi = [\frac {p(\tau) - y}{\tau - X}]_1$$ 111 | But the prover doesn't have $\tau-X$ so she can't calculate it directly. 112 | The prover passes $\pi$ to the Verifier 113 | 114 | 4. Verifier calculates $[\tau-X]_2$ using SRS, checks if $e( -[y]_1 + C,[1]_2) = e(\pi, [\tau-X]_2)$ where $e$ is a pairing on BLS12-381 which can be thought as multiplication of encrypted values. More on this topic here in the layman terms [here](https://hackmd.io/@benjaminion/bls12-381#Pairings). if equality holds, she accepts, rejects otherwise 115 | 116 | ##### Overview 117 | As you have seen the protocol itself is pretty elegant and not that complicated, at least if we treat pairings as a black box. KZG is not the only solution for computational integrity, there are also other schemes based on FRI, Inner Product Argument, but KZG has some distinct and very usefull features such as 118 | 1. It is binding, which means that the polynomial at open phase must be the same as previously committed (if the setup is made correctly) 119 | 2. It is hiding, which means that verifier doesn't find out anything about $p$ because Prover gives only $C$ which is a commitment to evaluation of $p$ 120 | 3. Proof size is constant and consists of a single elliptic curve point 121 | 4. Verification time is constant and requires two pairing operations 122 | 5. We can effectively batch multiple proofs for a single polynomial 123 | 124 | 125 | #### Forging a proof 126 | We need to forge a proof for a false statement that for $X=1, p(1)=y'=3$. Since it's not true and in fact $p(1)=1564$, then $p(x)-3$ doesn't have a root at $1$, and therefore $(x - 1)$ doesn't divide $p(x) - 3$. If provided with an honest proof the pairings check would show that $\tau-1$ doesn't divide $p(\tau)-3$ without remainder and the statement is false. 127 | 128 | In order to cheat we need to find some other proof $\pi'= h'(\tau)$ such it satisfies the divisibility check just at $\tau$ even if it doesn't hold anywhere else 129 | $e( -[y']_1 + C,[1]_2) = e(\pi', [\tau-X]_2)$ 130 | Or with substituted values 131 | $e( [C-3]_1,[1]_2) == e(\pi', [\tau-1]_2)$ 132 | That would fool the verifier if the prover have used completely different polynomial to get the fake value $3$. 133 | 134 | We need to find $h'$ for $C - 3 = h'(\tau)(\tau-1)$. But the only thing that we got from honest proof generation is that $C -1564=h(\tau)(\tau-1)$. So we can try to express the difference in the LHS as a multiple of $\tau-1$ 135 | $C -1564= C-3-1561 = h(\tau)*(\tau-1)$ 136 | Moving $1561$ to the RHS gives: 137 | $C-3 =h(\tau)*(\tau-1)+1561$ 138 | If only we could express 1561 as some $R(\tau)$,which is a multiple of $\tau -1$: $R(\tau) = (\tau-1)*r(\tau)$ 139 | That would give us that RHS would be divisible by $\tau-1$ in the following: 140 | $$C - 3 = h(\tau)*(\tau-1) + R(\tau) = (h(\tau)+r(\tau))*(\tau-1)\tag{1} = h'(\tau)*(\tau-1)$$ 141 | Then the proof then would be $\pi'=[h'(\tau)]_1=[h(\tau)]_1+[r(\tau)]_1 \tag{2}$ 142 | The proof verification check would be give followng: 143 | $$e( C - [3]_1,[1]_2) == e([h(\tau)+r(\tau)]_1, [\tau-1]_2) = e(\pi', [\tau-1]_2)$$ 144 | which would hold since $(1)$ holds, because it's the same statement but that is checked using pairings. 145 | 146 | How do we find to find $r(\tau)$? 147 | 148 | Normaly we would need to somehow extract $\tau$ from $SRS$ for this which is infeasible due to [q-SDH assumption](https://ai.stanford.edu/~xb/eurocrypt04a/bbsigs.pdf). But in this case $\tau$ was chosen from a set of 64th roots of unity of the base field which means that $\tau^{64}=1$ and what is more useful $\tau^{32}=-1$. Substracting $1$ from both sides gives us that $\tau^{32}-1=-2$. 149 | 150 | Again if $\tau$ would have been chosen correctly we wouldn't be able to do this, since SRS only gives us monomials in the form of $\{[τ^i]_1\} = \{G_1*\tau^i\}$, so there wouldn't be any way to get rid of $G_1$. 151 | It is the verifier who performs multiplication in the RHS when verifying proof, and the prover is unable to effectively find any value, that would give LHS when multiplied by $\tau -1$. But in this particular case we're able to express the difference between true and fake evaluation 152 | $y-y'=1561=-\frac{1561}{2}*(\tau^{32}-1)$ 153 | Then we use [factorization of differences of power](https://proofwiki.org/wiki/Difference_of_Two_Powers) to factor out the $\tau-1$: 154 | 155 | $(\tau^{32}-1)=(\tau-1) * \sum_{i=0}^{31}\tau^i$ 156 | 157 | Now we are ready to express the difference between true and fake evaluations as a multiple of $\tau-1$ 158 | $y-y'=1561=-\frac{1561}{2}*(\tau^{32}-1)=(\tau-1)*\frac{-1561}{2}*(\sum_{i=0}^{31}\tau^i)$ 159 | 160 | Which gives us that $r(\tau) = \frac{-1561}{2}*(\sum_{i=0}^{31}\tau^i)$ 161 | 162 | Substituting $r$ to $(1)$ to find the fake proof gives us 163 | $$\pi'=[h'(\tau)]_1=\pi+[\frac{-1561}{2}*(\sum_{i=0}^{31}\tau^i)]_1$$ 164 | which can be calculated using $SRS$ 165 | 166 | #### The code 167 | 168 | We're proving evaluation at this point 169 | ```rust 170 | let x = FieldElement::one(); 171 | ``` 172 | 173 | This is real evaluation 174 | ```rust 175 | let y_true = challenge_polynomial().evaluate(&x); 176 | ``` 177 | This is the fake evaluation that we're trying to prove using the fake proof 178 | ```rust 179 | let y_fake = FieldElement::from(3); 180 | ``` 181 | This is commitment to p equal to evaluation at tau 182 | ```rust 183 | let p_commitment: G1Point = kzg.commit(&p); 184 | ``` 185 | This is prime group generator, we use it just to check that setup is broken 186 | ```rust 187 | let g1 = BLS12381Curve::generator(); 188 | ``` 189 | The code below would print the following lines, which proves that ideed $\tau$ is the 64th prime root of unity, which means that every $64*n$ power gives 1, and every $32+64*n$ power gives -1: 190 | ```rust 191 | for (pow, tau_pow) in srs.powers_main_group.clone().into_iter().enumerate() { 192 | if tau_pow == g1 { 193 | println!("tau^{:?}=1", pow); 194 | } else if tau_pow == g1.neg() { 195 | println!("tau^{:?}=-1",pow); 196 | } 197 | } 198 | ``` 199 | ``` 200 | tau^0=1 201 | tau^32=-1 202 | tau^64=1 203 | tau^96=-1 204 | tau^128=1 205 | tau^160=-1 206 | ``` 207 | 208 | 209 | this is scaling factor that allows to express 1561 as multiple of $\tau-1$ using the fact that $\tau^{32}-1 = -2$ 210 | ```rust 211 | let scaling_factor = ((y_true.clone() + y_fake.clone().neg()) * FieldElement::from(2).inv()).neg(); 212 | ``` 213 | We calculate $[\sum_{i=0}^{31}\tau^i]_1$ using $SRS$: 214 | ```rust 215 | let mut tau_sum: ShortWeierstrassProjectivePoint = 216 | srs.powers_main_group.get(0).unwrap().clone(); 217 | 218 | for pow in 1..32 { 219 | let tau_pow: ShortWeierstrassProjectivePoint = 220 | srs.powers_main_group.get(pow).unwrap().clone(); 221 | tau_sum = tau_sum.operate_with(&tau_pow) 222 | } 223 | ``` 224 | Next we get $[r(\tau)]_1 = \frac{-1561}{2}*(\sum_{i=0}^{31}\tau^i)$: 225 | ```rust 226 | let r = tau_sum.operate_with_self(scaling_factor.representative()); 227 | ``` 228 | 229 | This is a true proof for value 1564 $\pi = [h(\tau)]_1$ 230 | ```rust 231 | let pi = kzg.open(&x, &y_true, &p); 232 | ``` 233 | Finaly this is $\pi' = \pi+[r(\tau)]_1$ 234 | 235 | ```rust 236 | let fake_proof = r.operate_with(&h); 237 | ``` 238 | 239 | -------------------------------------------------------------------------------- /lost_funds.md: -------------------------------------------------------------------------------- 1 | # Lost funds 2 | 3 | ## Challenge 4 | 5 | > So last night I was going to transfer some crypto to a friend of mine. But when I copy-pasted his address - it was replaced by the different one, which i didn't notice till it was too late! Seems like I had some kind of malware installed on my computer... Anyway, can you track down where my funds went? I heard it's possible in blockchains. https://goerli.etherscan.io/tx/0xf681ca8c2999a11dc41983657af5afd164c56322925227b88f5693f1de8130f7 is the transaction link (whatever that is) 6 | 7 | ## Solution 8 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 9 | 10 | Following the trail of transactions on Goerli etherscan, we see that the culprit bridged the funds to the zkSync Era testnet. They then proceeded to transfer them repeatedly from one wallet to another on ZkSync. In order to trace the funds, I wrote a small Python script. 11 | 12 | ```python 13 | # Part 1 14 | block = 0 15 | address = "0xa2041c55902585ca3295f034f18d9000ad07738d" 16 | while True: 17 | print(address) 18 | txs = get_txs(address) 19 | 20 | for tx in txs: 21 | if int(tx["blockNumber"]) < block: 22 | continue 23 | if "transfer" not in tx: 24 | continue 25 | if ( 26 | tx["transfer"]["tokenInfo"]["address"] 27 | != "0x0000000000000000000000000000000000000000" 28 | ): 29 | continue 30 | if tx["transfer"]["from"] != address: 31 | continue 32 | if tx["transfer"]["to"] == address: 33 | continue 34 | 35 | block = int(tx["blockNumber"]) 36 | address = tx["transfer"]["to"] 37 | break 38 | else: 39 | break 40 | print(address) 41 | # 0xd7120a6b038ad942a2966c2769451d21d608006f 42 | ``` 43 | 44 | The funds lead to [this address](https://goerli.explorer.zksync.io/address/0xD7120a6B038aD942A2966c2769451d21d608006f) and are then transferred back to Goerli. After a couple of hops, they are sent back to the zkSync Era testnet and end up at this [final address](https://goerli.explorer.zksync.io/address/0x475e3f18be51a970a3079dff0774b96da9d22dbe), where they are used up on gas across multiple transactions. Weird. 45 | 46 | I wrote a script to decode all these transactions. Cross-referencing the function signatures with similar functions on a contract database, I found out that these belong to contracts for an on-chain casino hosted at [zkasino.io](zkasino.io). 47 | 48 | ```python 49 | # Part 2 50 | address = "0x475E3f18Be51A970a3079Dff0774B96Da9d22dbE" 51 | txs = get_txs(address) 52 | 53 | for tx in txs: 54 | result = subprocess.run( 55 | ["cast", "4byte-decode", tx["data"]["calldata"]], capture_output=True, text=True 56 | ) 57 | print(result.stdout) 58 | ``` 59 | 60 | ![transactions](https://github.com/shuklaayush/ingonyama-ctf-solutions/assets/27727946/a589a52e-4a95-4143-acd5-be240fecc2dd) 61 | 62 | I found the [profile](https://play.zkasino.io/profile?u=0x475e3f18be51a970a3079dff0774b96da9d22dbe) corresponding to the exploiter's address on zKasino. It had the this badge: 63 | 64 | ![crypt0cr1m1nal zkasino](https://github.com/shuklaayush/ingonyama-ctf-solutions/assets/27727946/c44191d3-7139-4f00-830f-36266fe63ed9) 65 | 66 | The username piqued my curiosity but at this point, I felt as though I'd hit a dead-end. After a while, I went back to this challenge again and searched for the username on various search engines. This led me to a [Twitter profile](https://twitter.com/crypt0cr1m1nal) with this tweet. 67 | 68 | ![crypt0cr1m1nal twitter](https://github.com/shuklaayush/ingonyama-ctf-solutions/assets/27727946/ac808f3c-9162-4ad6-a763-fad2f4d61853) 69 | 70 | Hurray, we found the flag! 71 | -------------------------------------------------------------------------------- /operation_zk_rescue.md: -------------------------------------------------------------------------------- 1 | # Operation ZK rescue 2 | 3 | ## Challenge 4 | > Message from HQ: High priority 5 | > 6 | > **Agent Zulu has gone M.I.A.** 7 | > 8 | > We have received reports that he has been kidnapped by the notorious Woe Jinx. Direct intervention without evidence is not an option. Our friendly enemy The Concierge of crime: Red, has managed to get one of his associates (The forger) infiltrate into Jinx's organization, who will be your point contact. 9 | > 10 | > Your task is to send us a confirmation message that indeed Zulu is inside Jinx's base so we can rescue our man quietly. 11 | > 12 | > Go to the Github Repository to get the full details on this challenge. 13 | > 14 | > https://github.com/ingonyama-zk/operation_zk_rescue 15 | > 16 | 17 | After executing the challenge using `cargo run`, you get an interactive story-telling prompt: 18 | 19 | ``` 20 | Agent Zulu has gone M.I.A. 21 | 22 | We have received reports that he has been kidnapped by the notorious Woe Jinx. 23 | Direct intervention without evidence is not an option. 24 | Our friendly enemy The Concierge of crime: Red, has managed to get one of his associates 25 | (The forger) infiltrate into Jinx's organization, who will be your point contact. 26 | 27 | Your task is to send us a confirmation message that indeed Zulu is inside Jinx's base so we 28 | can rescue our man quietly. 29 | 30 | Jinx uses a sumcheck protocol that validates the sender's identity in the base, 31 | when the sumcheck evaluates to zero. 32 | 33 | The Forger has forged an identity for you in order to faciliate a one time message. 34 | However, we ran some tests and found that it may not pass the validation. 35 | We have no idea what game Red and the forger are playing here. 36 | 37 | We do know that Woe Jinx protects his men from HQ by anonymizing the validation process, this basically 38 | adds a random polynomial to the claimed polynomial. This is usually a real pain in the butt. 39 | But, perhaps the anonymization can be used to your advantage this time. Just watch out that Jinx double checks the anonymization, 40 | so if you use a constant polynomial for anonymization, you will get caught! 41 | 42 | Once you have cleared the validation, we will use a security lapse window to activate recieving a one time message from you. 43 | We have been told by Red that you will have to eventually find some of the information you need on your own. 44 | U have got Big intELLIGENCE, be YOURSELF! We are expecting your message in 8 in the futURE. 45 | 46 | Note that if Jinx learns the message during the validation, the probability you will live is pretty low. 47 | 48 | One more thing, Red and the Forger cannot be trusted, there is always more to what meets the eye! 49 | Watch out! Good luck!! - HQ 50 | ``` 51 | 52 | ## Solution 53 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 54 | 55 | I wasted a lot of time analyzing the weird capitalization in one of the sentences. This turned out to be a red-herring and actually, none of the prompt is important. 56 | 57 | If you convert the polynomial evaluations in the code into ASCII, you get the following list: 58 | ``` 59 | INGONYAMA_THEö 60 | #@Åç" 61 | PINOCCHIO 62 | TinyRAM 63 | GROTH16 64 | BULLETPROOFS 65 | STARK 66 | SONIC 67 | PLONK 68 | TURBOPLONK 69 | SPARTAN 70 | HALO 71 | AURORA 72 | MARLIN 73 | FRACTAL 74 | LUNAR 75 | VIRGO 76 | SUPERSONIC 77 | PLOOKUP 78 | BRAKEDOWN 79 | NOVA 80 | PLONKY2 81 | HALO2 82 | GEMINI 83 | CAULK 84 | CAULK+ 85 | ORION 86 | FLOOKUP 87 | HYPERPLONK 88 | BALOO 89 | CQ 90 | SUPERNOVA 91 | CQlin 92 | ``` 93 | 94 | Apart from the first one, the others are all names of modern ZK proving systems. The first entry looks like a corrupted flag and gives away a hint that the flag is related to the evaluations somehow. 95 | 96 | Looking into the sum-check code, I realized that this is exactly the [ZK-hack puzzle](https://gist.github.com/shuklaayush/b92e6b53b0ff8571c0e73d42b504f7e3) that I did a few months ago. It's a sum-check protocol where the masking is improper. We can select any masking polynomial whose sum over the domain is 0, and the check will pass. If we calculate this sum over domain and turn that number into ASCII, we get the flag `INGONYAMA_THE_LION_INSIDE`. Upon entering this flag when the code prompts, the code calculates its hash and confirms that this is indeed the solution. 97 | -------------------------------------------------------------------------------- /safe_bn254.md: -------------------------------------------------------------------------------- 1 | # Safe bn254 2 | 3 | ## Challenge 4 | 5 | > doesn’t bn254 look safer now? 6 | > 7 | > y^2=x^3+2023 if you are not afraid of generating curves and discrete logarithms, you could try looking for a flag in x, where res = x * curve_gen 8 | > 9 | > The generators are given by (in affine coordinates) 10 | > 11 | > curve_gen_x=14810849444223915365675197147935386463496555902363368947484943637353816116538 curve_gen_y=742647408428947575362456675910688304313089065515277648070767281175728054553 12 | > 13 | > The result coordinates are res_x=5547094896230060345977898543873469282119259956812769264843946971664050560756 res_y=14961832535963026880436662513768132861653428490706468784706450723166120307238 14 | > 15 | > you can use any language for finding the solution and convert the flag into text format 16 | > 17 | > The prime modulus in GF(p) p=21888242871839275222246405745257275088696311157297823662689037894645226208583 18 | 19 | ## Solution 20 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 21 | 22 | We've been provided with a modified BN-254 elliptic curve, and our task is to calculate a discrete logarithm on this curve (ECDLP). 23 | 24 | First, I set up the problem in [Sagemath](https://www.sagemath.org/): 25 | 26 | 27 | 28 | ```python 29 | import math 30 | from sage.all import * 31 | 32 | # BN-254 prime 33 | p = 21888242871839275222246405745257275088696311157297823662689037894645226208583 34 | 35 | # Generator 36 | Gx = 14810849444223915365675197147935386463496555902363368947484943637353816116538 37 | Gy = 742647408428947575362456675910688304313089065515277648070767281175728054553 38 | 39 | # P = kG 40 | Px = 5547094896230060345977898543873469282119259956812769264843946971664050560756 41 | Py = 14961832535963026880436662513768132861653428490706468784706450723166120307238 42 | 43 | F = GF(p) 44 | E = EllipticCurve(F, [0, 2023]) 45 | 46 | print(E) 47 | ``` 48 | 49 | ```bash 50 | Elliptic Curve defined by y^2 = x^3 + 2023 over Finite Field of size 21888242871839275222246405745257275088696311157297823662689037894645226208583 51 | ``` 52 | 53 | The ECDLP problem, generally regarded as computationally infeasible, hinges on the order of the subgroup of the elliptic curve. The curve should have a large prime order subgroup, as operations are conducted in this group. If the subgroup's order is small, the [Pohlig-Hellman algorithm](https://en.wikipedia.org/wiki/Pohlig%E2%80%93Hellman_algorithm) can be used to fragment the problem into smaller, solvable parts. These parts can then be pieced back together using the Chinese Remainder Theorem (CRT) to arrive at the final solution. 54 | 55 | To check this, I factorized the order of the subgroup containing $G$: 56 | 57 | ```python 58 | G = E(Gx, Gy) 59 | order = G.order() 60 | factors = factor(order) 61 | print(f"Order of G = {order}") 62 | print(f" = {factors}") 63 | 64 | P = E(Px, Py) 65 | ``` 66 | ```bash 67 | Order of G = 2203960485148121921418603742825762020959382274778627105322 68 | = 2 * 3^2 * 13 * 19 * 29 * 37 * 613 * 983 * 11003 * 346501 * 6248149 * 405928799 * 79287328374952431757 69 | ``` 70 | 71 | The largest prime in the prime factorization of the order is huge, making it computationally infeasible to solve. Therefore, I ignored the largest prime while calculating the discrete log. This gives us a solution modulo the remaining factors. 72 | 73 | ```python 74 | dlogs = [] 75 | moduli = [p ** e for p, e in factors] 76 | # Ignore largest factor to make problem computationally feasible 77 | for m in moduli[:-1]: 78 | t = order // m 79 | dlog = discrete_log(t * P, t * G, operation="+") 80 | dlogs.append(dlog) 81 | 82 | k = crt(dlogs, moduli[:-1]) 83 | print(f"Secret k: {k} mod {math.prod(moduli[:-1])}") 84 | 85 | flag = bytearray.fromhex(hex(k)[2:]).decode("utf-8") 86 | print(f"flag: {flag}") 87 | ``` 88 | 89 | Decoding the secret reveals that it conforms to the flag format: 90 | 91 | ```bash 92 | Secret k: 381276930664415168886989783656063357 mod 27797133921898830561267529521791838546 93 | flag: IngoCTF{b67e8a} 94 | ``` 95 | 96 | So, we've successfully found the flag. 97 | -------------------------------------------------------------------------------- /the_day_of_sagittarius_iv.md: -------------------------------------------------------------------------------- 1 | # The Day of Sagittarius IV 2 | 3 | ## Challenge 4 | > We are excited to announce the release of the latest version of the retro terminal game "The Day of Sagittarius IV". This new version is designed to be played peer to peer and does not require any servers to play. Instead, the game data is stored on the players' computers, creating a truly decentralized gaming experience. 5 | > 6 | > To ensure that players adhere to the rules of the game, the new version utilizes a zero-knowledge virtual machine. This feature allows players to verify that their opponents are playing fairly without revealing any confidential information. 7 | > 8 | > We are currently in the beta-testing phase, and we invite all gaming enthusiasts to try out the new version of "The Day of Sagittarius IV". 9 | > 10 | > As part of the beta-launch, we are also excited to announce a special promotional event. Players who win a bot in the game on 52.7.211.188:6000 will be eligible for a prize. 11 | > 12 | > We believe that the new peer-to-peer version of "The Day of Sagittarius IV" will revolutionize the world of retro gaming, and we are thrilled to share it with you. Join us in this exciting new adventure, and let's explore the universe together! 13 | > 14 | > https://github.com/ingonyama-zk/ctf-day-of-sagittarius 15 | 16 | ## Solution 17 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 18 | 19 | This was one of the most intriguing challenges: a modified [battleship game](https://en.wikipedia.org/wiki/Battleship_(game)) implemented using Zero-Knowledge (ZK) proofs in RISC-Zero. Each round, you could either: 20 | 21 | 1. Fire a single shot in the cell 22 | 2. Send scouts to reveal enemy spaceships 23 | 3. Fire a claster charge, to hit from 2 to 4 random cells in an area 24 | 25 | The game was played on an 8x8 board like the one below 26 | 27 | ``` 28 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 29 | ┃ Player Board ┃ 30 | ┣━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┫ 31 | ┃ ┃ a ┃ b ┃ c ┃ d ┃ e ┃ f ┃ g ┃ h ┃ 32 | ┣━━╋───┼───┼───┼───┼───┼───┼───┼───┨ 33 | ┃ 1┃ A │ A │ A │ A │ │ │ │ ┃ 34 | ┣━━╋───┼───┼───┼───┼───┼───┼───┼───┨ 35 | ┃ 2┃ │ │ D │ D │ B │ B │ B │ ┃ 36 | ┣━━╋───┼───┼───┼───┼───┼───┼───┼───┨ 37 | ┃ 3┃ │ │ │ │ C │ │ │ ┃ 38 | ┣━━╋───┼───┼───┼───┼───┼───┼───┼───┨ 39 | ┃ 4┃ │ │ │ │ C │ │ │ ┃ 40 | ┣━━╋───┼───┼───┼───┼───┼───┼───┼───┨ 41 | ┃ 5┃ │ │ │ │ C │ │ │ ┃ 42 | ┣━━╋───┼───┼───┼───┼───┼───┼───┼───┨ 43 | ┃ 6┃ │ │ │ │ │ │ │ ┃ 44 | ┣━━╋───┼───┼───┼───┼───┼───┼───┼───┨ 45 | ┃ 7┃ │ │ │ │ │ │ │ ┃ 46 | ┣━━╋───┼───┼───┼───┼───┼───┼───┼───┨ 47 | ┃ 8┃ │ │ │ │ │ │ │ ┃ 48 | ┗━━┷━━━┷━━━┷━━━┷━━━┷━━━┷━━━┷━━━┷━━━┛ 49 | ``` 50 | 51 | After playing (and losing) the game a few times against the bot, it became clear that the bot had some unfair advantage. It was hitting the target at each shot. Given that the game was based on a ZK design and supposed to keep the state private, it wasn't immediately clear to me how the bot was cheating. I felt that either a backdoor was present, or the proofs were leaking information. 52 | 53 | I started digging into the code to figure out where the bug was. In RISC Zero, [a proof](https://www.risczero.com/docs/explainers/proof-system/) comprises of a zk-SNARK and a journal entry that includes the public outputs of the computation. My hunch was that the journals were leaking state information so I started looking into the journals of all the actions. The cluster bomb one was interesting since it contained many fields: 54 | 55 | ```rust 56 | pub struct ClusterCommit { 57 | pub old_state_digest: Digest, 58 | pub new_state_digest: Digest, 59 | pub config: ClusterBombParams, 60 | pub shots: alloc::vec::Vec, 61 | pub hits: alloc::vec::Vec, 62 | } 63 | ``` 64 | 65 | Everything seemed fine except for the config variable. Checking the definition of `ClusterBombParams`, we see that it contained the `GameState`, which should have been private. Voila! 66 | 67 | ```rust 68 | pub struct ClusterBombParams { 69 | pub state: GameState, 70 | pub upper_left_coordinates: Position, 71 | pub down_right_coordinates: Position, 72 | pub seed: u8, 73 | } 74 | ``` 75 | After finding this, I wrote a small piece of code that took this game state, rendered it and dumped it into a file. Now, all I had to do was start the game with a cluster charge, get the opponent's board state and use it to plan optimal moves. 76 | 77 | Both us and the bot play optimally now. But since we start first and the bot wastes a turn sending a scout, there we'd almost surely win. After winning, we are awarded with the the flag. 78 | 79 | *Shoutout to another team who skipped all of this and instead tackled this challenge in true hacking spirit by writing a script to make the bot play itself and win.* 80 | -------------------------------------------------------------------------------- /the_lost_relic.md: -------------------------------------------------------------------------------- 1 | # The Lost Relic 2 | 3 | ## Challenge 4 | > During their quest to find the greatest treasure in the world, the One Piece, Luffy and his friends are wandering inside a subterranean maze. After many hours, they arrive at the door hiding an old relic, which can be instrumental to achieving their goal. The big problem is that it is made of sea stone and Luffy is unable to use his strength to break it. There are some inscriptions on the walls, which Nico Robin is able to translate. 5 | > 6 | > It says: "If you can find the secret hidden among these texts, the door will open." 7 | > 8 | > There are many input plaintexts and their corresponding ciphertexts, all of them encrypted using a custom MiMC algorithm under the same key. There are also many skeletons around, of all the people who have so far failed this test. Luckily, Usopp brought his computing device and will try to break the secret. What can he do to recover the secret? 9 | > 10 | > https://github.com/ingonyama-zk/the_lost_relic 11 | 12 | ## Comments 13 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 14 | We're given a bunch of plaintext-ciphertext pairs. The block cipher function, a modified MiMC hash, for each round is given as: 15 | 16 | $$ 17 | x_{n+1} = (x_n + k)^2 18 | $$ 19 | 20 | Our goal is to find the secret key, $k$. 21 | 22 | I spent a lot of time trying to devise an algebraic attack on the cipher, given its simplicity. However, I couldn't figure out a way. After the CTF was over, I was given the hint that one of the pairs was a slide pair and we had to do a [slide attack](https://en.wikipedia.org/wiki/Slide_attack). Below is the implementation of this attack in Sage: 23 | 24 | ```python 25 | from sage.all import * 26 | from itertools import permutations, product 27 | 28 | # Stark252 prime field 29 | p = 0x800000000000011000000000000000000000000000000000000000000000001 30 | Fp = GF(p) 31 | 32 | g = Fp.multiplicative_generator() 33 | print(g.order()) 34 | print(factor(p - 1)) 35 | 36 | # Plaintext-ciphertext pairs 37 | data = [ 38 | (0x28b0063846b89588ca3685b89a541f0d928fe5a9cbe96d5abb7f7abb629d3e2, 0xce80a279e03ffd68f6394329f7d10991cc93950cddb7506485d8ff6ed7b8e2), 39 | (0xbf1ce8008359702c83d1bac70b7612c928687a3eeb1f753968d2e194868429, 0x45230772a45ccf3dd959fe19551b8e8a2d477bb9cc34ddada45f8b5e1df7c60), 40 | (0x7e2b171d99abe9032dfea6b45a926f5ee48f8e12b0ff78adcdf584c4c639bd1, 0x305f75862c31d4b39d39d153bbcfdc057ad3c1be8ea7ff4caf98c19b902063a), 41 | (0x2e15cb9cc16936d3ae45a48908ae64188ad3e54b6461d026de2e9119a0fb92c, 0x6a03ffde37a63f1d6f8457c32319095fdd66f0aaa2bcd11aae2c392c2b2a5bd), 42 | (0x5302462b15a1278251a78d55d73808c6baea5f97239698ec01ffe8a9ae08b9d, 0x6f6242d42fff75a04ff5616c1e8462885c9ae45823f66313f99f2e746df9add), 43 | (0x136d355d557ae436cb02f0b10b846771cfc84b9179043c846508844b14027f7, 0x4f052e8a02fc19db85484ab772b86e873d225077fc62050e42611d104283cae), 44 | (0x122fead16809905bdeca0792e650b0c14d217a2a6e1228621f51361594b4f06, 0x3f7755174d8812e217affe00a53cf48ecf507d0ba0f7ffd0a52eda877ae67d2), 45 | (0x77c1a014b96c0de113bd4594a98d5e15241969b88629ad1a18ca4a324af353b, 0x6f87bb1349583cf1d382f0ddf9987d21cbbdb54afe279e87df057ee8516318b), 46 | (0x77365982f9d84204a371ee8301edc2eff4a907094b2c81992727738758db99c, 0xc40395a16b69960867177ccad31c172dfc205fa4f305eaf7f4f44e08629da9), 47 | (0x5d46134ee8397c82949da3f15831730dae280e40d0ee4af4f9be056cd1ce05a, 0x3690fd96de5feadedb4a588eb8a27c4b279d384c3529a0b9eabd5546d79d331), 48 | ] 49 | data = [(Fp(x), Fp(y)) for x, y in data] 50 | 51 | # Find slide pair by looking at all permutations and calculate the secret key 52 | for (x1, y1), (x2, y2) in permutations(data, 2): 53 | if pow(y2, (p-1)//2) == 1 and pow(x2, (p-1)//2) == 1: 54 | x2roots = [x2.sqrt(), -x2.sqrt()] 55 | y2roots = [y2.sqrt(), -y2.sqrt()] 56 | 57 | for (x2root, y2root) in product(x2roots, y2roots): 58 | if y2root - x2root == y1 - x1: 59 | k = x2root - x1 60 | print(f"k: {k}") 61 | print(f"k: {hex(k)}") 62 | break 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /the_power_of_integers.md: -------------------------------------------------------------------------------- 1 | # The Power of iNTEgers 2 | 3 | ## Challenges 4 | > 59-213-402-213-964-402-213-149-310-534 5 | > 6 | > Format: XXX XXX XXX XXX XXX XXX XXX XXX XXX XXX (not specifically 3 letters, English) 7 | > 8 | > **Hint** 9 | > Greek: equal pebble 10 | 11 | ## Solution 12 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 13 | From the hint, it was clear that this challenge was based on Isopsephy ("equal pebble"), the Greek practice of adding up the numerical values of the letters in a word to form a total. I constructed an Isopsephy table for English letters to use: 14 | 15 | | Key | Value | | Key | Value | | Key | Value | | Key | Value | 16 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 17 | | a | 1 | | k | 20 | | t | 200 | | y | 700 | 18 | | b | 2 | | l | 30 | | u | 300 | | z | 800 | 19 | | c | 3 | | m | 40 | | v | 400 | | | | 20 | | d | 4 | | n | 50 | | w | 500 | | | | 21 | | e | 5 | | o | 60 | | x | 600 | | | | 22 | | f | 6 | | p | 70 | | | | | | | 23 | | g | 7 | | q | 80 | | | | | | | 24 | | h | 8 | | r | 90 | | | | | | | 25 | | i | 9 | | s | 100 | | | | | | | 26 | | j | 10 | | | | | | | | | | 27 | 28 | The task was to identify meaningful words for each provided number that together also form a meaningful sentence/phrase. I used [NLTK](https://www.nltk.org/) library's Brown word list as my reference dictionary and wrote a small python script to find candidate words for each number. Here are the words that I found (ordered by decreasing frequency): 29 | 30 | ```bash 31 | ['in', 'end', 'aimed', 'media', 'cane'] 32 | ['the', 'desire', 'homes', 'gate', 'helps', 'acted', 'orange', 'ribbon'] 33 | ['guard', 'bus', 'urge', 'faster', 'jungle', 'crude'] 34 | ['the', 'desire', 'homes', 'gate', 'helps', 'acted', 'orange', 'ribbon'] 35 | ['mighty'] 36 | ['guard', 'bus', 'urge', 'faster', 'jungle', 'crude'] 37 | ['the', 'desire', 'homes', 'gate', 'helps', 'acted', 'orange', 'ribbon'] 38 | ['fiscal', 'flesh', 'pink', 'ladies', 'ideals', 'lion', 'limp', 'shelf'] 39 | ['not', 'father', 'facts', 'tragic', 'dates', 'sons', 'ton', 'pepper'] 40 | ['liver', 'drums'] 41 | [59, 213, 402, 213, 964, 402, 213, 149, 310, 534] 42 | ``` 43 | 44 | Analyzing the potential word combinations and keeping in mind the CTF's lion-themed context, it was easy to figure out that "[in the jungle, the mighty jungle, the lion sleeps tonight](https://www.youtube.com/watch?v=OQlByoPdG6c)" was the solution. 45 | 46 | -------------------------------------------------------------------------------- /umculo.md: -------------------------------------------------------------------------------- 1 | # Umculo 2 | 3 | ## Challenge 4 | > ![Umculo](https://github.com/shuklaayush/ingonyama-ctf-solutions/assets/27727946/8508d5f2-a8ce-42a3-bd70-08afe1c2a0b7) 5 | > Format: XXX XXX XXX (not specifically 3 letters,English) 6 | > 7 | > **Hint** 8 | > Within the many challenges, the songs' name is already familiar, Seek the singers' words, for the riddle to be figured. 9 | 10 | ## Comments 11 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 12 | I realized that the top-left image referred to Imagine Dragons but I couldn't figure out anything else. Even after the CTF is over and multiple hints, I still don't know what the solution is. 13 | -------------------------------------------------------------------------------- /zk_rescue_part_2.md: -------------------------------------------------------------------------------- 1 | # ZK Rescue Part 2 2 | 3 | ## Challenge 4 | > Hey it's me, Red. In today's fast-paced jungle, where communication can be as intricate as the pages of a book, the real art of understanding is to read between the lines, and not just hear what people say. I remember hearing this wisdom from an old man in India 31 years ago, just before I visited the Louvre in Paris. Much like the Louvre's many artworks, people's true meanings and intentions are often hidden beneath the surface, waiting to be uncovered. What you did to get the first flag can also be done in other places, just need to lookup. 5 | 6 | ## Comments 7 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 8 | This was a continuation of the ZK Rescue challenge. I had no idea how to solve this. 9 | 10 | After the CTF, I came to know that the flag was "BALOO." Baloo, in addition to being a [ZK proof system](https://eprint.iacr.org/2022/1565.pdf), is also a character from the classic tale, The Jungle Book. Looking back, I think the subtle hints from Red's message, "jungle" and "book," were meant to lead us towards this answer. However, I'm still not entirely sure if this was supposed to be the only hint. Some teams managed to brute force their way to the solution by putting in all the decoded strings from the polynomial evaluations. 11 | -------------------------------------------------------------------------------- /zokclub.md: -------------------------------------------------------------------------------- 1 | # ZokClub 2 | 3 | ## Challenge 4 | > In the last night's operation, the Crypto Police have discovered the former meeting place of a notorious anonymous crypto criminal secret society known as the "ZokClub." The police raided the location after receiving a tip-off from an anonymous source and found nothing except for a single note. 5 | > 6 | > Unfortunately, the note said nothing... The meaning behind the note remains a mystery, but it is suspected to be a code that only members of the ZokClub would understand. 7 | > 8 | > The ZokClub has been known to operate in the shadows of the dark web, engaging in illegal activities such as money laundering and hacking. The group's anonymity and sophisticated use of blockchain technology have made it difficult for law enforcement agencies to track them down, despite the fact they have their own public web page: https://zokclub.ctf.ingonyama.com/ 9 | > 10 | > We need your help to understand what are they up to. Try to infiltrate their group and gather the secret information. 11 | 12 | ## Solution 13 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 14 | The website made it clear that only holders of the ZokClub NFT could access the secret information. The site featured a detailed diagram outlining the club's operational blueprint: each club member possesses a secret and a nullifier that are used to create a Merkle tree. Using the secret-nullifier combo and a given Zokrates program, they generate a zero-knowledge proof proving their club membership. By submitting this proof on another page, they could mint a club NFT. Once the NFT is in the wallet, the secret can be revealed. 15 | 16 | As we're not give a secret or a nullifier, I guessed that the objective was to forge a proof using arbitrary values. The Zokrates code, which generates the ZK proof, is as follows: 17 | 18 | ```rust 19 | def main(u32[8] root, private MerkleProof merkleProof, field nullifierHash, private field nullifier, private field secret) -> bool { 20 | // Check that note hash is in the merkle tree 21 | assert(checkMerkleProof(root, merkleProof)); 22 | 23 | // Check that the nullifier hash match with public one 24 | field trueNullifierHash = calculateNullifierHash(nullifier); 25 | assert(nullifierHash == trueNullifierHash); 26 | 27 | // Construct note from secret and nullifier 28 | u8[16] nullifierBytes = cast::<128, 16>(nullifier); 29 | u8[16] secretBytes = cast::<128, 16>(secret); 30 | u8[32] constraintPreimageBytes = [...nullifierBytes, ...secretBytes]; 31 | u32[8] trueConstraint = sha256padded::<32>(constraintPreimageBytes); 32 | 33 | return trueConstraint == merkleProof.leaf; 34 | } 35 | ``` 36 | 37 | On closer inspection, one can spot that there's no assert statement in the last line. Instead, the function merely returns a boolean indicating whether or not the nullifier+secret combo is contained in the tree. Even if the boolean is false, a proof can still be generated. So the exploit is to use an arbitrary nullifier, its hash, and the correct Merkle root and Merkle proof to generate a counterfeit proof. 38 | 39 | Upon submitting the forged proof and minting the club NFT, we get access to the secret flag, thereby accomplishing our infiltration into the ZokClub. 40 | -------------------------------------------------------------------------------- /zokclub_-_as_it_should've_been.md: -------------------------------------------------------------------------------- 1 | # ZokClub - As it should've been 2 | 3 | ## Challenge 4 | > After our last infiltration ZokClub layed low and moved to another website: http://52.7.211.188:4000/ 5 | > 6 | > Crypto Police have discovered another former meeting place of a secret society. The police once again found nothing except for a single note. 7 | > 8 | > **This time the note said: `S: ukEaZLLyQhfoKiKD, N: OCjw4gAbjXtTiJWJ`** 9 | > 10 | > We looked through their website briefly, the only thing changed was that weird archive at GPI.zip 11 | > 12 | > We again need your help to understand what are they up to. Try to infiltrate their group and gather the secret information. 13 | 14 | ## Solution 15 | ### By [Ayush Shukla](https://hackmd.io/@shuklaayush) 16 | 17 | This challenge was an extension to the previous ZokClub one. It introduced a bug fix, now checking that the boolean output is `true` within the NFT smart contract. This time, we're also given a valid secret and nullifier. However, the nullifier had already been used to mint an NFT and trying to use it again would be rejected by the NFT contract. 18 | 19 | So I went back to the Zokrates code: 20 | 21 | ```rust 22 | def main(u32[8] root, private MerkleProof merkleProof, field nullifierHash, private field nullifier, private field secret) -> bool { 23 | // Check that note hash is in the merkle tree 24 | assert(checkMerkleProof(root, merkleProof)); 25 | 26 | // Check that the nullifier hash match with public one 27 | field trueNullifierHash = calculateNullifierHash(nullifier); 28 | assert(nullifierHash == trueNullifierHash); 29 | 30 | // Construct note from secret and nullifier 31 | u8[16] nullifierBytes = cast::<128, 16>(nullifier); 32 | u8[16] secretBytes = cast::<128, 16>(secret); 33 | u8[32] constraintPreimageBytes = [...nullifierBytes, ...secretBytes]; 34 | u32[8] trueConstraint = sha256padded::<32>(constraintPreimageBytes); 35 | 36 | return trueConstraint == merkleProof.leaf; 37 | } 38 | ``` 39 | 40 | Inspecting further, specifically at the casting function, I found that higher order bytes were ignored during the casting of the nullifier: 41 | 42 | ```rust 43 | def cast(field input) -> u8[P] { 44 | bool[FIELD_SIZE_IN_BITS] bits = unpack(input); 45 | bool[N] bits_input = bits[FIELD_SIZE_IN_BITS-N..]; 46 | assert(N == 8 * P); 47 | u8[P] mut r = [0; P]; 48 | for u32 i in 0..P { 49 | r[i] = u8_from_bits(bits_input[i * 8..(i + 1) * 8]); 50 | } 51 | return r; 52 | } 53 | ``` 54 | 55 | Knowing this, I realized it was possible to take the given nullifier (which was known to be in the merkle tree), prepend some bytes to it, compute the nullifier hash, and generate a valid proof. Since the new nullifier differed from the old one, it wasn't rejected by the smart contract. 56 | 57 | So we again generate a fake proof, mint the NFT, and obtain the secret. 58 | --------------------------------------------------------------------------------