SMT(SmartMesh) 토큰 무한생성 해킹 설명과 대비책

안녕하세요. 온더의 철학자입니다.

SMT라는 ERC20토큰 무한생성 해킹이 일어났습니다. 이 해킹건에 대해 설명이 필요한 것 같아 긴급하게 글을 남깁니다.

결론부터 말씀드리면, SMT토큰은 표준 ERC20함수가 아닌 기능을 구현해 놓고 사용했고, 이에 코드 보안점검(audit)이 제대로 이뤄지지 않아 일어난 사고입니다. (ERC20 혹은 블록체인 자체의 문제는 아닙니다.)

해킹 시나리오

해커는 함수의 취약점을 이용해 엄청난 양의 토큰을 만들어 본인의 계정으로 받은 후에, 이 중 1경개의 토큰을 거래소로 보냈습니다.

해킹은 구체적인 다음 시나리오로 이뤄졌습니다.

  1. transferProxy함수를 이용해 해커 본인의 계정으로 65,133,050,195,990,400,000,000,000,000,000,000,000,000,000,000,000,000,000,000.891004451135422463 토큰을 만들어냄
  2. transferProxy함수를 또 이용해 해커 본인의 계정으로50,659,039,041,325,800,000,000,000,000,000,000,000,000,000,000,000,000,000,000.693003461994217473 토큰을 만들어냄
  3. 받은 토큰 중 10,000,000,000,000,000개의 토큰을 거래소(후오비)로 옮김
  4. 옮긴 토큰을 거래소에서 판매(다른 코인or 현금으로 교환, 팩트확인 필요)
  5. 다른코인 or 현금을 출금(팩트확인 필요)

오버플로우

오버플로우란 이더리움 프로그래밍 언어에서 선언한 변수의 제한한 표현형을 넘어갈 경우, 해당 값이 0부터 시작하는 현상을 뜻합니다. 일반적으로 토큰을 전송할 때 uint256(양의 정수, 2²⁵⁶까지 값 표현 가능)형을 선언해서 사용합니다. 만약 함수 호출 간 입력한 값이 2²⁵⁶을 넘어설 경우, 이 값은 0이 됩니다.

예를들어 uint256형으로 a라는 변수를 선언했는데, a에 2²⁵⁶+1이라는 값을 할당하려고 하면, a는 0이 됩니다.

transferProxy()

이 함수는 SMT토큰 팀에서 특별히 만든 함수로써, 이더가 없는 사용자가 다른 제3자를 이용해 SMT토큰을 수수료로 내고 토큰을 보낼 수 있는것이 가능케 하는 기능을 구현해 놓았습니다. 소스코드에는 다음과 같은 주석이 붙어있습니다.

Proxy transfer SmartMesh token. When some users of the ethereum account has no ether, he or she can authorize the agent for broadcast transactions, and agents may charge agency fees

이 함수는 다음 여섯개의 파라미터를 받습니다.

address _from //보내는 사람
address _to //받는사람
uint256 _value//보낼값
uint256 _feeSmt// 제3자에게 지불할 수수료
uint8 _v //v, r, s는 트랜잭션 서명값으로 사용됨
bytes32 _r
bytes32 _s

이 함수의 기본적인 구성 로직은 다음과 같습니다.

  1. 보내는 사람의 잔액(from)이 충분한지 확인한다(보내려는 토큰 수량이 보내는 사람의 잔액보다 작어야 함)
  2. v,r,s를 이용해 거래의 서명값이 보내는 사람과 일치하는지 확인한다.
  3. 토큰을 보낸다

문제는 1번 과정에서 터졌습니다. 오버플로우를 이용해서 공격이 이뤄졌습니다.

transferProxy()에서 오버플로우 호출

위에서 확인할 수 있듯이, 세번째 파라미터인 value는 uint256(2²⁵⁶)의 데이터를 표현할 수 있습니다. 그런데 해커는 이 세번째 파라미터에 엄청나게 큰 값을 집어넣어 함수를 호출합니다. 해커가 호출한 값들은 다음과 같습니다.

해커가 65,133,050,195,990,400,000,000,000,000,000,000,000,000,000,000,000,000,000,000.891004451135422463 토큰을 만들어 낸 트랜잭션인데, 링크를 타고 들어가서 호출한 내역을 보면 특이한 점을 확인할 수 있습니다.

해커가 호출한 transferProxy

값으로 전달한 세번째 값이

8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

인 것을 확인할 수 있습니다. 이 값을 2진수로 환산하면

1000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

가 됩니다.

네번째 값도 중요한데 이 값은7000000000000000000000000000000000000000000000000000000000000001

입니다.

2진수로 환산하면

111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001

이 되죠.

세번째 값과 네번째 값을 더하면, 선언된 표현형인 2²⁵⁶을 넘어가게됩니다.

transferProxy()에서 보내는 사람의 잔액 확인 조건문

앞서 말씀드렸다시피, 이 함수는 3개의 로직으로 구성되어 있습니다.

  1. 보내는 사람의 잔액(from)이 충분한지 확인한다(보내려는 토큰 수량이 보내는 사람의 잔액보다 작어야 함)
  2. v,r,s를 이용해 거래의 서명값이 보내는 사람과 일치하는지 확인한다.
  3. 토큰을 보낸다

1번 조건문이 중요한데, 이 부분만 통과하면, 없는 토큰을 만들어 낼 수 있습니다. 해커는 앞의 오버플로우의 특성을 이용해 1번 로직을 가볍게 통과해버립니다.

조건문은 다음의 코드로 구현되어 있습니다

if(balances[_from] < _feeSmt + _value) revert(); // 1번 조건문의 구현

방금 전 말씀드린 세번째 값과 4번째 값이 _value와 _feeSmt입니다. 둘을 합치면 2²⁵⁶비트의 표현형을 넘어가 값이 0으로 오버플로우가 발생했고, 조건문을 손쉽게 통과했습니다.

조건문을 통과했으니, 잔액보다 더 큰 값인 8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff을 보낼 수 있게 되어버렸고, 이 값을 10진수로 바꾸면 65133050195990359925758679067386948167464366374422817272194891004451135422463가 되는데, 이는 해커가 허공에서 만들어낸 토큰의 수량과 정확하게 일치합니다.

SafeMath()를 이용한 대비책

우리 똑똑한 개발자들이 이러한 문제를 가만히 뒀을까요? 아닙니다.

EVM의 스택구조의 특징으로 인해 생기는 이러한 특징(문제라기보다는 특징에 가깝습니다. 솔리디티라는 언어만 겪는 문제도 아니구요)을 이해하고 있던 프로그래머들은 이미 초기부터 SafeMath라는 함수를 만들어 사칙연산을 수행해 왔습니다.

SafeMath는 사칙연산 중 이러한 오버/언더플로우 문제가 발생했을 경우 곧바로 에러를 발생시키도록 설계되어 있습니다.

요즘 개발되는 컨트랙트 중 SafeMath를 사용하지 않는 경우는 매우 드뭅니다. 만약 safeMath를 사용하지 않은 컨트렉이 있다면, 이는 매우 초보적인 컨트랙트 엔지니어이거나, 오류의 가능성을 알고도 일부러 했다고 밖에 볼 수 없습니다.

다시 transferProxy()로 돌아가서 구현된 코드를 확인 해 볼까요?

if(balances[_from] < _feeSmt + _value) revert(); // SMT의 현재 구현코드, 잘못된 구현

오버플로우 가능성이 있는 사칙연산을 곧바로 사용한 문제가 있습니다. 이를 다시 올바른 방식으로 짜게 되면

if(balances[_from] < _feeSmt.add(_value) ) revert(); //safeMath library를 이용한 올바른 코딩 패턴

이렇게 바꿀 수 있습니다.

코드 감사(Audit, 오딧 업무)

스마트 컨트랙트의 비즈니스 분야중에 오딧(코드감사)이라는 분야가 있습니다. 알려진 보안 패턴들을 바탕으로 코드를 점검해 문제를 사전에 예방해주는 업무입니다. 이러한 일만 하는 전문적인 팀도 있습니다.(Zeppelin, Onther Inc.(info@onther.io), jason kim님 등)

정수형 오버/언더플로우 문제는 모든 오딧 업무의 첫번째 점검 항목이자, 기본중에 기본입니다.

결론

금번 SMT 사례는 전 4가지 관점으로 보고 있습니다.

  1. 엄청나게 초보인 스마트 컨트랙트 엔지니어가 개발을 진행
  2. 엔지니어는 알고도 일부러 이런 방식으로 개발을 했을 가능성(?)
  3. 기본적인 코드 감사(audit)를 하지 않음
  4. 거래소는 함량미달의 코인을 무차별 상장

그런데 일부 사용자들은 이러한 문제를 보고 마치” ERC20의 구조적인 문제다”, “이더리움의 위기다"라고 사실을 왜곡해 확대재생산을 하는 “세력”이 있는 것 같습니다.

이를 통해 무엇을 얻고자 하는지 모르겠지만 이는 이더리움과 ERC20의 구조적인 문제가 아니며, 개발팀이 오딧이라는 필수적인 업무를 누락했을 가능성이 높습니다.

블록체인에 기록되는 스마트 컨트랙트 코드는 직접적으로 금전과 연결된 경우가 매우 많고, 이에 대한 세밀한 설계와 점검이 필요합니다.

금번의 사태를 바탕으로 블록체인 프로젝트를 진행하는 팀들의 코드 보안감사의 중요성을 인식하고, 거래소는 무차별적인 상장이 아닌 엄격한 검증을 거친 토큰을 상장하는 등의 방식으로 올바른 산업의 성장이 이루어 졌으면 좋겠습니다.

감사합니다.

참고자료

알리는글

온더에서는 알려진 솔리디티의 보안점검 항목과 자료에 대한 레퍼런스를 수집해 깃헙에 모아 오픈하고 공유하여 이러한 일이 일어나지 않도록 노력을 기울이고 있습니다[Auditing Reference]. 공부나 업무를 하는 과정에서 알게 된 지식들은 서로 공유하여 보다 나은 생태계 기여에 도움을 주시면 좋을 것 같습니다.

Like what you read? Give 철학자(정순형) a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.