Glad to participate in the on-site finals of Real World CTF in Beijing! Here I would like to share the solution to the challenges I solved.

Bank2

• Type: Crypto
• Keywords: Elliptic curve, Schnorr signature algorithm

Description

Our bank has invested in a HUGE security upgrade. Now we are equipped with the latest interactive multi-signature protocol to keep your assets safe. Your satisfaction is our first priority.

Solution

Similar to the challenge Bank in Real World CTF 2019 Quals (Ref), the server-side implements the Schnorr signature algorithm. The difference between these two implementations is the verifying function:

The main logic of the server’s code is:

In the deposit method, T and PK are provided by us. The bug happens at line 14, it should be s = (r + c * sk) % n instead (credit to @fweasd). Notice that the s we received equals to p - r' + c * sk, where r' is around 240 bits. Since we know the value of p, c, we can calculate the value of sk = (s - p)//c + 1, which is the secret key of the server. Then, we sign the message "WITHDRAW" with the secret key and get the flag by the withdraw method.

Exploit:

Montagy

• Type: Smart contract
• Keywords: Solidity inline assembly, EVM bytecode, z3

Solution

TL;DR

By the newPuzzle method, create a new puzzle that has the same tag value as one of the existing puzzles to pass the isOfficialChecksum check. Let our new puzzle call solve to the proxy contract, and the proxy contract will send out all the ether it has.

Detailed Write-up

In this challenge, our goal is to let the balance of the proxy contract become 0, and then we will receive the flag in a transaction sent from the game server.

The code of the proxy contract is:

The full source code can be found on Ropsten: Proxy contract

By inspecting the transactions to the proxy contract, we may notice that the owner had registered and deployed two puzzles via the registerCode and newPuzzle methods. Also, their source code can be found on Ropsten: side contract 1, side contract 2. However, these two puzzles are not computationally feasible to solve.

How about creating a new puzzle that calls server.solve() directly? To make this happen, the new puzzle should be deployed via newPuzzle and should pass the isOfficialChecksum check at line 20. While only the owner is allowed to register a new checksum (tag value), we have to construct a new puzzle that has the same tag value as one of the existing puzzles.

First, write a contract which simply calls solve to the proxy contract:

Compile this contract and get its deploy bytecode. Note that whatever we pad at the end of the bytecode will not change the deploy result. Therefore, we choose to find proper padding to our bytecode to control its tag value. According to the source code of the proxy contract, to calculate the tag value, the method tag iterates from the start of the bytecode to the end. Since we can calculate the tag value of the unpadded bytecode beforehand, we only have to focus on the last iteration, where the padding is the input bytes.

Here is a script for finding proper padding:

Append the padding to our deploy bytecode, and call newPuzzle in the proxy contract with the padded bytecode as the parameter. After our contract is deployed, call the do_solve method in our contract. All ether that the proxy contract has will be sent out, and then we will get the flag: rwctf{cOd3_i5_CH3ep_sHoW_mE_tHe_OPcdE...Oh_&&_1_CUP_of_T_PlEA53_:P}