[Mastering Ethereum] Chapter9–1

Flat_Water
HYBLOCK
Published in
6 min readJul 22, 2022

Smart Contract Security

Publisher

HYU Information System 박평수

스마트 컨트랙트에 있어서 가장 중요한 요소 중 하나는 바로 ‘보안’이다. 스마트 컨트랙트는 모두 공개되어 있으며, 스마트 컨트랙트의 취약점으로 인한 손실을 결코 복구될 수 없다. 이번 포스팅에서는 이러한 보안의 모범 사례, 디자인 패턴, 그리고 스마트 컨트랙트에서 취약성을 가져올 수 있는 사례와 패턴을 의미하는 ‘보안 안티패턴’에 대하여 절반 가량 정도를 알아볼 것이다.

📌 목차

✔ 보안 모범 사례

✔ 보안 위험 및 안티패턴 ( 취약점 / 예방 기법 / 실제 사례 )

1️⃣ 보안 모범 사례

스마트 컨트랙트 코드를 작성함에 있어 기본적으로 요구되는 사항들에 대해 소개한다.

  • 미니멀리즘 / 단순성 : 단순하고 간결한 코드가 곧 보안과 직결된다. 복잡하고 기능이 많을수록 버그나 예상치 못한 효과가 생길 가능성이 높다. 따라서 스마트 컨트랙트 프로그래머는 최대한 단순하고 간결하게 컨트랙트를 작성해야 한다.
  • 코드 재사용 : 초반부에 언급한 DRY (Don’t Repeat Yourself) 원칙을 고수해야 한다. 이미 있는 코드가 더 많이 사용되고, 그 안전성을 검증받았다. 따라서 기존에 존재하는 코드들을 재사용하려는 노력을 기울여야 한다.
  • 코드 품질 : 스마트 컨트랙트로 쓰여진 코드는 다시 고칠 수 없다. 코드 한줄한줄이 금전적인 부분과 연결되어있기에, 스마트 컨트랙트 프로그래머는 코드를 작성함에 있어 배포 전까지 실수 없이 작성해야만 한다.
  • 가독성 / 감사 용이성 : 이더리움 커뮤니티의 스타일과 규칙들에 따라 다른 구성원들과 협업 가능성을 염두에 두고 단순하고 알아보기 쉽게 코드를 작성해야 한다.
  • 테스트 범위 : 개발 도중 시도해볼 수 있는 모든 경우의 수들을 시험해봐야 한다. 한 번 배포되면 그것으로 끝이니, 모든 변수가 차단되었는지 끊임없이 테스트 하여야 한다.

2️⃣ 보안 위험 및 안티 패턴

스마트 컨트랙트는 위험에 노출되지 않고 피할 수 있어야 한다. 이를 위해서 공통적인 보안 위험, 취약성이 발생할 수 있는 사례와 해결 방법에 대해 소개한다.

✔ 재진입성

스마트 컨트랙트는 외부 컨트랙트의 코드를 호출하고 사용할 수 있다. 이때, 특정 컨트랙트를 호출했을 때 만약 해당 호출 주소가 내부에서 확인되지 않으면, 디폴트 함수인 폴백 함수(Fallback)를 자동으로 실행한다. 공격자는 이를 악용하여 대체 코드를 실행하도록 강제하여 공격을 시도한다.

  • 취약점

공격자는 공격하려는 컨트랙트가 호출하려는 외부 주소에 악의적인 코드를 만들어 놓을 수 있다. 이러한 악성 코드는 공격자가 만들어 놓은 외부 주소가 호출되는 즉시 호출자가 원치 않는 작업을 시행한다. 이를 우리는 악성 코드가 취약한 컨트랙트의 함수를 호출하고 이에 진입하다는 의미를 담아 ‘재진입성 ’(Re-entrancy)이라고 부른다.

  • 예방 기법

이러한 ‘재진입성’을 예방하는 방법은 크게 세 가지가 있다. 첫 번째는 외부 컨트랙트를 호출할 때 transfer 함수를 사용함으로서 송신 컨트랙트에 재진입하기에 충분하지 않은 가스를 보내는 것을 통해 재진입성을 예방하는 방법이다. 두 번째는 이더가 컨트랙트에서 전송되기 전에 모든 상태 변수를 변경하는 것이다. 즉, 외부 함수 호출 코드가 가장 마지막 작업이 되게 만드는 것이다.(체크 효과 상호작용 패턴) 마지막으로, 뮤텍스를 도입하는 방법이 있다. 코드 실행 중에 컨트랙트를 잠그는 상태 변수를 추가하면 재진입 호출을 방지할 수 있을 것이다.

  • 실제 사례 : DAO 공격

✔ 산술 오버플로 / 언더플로

오버플로(overflow)는 숫자가 최댓값 이상으로 증가하는 것을 의미한다. 언더플로(underflow)는 숫자가 최솟값 이하로 감소하는 경우를 의미한다. 이러한 현상이 발생하는 이유는 이더리움 가상 머신이 특정 범위의 숫자만 나타낼 수 있기 때문이다. (예를 들어, uint8은 0~255 범위의 숫자만 저장할 수 있다.) 즉, 사용자의 입력을 점검하지 않은 채 계산을 수행하면 데이터의 유형에 따라 범위를 벗어나는 숫자가 될 수 있어 솔리디티 변수를 악용할 수 있게 되는 것이다.

  • 취약점

변수의 데이터 타입 범위를 벗어나는 숫자를 고정 크기 변수에 저장해야 하는 연산이 수행되면 오버플로 / 언더플로가 발생한다.

예를 들어, 위에 언급한 uint8의 데이터 타입을 갖고 있는 변수는 0~255 범위의 숫자만을 저장할 수 있다. 이때, 사용자가 값이 0인 변수에 260을 더하면 0~255의 범위를 한바퀴 돌고 4가 된다. 이를 우리는 데이터 타입 범위의 숫자를 추가하였다고 하여 오버플로라고 일컫는다. 반대로 값이 0인 변수에 1을 빼주면 -1의 값을 데이터 타입 범위 보다 작은 숫자를 표현하기 위해 순환하여 255가 되는데, 이러한 현상을 언더플로라 한다. 즉, 값이 의도치 않은 값으로 바뀔 수 있는 것이다.

이러한 특성을 이용하여 공격자는 예상치 못한 논리적 흐름을 만들어 낼 수 있다.

  • 예방 기법

현재 이더리움 가상 머신에서 사용할 수 있는 자체적인 방법은 없다. 하지만 표준 수학 연산자인 사칙연산을 할 수 있는 라이브러리를 사용하거나, 혹은 만들어 적용한다면 오버플로/언더플로로 인해 발생하는 취약점을 해결할 수 있다.

# 오픈제플린 (SafeMath 라이브러리)

  • 실제 사례 : PoWHC 및 일괄 전송 오버플로(CVE-2018–10299)

✔ 예기치 않은 이더

보통 이더가 컨트랙트에 전달되는 방법은 두 가지인데, 컨트랙트에 정의된 함수를 이용하여 실행하는 방법, 그리고 폴백 함수를 실행하는 것이다. 하지만 예외 또한 존재하는데, 어떤 코드를 실행하지 않고 컨트랙트 내에 이더가 존재할 수 있는 경우이다. 따라서 코드 실행에 의존하는 컨트랙트는 이더가 강제로 전송되는 공격에 취약하다.

# 참고 자료 : ‘이더를 강제 전송하기’

  • 취약점

올바른 상태 전이 또는 유효성 검사를 강제하는데 가장 유용한 방법은 ‘불변 검사’ 이다. 이 기법은 연산 후에 변경되지 않는 불변량을 정의하고 변경되지 않았는지 확인한다. 보통 프로그래머들은 이 기법을 사용하는데, 한 가지 간과하고 있는 사실이 있다. “ 내부의 이더 잔액” 이다. 개발자들은 종종 컨트랙트가 payable 함수를 통해서만 이더를 받아들이거나 얻을 수 있다고 착각한다. 하지만 payable 함수를 사용하지 않고 컨트랙트로 보내질 수 있는 방법이 두 가지가 존재한다.

1.자기 파괴 (self-destruct / suicide)

모든 컨트랙트는 self-destruct 함수를 구현할 수 있다. 이 함수는 컨트랙트 주소에서 모든 바이트 코드를 제거하고 저장된 모든 이더를 파라미터로 지정된 주소로 보낸다. 이렇기에 self-destruct 함수는 컨트랙트의 코드와 상관없이 이더를 강제로 임의의 컨트랙트에 전송할 수 있다.

2.미리 보내진 이더

컨트랙트의 주소는 미리 알아낼 수 있기 때문에 미리 계산된 주소로 이더를 전송할 수 있다. 컨트랙트의 코드가 실행되기 전에 미리 보내놓는 것이다. 그렇게 되면 실제 컨트랙트 코드가 실행되었을 때 해당 주소에 있는 이더의 값은 0이 아니게 된다.

  • 예방 기법

예기치 않은 이더의 경우 대부분의 문제가 컨트랙트 잔액을 의미하는 this.balance에서 발생한다. 그렇기 때문에 this.balance에 의존하지 않는 로직을 만드는 것이 중요하다. ( 자체 정의된 변수를 사용 )

  • 추가 예제

악용 가능한 컨트랙트의 몇 가지 예를 솔리디티 코딩 콘테스트에서 찾아볼 수 있다.

✔ DELEGATECALL

DELEGATECALL은 솔리디티 메세지 호출 함수이다. DELEGATECALL 연산 코드는 이더리움 개발자가 코드를 모듈화할 수 있게 하는데 유용하다. 타깃 주소의 코드가 호출하는 컨트랙트 상황에서 실행되는데, 이때 호출 당하는 함수의 제어자가 public 이라면, 누구든지 접근 권한을 갖게 되기 때문에 심각한 보안 문제가 생길 수 있다.

  • 취약점

위에서 언급하였듯, 다른 컨트랙트의 컨텍스트에서 라이브러리 컨텍스트를 실행할 때, 타깃 함수의 제어자가 public이라면 취약점이 발생한다.

  • 예방 기법

솔리디티는 라이브러리 컨트랙트 구현을 위한 키워드를 제공해서 라이브러리 컨트랙트가 stateless이고 비자기파괴적(non-self-destructable)임을 보장해준다. 이를 통해 개발자들은 DELEGATECALL 라이브러리 컨트랙트와 호출 컨트랙트의 가능한 호출 컨트랙트에 세심하게 신경을 쓰고, 가능한 경우 stateless 라이브러리를 만들어야 한다.

  • 실제 사례 : 패리티 멀티시그 지갑

✔ 디폴트 가시성

솔리디티 함수는 총 네 가지 함수 호출 방법이 있다. (external / public / internal / private) 이러한 함수 호출 방식을 지정하는 것을 “가시성” 이라고 한다. 이 중에서 함수는 기본적으로 public인데, 이를 디폴트 가시성이라 한다.

# 솔리디티 문서

  • 취약점

함수에 대한 디폴트 가시성이 public이므로, 따로 가시성을 지정해주지 않으면 자동으로 그 함수는 외부에서 호출할 수 있는 함수가 된다는 취약점을 갖는다.

  • 예방 기법

컨트랙트의 모든 함수에는 반드시 가시성을 지정해주어야 한다.

  • 실제 사례 : 패리티 멀티시그 지갑

✔ 엔트로피 환상

이더리움 블록체인의 모든 트랜잭션은 결정론적 상태 전이 연산이다. 이는 곧 모든 트랜잭션이 이더리움 생태계의 전체 상태를 불확실성 없이 계산 가능한 방식으로 변경한다는 것을 의미한다.

  • 취약점

이더리움 플랫폼을 기반으로 한 최초의 컨트랙트 중 일부는 도박을 기반으로 하였다. 도박에는 근본적으로 불확실성이 필요하기 때문에 블록체인과 같은 결정론적 시스템에 도박 시스템을 구축하는 것은 어렵다. 하지만 미래의 블록 변수 ( 해시, 타임 스탬프, 블록 번호와 가스 한도)는 이를 채굴하는 채굴자가 통제하기 때문에, 궁극적인 무작위 값이 아닌 조작할 수 있는 값이 되어버린다는 취약점을 갖는다.

  • 예방 방법

이러한 취약점을 예방하기 위한 방법으로는 내부의 블록 변수들을 채굴자가 조정할 수 있으므로, 엔트로피(무작위성)의 원천은 블록체인 외부에 설정하는 방법이 존재한다. 즉, 블록 변수는 채굴자가 조작할 수도 있기에 엔트로피의 원천으로 사용하면 안된다.

  • 실제 사례 : PRNG 컨트랙트

--

--