[PROOFER] 스마트 컨트랙트 보안::Re-entrancy

Park Soomin
proofer-tech
Published in
9 min readOct 2, 2019

Writer : 박수민(SooMin Park), 송승운(SeungWoon Song), 박현주(HyunJu Park)
Reviewer : 맹주현(Juhyun Maeng)
Blockchain Academy PROOFER

Photo by Sebastian Herrmann on Unsplash

블록체인 학회 ‘PROOFER’에서 블록체인 스마트 컨트랙트 보안 이슈에 관한 글을 작성합니다. 이번 주제는 ‘Re-entrancy’이며, 이더리움의 Re-Entrancy (재진입성) 공격, Send함수와 Transfer함수의 차이점, Re-Entrancy 솔루션의 함정으로 나누어 설명합니다.

블록체인 기술은 해킹으로부터 완전히 자유롭다는 인식이 퍼져있지만 Ethereum, 즉 블록체인 코인도 해킹에서 완전히 자유로운 것은 아니다. 오늘 우리는 블록체인 코인에서 발생한 해킹 사건들 특히, 이더리움에서 발생했던 기술적인 문제 예시들을 중심으로 스마트 컨트랙트 보안 이슈를 분석해볼 것이다.

이더리움은 어떤 특징을 가지고 있는가?

이더리움 스마트 컨트랙트를 직접 작성하거나 외부 컨트랙트를 호출하여 사용할 수 있다. 이더를 전송하는 것조차 외부 컨트랙트를 호출하는 행위이다. 하지만 외부 컨트랙트를 무분별하게 사용하게 되면 검증되지 않은 코드로 인해 보안에 취약해질 수 있다. 따라서 External Contract 호출의 위험을 인지하고 있어야 하며, 알려져 있는 취약점들에 대해서는 반드시 방어하여야 한다. 먼저, 이 취약점이 정확히 어떤 의미인지 명확히 하고 코드로 살펴본다. 그 다음 이 취약점에서 중요한 해결법 중 하나인 Send 함수가 기존 Transfer 함수와 어떤 차이가 있는지 알아본다. 마지막으로 이 해결법이 가지는 함정을 지적하고 근본적인 해결책에 대해 이야기할 것이다.

  1. Re-entrancy의 정의
  2. Send 함수와 Trnasfer 함수의 차이점
  3. 재진입성 솔루션의 함정

1. 이더리움의 Re-Entrancy (재진입성) 공격이란?

재진입성은 ‘검증 및 신뢰되지 않은 외부 스마트 컨트랙트가 보안에 취약한 함수를 호출하고, 다시 취약성을 가지는 컨트랙트로 진입 하는 것’이다. 이것이 무슨 말일까?

코드로 살펴보도록 하자. 다음 예시 코드는 함수를 Call하는 msg.sender에게 amountToWithdraw 만큼의 이더를 전송한다. Re-entrancy 취약점을 모르고 보면 어떤 부분이 insecure한지 알 수 없다.

출처 : Ethereum Smart Contract Best Practices [https://consensys.github.io/smart-contract-best-practices/known_attacks/#reentrancy-on-a-single-function]

만약 msg.sender가 EOA가 아니라 CA라면?

msg.sender가 CA라서 msg.sender.call()이 다른 스마트 컨트랙트를 호출하는 경우, msg.sender.call.value 함수에서 다른 함수를 재귀 호출하게 되어 있다면 반복적으로 토큰을 전송하거나 같은 예상치 못한 결과가 발생할 수 있다.

그 결과, 신뢰할 수 없는 External Contract 호출이 “Call-back 함수 등을 통해 악의적 행위를 하는 코드를 실행”시켜

  • 나의 프로그램 제어 흐름을 탈취할 수 있으며
  • 예상치 못한 함수 호출로 나의 데이터를 변조할 수 있다.

공격자는 외부 컨트랙트 호출로 인해 발생할 수 있는 재진입성 취약점을 활용하여 위와 같이 단일 함수에서 재진입을 이용하여 공격하거나, 동일한 상태를 공유하는 두 가지 다른 함수를 사용하여 비슷한 공격을 수행할 수 있다. 전자는 Re-entrancy on a Single Function, 후자는 Cross-function Reentrancy 라고 불리며 The DAO 해킹에 사용되었다.

(1) Re-entrancy on a Single Function — 단일함수에서의 재진입

초기의 Re-entrancy 공격은 함수의 첫번째 호출이 완료되기 전에 반복적으로 호출할 수 있는 함수(재귀함수)를 이용했다. 위와 같은 코드를 공격할 수 있는 Attacker는 Fall-back*함수를 이용해 재귀적으로 withdraw함수를 계속해서 호출하고, 함수가 끝날 때까지 사용자의 잔액이 0으로 설정되지 않았으므로 두 번째 및 그 이후의 ​​호출은 계속 성공하고 잔액을 계속해서 빼낼 수 있는 것이다.

*fallback이란? 함수명이 없는 no-name함수. Contract 내에 존재하지 않는 함수를 호출하려고 시도할 때 호출되는 함수.

(2) Cross-function Re-entrancy — 함수 간 재진입

The DAO 해킹에 사용되었던 공격인 Cross-function Re-entrancy는 동일한 상태를 공유하는 두 가지 다른 함수를 사용한다.

출처 : Ethereum Smart Contract Best Practices [https://consensys.github.io/smart-contract-best-practices/known_attacks/#cross-function-reentrancy]

외부 호출을 하는 ‘withdrawBalance’ 함수가 실행될 때 공격자는 자신의 코드에서 transfer() 함수를 의도적으로 호출할 것이다. 이 때 withdrawBalance 함수가 끝나지 않았기 때문에 sender의 잔액은 아직 0이 되지 않아서 계속적으로 인출을 받았더라도, 토큰은 전송이 가능하다.

이를 방지하기 위한 Solution은 Transfer함수가 아닌 다른 이더 송신 함수를 이용하는 것이다. Transfer함수의 어떤 점이 취약한 지는 지금부터 설명할 것이다.

2. Send함수와 Transfer함수의 차이점

모든 솔리디티 언어 등 이더리움에서 배포할 수 있는 언어들은 다 컴파일러를 통해 Bytecode로 변환되고, 그 이후에 런타임 도구에 의해서 OP CODE로 변환되어 수행되어진다.

이더리움을 송금하는 방식에는 두가지가 있다.

첫번째는, Transfer 함수, 그리고 두번째로는 Send함수가 있다.

둘 다 이더를 송금하는 함수지만 그 방법과 과정에서 다른 모습들을 보여준다. 일단 Transfer 함수를 사용할 때, 가스가 없거나 에러가 내부적으로 발생하게된다면 예외처리 되면서 이전 상태로 돌아가게 된다. 그리고 만약 대상이 컨트랙트 주소라면 Fallback 함수를 호출하게 된다.

하지만, Send의 경우 Transfer과 같은 기능을 하지만, 그 결과에 대해서는 다른 모습을 보인다. Transfer은 리턴 값을 가지고 있는 반면, Send는 리턴 값을 가지고 있지 않다. 에러가 발생하면 이전 상태로 되돌아가는 Transfer과 다르게 Send는 False를 리턴하고 계속 실행이 된다. 그리고 이점 때문에, 이더리움 공식문서에서는 최대한 Transfer을 사용하기를 권하고 있다. 또 이 차이점이 Re-entrancy의 원인이 된다.

그렇다면 이게 왜 문제인가?

위에서 설명한 요소들은 해커들에게 취약점이 밝혀짐으로써 큰 문제를 야기하게 되었다. 이 문제들을 이용하여 발생된 해킹 사건이 The DAO Hacking* 문제이고, 이 때문에 우리는 현재 이더리움과 이더리움 클래식이라는 서로 다른 2개의 이더리움의 탄생을 보게 되었다.

3. 재진입성 솔루션의 함정

재진입성은 여러 함수 및 스마트 컨트랙트에서 발생할 수 있으므로 단일 함수에서의 재진입성을 방지하기 위한 솔루션 만으로는 충분하지 않다. 하지만 재진입성 취약점을 가장 강력하게 피할 수 있는 방법이 있다.

그것은 바로 내부 작업이 모두 완료된 후에 외부 함수를 호출하는 것이다.

이더리움은 이러한 공격 원인을 가진 스마트 계약에 이러한 외부와 연계된 호출을 사용할 경우에 더욱 견고한 개발 가이드라인을 제시한다. 아래 문서에 외부 스마트 컨트랙트 호출에 관한 신뢰 기준을 제시하고 있으니 개발 시에 참고 할 수 있다.

참고 : Ethereum Smart Contract Best Practices

외부 함수를 호출하는 방법에는 CallDelegateCall이 있으며, 이 문서에서는 신뢰할 수 없는 컨트랙트에 대해서는 DelegateCall을 수행하지 않을 것을 권고하고 있다.

  • Call이란?

Call은 컨트랙트에서 다른 컨트랙트의 함수를 호출하는 방식이다. 간단하게 A라는 컨트랙트에서 B에 있는 SING() 컨트랙트를 이용하고 싶을 때 Call.B.SING()과 같은 코드로 컨트랙트를 호출해내는 것을 Call이라고 말한다. 이 Call이라는 기능은 리턴 값을 가지면서 작동하게 된다.

  • DelegateCall은?

반면, DelegateCall은 리턴값을 가지고 있지 않다. 만약, DelegateCall에서 리턴값을 가지게 하고 싶으면 assembly코드로 작성함으로써 가능하다.

Call 실행시 => 호출한 컨트랙트에서 실행되고, 호출한 컨트랙트에서 저장이 된다.

DelegateCall => 예를 들어, B가 A를 호출한다고 했을 때, A에서 실행된 정보들은 A의 상태변수의 이름과 타입이 B의 것과 같다고 했을 때 B에 저장되지 않고 A에 저장된다. 마치 우리가 프로그래밍할때 패키지를 이용하듯이 사용된다.

스마트 컨트랙트에 대한 개발을 시작하는 입문자들은 이런 것 까지 신경쓰면서 개발할 필요가 없다. 테스트 환경에서 동작할 것이며, 실제 Ether나 토큰을 사용하지 않기 때문이다. 그러나, 입문을 넘어 한 단계 올라가기 위해서는 보안 이슈에 대해 인지하고 있는 것은 필수적이라고 생각한다. 이 글에서는 재진입성 취약점에 대해 살펴보았다. 외부 컨트랙트의 함수를 Call 할 때 안전하다고 확인된 것만 호출하는 것이 근본적인 해결책이지만, 그럴 수 없다면 이더 송신 방식에서 좀 더 Secure한 방식을 사용하는 것으로 이 장의 결론을 내릴 수 있을 것 같다.

* The DAO Hacking이란?

DAO(디지털 자율 조직)는 스마트 컨트랙트들로 연결된 조직 거버넌스 매커니즘이다. DAO 프로젝트 중 가장 많은 돈을 모금하였던 The DAO가 2016년에 Re-entrancy 취약점으로 해킹을 당하고 보상에 대한 논의가 뜨겁게 불거졌다. 이 논쟁의 계기로 이더리움과 이더리움 클래식이 분리되었다.

Reference

  1. <Ethereum Smart Contract Best Practices>
  2. <마스터링 이더리움 — 스마트컨트랙트 댑 구축하기>, 안드레아스 M. 안토노풀로스, 개빈 우드

3. <이더리움 솔리디티 — (Chap. 3) > by JeungJoo Lee님

4. <이더리움 클래식 — 해시넷>

--

--