EtherSolve: 바이트코드에서 CFG를 추출하자

김성준
CURG
Published in
14 min readApr 17, 2021

김성준, Virtual Machine & Optimization Lab in Seoul Nat’l University

본 포스팅에서는 Contro, Filippo, et al. “EtherSolve: Computing an Accurate Control-Flow Graph from Ethereum Bytecode.” arXiv preprint arXiv:2103.09113 (2021).[1]을 다룬다.

스마트 컨트랙트는 블록체인 상에서 자가 실행되는 프로그램이다.

들어가며
스마트 컨트랙트(smart contract)는 이더리움과 비트코인을 구분짓는 가장 대표적인 요소로, 블록체인이 자산의 단순한 송금 이외의 기능을 할 수 있게끔 만든다. 현재 논의되고 있는 다양한 블록체인 서비스들은 스마트 컨트랙트 위에서 설계된다고 봐도 과언이 아니다.
블록체인의 불변성(immutable)한 특성에 기반하여 동작하기 때문에, 한번 배포된 스마트 컨트랙트 및 이를 실행하는 트랜잭션들은 일단 블록체인에 저장된 이상 절대로 삭제 또는 업데이트가 불가능하다. 배포한 스마트 컨트랙트가 추후 결함이 있다고 판명되더라도 말이다. 스마트 컨트랙트가 특정 암호화폐의 자산을 관리하는 기능을 가지고 있음을 감안하면, 이러한 결함 또는 취약점은 스마트 컨트랙트의 자산이 동결되는 등 실제적인 금융 피해를 야기할 수 있다. 가장 대표적인 사례가 The DAO 해킹이며, 이는 기존 이더리움 네트워크가 이더리움 및 이더리움 클래식으로 분열되는 원인으로 작용했다.
이처럼 블록체인에 배포된 스마트 컨트랙트 중 프로그래밍 에러 또는 취약점을 내포한 것들을 호출하는 트랜잭션은 매우 위험하다. 따라서 결함이 있는 스마트 컨트랙트를 소스 코드에서 참조하거나 이를 호출하는 트랜잭션을 발생시키기 전에, 다양한 분석 도구를 통한 코드 리뷰는 필수적이다.
오늘 소개할 논문의 EtherSolve 또한 이러한 스마트 컨트랙트의 취약점 분석을 돕는 도구이다. 이제부터 EtherSolve가 어떻게 동작하며, 그 성능은 어떠한지 살펴보도록 하자.

솔리디티(Solidity)
이더리움은 블록체인 기술에 기반한 탈중앙화 어플리케이션을 위한 오픈소스 플랫폼이다. 이더리움에서는 스마트 컨트랙트라고 불리는 간단한 프로그램을 작성할 수 있는데, 이러한 스마트 컨트랙트를 통해 이더(Ether)의 흐름을 관리할 수 있다.
솔리디티는 이더리움에서 가장 널리 사용되는 프로그래밍 언어로 스마트 컨트랙트를 작성하는데 사용된다. 솔리디티는 객체 지향인 고수준 언어이며 튜링 완전하다.

솔리디티 소스 코드 예제

위의 소스 코드는 SimpleBank라는 이름의 스마트 컨트랙트를 정의한다. balances라는 필드는 스마트 컨트랙트의 내부 상태를 저장하는 키-밸류 매핑 자료구조로, 위 예시에서는 이더리움 주소를 잔고를 표현하기 위한 256 bit 정수로 매핑한다. 그 밖에 3개의 함수(deposit, deposit100, withdraw)가 스마트 컨트랙트 내부에 정의되어 있으며, transfer, revert, require 등의 구문이 활용된다.

이더리움 바이트코드(Ethereum Bytecode)
스마트 컨트랙트가 이더리움 블록체인에서 실행되기 전에, 솔리디티 소스 코드는 이더리움 바이트코드로 컴파일되는 과정을 거친다. 컴파일된 바이트 코드는 스택 자료구조 기반의 이더리움 가상 머신(Ethereum Virtual Machine, EVM)에서 실행된다.
흔히 블록체인이 제공하는 특성 중 가용성(availability)이 언급된다. 여기서 가용성이란 블록체인의 데이터는 모든 참여자의 PC(노드)에 분산 저장되므로, 그 중 어느 하 나가 문제를 일으키더라도 전체 시스템이 유지되며 24시간 중단되지 않음을 의미한다. 이더리움 스마트 컨트랙트 또한 임의의 사용자가 항상 이를 블록체인 상에서 확인할 수 있는데, 엄밀히는 스마트 컨트랙트의 소스 코드가 아닌 컴파일된 바이트코드만이 가용하다.

이더리움 바이트코드

왜 스마트 컨트랙트 취약점은 찾기 어려운가?
기존의 취약점 정적 분석 도구는 주어진 바이트코드로부터 정확한 CFG(Control-Flow Graph)를 재구축할 수 있다는 전제 하에 동작한다. 하지만 EVM 상에서 실행되는 이더리움 바이트코드는 그 디자인 방식이 일반적인 바이트코드와는 사뭇 다르다.

  • Jump destination이 opcode 파라미터로 주어지지 않는다. 대신 이전 코드에 의해 동적으로 계산된 destination address가 EVM 스택에서 가용하다고 가정한다.
  • 함수의 리턴과 관련된 opcode가 없다. 이는 함수의 리턴 주소를 스택에 push 한 뒤 jump opcode를 수행하는 과정으로 대체된다.
  • 함수는 컴파일러에 의해 제거된다. 컨트랙트 내부에서의 함수 호출은 jump 로 대체되며, 컨트랙트 간의 함수 호출에서는 디스패처(dispatcher)에 의해 관리된다.
  • 스마트 컨트랙트의 생성자(constructor)는 처음 블록체인에 배포될 때 딱 한 번만 실행된 뒤 버려지며, 생성자 바이트코드는 블록체인에 기록되지 않는다.

이러한 차이가 존재하기에 기존의 분석 도구로는 스마트 컨트랙트의 결함 및 취약점을 정확히 분석해내지 못한다. 본 논문의 EtherSolve는 이더리움 바이트코드로부터 정확한 CFG를 추출해내는 데 중점을 두고 있으며, 이렇게 생성된 CFG를 사용하여 재진입 취약점을 효과적으로 감지할 수 있는지를 평가한다.

EhterSolve의 동작 과정
EtherSolve는 아래 다섯 단계를 거쳐 정확한 CFG를 생성해낸다.

  • Bytecode parsing
  • Basic blocks identification
  • Symbolic stack execution
  • Static data separation
  • CFG decoration
Control-Flow Graph 예시

Bytecode parsing
이더리움 바이트코드는 런타임 코드와 메타데이터로 구분할 수 있다. 솔리디티 컴파일러 버전 정보는 메타데이터로부터 추출되며, 이후 메타데이터는 제거한 뒤 남은 코드를 opcode로 파싱한다.

Basic blocks identification
Basic block은 control flow 변화 없이 명령어가 수행되는 opcode의 sequence를 의미한다. JUMP, JUMPI, STOP, REVERT, RETURN, INVALID, SELFDESTRUCT 등의 opcode는 control flow를 변화시키므로 basic block의 마지막 opcode로 마크한다. JUMPDEST는 basic block의 첫 opcode로 마크한다.
일반적으로 jump는 CFG에서 basic block 간의 엣지로 표현된다. 분석을 쉽게 하기 위해, jump를 pushed jumporphan jump로 구분한다. pushed jump는 JUMP 및 JUMPI opcode 직전에 PUSH opcode가 존재하여 jump destination을 바로 알 수 있는 jump를 의미하며, 이들은 즉시 CFG의 엣지로 표현 가능하다. 추가적으로 STOP, REVERT, RETURN, INVALID, SELFDESTRUCT opcode에 대한 엣지 또한 즉각적으로 처리 가능하다.
JUMP 또는 JUMPI 직전에 PUSH opcode가 수반되지 않는 상황에서는 jump destination을 알 수 없기 때문에 적절한 엣지를 추가할 수 없다. 위의 예시에서의 131: JUMP opcode가 그러한 상황이다. 이러한 jump를 orphan jump라고 부르며, 이들은 다음 symbolic stack execution 단계에서 처리된다.

Symbolic stack execution
앞서 다룬 orphan jump의 destination을 처리하는 것이 정확한 CFG 구축의 핵심이다. 이러한 orphan jump는 함수 호출 뒤 리턴을 대체하는데 사용되기 때문에 매우 흔하다.
Symbolic stack execution 단계에서는 가상의 symbolic stack을 두어, 주어진 opcode를 순차적으로 실행해가며 jump destination에 대한 정보를 수집한다. 이때 jump 주소와 연관되는 AND, POP, PUSH, DUP, SWAP opcode만 고려하며, 다른 모든 opcode는 unknown 요소를 스택에 push 또는 pop한다.

Symbolic stack execution

위의 예시에서, 40: ADD opcode에 대하여 2개의 요소를 pop 한 뒤 1개의 unknown 요소를 push함을 확인할 수 있다. 이후 41: POP opcode까지 수행되면 symbolic stack의 top에는 0x10이 남는데, 이것이 뒤따르는 42: JUMP opcode의 destination address가 된다.

이러한 orphan jump들은 아래의 DFS 기반 알고리즘을 통해 해결된다.

Orphan jump를 해결하는 알고리즘.
  • Line 9–11: Basic block의 opcode들을 실행해가며 symbolic stack 상태를 업데이트한다.
  • Line 12–16: 업데이트된 symbolic stack으로부터 orphan jump의 destination을 해결한다. 타겟 블록은 현재 basic block의 자손 블록으로 추가된다.
  • Line 17–31: DFS 큐를 업데이트한다. 위에서 얻어진 엣지가 동일한 스택 상태에 기반하지 않았다면 자손 블록들을 큐에 추가한다. 만약 마지막 opcode가 JUMP였다면 타겟 블록만 큐에 추가한다.

Static data separation
CFG에서 static data 섹션의 모든 정보를 삭제한다. Static data section은 opcode로부터 구분할 수 있다.

CFG decoration
디스패처, fallback 함수 및 마지막 basic block 등을 하이라이트한다.

평가
본 논문에서는 총 3가지 평가 기준을 정의한다.

  • RQ1: 서로 다른 솔리디티 컴파일러 버전에서도 성공적으로 동작하는가?
  • RQ2: 기존의 다른 분석 도구들 대비 성능은 어떠한가?
  • RQ3: EtherSolve에서 얻어진 CFG로부터 재진입 취약점을 얼마나 효과적으로 감지하는가?

데이터셋은 Etherscan으로부터 무작위 스마트 컨트랙트 1,000개를 추출하여 사용하였다.

RQ1
전체 1,000개의 스마트 컨트랙트 중 3개를 제외한 모든 경우인 99.7%의 성공률로 CFG를 재구축했다. CFG 재구축에 실패한 케이스는 일반적인 솔리디티 컴파일러에 의해 생성된 패턴과 맞지 않은 경우로, 1) 솔리디티가 아닌 바이퍼(Vyper)로 컴파일된 바이트코드, 2) 길이가 0 bytes인 빈 컨트랙트, 3) 시작 세션이 일반적인 것과는 다르게 정의되어 파싱에 실패한 경우다. 이들 셋을 제외하면 솔리디티 컴파일러 버전에 구애받지 않고 모든 경우에 대해서 CFG 재구축에 성공했다.

RQ2
비교를 위한 도구로 Oyente-EthIR[2], Octopus[3], Mythril[4], Vandal[5], Gigahorse[6] 다섯 가지를 선정하였다. 솔리디티 소스 코드가 아닌 이더리움 바이트코드만을 입력으로 받으며 CFG를 출력으로 내는 도구만을 선정하였다. (Mythril만 예외적으로 trace tree를 출력으로 내보낸다.)

다른 분석 도구와의 비교

실험 결과, Oyente-EthIROctopus는 EtherSolve 대비 저조한 성공률을 보였다. Mythril의 경우 간혹 JUMPDEST opcode에 의해 basic block이 분리되지 않았고, Vandal은 SELFBALANCE opcode를 유효하지 않은 opcode로 간주 및 jump destination이 불분명할 때 모든 basic block을 자손 블록으로 가정했으며, 컨트랙트 진입점으로부터 도달할 수 없는 basic block을 계산해내는 EtherSolve와는 다르게 Gigahorse에서는 이러한 basic block들이 CFG 상에 표현되지 않았다.

RQ3
Ghaleb 및Pattabiraman에 의해 공유된 벤치마크를 활용하여 성능을 평가하였다. 해당 벤치마크는 재진입 취약점을 내포하는 42가지의 코드 스니펫이 주입된 50개의 이더리움 스마트 컨트랙트를 포함한다. 재진입 취약점에 대하여 자세히 알아보고 싶다면 다음 글을 참조하자.

재진입 취약점 분석 결과 비교

실험 결과 EtherSolve는 Slither 다음으로 높은 정확도로 재진입 취약점을 감지해냈다. 하지만 Slither가 솔리디티 소스 코드를 기반으로 취약점 분석을 수행하는데 반해, EtherSolve는 이더리움 바이트코드만을 사용했다는 점에서 의의가 있다.

결론
본 포스팅에서는 이더리움 바이트코드로부터 정확한 CFG를 추출해내고, 이를 기반으로 재진입 취약점을 효과적으로 감지해낼 수 있는 EtherSolve를 다루었다.
최근 출시되는 수많은 디파이 서비스들은 코드 보안 감사를 받는 것이 필수적으로 여겨지는 만큼, 스마트 컨트랙트와 보안은 뗄레야 뗄 수 없는 관계로 남게 되었다. 이들 서비스에 사용되는 스마트 컨트랙트가 고도화됨에 따라 더 많은 잠재적인 취약점들이 숨어있을 가능성이 높아지므로, 이에 발맞춰 더욱 정확하고 효과적인 취약점 분석 도구 개발 또한 뒷받침되어야 할 것이다.

레퍼런스
[1] https://arxiv.org/pdf/2103.09113.pdf
[2] Albert, Elvira, et al. “Ethir: A framework for high-level analysis of ethereum bytecode.” International symposium on automated technology for verification and analysis. Springer, Cham, 2018.
[3] https://github.com/pventuzelo/octopus
[4] Parizi, Reza M., et al. “Empirical vulnerability analysis of automated smart contracts security testing on blockchains.” arXiv preprint arXiv:1809.02702 (2018).
[5] Brent, Lexi, et al. “Vandal: A scalable security analysis framework for smart contracts.” arXiv preprint arXiv:1809.03981 (2018).
[6] Grech, Neville, et al. “Gigahorse: thorough, declarative decompilation of smart contracts.” 2019 IEEE/ACM 41st International Conference on Software Engineering (ICSE). IEEE, 2019.

--

--