Sec3 Ranked First in the Aptos CTF MOVEment 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
| Source | https://github.com/movebit/ctfmovement-1 |
| Link | http://47.243.227.164:20000/web/ |
| Score | 100 |
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
| Source | https://github.com/movebit/ctfmovement-2 |
| Link | http://47.243.227.164:20001/web/ |
| Score | 200 |
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
| Source | https://github.com/movebit/ctfmovement-3 |
| Link | http://47.243.227.164:20002/web/ |
| Score | 200 |
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
5Coin1 and5Coin2 from airdrop.- User:
5Coin1,5Coin2; - Reserve:
50Coin1,50Coin2
- User:
-
The attacker swaps
5Coin2 to5 * 50 / 50 = 5Coin1- User:
10Coin1,0Coin2; - Reserve:
45Coin1,55Coin2
- User:
-
The attacker swaps
10Coin1 to10 * 55 / 45 = 12Coin2- User:
0Coin1,12Coin2; - Reserve:
55Coin1,43Coin2
- User:
-
The attacker swaps
12Coin2 to12 * 55 / 43 = 15Coin1- User:
15Coin1,0Coin2; - Reserve:
40Coin1,55Coin2
- User:
-
…
By repeating this process, a malicious user could drain almost all the tokens in the reserved accounts.
Challenge 4: Simple Swap
| Source | https://github.com/movebit/ctfmovement-4 |
| Link | http://47.243.227.164:20003/web/ |
| Score | 300 |
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_directandswap_exact_y_to_x_directfunctions 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
| Source | https://github.com/movebit/ctfmovement-5 |
| Link | http://47.243.227.164:20004/web/ |
| Score | 400 |
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.