Sharing some Paradigm CTF solutions

FURUCOMBO
FURUCOMBO
Published in
7 min readFeb 11, 2021

What a fun and exciting weekend!

Furucombo team participated as team “MemeRescuer” and ranked #9 overall at the Paradigm CTF 2021 competition. It was a 48-hour Ethereum focused security competition held over the last weekend. We solved 8 out of 17 challenges, and here is to share our solutions in the below article.

BABYCRYPTO

This challenge is about a DSA vulnerability in ECDSA, using ECDSA private key to sign messages with the same `nonce k`.

Some references from the internet: https://medium.com/asecuritysite-when-bob-met-alice/cracking-ecdsa-with-a-leak-of-the-random-nonce-d72c67f201cd.

Our source code to get the flag of this challenge: https://gist.github.com/chouandy/cdab00469577f04c60a02e4c265dfd70

BABYSANDBOX

To get the flag, we will need to destroy the code of BabySandbox on the deployed address.

There is only one function in the BabySandbox contract which allows the user to pass an address as a parameter. This function will

  1. `delegatecall` to the passed address if the caller is the contract itself
  2. Check if there is enough gas left
  3. `staticcall` to itself and reverts if a static call fails
  4. `call` to itself if all the operations above are passed

The key point is that the `staticcall` does not allow any state modification inside the message call. To assert whether a call is a `staticcall`, we can use a state-modify `call` and check if it succeeds.

We will deploy two contracts: Suicide and Foo. The Suicide contract contains the `call` operation to the Foo contract, which is to check whether the current message call is static or not. After that, it will commit suicide if it is state changeable.

Suicide contract: https://gist.github.com/robinpan1/30d344f1ab4a048281e8a3b83ff88bde
Foo contract: https://gist.github.com/robinpan1/0f585830e5f32497ef9293eb5ba64d51

Notice: Need to deploy the Foo contract first and hardcode the address in the Suicide contract since Suicide is being `delegatecall`ed without passing any arguments.

BOUNCER

The goal is to drain the balance of the Bouncer contract. There are many functions in Bouncer. User flow is like this:

  1. Call `enter` and being charged for 1 ether entry fee
  2. Call `convert/convertMany` to convert entries
  3. Do the rest

The `convert` function will check your entry time, identity, and pull the requested funds in the `proofOfOwnership` function. It uses `token.transferFrom` for ERC20 token, and that is not a problem. However, there is no such function for ether, so it will check if there is an exact amount of `msg.value` provided when calling the function. And here comes the problem.

There is a `convertMany` function that is able to invoke `convert` in a loop. The same msg.value context will apply to all subsequent calls to `convert` function in the for loop of `convertMany`. This makes it possible to fool Bouncer that we have lots of tokens with only 1 entry being sent.

There are already 52 ETH sitting in the bouncer’s pocket when the Setup contract being deployed. We will have to initiate 2 txs to fool the bouncer and then redeem all funds.

  1. `Enter` with {token=ETH, amount=1 ether} and pay 1 ETH for the entry fee
  2. `ConvertMany` with array.length equals to 54 (52 + 2), and pay 1 ETH as msg.value to fool the bouncer
  3. `Redeem` 54 ETH

Script: https://gist.github.com/robinpan1/c6b80b12b69208703c2fc7e87e016960

BROKER

There were several well-known attacks performed through the unreliable oracle service, such as using AMMs as pricing oracles. When the liquidity is not deep enough, the price might be manipulated, and using flashloan makes these actions practical by reducing the cost to almost 0 (still requires fee though).

The Setup contract deploys a Token and a Broker. The Broker is an over-collateralized loan bank. Using WETH as collateral and providing Token as borrowing. When the collateral is considered under-collateralized, the account can be liquidated. As we mentioned earlier, the collateral factor will be decided based on the price of an Uniswap trading pair. The initial balance in the Broker is 25 WETH and 500,000 Token. The solving requirement is the WETH balance of the Broker is less than 5 ether. The attack can be performed by the following sequence.

  1. `Deposit` WETH as collateral.
  2. `Borrow` as much Token as we can, which makes liquidation easier.
  3. Buy Token in Uniswap trading pair, which elevates the price of Token.
  4. Since the price of Token is elevated, the account we used became under-collateralized.
  5. `Liquidate` the account, payback the appropriate amount of Token to withdraw the WETH in Broker.

In the testing environment, we have a sufficient amount of Ether and there is no other user, so we can do the actions step-by-step without writing them in a contract. However, to perform this in a production environment, we can modify the steps into writing a smart contract which does:

  1. Flashloan a proper amount of WETH from flashloan service such as Aave.
  2. `Deposit` WETH as collateral.
  3. `Borrow` as much Token as we can, which makes liquidation easier.
  4. Buy Token in Uniswap trading pair, which elevates the price of Token.
  5. Since the price of Token is elevated, the account we used became under-collateralized.
  6. `Liquidate` the account, payback the appropriate amount of Token to withdraw the WETH in the Broker.
  7. Sell the rest of Token and use the WETH we get from liquidation to repay the flashloan.

FARMER

Farmer is a classic character since Compound introduced COMP framing in 2020. Setup contract deploys two contracts: CompFaucet and CompDaiFarmer. CompFaucet is injected by framing rewards a.k.a. COMP and users could deposit and withdraw DAI to CompDaiFarmer which includes farming functions including `claim`, `recycle`, `mint` etc. As the solving requirement is to clean up COMP to DAI in CompFaucet and CompDaiFarmer, so the steps are:

  1. Call CompDaiFarmer `claims()`, it collects all COMP in CompFaucet to CompDaiFarmer.
  2. Call CompDaiFarmer `recycle()`, it swaps all COMP in CompDaiFarmer to DAI by Uniswap.
  3. Call CompDaiFarmer `mint()`, it converts all DAI in CompDaiFarmer to cDAI

Then the problem is solved when we have done the regular activity for farmers.

HELLO

To get the flag, we can find out `isSolved()` just read a variable `solved` and we also could find a function called `solve()` which will set the variable from false to true. For now, we got all we need. We should call `solve()` at first, after this action we can get the flag now by calling `isSolved()`.

SECURE

Setup contract deploys two contracts: Wallet and TokenModule. There are two roles in Wallet: Module and Operator, which describe the execution logic and the operating authority. When initiating the Setup contract, the Setup contract deposits 50 WETH into the Wallet through TokenModule’s logic.

However, the solving requirement is having 50 WETH in Setup, which means transferring 50 WETH to Setup solves the problem.

YIELD_AGGREGATOR

Yield farming is a popular service in DeFi since 2020. The main concept is to aggregate user’s funds to do the same action, such as farming tokens.

Setup contract deploys two contracts: MiniBank and YieldAggregator. Users may deposit to or withdraw from YieldAggregator, and YieldAggregator records the users’ balance and mint or burn in MiniBank. The Setup contract deposits 50 WETH to YieldAggregator initially. The solving requirement is to extract all the WETH in MiniBank and YieldAggregator.

To remove the WETH that is not deposited by us, an idea is to make YieldAggregator record an excessive amount of our deposit.

https://gist.github.com/hihiben/ab32faec47b4e15066b006d52ca2e501.js

To be honest, the code of YieldAggregator is quite buggy, but I will just focus on the problem solution. The deposit function updates the user’s balance through these steps

  1. Get the initial balance of MiniBank’s underlying token.
  2. Transfer user’s funds from the user’s account to YieldAggregator.
  3. Call `mint` in MiniBank.
  4. Loop step 2, 3 for all the assigned token and token amount.
  5. Get the balance difference of MiniBank of its underlying token.
  6. Apply the difference to the user’s account.

There are several problems with the procedures above.

  • No verification of the token assigned by the user.
  • No verification on whether the token is identical to the underlying token of MiniBank.

The attack can be performed by creating a contract and executing it in the following sequence.

  1. Create an attacking contract that has transferFrom and approve for ERC20-like interface. The function detail will be described later.
  2. Deposit 50 WETH to the attack contract.
  3. Call `deposit` in YieldAggregator to deposit the token to MiniBank. The amount does not matter. The execution of the deposit will be like:
(a) Record the initial MiniBank underlying balance. (Should be 50 WETH)(b) Call the assigned token’s `transferFrom`. 
`TransferFrom` will `approve` MiniBank and call `mint` to deposit the 50 WETH into MiniBank.
(c) `Approve` will simply return true.(d) Call `mint` in MiniBank for depositing the token.
It will fail if the amount is not 0 in this case. However, YieldAggregator uses try/catch to handle the revert, so it does not matter.
(e) Since we deposited 50 WETH into MiniBank in step (b), the balance difference of MiniBank will be 50 WETH.(f) The balance of user in YieldAggregator is updated as 50 WETH

4. `Withdraw` 50 WETH from YieldAggregator. Notice that we did not deposit any WETH from YieldAggregator, so the WETH is actually not ours.

5. Call `burn` from MiniBank to extract the 50 WETH that we deposited in 3(b).

That’s it!

Thanks to Paradigm for organizing the exciting event. We had a great time and can’t wait for the next competition 😄

--

--

FURUCOMBO
FURUCOMBO

Build your own DeFi legos into one transaction without knowing how to code.