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$
Coin1
and $5$Coin2
from airdrop User: $5$Coin1
, $5$Coin2
; Reserve: $50$Coin1
, $50$Coin2
Attacker swap $5$
Coin2
to $5\cdot\frac{50}{50}=5$Coin1
User: $10$Coin1
, $0$Coin2
; Reserve: $45$Coin1
, $55$Coin2
Attacker swap $10$
Coin1
to $10\cdot\frac{55}{45}=12$Coin2
User: $0$Coin1
, $12$Coin2
; Reserve: $55$Coin1
, $43$Coin2
Attacker swap $12$
Coin2
to $12\cdot\frac{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.
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_direct
andswap_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 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
public
visibility 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
|
|