4주차 공부 - 1

박세연·2021년 1월 18일
0

Mastering Ethereum

목록 보기
4/10
post-thumbnail

Chapter 9-1. 스마트 컨트랙트 보안

보안 모범 사례

✔️ 미니멀리즘/단순성 : 코드가 단순할수록, 코드가 적을수록 버그나 예기치 못한 효과가 발생할 확률이 낮다.
✔️ 코드 재사용 : 두 번 이상 반복된 코드 문장이 있으면 함수 또는 라이브러리로 작성하여 재사용한다.
✔️ 코드 품질 : 스마트 컨트랙트 코드는 일단 배포되고 나면 문제를 해결할 수 있는 방법이 거의 없다. 컨트랙트의 버그는 금전적 손실을 발생시킬 수 있으므로 스마트 컨트랙트 코드는 실수를 용납하지 않는다.
✔️ 가독성/감사 용이성 : 코드는 명확하고 이해하기 쉬워야 한다. 읽기 쉬울수록 감시하기도 쉽다. 이더리움 커뮤니티의 스타일과 명명 규칙에 따라 잘 문서화되고 읽기 쉬운 코드를 작성해야한다.
✔️ 테스트 범위 : 모든 인수를 테스트하여 코드 실행을 계속하기 전에 모든 인수가 예상 범위 내에 있는지와 올바르게 형식이 지정되었는지를 확인한다.

보안 위험 및 안티 패턴

스마트 컨트랙트 프로그래머라면 컨트랙트를 위험에 노출시키는 프로그래밍 패턴을 감지하고 피할 수 있도록 가장 공통적인 보안 위험에 익숙해야 한다.


💡 재진입성

✔️ 이더리움 스마트 컨트랙트의 특징 중 하나는 다른 외부 컨트랙트의 코드를 호출하고 활용할 수 있다는 점이다.
✔️ 컨트랙트는 일반적으로 이더를 처리하기 때문에 종종 다양한 외부 사용자 주소로 이더를 전송하는데, 이 경우 컨트랙트는 외부 호출을 요청해야함.
✔️ 이 경우, 공격자가 컨트랙트에 콜백을 포함하여 대체 코드를 실행하도록 강제할 수 있다.

취약점

이러한 유형의 공격은 컨트랙트가 알 수 없는 주소로 이더를 전송할 때 발생할 수 있다.

depositFunds함수는 발신자의 잔액을 증가시킨다.
withdrawFunds함수를 사용하여 발신자가 출금할 금액을 지정하고, 출금시킬 수 있다.
✅ 이 컨트랙트에서의 취약점은 이더를 전송하는 16행에 있다.


Attack컨트랙트에서 생성자는 etherStore변수를 EtherStore의 스마트 컨트랙트 주소(공격 대상 컨트랙트 주소)로 초기화한다.
✅ 공격자는 attackEtherStore함수를 1보다 크거나 같은 양의 이더로 호출한다.
EtherStore컨트랙트의 현재 잔액은 10 이더라고 가정한다.

✅ 15행에서 발신자(현재 공격자 컨트랙트 주소)의 balances에 1 이더를 예치한다.
✅ 17행에서 출금함수를 실행한다. (Attack 컨트랙트로 1 이더를 전송)
✅ 이더를 전송하는 트랜잭션이 실행되었으므로 25행의 폴백함수가 실행된다. 여기서 EtherStore 컨트랙트로의 재진입이 발생한다.
etherStore 컨트랙트의 17행이 실행되지 않아서 매핑한 balances[공격자컨트랙트 주소]의 잔액은 계속 1 ether로 남아있게 된다.
etherStore의 잔액이 1 ether 이거나 그 이하가 될 때 까지 withdrawFunds()함수를 계속 실행하게된다.
✅ 결국 etherStore 컨트랙트에 1 ether만 남기고 나머지를 모두 공격자의 컨트랙트로 출금할 수 있게 된다.

예방 기법

  • 이더를 외부의 컨트랙트에 보낼 때 transfer함수를 사용한다.
    transfer함수는 외부 호출에 대해 가스 제한이 있으므로 컨트랙트에 재진입하는 공격을 막을 수 있다.

  • 이더가 전송되기 전에 상태 변수를 변경하는 모든 로직이 발생하도록 한다.
    EtherStore 컨트랙트의 17, 18행을 이더를 전송하는 16행 앞에 넣는 것이다.
  • 뮤텍스(mutex) 도입. 코드 실행 중에 컨트랙트를 잠그는 상태 변수를 추가하여 재진입 호출을 방지한다.



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

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

취약점

✔️ 언더플로(underflow) : uint8(부호 없는 8비트 정수) 변수의 값이 0일때, 1을 빼면 결과는 255(8비트에서 가장 큰 수). 나타낼 수 있는 범위 아래에 숫자를 할당할 경우
✔️ 오버플로(overflow) : uint8 변수는 나타낼 수 있는 가장 큰 값이 255인데, 이 변수의 초기값이 0일 때 256을 더하면 0이 되고 257을 더하면 1이 된다. 나타낼 수 있는 범위 이상의 숫자를 할당할 경우
이렇게 오버플로/언더플로를 이용해 공격자가 코드를 악용하여 예기치 않은 논리 흐름을 생성할 수 있다.

✅ 10행과 19행을 보면, 해당 컨트랙트에 이더를 보관하면 일주일 동안은 출금할 수 없게 설계한 것을 알 수 있다.
increaseLockTime함수를 이용해서 보관 기간을 연장할 수 있지만 최소 일주일은 컨트랙트에 이더를 잠궈놓을 수 있다.
✅ 여기서 공격자가 오버플로를 사용하여 lockTime과 무관하게 컨트랙트에서 이더를 빼낼 수 있다.
lockTimepublic으로 선언된 공개 변수이기 때문에 마음대로 접근 혹은 변경할 수 있다.
increaseLockTime함수의 매개변수로 2^256 - userLockTime을 전달하게 되면 오버플로가 발생한다.

즉, lockTime[msg.sender] = 2^256을 대입하게 되므로, uint의 최대 범위를 초과하게 되어 lockTime[msg.sender]가 0으로 설정된다.

✅ 그러면 19행의 조건을 1주일이 지나지 않더라도 만족하게 되므로 컨트랙트에서 이더를 빼낼 수 있게 된다.

예방 기법

표준 수학 연산자인 더하기, 빼기, 곱하기를 대체하는 수학 라이브러리를 사용한다.
책 205쪽의 SageMath 라이브러리처럼 오버플로가 발생하지 않는 수학 연산 라이브러리를 추가하고, 모든 수학 연산을 새로 정의한 연산으로 대체하면 오버플로/언더플로에 의한 취약점을 방지할 수 있다.


💡 예기치 않은 이더

일반적으로 이더가 컨트랙트에 전달될 떄는 폴백 함수나 컨트랙트에 정의된 또 다른 함수를 실행해야한다.
전송되는 모든 이더에 대해 코드 실행에 의존하는 컨트랙트는 이더가 강제로 전송되는 공격에 취양하다.

취약점

보통 컨트랙트가 payable 함수를 통해서만 이더를 받아들이거나 얻을 수 있다고 오해한다.
여기서 this.balance를 잘못 사용하여 컨트랙트 내부의 이더 잔액에 대한 잘못된 가정을 하게될 수 있다.

payable함수나 컨트랙트의 코드를 실행하지 않고 컨트랙트에 이더를 보낼 수 있는 방법

▶️ 자기파괴(self-destruct/suicide)
컨트랙트는 (이전 챕터에서 배웠던) selfdestruct 함수를 구현할 수 있는데, 이 함수는 컨트랙트 주소에서 모든 바이트코드를 제거하고 컨트랙트에 저장된 모든 이더를 파라미터로 지정된 주소로 보낸다. 이렇게 이더를 전송할 때 폴백함수 혹은 다른 함수를 호출하지 않는다. 이 함수를 악용해서 공격자는 selfdestruct 함수를 가진 컨트랙트를 만들고 이 컨트랙트에 이더를 보낸 다음 selfdestruct(target)을 호출해서 target 컨트랙트에 강제로 이더를 전송할 수 있다.


▶️ 미리 보내진 이더
컨트랙트의 주소는 결정론적이기 때문에(해시 값 계산) 누구나 컨트랙트가 생성되기 전에 컨트랙트의 주소를 알 수 있고, 이 주소로 이더를 보낼 수 있다. 그러면 실제 컨트랙트가 생성된 시점에 해당 컨트랙트의 잔액은 0이 아니게 된다.


✅ 위 컨트랙트의 목적은 플레이어가 0.5이더를 컨트랙트에 보내는데 누군가 마지막 이정표(10이더)에 도달하면 보상을 받는 그런 게임이다.
✅ 15행에서 currentBalance는 현재 이 컨트랙트의 잔액에 플레이어가 보낸 이더(0.5이더)를 더한다.
✅ 그래서 currentBalance가 항상 0.5이더의 배수라고 기대하고 코드를 실행할 것이다.
✅ 그런데 여기서 앞서 설명한 selfdestruct함수를 이용해 이 컨트랙트에 강제로 이더를 전송할 수 있다.
✅ 예를 들어 0.1이더를 보내면 currentBalace가 0.5의 배수가 되는 상황이 일어나지 못해서 게임이 원하는대로 수행되지 않게 만들 수 있다.

예방 기법

✅ 위에서의 문제는 this.balance의 사용에서 발생한다.
✅ 따라서 컨트랙트의 잔액(this.balance)에 의존하는 로직을 짜지 말고 새로운 변수를 정의하는 것이 안전하다.

depositedWei라는 새로운 변수를 정의하고, 이 변수를 이용해서 입금된 이더를 추적하도록 수정한다.
✅ 이 변수는 selfdestruct호출로 보내진 이더에 영향을 받지 않는다. (즉, 컨트랙트의 잔액을 이용한 변수가 아니다.)


💡 DELEGATECALL

✔️ Delegatecall은 호출하는 컨트랙트의 맥락에서 타겟 주소의 코드가 실행된다.
✔️ 이 경우 msg.sendermsg.value의 값이 변경되지 않는다.
➡️ 컨트랙트가 실행될 때 다른 주소로부터 코드를 동적으로 읽어들일 수 있다는 것을 의미한다.
저장 장소, 현재 주소, 잔액은 호출하는 주소를 참조하고, 실행하는 코드는 호출된 주소에서 읽어들인다.
단순하게 생각하면 A 컨트랙트에서 B라는 컨트랙트에 있는 코드를 A에 있는 코드인 것처럼 사용할 수 있다.
이러한 특성으로 calldelegatecall 연산코드는 이더리움 개발자가 코드를 모듈화할 수 있게 해준다.

취약점

다른 애플리케이션의 컨텍스트에서 라이브러리 컨트랙트를 실행할 때 취약점이 발생할 수 있다.


✅ n번째 피보나치 수를 계산하는 함수를 갖는 컨트랙트 (라이브러리)


✅ 위 컨트랙트를 통해 사용자는 이더를 출금할 수 있는데, 출금하는 금액이 피보나치 수에 상응하게된다.
✅ 즉, 첫번째 - 1이더, 두번째 - 1이더, 세번째 - 2이더, 네번째 - 3이더, 다섯번째 - 5이더 ...
fibSig 변수 : setFibonacci(uint256)문자열의 Keccack-256(SHA-3)해시 값의 처음 4 바이트 값
✅ 앞의 FibonacciLib 컨트랙트에서 실행할 함수를 지정하는 역할을 하고, 그 다음 인수는 해당 함수에 건네주는 파라미터이다.
✅ 즉, FibonacciBalancewithdraw함수의 delegatecall함수에서 FibonacciLibsetFibonacci함수를 실행하고 그 함수에 전달하는 파라미터는 withdrawalCounter값이다.

상태변수가 실제로 컨트랙트에 저장되는 방법

상태 변수 혹은 스토리지 변수는 컨트랙트에 도입될 때 순차적으로 슬롯(slot)에 배치된다.

✅ 여기서 delegatecall을 통해 실행되는 코드는 호출하는 컨트랙트의 상태에 따라 동작한다.
✅ 위의 FibonacciBalance컨트랙트의 18행에서FibonacciLibsetFibonacci를 호출하고, fibonacci함수를 실행하게 되는데, 이때 현재 호출 컨텍스트의 Slot[0]을 참고하게 되어 fibonacciLibrary인 주소를 start로 사용하게 된다.
✅ 주소값은 매우 크기 때문에 초기값인 start값이 매우 커지게 되고 withdraw함수를 원하는 대로 실행 할 수 없게 된다.

더 큰 문제점

FibonacciBalance 컨트랙트 23행의 폴백함수는 사용자가 FibonacciLibrary의 모든 함수를 호출할 수 있게 허용한다.
FibonacciLibsetStart함수는 스토리지 슬롯의 0번째 값을 수정할 수 있게 하기 때문에 이 슬롯에 공격 컨트랙트의 주소를 넣게 되면 악의적인 컨트랙트를 실행되게 할 수 있다.

예방 기법

라이브러리 컨트랙트 구현을 위한 키워드를 제공해서 라이브러리 컨트랙트가 스테이트리스이고 비자기파괴적임을 보장해준다.

  • 스테이트리스: 스토리지 컨텍스트의 복잡성을 완화하고 공격자가 라이브러리의 상태를 수정하는 공격을 방지한다.


💡 디폴트 가시성

✔️ 가시성 : 함수를 호출할 수 있는 범위를 지정한다.(external / public / internal / private)

취약점

함수에 대한 기본 가시성(디폴트 값)은 public이므로 가시성을 따로 지정하지 않으면 자동으로 그 함수는 외부에서 호출할 수 있는 public 함수가 된다.

✅ 컨트랙트의 잔액을 호출한 주소에 전송하는 _sendWinnings()함수가 public이므로 외부에서 호출하여 잔액을 훔칠 수 있다.

예방 기법

✅ 간단하게 함수에 대한 가시성을 항상 지정해주는 것이 좋다. 컴파일러에서는 함수 가시성을 명시하는 것을 권장하기 위해 가시성을 설정하지 않으면 경고를 표시한다.


💡 엔트로피 환상

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

취약점

✅ 이더리움 플랫폼을 기반으로 구축된 최초의 컨트랙트 중 일부는 도박을 기반으로 했다.
✅ 도박에는 불확실성(베팅할 대상)이 필요하기 때문에 블록체인(결정론적 시스템)에서는 도박 시스템을 구축하기 어렵다.
✅ 블록체인 내부에서 블록에 대한 정보를 포함한 변수를 베팅 대상으로 하는 경우, 블록은 채굴자에 의해 통제되므로 완벽한 무작위 값이라고 볼 수 없다.

예방 기법

✅ 따라서 무작위성(엔트로피)의 원천은 블록의 정보와 관계 없는 외부의 요소를 사용해야 한다.


💡 외부 컨트랙트 참고

이더리움의 장점중 하나는 코드를 재사용하고 네트워크에 이미 배포된 컨트랙트와 상호작용할 수 있다는 점이다.
즉 대부분의 컨트랙트가 외부 컨트랙트를 참고할 수 있는데, 이때 공격자가 그 의도를 숨기는 공격이 가능하다.

취약점

✅ 솔리디티에서는 어떤 주소를 사용할 때 해당 주소에 있는 코드가 실제 컨트랙트를 표현하고 있는지를 검사하지 않고 캐스트한다.


💡 짧은 주소/파라미터 공격

솔리디티 컨트랙트 자체에서는 수행되지 않지만, 컨트랙트와 상호작용하는 제3자의 애플리케이션에서 발생할 수 있는 공격

취약점

✅ 솔리디티로 작성된 코드 파일을 컴파일하면 컨트랙트 ABI파일을 생성하고 EVM에서는 이 파일을 이용해서 컨트랙트을 호출하고 트랜잭션에서 데이터를 읽는다.
✅ 지정된 파라미터 길이보다 짧게 인코딩된 파라미터가 전송되기도 하는데 이런 경우에 EVM은 파라미터 끝에 0을 추가해서 길이를 맞춘다.
✅ 이때 제3자 애플리케이션이 입력의 유효성을 검사하지 않을 경우 문제가 발생한다.

  • transfer함수가 인코딩되면 파라미터인 to, tokens순서대로 인코딩된다.
  • 주소: 0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead, 토큰 수: 100이라고 가정
  • 인코딩 => a905 9cbb / 0000 0000 0000 0000 0000 0000 dead dead dead dead dead dead dead dead dead dead / 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0005 6bc7 5e2d 6310 0000
  • 첫 4바이트(a905 9cbb)는 transfer함수를 나타내고, 그 다음 32바이트는 주소, 그다음 32바이트는 토큰 100개를 의미한다.

✅ 만약 주소를 보낼 때 1바이트가 누락된 주소(0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde)를 보낸다면 인코딩 되는 값이 달라진다.
a905 9cbb / 0000 0000 0000 0000 0000 0000 dead dead dead dead dead dead dead dead dead de00 / 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 056b c75e 2d63 1000 0000
✅ 주소 파라미터와 토큰 파라미터의 값이 달라지게 된다.
✅ 컨트랙트는 함수 호출 시에는 100개의 토큰을 출금한다고 생각하지만 인코딩되고 실제 출금되는 토큰은 25600개(256배)가 되버린다.

예방 기법

✅ 외부 애플리케이션의 모든 입력 파라미터는 블록체인에 보내기 전에 유효성을 검사해야 한다.
✅ 파라미터의 순서도 고려해야한다.


profile
안녕하세요

0개의 댓글