본문으로 건너뛰기

슬리더를 사용하여 스마트 컨트랙트 버그를 찾는 방법

Solidity
스마트 컨트랙트
보안
테스트
고급
Trailofbits
2020년 6월 9일
14 분 소요

슬리더 사용 방법

이 튜토리얼의 목적은 슬리더를 사용하여 스마트 컨트랙트에서 자동으로 버그를 찾는 방법을 보여주는 것입니다.

설치

슬리더는 Python 3.6 이상이 필요합니다. pip를 통해 설치하거나 Docker를 사용하여 설치할 수 있습니다.

pip를 통한 슬리더 설치:

pip3 install --user slither-analyzer

Docker를 통한 슬리더 설치:

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox

마지막 명령은 현재 디렉터리에 액세스할 수 있는 Docker에서 eth-security-toolbox를 실행합니다. 호스트에서 파일을 변경하고 Docker의 파일에서 도구를 실행할 수 있습니다.

Docker 내부에서 다음을 실행합니다:

solc-select 0.5.11
cd /home/trufflecon/

스크립트 실행

Python 3으로 Python 스크립트를 실행하려면:

python3 script.py

명령줄

명령줄 대 사용자 정의 스크립트. 슬리더에는 일반적인 버그를 찾는 사전 정의된 탐지기 세트가 함께 제공됩니다. 명령줄에서 슬리더를 호출하면 모든 탐지기가 실행되며, 정적 분석에 대한 자세한 지식이 필요하지 않습니다:

slither project_paths

탐지기 외에도 슬리더는 프린터(printers) (새 탭에서 열림)도구(tools) (새 탭에서 열림)를 통한 코드 리뷰 기능을 갖추고 있습니다.

비공개 탐지기 및 GitHub 연동에 액세스하려면 crytic.io (새 탭에서 열림)를 사용하세요.

정적 분석

슬리더 정적 분석 프레임워크의 기능과 설계는 블로그 게시물(1 (새 탭에서 열림), 2 (새 탭에서 열림)) 및 학술 논문 (새 탭에서 열림)에 설명되어 있습니다.

정적 분석은 다양한 형태로 존재합니다. clang (새 탭에서 열림)gcc (새 탭에서 열림)와 같은 컴파일러가 이러한 연구 기술에 의존한다는 것을 알고 계실 것입니다. 하지만 이는 Infer (새 탭에서 열림), CodeClimate (새 탭에서 열림), FindBugs (새 탭에서 열림)와 같은 도구와 Frama-C (새 탭에서 열림)Polyspace (새 탭에서 열림)와 같은 정형 기법(formal methods) 기반 도구의 기반이 되기도 합니다.

여기서 정적 분석 기술과 연구를 철저하게 검토하지는 않겠습니다. 대신, 슬리더의 작동 방식을 이해하여 버그를 찾고 코드를 이해하는 데 더 효과적으로 사용할 수 있도록 필요한 내용에 집중할 것입니다.

코드 표현

단일 실행 경로에 대해 추론하는 동적 분석과 달리, 정적 분석은 모든 경로를 한 번에 추론합니다. 이를 위해 다른 코드 표현에 의존합니다. 가장 일반적인 두 가지는 추상 구문 트리(AST)와 제어 흐름 그래프(CFG)입니다.

추상 구문 트리(AST)

AST는 컴파일러가 코드를 파싱할 때마다 사용됩니다. 아마도 정적 분석을 수행할 수 있는 가장 기본적인 구조일 것입니다.

간단히 말해, AST는 구조화된 트리로, 일반적으로 각 리프(leaf)에는 변수나 상수가 포함되고 내부 노드(internal nodes)는 피연산자나 제어 흐름 연산입니다. 다음 코드를 살펴보겠습니다:

function safeAdd(uint a, uint b) pure internal returns(uint){
    if(a + b <= a){
        revert();
    }
    return a + b;
}

해당 AST는 다음과 같습니다:

AST

슬리더는 solc에서 내보낸 AST를 사용합니다.

구축하기는 간단하지만 AST는 중첩된 구조입니다. 때로는 분석하기가 가장 직관적이지 않을 수 있습니다. 예를 들어, a + b <= a 표현식에서 사용된 연산을 식별하려면 먼저 <=를 분석한 다음 +를 분석해야 합니다. 일반적인 접근 방식은 트리를 재귀적으로 탐색하는 이른바 방문자 패턴(visitor pattern)을 사용하는 것입니다. 슬리더는 ExpressionVisitor (새 탭에서 열림)에 제네릭 방문자를 포함하고 있습니다.

다음 코드는 ExpressionVisitor를 사용하여 표현식에 덧셈이 포함되어 있는지 감지합니다:

제어 흐름 그래프(CFG)

두 번째로 일반적인 코드 표현은 제어 흐름 그래프(CFG)입니다. 이름에서 알 수 있듯이 모든 실행 경로를 노출하는 그래프 기반 표현입니다. 각 노드에는 하나 또는 여러 개의 명령어가 포함됩니다. 그래프의 간선(edges)은 제어 흐름 연산(if/then/else, 루프 등)을 나타냅니다. 이전 예제의 CFG는 다음과 같습니다:

CFG

CFG는 대부분의 분석이 구축되는 기반이 되는 표현입니다.

이 외에도 많은 코드 표현이 존재합니다. 각 표현은 수행하려는 분석에 따라 장단점이 있습니다.

분석

슬리더로 수행할 수 있는 가장 간단한 유형의 분석은 구문 분석(syntactic analyses)입니다.

구문 분석

슬리더는 패턴 매칭과 유사한 접근 방식을 사용하여 코드의 다양한 구성 요소와 그 표현을 탐색하여 불일치 및 결함을 찾을 수 있습니다.

예를 들어 다음 탐지기는 구문 관련 문제를 찾습니다:

의미 분석

구문 분석과 달리 의미 분석(semantic analysis)은 더 깊이 들어가 코드의 "의미"를 분석합니다. 이 제품군에는 몇 가지 광범위한 유형의 분석이 포함됩니다. 더 강력하고 유용한 결과를 도출하지만 작성하기가 더 복잡합니다.

의미 분석은 가장 진보된 취약점 탐지에 사용됩니다.

데이터 종속성 분석

variable_a의 값이 variable_b의 영향을 받는 경로가 있는 경우 변수 variable_avariable_b에 데이터 종속적이라고 합니다.

다음 코드에서 variable_avariable_b에 종속됩니다:

// ...
variable_a = variable_b + 1;

슬리더는 중간 표현(이후 섹션에서 설명) 덕분에 내장된 데이터 종속성 (새 탭에서 열림) 기능을 제공합니다.

데이터 종속성 사용의 예는 위험한 엄격한 동등성 탐지기(dangerous strict equality detector) (새 탭에서 열림)에서 찾을 수 있습니다. 여기서 슬리더는 위험한 값에 대한 엄격한 동등성 비교를 찾고(incorrect_strict_equality.py#L86-L87 (새 탭에서 열림)), 공격자가 컨트랙트를 함정에 빠뜨리는 것을 방지하기 위해 == 대신 >= 또는 <=를 사용해야 함을 사용자에게 알립니다. 무엇보다도 탐지기는 balanceOf(address) 호출의 반환 값을 위험한 것으로 간주하고(incorrect_strict_equality.py#L63-L64 (새 탭에서 열림)), 데이터 종속성 엔진을 사용하여 그 사용을 추적합니다.

고정점 계산

분석이 CFG를 탐색하고 간선을 따라가다 보면 이미 방문한 노드를 보게 될 가능성이 높습니다. 예를 들어 루프가 아래와 같이 표시되는 경우입니다:

for(uint i; i < range; ++){
    variable_a += 1
}

분석은 언제 중지해야 하는지 알아야 합니다. 여기에는 두 가지 주요 전략이 있습니다. (1) 각 노드에서 유한한 횟수만큼 반복합니다. (2) 이른바 고정점(fixpoint)을 계산합니다. 고정점은 기본적으로 이 노드를 분석해도 의미 있는 정보가 제공되지 않음을 의미합니다.

고정점이 사용된 예는 재진입 탐지기에서 찾을 수 있습니다. 슬리더는 노드를 탐색하고 외부 호출, 스토리지 쓰기 및 읽기를 찾습니다. 고정점에 도달하면(reentrancy.py#L125-L131 (새 탭에서 열림)) 탐색을 중지하고 결과를 분석하여 다양한 재진입 패턴(reentrancy_benign.py (새 탭에서 열림), reentrancy_read_before_write.py (새 탭에서 열림), reentrancy_eth.py (새 탭에서 열림))을 통해 재진입이 존재하는지 확인합니다.

효율적인 고정점 계산을 사용하여 분석을 작성하려면 분석이 정보를 전파하는 방식에 대한 충분한 이해가 필요합니다.

중간 표현

중간 표현(IR)은 원본 언어보다 정적 분석에 더 적합하도록 만들어진 언어입니다. 슬리더는 Solidity를 자체 IR인 SlithIR (새 탭에서 열림)로 변환합니다.

기본적인 검사만 작성하려는 경우 SlithIR을 이해할 필요는 없습니다. 하지만 고급 의미 분석을 작성할 계획이라면 유용할 것입니다. SlithIR (새 탭에서 열림)SSA (새 탭에서 열림) 프린터는 코드가 어떻게 변환되는지 이해하는 데 도움이 됩니다.

API 기초

슬리더에는 컨트랙트 및 해당 함수의 기본 속성을 탐색할 수 있는 API가 있습니다.

코드베이스를 로드하려면:

from slither import Slither
slither = Slither('/path/to/project')

컨트랙트 및 함수 탐색

Slither 객체에는 다음이 있습니다:

  • contracts (list(Contract): 컨트랙트 목록
  • contracts_derived (list(Contract): 다른 컨트랙트가 상속하지 않는 컨트랙트 목록(컨트랙트의 하위 집합)
  • get_contract_from_name (str): 이름으로 컨트랙트 반환

Contract 객체에는 다음이 있습니다:

  • name (str): 컨트랙트 이름
  • functions (list(Function)): 함수 목록
  • modifiers (list(Modifier)): 함수 목록
  • all_functions_called (list(Function/Modifier)): 컨트랙트에서 도달할 수 있는 모든 내부 함수 목록
  • inheritance (list(Contract)): 상속된 컨트랙트 목록
  • get_function_from_signature (str): 서명으로 함수 반환
  • get_modifier_from_signature (str): 서명으로 제어자(Modifier) 반환
  • get_state_variable_from_name (str): 이름으로 상태 변수(StateVariable) 반환

Function 또는 Modifier 객체에는 다음이 있습니다:

  • name (str): 함수 이름
  • contract (contract): 함수가 선언된 컨트랙트
  • nodes (list(Node)): 함수/제어자의 CFG를 구성하는 노드 목록
  • entry_point (Node): CFG의 진입점
  • variables_read (list(Variable)): 읽은 변수 목록
  • variables_written (list(Variable)): 쓴 변수 목록
  • state_variables_read (list(StateVariable)): 읽은 상태 변수 목록(variables`read의 하위 집합)
  • state_variables_written (list(StateVariable)): 쓴 상태 변수 목록(variables`written의 하위 집합)