스마트 컨트랙트를 어떻게 보호할 것인가: 솔리디티의 6가지 취약점 및 대비책 (1부)

Loom Network Korean
Loom Network Korean
8 min readSep 22, 2018

--

이 글은 Georgios Konstantopoulos가 쓴 How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 1)을 번역한 글입니다. 오역이 있으면 Private Note 기능으로 알려주세요!

이전 글에서 우리는 Devcon3에서 발표된 컨셉을 분석하면서 이더리움의 확장성의 미래에 관해 이야기해 보았습니다. 잠깐 쉬는 시간을 가져보죠. 이러한 확장성 문제들이 모두 해결되고, 이더리움의 스마트 컨트랙트가 문제없이 작동하고 있다고 가정해 봅시다.

사용자들은 이것을 좋은 의도로 사용할까요, 아니면 컨트랙트의 원활한 기능을 방해하는 적이 될 가능성이 있을까요?

스마트 컨트랙트는 “변경 불가능”합니다. 한 번 배포하고 나면, 코드는 변경이 불가하며, 발견된 어떤 버그도 고칠 수 없습니다.

전체 조직이 스마트 컨트랙트 코드에 의해 운영될 수도 있는 미래에서는, 적절한 보안이 매우 필요합니다. TheDAO나 올해의 Parity hacks(7월, 11월)와 같은 과거의 해킹들이 개발자들의 인식을 높였지만, 그래도 아직 갈 길이 멉니다.

“해커들에게 이곳은 디즈니랜드입니다.”

이 글에서 우리는 유명한 보안 함정들과 이에 대한 대책에 관해 살펴볼 것입니다.

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

오버플로우는 숫자가 최댓값 이상으로 증가한 경우를 말합니다. 솔리디티는 256비트의 숫자(2²⁵⁶-1까지)를 처리할 수 있기 때문에, 여기서 1이 증가하면 결과는 0이 됩니다.

0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+ 0x000000000000000000000000000000000001
----------------------------------------
= 0x000000000000000000000000000000000000
최댓값에 도달하고 나면, 주행 기록계 또는 트립 미터가 0부터 다시 시작됩니다. 이를 주행 기록계 롤오버라고 하죠. [출처]

반대의 경우도 마찬가지입니다. 부호가 없는 숫자일 경우, 감소하다가 언더플로우가 발생하면 최댓값이 됩니다.

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

여기서 버그를 테스트해보세요:

두 경우 모두 위험하지만, 예를 들어 X만큼의 토큰을 소유하고 있는 사람이 X+1만큼을 사용하려고 할 경우에는 언더플로우가 발생할 확률이 높습니다. 코드가 이 부분을 확인하지 않는다면, 공격자는 자신이 가지고 있는 것보다 더 많은 토큰을 사용하게 되고, 이로 인해 잔액 초과가 될 것입니다.

대책: 요즘에는 일반적으로 OpenZeppelin의 SafeMath 라이브러리를 사용합니다.

고쳐진 버그를 여기서 테스트해보세요:

2. Visibility & delegatecall

7월경에 사용하던 사람들에게 이 버그는 매우 익숙할 것입니다. 결국 Parity 지갑 해킹으로 사용자들은 약 3천만 달러를 잃었습니다.

솔리디티 접근 제어자들과 그들 간의 차이점

Public 누구나 호출 가능한 함수들입니다(컨트랙트 내부의 함수, 상속된 컨트랙트의 함수 또는 외부 사용자에 의해 호출 가능).

External 외부에서만 접근할 수 있는 함수들입니다. 즉, 컨트랙트의 다른 함수들에 의해 호출될 수 없다는 것이죠. 다음 코드는 컴파일되지 않습니다. cannotBeCalled 함수에 external 접근 제어자가 있어서 컨트랙트의 함수에서 호출할 수 없기 때문이지요(하지만, 다른 컨트랙트에서는 호출이 가능하답니다).

External은 calldata opcode를 사용하기 때문에 더 싸게 사용할 수 있습니다. 반면에, public은 여기 설명된 것처럼, 메모리에 매개변수들을 모두 복사해야 하죠.

Private internal 은 더 간단합니다: private 은 컨트랙트 내부에서만 함수를 호출할 수 있습니다. 하지만, internal은 부모 컨트랙트에서 상속된 컨트랙트들도 해당 함수를 사용할 수 있도록 제약을 완화한 접근 제어자 입니다.

즉, 외부 컨트랙트들과 상호작용할 필요가 없다면, 함수에 private 또는 internal 접근 제어자를 사용하는 것이 좋다는 것이죠.

Delegatecall

다음은 솔리디티 문서를 풀어쓴 것입니다:

“Delegatecall은 메시지 호출과 같습니다. 타깃 주소의 코드가 호출하는 컨트랙트의 컨텍스트에서 실행되고, msg.sendermsg.value의 값이 변하지 않는다는 것을 제외하면 말이죠.

이는 컨트랙트가 런타임에 다른 주소에서 코드를 동적으로 로드할 수 있다는 것을 의미합니다. 스토리지, 현재 주소 및 잔액은 여전히 호출하는 컨트랙트를 참조하며, 호출되는 주소에서는 코드만 가져오는 것이죠.”

이 로우 레벨 함수는 라이브러리를 구현하고 코드를 모듈화하기 위한 백본으로써 상당히 유용한 함수입니다. 하지만, 이로 인해 취약점들이 생겼습니다. 기본적으로 여러분의 컨트랙트가 누구든지 자신의 상태에 따라 원하는 대로 할 수 있기 때문이죠.

아래의 예시에서, 공격자는 컨트랙트 Delegate의 public 함수인 pwn을 호출할 수 있습니다. 호출이 Delegation의 컨텍스트에 있기 때문에, 공격자는 컨트랙트의 소유권을 주장할 수 있습니다.

Parity 해킹은 안전하지 않은 접근 제어자들을 사용하고, 임의의 데이터로 delegate호출을 남용하여 발생했습니다. 취약한 컨트랙트의 함수는 delegatecall을 사용했고, 소유권을 변경할 수 있는 다른 컨트랙트의 함수는 public 함수였죠. 이는 공격자가 취약한 함수를 호출하기 위해 msg.data 필드를 변경할 수 있게 해주었습니다.

msg.data 필드에는 호출하고자 하는 함수의 서명이 포함됩니다. 여기서 서명은 함수 프로토타입의 sha3(keccak256의 별칭) 해시의 처음 8 bytes를 의미합니다.

이 경우에는:
web3.sha3("pwn()").slice(0, 10) --> 0xdd365b8b
함수가 다음과 같이 매개변수를 전달받으면, pwn(uint256 x):
web3.sha3("pwn(uint256)").slice(0,10) --> 0x35f4581b

3. Reentrancy (TheDAO 해킹)

솔리디티의 call 함수가 value와 함께 호출될 때, 받은 모든 가스를 전달합니다. 아래 코드에서는, 실제로 송금자의 잔액이 줄어들기 전에 호출이 일어납니다. 이로 인해, TheDAO 해킹이 일어났을 때 레딧 댓글에 아주 잘 설명되어 있었던 취약점이 발생했습니다:

“간단히 말해서, 이것은 여러분이 요청한 돈을 모두 줄 때까지 은행원이 여러분의 잔액을 바꾸지 않는 것과 같습니다. “500달러를 찾을 수 있을까요? 잠시만요, 그 전에, 500달러를 찾을 수 있나요?”

이처럼 설계된 스마트 컨트랙트는 여러분이 500달러를 처음에 가지고 있었다는 것만 확인하고, 그리고 나면 취약점이 발생하게 되는 것이죠.”

여기에 자세히 설명되어 있듯이, 이를 해결하기 위해서는 값을 전달하기 전에 송금자의 잔액을 줄이는 것입니다. 병렬 프로그래밍을 해본 사람들에게는 다른 해결책도 있습니다. 뮤텍스를 사용하여 모든 종류의 race condition들을 완전히 방지하는 것이죠.

현재는 msg.sender.transfer(_value)를 사용하는 것이 가장 좋은 해결책입니다. send를 사용해야 한다면, require(msg.sender.send(_value));를 사용하세요.

(위의 방법으로 해결한 Hayden AdamsPaulius에게 감사의 말씀을 전합니다!)

1부는 여기까지입니다. 다음 글에서는, 덜 알려진 취약점 공격 방법, 여러분의 워크플로우에 추가해야 하는 도구들 및 스마트 컨트랙트 보안의 미래에 관해 이야기할 것입니다.

아래에 가입하시면 글이 올라왔을 때 알림을 받으실 수 있어요!

Loom Network는 고성능 디앱을 확장하기 위한 멀티체인 상호운용 플랫폼입니다 — 이미 상용 가능한 상태이며, 감사 및 실제 테스트를 거쳤습니다.

Loom 베이스체인에 여러분의 디앱을 한 번 배포하고 나면, 오늘날 모든 주요 블록체인에 걸쳐 가능한 가장 광범위한 사용자 기반에 접근할 수 있습니다.

Loom Network를 처음 접하시나요? 여기서 시작하세요.

LOOM 토큰을 스테이킹해서 베이스체인을 보호하는 데 참여하고 싶으신가요? 여기서 그 방법을 알아보세요.

우리가 하는 일이 마음에 드시나요? 그렇다면 어서 우리의 프라이빗 메일링 리스트에 가입하고, 우리가 계속 전달하는 모든 업데이트를 계속 받아보세요.

--

--