Click to expand challenge code
| |
Now we’re getting into intermediate territory. This is the only ZKP challenge in the ctf, and considering that not many ctfs in tunisia incorporate zero knowledge proofs in crypt challenges, I thought that this one might be alien to them, (espceially since I hit them with pedersen commits from the get go xd)
Surprisingly, a good chunk of players managed to solve it! 15 teams exactly at the time of writing this. Even though some usually used LLMs to solve it, at least they learned something new, and prepare themselves for future ZKP tasks, they won’t be this easy x)
Analysis
NIZK
This task is a classic Non Interactive Zero Knowledge protocol NIZK where players are demanded to find a satisfying assignment for the 3-SAT problem, for clauses that are impossible to satisfy!
The Zero Knowledge property of this protocol is realize using pedersen commitments, which basically Bind the prover and Hide the underlying committed statement.
The security of this operation is fortified by the Discrete Log Problem, meaning that theoretically, it can only be broken if you break DLP (aka: weak curve, small subgroup… or attack relevant in the realm of DLP)
Unfortunately for players, h1 and h2 were securely generated (they’re derived from the hash of some value, I forgot exactly how), the point is, there is no known log for these two values in $GF(p)$, so this is out of the question.
There is another protocol in play here, the one assuring the Non Interactive part of the operation: it’s the Fiat Shamir Transformation which derives an appropriate challenge based from a random oracle which in this case is a hash value. This ensures the Soundness property of zero knowledge proofs, but is it implemented correctly here?
Implementation
| |
Hmmmmmm, isn’t the FS transform supposed to derive a challenge from a random oracle? A hash could be considered as a random oracle under ideal circumstances, but only if it is resistant to Preimage attacks! The derived challenge here is only 2 bits, derived from the commitment of the prover. Those 2 bits are vulnerable to some sort of a preimage attack since it’s derived every single round!
Looking at the two cases (where chal==0 and otherwise):
Chal = 0
| |
In this case, the server only checks if the commitment is sound, which should’t be an issue if you correctly implement predersen commit
Chal != 0
| |
In this case, the server actually checks if the masks we’re committing to actually verify a clause, which is literally impossible since they’re logically unverifiable: We should avoid this case at all cost!
Solution
Since we now know that we want the challenge to always equal 0, we can compute the FS_STATE locally before sending our commitment to the server, and only send it if the resulting challenge bit it zero!
We keep on doing that for 128 rounds and the server will spit out a flag
Solver
Click to expand solver code
| |
FL1TZ{B0ul3v4rD_0f_Br0k3n_C0mM1tm3nts}
