솔리 디티 단점 - solli diti danjeom

이전 포스팅에서 데브콘3에 제시된 개념을 분석하며 이더리움의 확장성의 미래에 대해 논의했다. 이제 모든 확장성 이슈가 해결되었고 이더리움 스마트 컨트렉트가 별 문제 없이 작동하고 있다고 가정해 보자.

유저들이 선한 의지를 가질 것인가 아니면 컨트렉트가 문제 없이 기능하는 것을 방해할 것인가?

스마트 컨트렉트는 “변경 불가능”하다. 일단 컨트렉트가 시작되면 그에 대한 코드를 바꾸는 불가능하고, 이에 따라 발견된 버그를 고치는 것도 불가능하게 된다.

아마도 앞으로는 모든 조직이 스마트 컨트렉트 코드에 따라 규율될 것인데, 이 때 적절한 보안성이 절실히 필요하다. 다오 (TheDAO)나 패리티 핵(7월, 11월)과 같은 지난 해킹 사건을 통해 개발자들의 인식이 고취되었으나, 여전히 갈 길이 멀다고 할 수 있다. .

“이곳은 해커들의 놀이 동산이다”

본 포스팅에서는 잘 알려진 보안 취약점 및 그에 대한 대비책에 대해 알아 보도록 하자.

1. 오버플로우 & 언더플로우

오버플로우란 숫자가 최대치 이상으로 증가하는 것을 말한다. 솔리디티는 최대 256비트의 숫자(0 ~ 2²⁵⁶-1)까지 처리할 수 있다. 그래서 처리 가능한 최대값에 1을 더하면 0이 된다.

0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+ 0x000000000000000000000000000000000001
----------------------------------------
= 0x000000000000000000000000000000000000

최대값에 도달한 다음, 주행 기록계가 0부터 다시 시작한다. 이를 주행 기록계 롤오버라 한다. [출처]

마찬가지로 반대 경우에는 숫자에 부호가 없을 때 숫자를 줄이면 언더플로우를 일으켜 최대 가능값이 나오게 된다.

0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

여기서 버그를 테스트할 수 있다:

두 경우 모두 위험하지만, 언더플로우가 더 일어날 가능성이 높다. 이를테면 토큰 보유자가 X 만큼의 토큰을 가지고 있으나 X+1 만큼 소비하고자 하는 하는 경우가 있다고 하자. 코드가 이에 대해 확인하지 않으면 공격자는 보유 토큰보다 더 많은 토큰을 소비할 수 있게 되고 최대 잔액을 갖게 된다.

대비책: 이제는 오픈 제플린 (OpenZepplelin)의 세이프매스 (SafeMath) 라이브러리를 이용하는 것이 표준이 되었다.

여기서 디버깅된 코드를 테스트할 수 있다:

2. 접근 제어자 & delegatecall

이 취약점은 2017년 7월에 발생한 패리티 지갑 해킹으로, 유저들은 이로 인해 약 3천만 달러의 손실을 입었다.

솔리디티 접근 제어자 및 각각의 차이점

Public 함수는 누구나 호출할 수 있다. 컨트렉트 내의 함수, 상속된 컨트렉트의 함수, 외부 유저가 Public 함수를 호출할 수 있다.

External 함수는 외부에서만 접근할 수 있다. 즉, 컨트렉트 내의 다른 함수가 External 함수를 호출할 수 없다. 아래 코드는 컴파일되지 않는데 이는 cannotBeCalle함수의 external 접근 제어자로 인해 해당 컨트렉트 내의 다른 함수에 의한 호출이 불가능하기 때문이다. (다른 컨트렉트에 의한 호출은 가능하기 하다)

External 접근 제어자는 보다 쉽게 이용할 수 있는데 이는 이 접근 제어자가 calldataopcode를 이용하기 때문이다. 여기에서 언급했듯이 public 접근 제어자는 메모리에 모든 전달 인자를 복사해야 한다.

Private과 internal 접근 제어자는 더 간단하다: private은 컨트렉트 내에서만 함수가 호출될 수 있음을 의미하며, internal는 부모 컨트렉트에서 상속된 컨트렉트가 해당 함수를 호출할 수 있음을 의미한다.

외부와 상호작용할 필요가 없다면 모든 함수를 private이나 internal로 선언하는 것이 좋다.

Delegatecall

다음은 솔리디티 문서의 내용을 유사어구로 표현한 내용이다:

“Delegatecall은 호출하는 컨트렉트의 맥락에서 타켓 주소의 코드가 실행되고 msg.sender

0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
0의 값이 변경되지 않는다는 점을 제외하면 메시지 콜과 동일하다.

이는 컨트렉트가 실행될 때 다른 주소로부터 코드를 다이내믹하게 읽어들일수 있다는 것을 의미한다. 저장 장소, 현재 주소, 잔액은 여전히 호출하는 주소를 참조하고, 코드만이 호출된 주소로부터 읽어 들이게 된다.”

이러한 저 수준 함수는 매우 유용하게 이용되고 있다. 왜냐하면 라이브러리를 구현하고 코드를 모듈화하는 데 있어서 중추적인 역할을 하기 때문이다. 그러나 이 함수로 인해 취약점이 생기게 되었는데, 이는 컨트렉트가 누구나 자신의 상태에 따라 하고 싶은 대로 할 수 있도록 허용하기 때문이다.

아래 예시 코드에서 공격자는 Delegate 컨트렉트의 public 함수

0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
1를 호출할 수 있다. 호출이 Delegation 컨트렉트 내에서 일어나기 때문에 공격자는 컨트렉트의 소유권을 주장할 수 있다.

패리티 해킹은 안전하지 않은 접근 제어자를 사용하고 임의의 데이터로

0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
2 호출을 남용하는 것과 함께 발생했다. 취약한 컨트렉트 함수는
0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
3를 호출했고 소유권을 변경할 수 있었던 또다른 컨트렉트의 함수는 공개되었다. 이를 통해 공격자는
0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
4필드를 조작하여 취약한 함수를 호출했다.

0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
4필드에는 호출하고자 하는 함수의 시그니처가 포함된다. 여기서 시그니처는 함수 원형의 sha3 (keccak256)해시 값 첫 8바이트를 의미한다.

위 예시의 경우:
web3.sha3("pwn()").slice(0, 10) --> 0xdd365b8b
해당 함수가 다음과 같이 인자를 전달받으면, pwn(uint256 x):
web3.sha3("pwn(uint256").slice(0,10) --> 0x35f4581b
3. Reentrancy (TheDAO hack)

솔리디티의

0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
6함수가
0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
7와 함께 호출될 때
0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
6함수는 받은 모든 가스를 전달한다. 아래 코드 예시에서 송금자의 잔액이 줄기 전에 호출이 진행된다. 이로 인해 아래 레딧 코멘트에 언급한 다오 (TheDAO) 해킹이 발생했다:

“쉽게 말해 이 문제는 요청된 금액을 고객에게 주기 전에 은행 출납원이 잔액을 변경하지 않는 것과 같다. “500달러를 인출할 수 있을까요? 기다려 보세요. 잔액 변경 전에, 500달러를 인출할 수 있을까요?”

설계된 스마트 컨트렉트는 처음에 한 번 500달러가 있는지만을 확인한다. 그래서 취약점을 노출한다.”

여기에서 자세히 설명한 대로, 해결책은 송금액을 정하기 전에 송금자의 잔액을 줄이는 것이다. 병렬 프로그래밍을 이용하는 개발자는 mutexes를 이용하여 모든 레이스 컨디션 (race conditions) 문제를 해결할 수 있다.