스마트 컨트랙 규칙을 우회하기 위한 ‘Curve Overtaking-곡선 추월’ | 청두 LianAn Tech, 취약점 분석 세번째 씨리즈
Thanks Mr KIM for the translation
Reentrancy와 Race Condition은 암호 자산을 위협하고, Limits와 interactions을 사용하여 보안을 무력화 시킵니다.
블록 체인 산업에 점점 더 많은 사람들의 관심과 이목이 집중될수록, 사고가 발생할 위험은 높아집니다.
그 이유는 많은 수의 투자자와 프로젝트 주최자들이 가능하면 빠르게 돈을 벌어보겠다는 야망을 가지고 블록체인 판으로 몰려 들기 때문입니다. 마치 영화 Fast and Furious에서처럼 말입니다.
이 와중에 누군가는 공정하게 경쟁하려 하지 않는 사람들이 있습니다. 이런 사람들은 취약한 스마트 컨트랙을 작성할 위험이 있습니다. 왜냐하면 고품질의 스마트 컨트랙은 개발하는 데 많은 시간이 걸리기 때문입니다.
이번시간에는 두 가지 유형의 Race Condition 취약점들, 1)재진입성(reentrancy) 및 2)트랜잭션 주문 종속성 (TOD — transaction ordering dependence)에 대해 이야기하겠습니다.
사건 Reveiw
2016 년 4 월에 완전히 독립적이고 분산된 프로젝트인 DAO가 출범해 매우 빠르게 대중화되었습니다.
그러나 일부 개발자들로부터 splitDAO 함수에 재귀 호출 문제 [1]가 포함되어 있다는 경고를 받았습니다.
2016년 6월 14일에 DAO는 취약점이 발견되었고 자산이 안전하게 보호되고 있으며, 스마트 컨트랙은 보안이 유지되고 있다고 밝혔습니다.
그러나 3 일 후인 6월 17일 해당 취약성을 발견했던 해커는 똑같은 취약성을 사용해 360 만 개의 이더리움을 공략하여, 결국 6 백만 달러 상당의 이더리움이 영원히 탈취하는데에 성공했습니다.
이 공격 이후, DAO 프로젝트의 담당자들은 손실 폭을 줄이기위한 몇 가지 조치를 취했으며, 이더리움 재단은 DAO가 잃어버린 부분을 회수하는 데 도움이되도록 몇 가지 조치를 취했습니다. 결국 이러한 조치로 인해 이더리움의 하드포크가 일어나게 되었습니다 [2].
이 상황에서 해커가 사용했던 방법을 분석하기 위해, 먼저 Race Condition에 대해 알아보아야 하겠습니다.
Race Condition이란 무엇일까요?
Race Condition은 장치 또는 시스템이 올바르게 작업을 수행하기 위해 정해진 순서로 수행되어야하는 ‘Condition-조건’입니다.
만약 순서가 어긋날 경우, 오작동을 일으키는 상황이 발생할 수 있습니다. [3].
스마트 컨트렉에서 Race Condition 취약점이 공격자에 의해 악용되면, 공격자는 공격 컨트랙을 사용하여 컨트랙의 결과값을 제어하고 변경합니다.
이것을 조금더 쉽게 설명하자면, 스마트 컨트랙을 ‘고속도로’와 비교해 볼 수 있습니다.
이 고속도로에서 블록체인의 함수는 자동차가 되고, 원래의 작업 실행 순서는 자동차의 순서를 규정한다고 합시다.
이때, 갑자기 F1 드라이버가 GTR(닛삿의 스포츠카)을 몰고 나타나 커브에서 추월을 시도하며, 원래의 진행 순서를 무너뜨리려고 할 경우, 이 프로 드라이버는 선두를 차지하고 고속도로의 규칙을 파괴하게 됩니다.
다시 블록체인으로 돌아와서 생각해 보면,
Ethereum의 특징 중 하나는 외부의 컨트랙을 호출하고 사용할 수 있다는 것입니다.
이때, 외부 컨트랙이 제어 흐름을 주도하여, 호출 기능의 데이터를 예기치 않게 변경할 수 있는 잠재적 리스크가 존재합니다.
이런 종류의 취약점은 여러 가지 형태로 존재하지만, 이 글에서는 재진입성(Reentrancy)과 트랜잭션 주문 종속성(Transaction-Ordering Dependence-TOD)에 중점을 두고 살펴보도록 하겠습니다.
Race Condition 취약점 분석 및 버그 수정 상세
Reentrancy
취약점 설명 :
현재의 스마트 컨트랙은 일반적으로 이더리움을 다루기 위해 사용됩니다. 그래서 일반적으로 이더리움을 다양한 외부 사용자 주소로 보내도록 설계됩니다.
외부 스마트 컨트랙을 호출하거나 특정 주소로 이더리움을 보내려면 외부 호출 요청-external call request-을 해야합니다.
해커는 이러한 외부 호출시의 취약점을 이용해 외부호출을 공격합니다. (이 외부 호출은 fallback 함수를 포함한 추가적 코드의 실행- fallback 함수를 통과-을 강제하기위해 사용됩니다.)
따라서 실행 명령은 스마트 컨트랙을 다시 입력합니다. 이 유형의 공격이 악명 높은 DAO 공격에 사용되었습니다.
문제가 있는 컨트랙은 다음의 샘플 컨트랙으로 간단히 표현해 보았습니다.
이 컨트랙에는 depositFunds () 및 withdrawFunds ()의 두 가지 함수가 포함되어 있습니다.
depositFunds () 함수는 msg.sender의 잔액을 증가시키는 것입니다.
withdrawFunds () 함수는 _weiToWithdraw의 지정 값으로 이더리움을 인출하는 것입니다.
그럼 이제, 해커가 다음과 같은 계약을 생성한다고 가정해 보겠습니다.
이 경우, 재진입(Reentrancy) 공격 때문에 잔액[msg.sender]이 넘쳐흐르게 됩니다.
따라서 모든 계산에 SafeMath를 사용하는 것이 좋습니다. 이 부분은 이미 1편에서 Phase I에서 논의했습니다.
이제 이 컨트랙이 어떻게 재진입(Reentrancy) 공격을 수행하는지 분석해 보겠습니다.
1. 일반 사용자가 이더리움을 원래 컨트랙 (Reentracny.sol)에 입금한다고 가정합니다.
2. 공격자는 공격 컨트랙(POC.sol)을 배포하고, 원래 계약의 배포 주소를 지정하도록 setInstance ()를 호출합니다.
3. 공격자는 공격 컨트랙에서 depositEther ()를 호출하여 미리 1 개의 이더리움을 원래의 컨트랙에 입금합니다.
바로 이 시점에, 원래 컨트랙의 입장에서 보면, 공격 컨트랙의 주소에는 이미 1 개의 이더리움이 존재하고 있게됩니다.
4. 공격자가 공격 컨트랙에서 withdrawFunds () 함수를 호출하면, 이 함수는 원래의 컨트랙에서 withdrawFunds ()를 호출하여 1 개의 Ether을 전송합니다.
5. 이때 원래 컨트랙의 withdrawFunds () 함수의 첫 번째 행을 살펴보십시오. — balances([msg.sender]> = _ weiToWithdraw); 공격 컨트랙 주소에는 1 개의 이더리움이 있고, 이는 _ weiToWithdraw와 동등하며, 이는 요구조건을 충족합니다.
그래서 코드는 다음으로 진행됩니다.
6. withdrawFunds () 함수의 두 번째 줄은 require (msg.sender.call.value (_weiToWithdraw) ())입니다.
이는 _weiToWithdraw (현재 1 이더리움)를 msg.sender로 전송하는 것을 의미합니다.
Solidity는 다른 유효한 함수가 지정되지 않으면, msg.sender를 계약 주소로 지정합니다.
이 경우 Default 값은 fallback 함수를 호출하는 것입니다. 이 경우, 실행명령은 공격 컨트랙으로 유입되고 공격 컨트랙의 fallback 함수를 호출합니다.
추가적으로, call.value () ()가 이더리움을 송금하기 위해 호출되기 때문에,이 메서드는 모든 잔액 가스까지 송금하게됩니다.
7. 실행명령은 결국, 공격 컨트랙의 fallback 함수로 진행되며, ‘if’ 함수는 원래 컨트랙의 잔액을 확인하는 데 사용됩니다.
이 순간, 잔액은 요구 사항을 충족하는 16 이더리움이므로 실행명령은 원래 컨트랙의 withdraw () 함수에 ‘재입력’됩니다.
8. balance[msg.send] — = _ weiToWithdraw 코드가 실행되지 않게되기 때문에, 공격 컨트랙의 주소에는 여전히 ‘이더리움’이 남아있게되고, 이는 ‘require’의 요구 사항을 충족하게됩니다.
따라서 원래 컨트랙의 withdrawFunds () 함수에서 두 번째 ‘require’를 실행합니다.
9.이 시점에서 6–8 단계는 원래 컨트랙의 잔액이 1 이더리움 미만으로 줄어들거나 모든 가스가 소모될 때까지 반복됩니다.
10. 마지막으로, 실행코드는 balances [msg.sender] — = _ weiToWithdraw; 을 처리하기위해 원래 계약을 입력합니다.
원본 컨트랙이 이 시점에서 인출된 이더리움을 모두 차감하게되고, 이는 결국 balances (msg.sender), SafeMath을 초과하게 만듭니다.
만약, Safemath가 사용되었다면, 이경우 예외를 출력시켜 재진입을 피할 수 있었을 것입니다.
결과적으로 공격자는 오직 1 개의 이더리움을 사용하여 원래의 컨트랙에서 모든 이더리움를 인출하게 됩니다.
버그 수정
1. 이더리움을 외부 주소로 전송하기 위해서 가능하면 Solidity에 내장된 transfer () 함수를 사용하시기 바랍니다.[4].
Transfer () 함수는 2300 개의 가스를 실행될 때만 송금합니다.
이는 다른 계약(재진입을 호출하는 계약)을 호출하기에는 충분하지 않습니다.
원래 컨트랙의 withdrawFunds를 다시 작성하기위해 transfer () 함수를 다음과 같이 사용하면,
2. 이더리움이 전송(또는 다른 외부 호출)되기 전에 상태 변수에 어떤 변화가 발생했는지 확인하십시오. 이는 Solidity가 권장하는 점검- 효과- 상호 작용입니다.
3. 뮤텍스(Mutexes) 사용하십시오. 재진입을 방지하기 위해 실행 중 컨트랙 잠금을 할 수 있는 상태변수를 추가하십시오.
사건 리뷰에 따르면, 재진입(reentrancy) 공격이 DAO 사건의 주요 공격 매소드였고, 이 사건을 계기로 이더리움 클래식의 하드포크가 발생하게 되었습니다.
원본 사건에 대한 자세한 분석은 Phil Daian의 기사를 참조하십시오.
거래 주문 의존성 공격(Transaction Ordering Dependence Attack)
취약점 설명
대부분의 블록 체인과 마찬가지로, 이더리움의 노드들은 거래들을 모아서 블록을 생성합니다.
블록 생산자가 합의 메커니즘 (이더리움 PoW 해시)을 해독하면 이러한 거래가 유효한 것으로 간주됩니다.
이런 블록들을 생성하는 마이너들은 또한 이 블록에 포함될 거래를 선택합니다.
이 거래의 선택은 일반적으로 gasPrice에 의해 결정합니다.
바로 이부분에 잠재적인 공격 벡터가 존재합니다.
공격자는 트랜잭션 풀이 문제에 대한 해결책을 포함하고 있는지 확인하고, 자체 액세스를 변경하거나 취소하거나, 바람직하지 않은 상태로 변경합니다.
그리고 나서 공격자는 이 트랜잭션으로부터 데이터를 얻고, 자기가 만들어낸 트랜잭션을 블록에 포함시키기 위해 우선 순위가 높은 트랜잭션 gasPrice를 생성할 수 있습니다.
다음 컨트랙을 한번 살펴봅시다.
이 컨트랙에는 1000 개의 이더리움이 포함되어 있으며, 정답을 찾고 제출하는 사용자는 해당 이더리움을 보상으로 받게 됩니다.
사용자가 정답(정답을 ‘Ethereum!’이라고 합시다.)을 발견하면, 그는 Solve 함수를 호출하고 ‘Ethereum!’ 을 변수로 사용합니다.
하지만 유감스럽게도 공격자는 트랜잭션 풀에 제출된 해답을 모니터링 할 수 있습니다.
그들은이 해답을 보고 그 타당성을 확인한 후, 훨씬 더 높은 가스 가격으로 새로운 컨트랙을 송부합니다.
이 때 마이너는 더 높은 가스 가격 때문에 공격자의 해답을 먼저 채택할 것입니다. 결과적으로 공격자는 1000 이더리움을 보상으로 얻어낼 수 있습니다.
최초로 문제를 풀었던 사람은 아무 것도 얻지 못하게 되고 말이죠.
버그 수정
이러한 종류의 공격을 사용할 수있는 사용자 유형은 두 가지입니다.
악성 사용자 (거래를 위해 gasPrice를 수정할 수 있는)와 마이너(자신의 선호도에 따라 거래 순위를 결정하는)입니다.
분명한 것은 첫 번째 유형의 사용자에 의해 공격받기 쉬운 컨트랙은 최악의 컨트랙이라는 것입니다. .
왜냐하면 마이너는 한 블록을 생성할 때만 공격을 시작할 수 있기 때문에, 한 마이너가 특정 블록을 겨냥하는 것은 거의 불가능하기 때문입니다.
이러한 두 가지 유형의 공격에 대한 몇 가지 대응책을 제시합니다.
첫 번째 대응책은 제약 조건, 즉 가스 가격 한도를 만드는 것입니다.
이 방법은 사용자가 자신의 트랜젝션 승인에 더 높은 우선순위를 부여하기 위해 gasPrice를 마음대로 증가시키지 못하도록합니다.
그러나 이 방법은 첫 번째 공격 유형의 위험을 완화하는데만 사용할 수 있습니다.
이 상황에서도 마이너들은 가스 가격에 상관없이 거래 순위를 매길 수 있기 때문에 공격 가능성은 여전히 남아있게 되지요.
보다 신뢰할 수있는 방법은 커밋-리빌 (commit-reveal)법을 사용하는 것입니다.
이 방법은 사용자가 숨겨진 정보(일반적으로 해시)를 사용하여 트랜잭션을 전송하도록 규정합니다.
트랜잭션이 블록에 포함되면, 사용자는 데이터가 전송되었음을 알리는 트랜잭션을 전송합니다(revealing phase-공개 단계).
이 방법을 사용하면 마이너와 사용자가 트랜잭션의 내용을 알 수 없으므로 트랜잭션을 선행하여 승인하지 못하게합니다.
하지만 이 방법도 거래 금액자체를 숨길수는 없습니다.
예를 들어, ENS 스마트 컨트랙은 사용자로 하여금 사용자가 지출하고자 하는 이더리움 금액을 포함하는 약정 데이터의 트랜젝션을 전송하도록 합니다.
이때, 사용자는 임의의 값의 트랜잭션을 보낼 수 있습니다.
위에서 말한 revealing phase 동안에 사용자는 거래에서 보낸 금액과 지출하고자하는 금액 간의 차이를 환불받습니다.
오답노트
DAO 사건은 블록 체인 산업에 엄청난 충격을 주었고, 이로 인해 많은 투자자들이 고통을 겪어야 했습니다.
개발자는 이러한 종류의 사고의 재발을 막기위해 다음 사항에 주의해야합니다.
1. Solidity 및 기타 언어의 내장기능 및 모델들을 사용할 때는 해당 언어의 공식 instruction을 꼭 참고하십시오.
특별한 내용이 있는 경우에는 반드시 해당 요구사항을 준수해야 합니다.
2. 상태 변수들에 발생할 수있는 예외 사항에 대해서 재고하시기 바랍니다. 잠재적 위험이 있는 상태 변수들은 반드시 봉인되어 있어야합니다.
3. 가스 프라이스 한도를 포괄적으로 사용하고 거래 정보가 안전한시기에 표시되도록 최선을 다하십시오.