개발하기좋은날

External Call 보안 본문

BlockChain

External Call 보안

devbi 2022. 10. 28. 12:15
반응형

모든 체인의 Contract는 보안성이 중요하다 순식간에 자본금이 사라질수있기 떄문이다. 

 

시큐리티 코딩은 기본적으로 스마트컨트랙트 개발자라면 알아두어야할 사항이라 생각하여 포스팅을 시작한다 

 

External Call 신뢰성 

- External Call 잠재적인 위험에 노출되어있다고 생각해도 무방 하다.

- External Call은 해당 계약및 의존하는 다른 계약에서 악성 코드를 실행 할 수 있기 때문이다. 

- 그래서 모든 External Call은 잠재적인 보안 위험으로 취급되어 최대한 외부호출을 제거하거나 

권장 사항을 사용하여 위험을 최소화 해야한다 

 

1. 신뢰할 수 없는 계약의 표시 

- 가장 먼저 외부 계약에 관련해서 상호 작용이 잠재적으로 안전하지 않다는 것을 분명히 하자 

 

// bad
Bank.withdraw(100); // Unclear whether trusted or untrusted

function makeWithdrawal(uint amount) { // Isn't clear that this function is potentially unsafe
    Bank.withdraw(amount);
}

// good
UntrustedBank.withdraw(100); // untrusted external call
TrustedBank.withdraw(100); // external but trusted bank contract maintained by XYZ Corp

function makeUntrustedWithdrawal(uint amount) {
    UntrustedBank.withdraw(amount);
}

- UntrustedBank에 대한 인출함수는 신뢰할수없는 호출이라 명시되어있고 

- TrustedBandk에 대한 인출함수는 신뢰할수있는 기업의 라이브러리를 사용하여 적용(xyz. 금융 플랫폼간의 통합을 구축하는 블록체인 개발 회사)

 

 

2. 외부 호출후 상태 변경 방지 

- raw call, (somAddress.call()), Contract Call을 사용하면 악성코드가 실행될 수 있다고 가정 합니다

- 이 문제점은 악성코드가 제어흐름을 가로채고 재진입으로 인한 취약점으로 이어질 수 있다는점 입니다

- 신뢰할 수 없는 외부 계약을 호출 하는 경우 상태 변경(value)을 반드시 interactions 발생하기전에 변경하기를 바란다 (Checks-Effects-Interactions Pattern)

 

 - Checks-Effects-Interactions Pattern?

Contract 작성시 가장 기본이 되어야하는 Base 패턴이다 

공식 문서에서도 알려지지 않는 계약로 인한 재진입 문제를 해결하기위한 패턴입니다 

1. 검사 

2. 영향

3. 상호작용 

각 함수는 위 3가지 요소의 순서를 반드시 지키길 권장하며 맨위와 같은 문제들을 방지 할 수 있습니다 

pragma solidity 0.8.13;


contract reentrancyVictim{
    mapping(address => uint256) public balances;
    uint256 public contractBalance;
    
    function payIn() public payable{
        balances[msg.sender] += msg.value; 
    }
    
    function withdraw() public payable{
        require(balances[msg.sender] > 0, "Insufficiant balance");
        payable(msg.sender).call{value: balances[msg.sender]}("");
        balances[msg.sender] = 0;
    }
    
    function updateContractBalance() public{
        contractBalance = address(this).balance;
    }
}

위 코드는 Checks-Effects-Interactions Pattern 적용 되지 않았을때 코드이다 

widthdraw를 보면 밸런스를 체크하고 msg.sender call 통해서 이더리움을 보내고 상태변수를 바꾸는걸 볼수있다

순서를 보면

1. Checks

2. Interactions

3. Effects 

형태를 띄고있다 

Checks-Effects-Interactions Pattern적용했을때 아래의 코드와 같다 

 

 function withdraw() public payable{
        
        // check
        require(balances[msg.sender] > 0, "Insufficiant balance");
        
        // effect
        uint256 payBalance = balances[msg.sender];
        balances[msg.sender] = 0;
        
        // interaction
        (bool success, bytes memory data) = payable(msg.sender).call{value: payBalance}("");
        if(!success){ // catch the case where the send was unsuccesful
            balances[msg.sender] = payBalance;
        }
}

1. Checks 동일하다 

2. effect

- 기존과 다르게 상태 변수를 먼저 초기화 시키고 임시변수를 통해서 해당 value를 바인딩하여 사용한다 

3. interaction 

- 똑같지만 성공여부를 return 받는다 이후 실패한경우를 catch하여 변수를 재할당 해준다

 

가장 중요한 interaction 

effects가 실행되지않고 interaction이 실행되면 재귀 함수 호출되므로 더 많은 잔고를 인출해 나갈수가 있습니다 

항상 interaction을 마지막에 실행시킨다는것을 잊으면 안된다 

 

 

3. Transfer(), Send() 대신 Call() 사용 

먼저 Transfer, Send의 특징을 알아보자 

 

- Send()

2300gas를 소비, 성공여부를 true, false를 리턴 payment 가 제대로 이루어지면 fallback(), receive() 호출 

 

- Transfer()

2300gas를 소비, payment가 이루어지면 fallback, receive가 호출됩니다 실패조건은 두가지이며 아래와 같다 

1. 송신자 컨트랙트 balnce가 충분하지 않을때 

2. 수진자 컨트랙트에서 payment를 rejects 할때 

 

Send, Transfer의 공통점은 2300의 낮은 가스를 소비한다는 겁니다 

이 계약은 EOA간의 단순한 전송만을 위해 설계되었고 동작하지만 CA간의 수행시 복잡한 연산을 처리하는데는 부족한 GAS를 지불하기 떄문에 Call 방식을 추천합니다 

 

- Call()

이름으로 함수를 호출하고 Ether를 보낼수있는 방식입니다 2019년 12월부터 권장된 방식입니다 

가스는 가변하고 지정할수있기에 다양한 로직을 처리 할 수 있습니다

 

하지만 수신자의 계약이 Call문이 주어진 곳에서 함수를 다시 호출하여 재진입 공격을 허용 합니다 

반드시 발신자의 계약에서 재진입 공격으로 인한 계획된거 보다 더 많은 자금 유출에 신경 써야합니다 

반드시!!!!!! 방지 합시다!!! 앞에서 설명한 Checks-Effects-Interactions Pattern 패턴을 사용하면 방지 할수 있겠조?

재진입 공격에대한 방지방법은 따로 포스팅 해보도록 하겠습니다

 

 

 

4. 외부 호출시 오류 처리

Solidity는 원시 주소에서 작동하는 raw call들을 제공합니다, adress.call(), address.callcode(), address.delegatecall(), address.send() 등 이러한 호출의 공통적인 특징은 예외를 throw하지 않지만 false호출에 예외가 발생하면 반환 됩니다 그래서 저수준 호출 방법을 사용하기로 선택했다면 반환 값을 확인하여 호출이 실패할 가능성을 처리하 합시다 

 

아래 방법은 예시 입니다

// bad
someAddress.send(55);
someAddress.call.value(55)(""); // this is doubly dangerous, as it will forward all remaining gas and doesn't check for result
someAddress.call.value(100)(bytes4(sha3("deposit()"))); // if deposit throws an exception, the raw call() will only return false and transaction will NOT be reverted

// good
(bool success, ) = someAddress.call.value(55)("");
if(!success) {
    // handle failure code
}

ExternalContract(someAddress).deposit.value(100)();

 

 

5. delegatecall 신뢰할수 없는 컨트랙트 주소에 사용하지 마세요 

delegatecall의 사용성은 굉장히 범용적입니다 A->B->C A의 호출자는 B를 실행했지만 B의 코드에서 마치 C의 함수가 B의 것인것마냥 실행시킵니다 이때 호출 주소를변경 시킬수있기에 안전하지 못할수있습니다 

 

아래는 잘못된 코딩으로 잔액 손실이 발생하는 예제 입니다 

contract Destructor
{
    function doWork() external
    {
        selfdestruct(0);
    }
}

contract Worker
{
    function doWork(address _internalWorker) public
    {
        // unsafe
        _internalWorker.delegatecall(bytes4(keccak256("doWork()")));
    }
}

해당 문제는 손쉽게 이해할수있습니다 

신뢰되지않는 _internalWorker의 주소의 doWorker() 호출로 인해 강제 계약의 파기되는 모습을 볼수있습니다 

절대적으로 신뢰할수있는 판단이 들거나 그러한 조건을 갖추었을때 delegatecall을 사용해야합니다 

 

다음엔 재진입 방법에 대한 솔루션을 제시 하겠습니다

반응형

'BlockChain' 카테고리의 다른 글

Fallback, Receive 그리고 재진입 공격  (0) 2022.10.30
재진입 공격 방지 (Re-Entrancy)  (0) 2022.10.28
채굴 (Mining)  (0) 2022.10.17
프루닝  (0) 2022.08.31
IPFS? IPFS 동작 방식  (0) 2022.08.24
Comments