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 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.
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 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.
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.
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.
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.
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.
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:
By repeating this process, a malicious user could drain almost all the tokens in the reserved accounts.
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.
There are two vulnerabilities in this contract.
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).
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.
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.
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