sec3 Ranked First in the Aptos CTF MOVEment 2022
December 23, 2022

We're very excited to announce that our sec3 team "Super Rookies” scored first place in the Aptos Capture The Flag competition MOVEment with Aptos Dec 2022. We got two first-bloods (the first player solved a challenge) and two second-bloods (the second player solved a challenge) in the four challenges except for the sanity check, ranking first in the end.

The competition was a lot of fun and gave us a good chance to demonstrate our understanding of the Move language and how we can help to secure them. In this blog post, we will briefly talk about the challenges and our solutions. For more details such as the exploits and possible fixes, please refer to our team's write-up.


Challenge 1: checkin

Challenge info

Target contract

Challenge 1 is a sanity check that gets the players familiar with using aptos-cli to communicate with the private Aptos chain where the contracts are deployed. There is a get_flag function in the contract, and once it’s called it will emit a Flag event.

Solution

After initializing an account and invoking the get_flag function via aptos-cli, we can submit the transaction hash. The server will check whether this transaction triggers the Flag event. If yes, the server will return the flag.

Challenge 2: hello move

Challenge info

Target contract

Challenge 2 is a simple challenge to let players get familiar with the Move language. The contract has five functions: init_challenge, hash, discrete_log, add, pow and get_flag.

The init_challenge function is used to initialize the challenge by sending the caller a Challenge object with 5 members, balance = 10, q1 = false, q2 = false, q3 = false, and an event handler.

The fields q1, q2, and q3 indicate the solving status of the 3 sub-problems in this challenge, and these statuses will be checked in the get_flag function.

Sub-problem q1: hash

The field q1 will be set to true if we invoke the hash function and provide a guess: vector<u8> satisfying len(guess)==4 && keccak256(guess+"move")=="d9ad5396ce1ed307e8fb2a90de7fd01d888c02950ef6852fbc2191d2baf58e79". This can be solved by writing a simple script to brute-force all the possible guesses, and the answer is good.

Sub-problem q2: discrete_log

In order to set q2 to true, we need to provide a guess: u128 satisfying pow(10549609011087404693, guess, 18446744073709551616) == 18164541542389285005, which is a classic discrete logarithm problem. We can solve this with discrete_log(18164541542389285005,Mod(10549609011087404693,18446744073709551616)) in sage, and the answer is 3123592912467026955.

Sub-problem q3: add

Similar to other checked arithmetic implementations, the Shl and Shr operations in Move language will raise an ARITHMETIC_ERROR if the shift amount is greater than or equal to the bit width of the operand as this is a CPU-level undefined behavior. And the Shl operations won’t raise ARITHMETIC_ERROR if there is an overflow. So we can shift the current balance of 10 to the left by more than 8 bits (e.g. shift left two times and 5 bits each time) to set the balance to 0.

Challenge 3: swap empty

Challenge info

Target contract

This target contract implements a very simple swap protocol, which allows users to swap between two tokens Coin1 and Coin2. Users can call function get_coin to get an airdrop of 5 Coin1 and 5 Coin2. Functions swap_12 and swap_21 can be used to swap between Coin1 and Coin2. Finally, function get_flag checks whether the amount of Coin1 or Coin2 in the reserved account is 0.

Vulnerability

The vulnerability lies in the design of the get_amouts_out function. It calculates the exchange amount based on the ratio of Coin1 and Coin2 in the reserve, which is unfortunately not safe. Consider the following PoC:

  • The attacker gets 5 Coin1 and 5 Coin2 from airdrop
    User: 5 Coin1, 5 Coin2;
    Reserve: 50 Coin1, 50 Coin2
  • The attacker swaps 5 Coin2 to 5 * 50 / 50 = 5 Coin1
    User: 10 Coin1, 0 Coin2;
    Reserve: 45 Coin1, 55 Coin2
  • The attacker swaps 10 Coin1 to 10 * 55 / 45 = 12 Coin2
    User: 0 Coin1, 12 Coin2;
    Reserve: 55 Coin1, 43 Coin2
  • The attacker swaps 12 Coin2 to 12 * 55 / 43 = 15 Coin1
    User: 15 Coin1, 0 Coin2;
    Reserve: 40 Coin1, 55 Coin2

By repeating this process, a malicious user could drain almost all the tokens in the reserved accounts.

Challenge 4: simple swap

Challenge info

Target contract

This contract implements a coin swap program similar to Uniswap v2, which allows users to swap between TestUSDC and SimpleCoin with a 0.25 fee and a 0.1 bonus. In the initialization process, the admin added 10^10 TestUSDC and 10^10 SimpleCoin to the pool. The get_flag function will check if the user has at least 10^10 SimpleCoin. If yes, the user will get the flag.

Vulnerability

There are two vulnerabilities in this contract.

  • The first vulnerability is that there is no limit on the amount of tokens that a user can claim via airdrop. An attacker can claim a large amount of tokens and then swap them for other tokens to drain the reserve pool.
  • The second vulnerability is that the swap_exact_x_to_y_direct and swap_exact_y_to_x_direct functions are incorrectly exposed to the public. An attacker can call this function to swap tokens without paying the fee.

Combining these two vulnerabilities, an attacker could first claim a large amount of TestUSDC and then swap an amount of TestUSDC equal to the current reserve pool for SimpleCoin each time to drain half of the reserve pool while receiving a 0.1 bonus. After n repetitions, the amount of SimpleCoin in the reserve pool will be (10^10)/(2^n)​.

Challenge 5: move lock v2

Challenge info

Target contract

This contract generates a number by using a polynomial whose coefficients are generated by a string encrypted with script hash and several pseudo-random numbers. The flag event will be emitted if the user guesses the correct number. Obviously, it is almost impossible to guess the correct number, since the number of possible guesses is 2^128.

Vulnerability

The vulnerability is that the pseudorandom number is generated with a timestamp in seconds and a counter. The counter is initialized to 0 and will be increased by 1 each time a random number is generated. Therefore, both the timestamp and the counter are predictable. An attacker can just reuse most of the code in the target contract to generate the same polynomial and the correct number directly. Because the string is encrypted by XORing script hash and a constant, we need to call the exploit contract via a script.


About sec3 (Formerly Soteria)

sec3 is a security research firm that prepares Solana projects for millions of users. sec3’s Launch Audit is a rigorous, researcher-led code examination that investigates and certifies mainnet-grade smart contracts; sec3’s continuous auditing software platform, X-ray, integrates with GitHub to progressively scan pull requests, helping projects fortify code before deployment; and sec3’s post-deployment security solution, WatchTower, ensures funds stay safe. sec3 is building technology-based scalable solutions for Web3 projects to ensure protocols stay safe as they scale.

To learn more about sec3, please visit https://www.sec3.dev