CTF MOVEment with Aptos Dec 2022 Writeup
Preface
It’s been half a year since I last wrote a blog. During that time, I’ve learned a lot about Web3 security, including Solana and Aptos. Last weekend, I participated in the CTF MOVEment with Aptos Dec 2022 jointly organized by MoveBit, Aptos, ChainFlag and OtterSec, and scored two first-bloods and two second-bloods in the four challenges except the sanity-check, ranking first in the end. In this post, I will briefly introduce the solutions to the five challenges.
Challenge 1: checkin
- Source: https://github.com/movebit/ctfmovement-1
- Link: http://47.243.227.164:20000/web/
- Score: 100
Target contract
The challenge 1 is a sanity-check to let players get familiar with how to use aptos-cli to communicate with the private chain where the challenge contract is deployed. There is a get_flag function in the contract, and once it’s called it will emit an Flag event.
Solution
After initializing an account and invoking the get_flag function via aptos-cli, we can submit the transaction hash to the challenge website, the server will check whether this transaction triggers the Flag event, and if so, 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
The 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. q1, q2, q3 indicates the solving status of the 3 sub-problems in this challenge, and these status will be checked in the get_flag function.
q1: hash
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.
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$.
q3: add
The sub-problem q3 is more interesting. Similar to other checked arithmetic implementation, 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 $10$ to the left by more than $8$ bits to set the balance to $0$.
Exploit contract
| |
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. The contract has a get_coin function to let the user get an airdrop of $5$ Coin1 and $5$ Coin2, two functions swap_12 and swap_21 to swap between Coin1 and Coin2, and a get_flag function checks whether the amount of Coin1 or Coin2 in the reserved account is 0.
Vulnerability
The vulnerability is the design of the get_amouts_out function. This contract uses a very naive way of calculating the amount of token that can be exchanged based on the ratio of Coin1 and Coin2 in the reserve account. However, this design is not safe, consider the following POC:
Attacker get $5$
Coin1and $5$Coin2from airdrop User: $5$Coin1, $5$Coin2; Reserve: $50$Coin1, $50$Coin2Attacker swap $5$
Coin2to $5\cdot\frac{50}{50}=5$Coin1User: $10$Coin1, $0$Coin2; Reserve: $45$Coin1, $55$Coin2Attacker swap $10$
Coin1to $10\cdot\frac{55}{45}=12$Coin2User: $0$Coin1, $12$Coin2; Reserve: $55$Coin1, $43$Coin2Attacker swap $12$
Coin2to $12\cdot\frac{55}{43}=15$Coin1User: $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.
Exploit contract
| |
Possible fix
One possible fix is to use the following formula to calculate the number of tokens that can be exchanged, to ensure that the product of the two token amounts is always constant:
| |
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 Uniswap v2 like coin swap program that allows users to swap between TestUSDC and SimpleCoin with a $0.25%$ fee rate and a $0.1%$ bonus if a user swaps TestUSDC to SimpleCoin. 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 so, 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 to 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 reduced to $10^{10}\cdot\frac{1}{2^n}$.
Exploit contract
| |
Possible fix
- Add a limit to the airdrop amount each account can claim
- Remove the
publicvisibility modifier of theswap_exact_x_to_y_direct<X, Y>function to make it private
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 generate a number by using a polynomial whose coefficients are generated by a string encrypted with script hash and several pseudo-random numbers. 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 pesudorandom 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 a same polynomial and the correct number directly. Recall that the string is encrypted by XORing script hash and a constant, we need to call the exploit contract via a script.
Exploit contract
| |