teEther: 스마트 컨트랙트 착취 코드 생성기

정다은
CURG
Published in
13 min readAug 21, 2020

본 글에서는 블록체인 기반 스마트 컨트랙트를 취약점으로부터 보호하기 위해 취약점을 자동으로 생성해주는 teEther[1]에 대해 알아본다.

비트코인과 이더리움 등의 분산 네트워크 플랫폼인 블록체인을 통해 암호화폐가 거래라는 형태로 이용되는데 이는 사전에 프로그래밍된 알고리즘에 의해 자동으로 처리된다. 특히 이더리움에서는 스마트 컨트랙트라는 프로그램을 통해 자산 가치가 있는 암호화폐를 다양한 응용 서비스에 활용할 수도 있는데 최근에는 디파이와 같은 대출 및 거래 서비스가 활성화되었다.

그러나 이러한 프로그래밍적인 처리법에는 버그나 에러가 존재할 수 있으므로 잘만 이용한다면 부당한 방법으로 이득을 취할 수 있는 이들에게 좋은 먹잇감이 된다. 그래서 스마트 컨트랙트의 다양한 취약점과 이를 막기 위한 분석기가 다양한 방면으로 연구되고 있다.

ZEUS[2], Oyente[3], 그리고 Securify[4] 등 스마트 컨트랙트의 취약점을 탐지 및 분석하는 방법도 있지만 본 글에서 다루고자 하는 teEther는 스마트 컨트랙트의 취약점을 이용해 허용되지 않은 기능을 수행하는 착취(exploit) 코드를 자동으로 생성하는 분석기이다.

스마트 컨트랙트

teEther의 스마트 컨트랙트 취약 코드 생성 과정을 이해하기 위해 스마트 컨트랙트에 대해 몇 가지 알아야 할 것들이 있다.

  • ‘스마트’란 말이 어울리게 계약 조건을 코딩하면 조건이 충족되었을 경우, 계약 내용이 자동으로 실행된다.
  • 스마트 컨트랙트 코드는 사람이 읽을 수 있는 형태의 고급언어(Solidity 등)로 작성되는데, 이 코드를 컴파일 과정을 통해 가상 머신이 읽을 수 있는 바이트 코드로 바꿔준 뒤 트랜잭션에 담겨 네트워크에 배포한다.
    (아래 그림 참고)
  • ABI(Application Binary Interface)라는 인터페이스를 통해 배포된 스마트 컨트랙트 코드에 정의된 함수를 호출하는 바이트코드를 생성한 뒤 트랜잭션으로 만들어 네트워크에 전달한다. 채굴자는 이 함수를 호출하는 바이트 코드를 스마트 컨트랙트 코드에 따라 EVM(Ethereum Virtual Machine, 이더리움 가상 머신) 위에서 실행시킬 수 있게 된다. (아래 그림 참고)
배포된 Contract 호출 과정 (출처)
  • 스마트 컨트랙트 또한 이더리움에서는 하나의 계정(Contract Account, CA)으로 존재할 수 있고 일반 노드의 계정(Externally Owned Account, EOA)이 그러한 것처럼 이더 잔액을 소유할 수 있다.
  • 모든 컨트랙트 계정은 스토리지(Storage)라는 영구적인 저장소를 가지고 있고, 컨트랙트가 종료되면 함께 사라지는 RAM과 유사한 임시로 사용하는 메모리(Memory)가 있다. 또 EVM은 총 1,024개의 명령어를 담을 수 있는 스택을 갖는다.
  • 다른 컴퓨터 연산에서와 마찬가지로 이더리움 스마트 컨트랙트는 정수(integer)에 대해 오버플로 및 언더플로가 발생하는 보안 취약점도 있고, 하나의 거래를 요청한 후 아직 그 거래가 처리되기 전에 다시 새로운 거래를 요청함으로써 이중 처리를 유도하는 공격 방법(Reentrancy) 등 다양한 약점이 존재한다.

외부 출금기능 연산

그러나 본 논문에서 다루는 teEther가 처리하는 스마트 컨트랙트의 취약점은 앞서 말한 다른 연구들에서 넓게 다루는 취약점보다는 조금 더 좁은 범위의 정의이다.

스마트 컨트랙트의 이더를 외부에 출금하는 기능을 하는 연산(Operation)들(CALL, SELFDESTRUCT, CALLCODE, DELEGATECALL)을 이용해 공격자가 이 연산들을 실행하는 동시에 그 연산들의 인자를 임의로 변경할 수 있는 상황을 취약점으로 다루고 있다.

이 중 앞의 두 연산 CALL과 SELFDESTRUCT는 이더의 직접 송금을 유발하고, 나머지 두 연산 CALLCODE와 DELEGATECALL은 스마트 컨트랙트 내 바이트 코드가 제멋대로 실행되도록 한다.

CALL 명령어

는 컨트랙트 계정 주소에 있는 이더를 to 인자에 할당된 주소로 송금한다. 공격자가 이 인자값을 바꿀 수 있다면 계약이 의도한 주소가 아닌 공격자가 원하는 계좌로 이더를 송금할 수 있게 된다.

SELFDESTRUCT 명령어

는 계약을 종료시킬 때 사용되는데, 계약이 종료될 때 컨트랙트 계정에 남아있는 모든 돈을 target 인자에 할당된 주소로 보낸다. 공격자는 마찬가지로 이 인자값을 바꾸고 SELFDESTRUCT 명령어를 실행시킬 수 있다면 계정의 모든 잔액을 취할 수 있게 된다.

CALLCODE 명령어

는 이 논문이 발표된지 3개월 후 솔리디티 v0.5.0 이후로는 사라졌기 때문에 설명은 생략하겠지만 돈을 받는 to인자에 할당된 주소값을 변경시키는 방법으로 공격자가 이득을 취할 수 있는 명령어였다.

DELEGATECALL 명령어

는 위임(delegate)이라는 뜻처럼 한 컨트랙트(caller)가 다른 컨트랙트(callee)를 호출할 때 callee의 코드가 caller 컨트랙트의 저장소, 잔액 등을 위임받아 실행이 된다. 이 때, callee의 컨트랙트 코드에 SELFDESTRUCT 명령어와 to인자값을 변경시키는 명령어가 담겨있는 채로 실행된다면 caller의 컨트랙트 코드가 종료되고 그 컨트랙트 계정의 잔액이 모두 공격자가 원하는 주소로 넘어가게 된다.

teEther가 다룰 취약 상태 정의

위의 네가지 외부 출금 기능을 하는 연산을 바탕으로 본 논문에서 다루고자 하는 ‘취약한’, 즉 컨트랙트의 잔액을 빼앗길 수 있는 상태를 정의해볼 수 있다.

정의 1) 치명적인 경로(Critical Path)

앞서 소개한 4가지 연산이 실행될 수 있는 경우들이다.
1) CALL명령어의 value 인자가 0이 아니고, to인자가 외부에서 변경가능한 상태, 2) SELFDESTRUCT 명령어의 target 인자가 변경 가능한 상태, 그리고3)CALLCODE 명령어와 DELEGATECALL 명령어의 to인자가 외부에서 변경 가능한 상태로 구성된다.

정의 2) 취약한 상태(Vulnerable State)

한 트랜잭션이 컨트랙트를 정의1 치명적인 경로의 상태 중 하나를 실행할 수 있는 가능성이 있다면 그 계약은 취약한 상태라고 할 수 있다. 그리고 취약한 상태의 계약을 유발하는 트랜잭션을 치명적인 트랜잭션(critical transaction)이라고 한다.

정의 3) 상태 변경 경로(State Changing Path)

이 외에도 컨트랙트의 스토리지를 이용해 외부 출금이 가능한 경우가 있다. 아래 그림의 컨트랙트를 예로 들자면, transfer는 컨트랙트 잔액을 attacker에게 보내야 하지만 line7의 require문이 충족되지 않으면 실행되지 않는다. line2에서 vulnerable 변수가 false로 선언되어 있기 때문에 transfer함수는 실행될 수 없다.

하지만 만약 SSTORE 명령을 통해 스토리지에 저장된 vulnerable 변수의 값을 변화시킨다면 line3의 makeVulnerable함수를 실행하지 않아도 line8의 transfer가 실행되어 attacker가 잔액을 얻을 수 있다.

치명적인 상태의 원인이 될 수 있기 때문에 SSTORE 명령을 실행하게 하는 경우를 상태 변경 경로로 정의한다.

Stateful contract

정의 4) 상태 변경 트랜잭션(State Changing Transaction)

실행과정에서 정의 3 상태 변경 경로를 유발할 수 있는 트랜잭션을 상태 변경 트랜잭션으로 정의한다.

정의 5) 취약(Vulnerable)

만약 정의 2의 취약한 상태로 이어지는 상태 변경 트랜잭션들이 하나 이상 연달아 일어나면 그 계약은 취약하다고 한다.

teEther !

를 이용하면 앞서 정의한 취약점들을 이용해 컨트랙트의 잔액을 빼돌릴 수 있는 착취 코드를 자동으로 생성할 수 있다.

아래 그림은 teEther의 전체 아키텍처이다. 먼저 분석하고자 하는 스마트 컨트랙트의 바이트코드를 OPCODE 단위의 실행 그래프인 제어 흐름 그래프(Control Flow Graph, CFG)로 만든다. 그런 다음 만든 제어 흐름 그래프에서 미리 정의한 치명적인 명령어들인 CALL, SELFDESTRUCT, CALLCODE(고인 명령어), DELEGATECALL들과 상태 변경 명령어인 SSTORE를 찾는다(critical instructions 단계). 그 뒤 이 4가지 연산을 목표로 하는 Critical Path, 즉 트랜잭션을 탐지하고, 연산의 중요 인자 값들에 대한 변조 가능여부를 분석하는 방식으로 착취 코드를 생성한다.

teEther의 전체 구조

바이트코드에서 착취 코드까지의 각 단계 모듈 하나 하나를 구체적으로 살펴보자.

CFG(Control Flow Graph) Recovery

teEther는 CALL이나 DELEGATECALL 같은 명령어 단위의 취약점을 이용한 착취 코드를 만들어야 한다. 그러나 분석해야 하는 스마트 컨트랙트는 컴파일된 바이트 코드의 형태이기 때문에 먼저 명령어 단위의 실행 흐름으로 복원시켜야 한다. 아래의 그림은 제어 흐름 그래프로 바이트 코드가 명령어 단위로 복원되고 전체 코드의 흐름을 볼 수 있다.

제어 흐름 그래프 예제

그러나 제어 흐름 그래프로 바꾸는 과정은 ‘JUMP’나 ‘JUMPI’같은 분기 명령어때문에 쉽지 않다. 그래서 teEther는 제어 흐름 그래프를 만들기 위해 반복적으로 ‘백워드 슬라이싱’기술을 이용한다.

백워드 슬라이싱은 프로그램의 어떤 라인에서 사용되는 특정한 변수에 대해 해당 변수에 영향을 주는 모든 코드를 찾아내는 기술로, 실행 순서의 역방향으로 코드를 잘라낸다는 의미에서 백워드 슬라이싱이라는 이름을 갖게 되었습니다. (출처)

Path Generation

스마트 컨트랙트의 명령어들로 이루어진 흐름 그래프를 찾아냈으니 이제 4가지 치명적 명령어인 CALL, DELEGATECALL, SELFDETRUCT를 찾아야한다.

한 번 해당 명령어를 찾게 되면 백워드 슬라이싱 기술을 통해 해당 명령어들의 치명적인 경로인 인자들을 찾아야한다(CALL의 to나 DELEGATECALL의 target). 공격자가 이 인자들을 조작할 수 있어야하기 때문에 (ORIGIN, CALLER, CALLVALUE, CALLDATALOAD, CALLDATASIZE, CALLDATACOPY) 명령어들과 (SLOAD, MLOAD)명령어들을 슬라이싱으로 필터링한다.

그리고 상태 변화 경로를 유발하는 SSTORE 명령어를 찾아야 한다. 단, 앞의 치명적 명령어와는 달리 공격자가 주소(address)나 금액(value)같은 값을 변경할 필요가 없기 때문에 이 경로에서는 인자에 대한 백워드 슬라이싱이 필요없다.

Constraint Generation

이 단계에서는 전 단계에서 찾은 착취 경로에 제약이 걸리는 조건들을 정의하고 있다.

아래 예시 코드를 보면 경로 생성에 의해 코드의 실행 순서는 1->2->4->5->6이다. 그러나 이 경로를 실행할 때, line5의 x의 값은 line3의 할당을 건너뛰기 때문에 항상 고정값 0이 된다. 하지만 경로 생성 과정에서는 이런 조건들을 고려하지 못하고 프로그램 카운터가 line6에서의 실행 여부를 모두 반영하기 때문에 실행 불가능한 경로들도 추출하게 된다. 이런 실행되지도 않을 비싼 기호를 실행하는 경로들을 제거하기 위해 마지막 명령엉 대해 백워드 슬라이스 처리를 하여 제거하는 기법을 사용한다.

실행 불가능한 Path 예제

이 외에도 EVM에서 SHA3 암호화의 메모리 사용 방식에 따라 스토리지 접근에 대한 추론을 반영하여 불필요한 경로 계산을 줄일 수 있도록 암호화 연산에 대한 특별한 모델링 기법을 추가한다.

Exploit Generation

teEther의 마지막 단계인 착취 생성 모듈은 이전 단계에서 생성된 제약 조건들에 대한 충족 가능성을 체크한 경로들로만 스마트 컨트랙트의 돈을 뽑아 먹을 수 있는 트랜잭션들을 출력해낸다.

성능

기호 실행 기법을 바탕으로 한 착취 코드를 자동으로 생성하는 분석기인 teEther에 대해 알아봤다. 총 38,757 개의 중복되지 않는 스마트 컨트랙트를 대상으로 점검하여 총 815개(2.10%)의 실행 가능한 착취 코드를 자동을 생성한다. (개중에는 고인 명령인 CALLCODE로 인한 착취 가능한 계약도 2개 포함되어 있다.) 아래 표의 exploit 행의 Contract 개수로 이를 확인할 수 있다.

Detailed exploit generation results

그리고 CFG를 만드는데 최대 30분, 치명적 연산자들 기반의 착취 코드를 찾는데 추가로 30분을 시간 제한으로 두었음에도 불구하고 33,195개(85.65%)의 컨트랙트만 주어진 제한 시간 안에 분석을 끝낼 만큼 시간이 매우 오래 걸린다.

결론

스마트 컨트랙트 코드의 치명적 경로와 상태 변화 경로를 탐색해서 결국 착취 가능 여부를 알려주는 teEther에 대해 살펴봤다. 성능 면에서 뛰어난 논문은 아니지만 바이트 코드를 복원하는 과정에서 기존의 백워드 슬라이싱을 반복적으로 적용해서 제어 흐름 그래프를 만든 점, 그리고 그래프를 이용해 치명적 연산자들의 호출 가능성을 분석한 점은 어렵지만 좋은 시도였다고 생각한다.

참고문헌

[1] J. Krupp and C. Rossow. teether: Gnawing at ethereum to automatically exploit smart contracts. In 27th USENIX Security Symposium (USENIX Security 18). 2018

[2] Sukrit Kalra, Seep Goel, Mohan Dhawan, and Subodh Sharma. 2018. ZEUS: Analyzing Safety of Smart Contracts. In 25th Annual Network and Distributed System Security Symposium (NDSS).

[3] L. Luu, D.-H. Chu, H. Olickel, P. Saxena, and A. Hobor. Making smart contracts smarter. Cryptology ePrint Archive, Report 2016/633, 2016.

[4] P. Tsankov, A. Dan, D. D. Cohen, A. Gervais, F. Buenzli and M. Vechev, Securify: Practical security analysis of smart contracts, 2018.

--

--