우리 중 거의 대부분은 계산을 위한 데이터를 입력하기 위해 Google Sheets나 Microsoft Excel을 사용해 왔습니다. 직원의 이름, 전화번호, 직위, 급여를 입력한다고 가정해 보겠습니다.
가장 간단한 형태로 스프레드시트나 Excel에서 기록이나 사례가 표시되는 방식은 다음과 같습니다.
알 수 있듯이 직원 이름과 직위는 모두 텍스트로 구성되고 전화번호와 급여는 일련의 숫자로 구성됩니다.
따라서 의미론적 관점에서 우리 인간은 이러한 필드가 실제 세계에서 무엇을 의미하는지 이해하고 이를 구별할 수 있습니다.
분명히 차이점을 알기 위해 컴퓨터 과학 학위가 필요하지 않지만 컴파일러나 인터프리터는 이 데이터를 어떻게 처리합니까?
이것은 데이터 유형이 들어오는 곳이며 프로그래머가 코딩하는 프로그래밍 언어에 따라 시간을 들여 지정하거나 지정하지 않는 것입니다.
즉, 직원 이름과 직위 아래의 데이터 포인트를 문자열이라고 합니다. 물론 급여는 소수점이 없기 때문에 분명히 정수입니다. 간단히 말해서, 이는 코딩할 때 선언해야 하는 데이터 유형이므로 해당 데이터 유형과 관련된 올바른 작업만 수행됩니다.
다음은 Solidity에서 정수 데이터 유형을 선언하는 방법입니다.
즉, 위 스프레드시트의 전화번호 필드에는 고유한 문자열로 사용될 데이터 포인트가 포함되어 있지만 이에 대한 논의는 나중에 다루겠습니다. 지금은 우리 모두가 기본 산술을 수행하는 기본 데이터 유형에 중점을 둘 것입니다.
예, 우리는 주요 산술 연산에 중요하지만 계산 범위가 제한된 정수 데이터 유형에 대해 이야기하고 있습니다.
아마도 현실 세계에서 가장 널리 알려진 정수 오버플로의 예는 차량에서 발생합니다. 주행거리계라고도 알려진 이 장치는 일반적으로 차량이 몇 마일을 이동했는지 추적합니다.
그렇다면 이동한 마일의 값이 6자리 주행 기록계에서 부호 없는 정수 값인 999999에 도달하면 어떻게 될까요?
이상적으로 마일이 하나 더 추가되면 이 값은 1000000에 도달해야 합니다. 그렇죠? 그러나 일곱 번째 숫자에 대한 규정이 있기 때문에 이런 일은 발생하지 않습니다.
대신, 아래와 같이 이동한 마일의 값이 000000으로 재설정됩니다.
정의에 따르면 일곱 번째 숫자를 사용할 수 없으므로 정확한 값이 표시되지 않아 '오버플로'가 발생합니다.
이해가 되시죠?
반대로, 이것이 흔하지 않더라도 반대의 경우도 발생할 수 있습니다. 즉, 기록된 값이 해당 범위에서 사용 가능한 최소 값보다 작은 경우를 '언더플로우'라고 합니다.
우리 모두 알고 있듯이 컴퓨터는 정수를 이진수로 메모리에 저장합니다. 이제 단순화를 위해 8비트 레지스터를 사용한다고 가정해 보겠습니다.
= 2⁸*1 + 2²*1 + 2⁶*1 + 2⁵*1 + 2⁴*1 + 2³*1 + 2²*1 + 2¹*1 + 2⁰*1
= 256 + 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= 111111111
각 비트가 1인 경우 알 수 있듯이 더 높은 값을 저장할 수 없습니다.
반면에 8비트 레지스터에 숫자 0을 저장하려는 경우 다음과 같습니다.
= 2⁸*0 + 2²*0 + 2⁶*0 + 2⁵*0 + 2⁴*0 + 2³*0 + 2²*0 + 21*0 + 2⁰*0
= 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
= 000000000
각 비트가 0인 경우 이는 더 낮은 값을 저장할 수 없음을 알려줍니다.
즉, 이러한 8비트 레지스터에 허용되는 정수 범위는 0~511입니다. 그렇다면 그러한 레지스터에 정수 512 또는 -1을 저장할 수 있습니까?
당연히 아니지. 결과적으로 주행 거리계 예제에서 이동한 마일의 재설정 값과 유사한 값을 이진 값으로 저장하게 됩니다.
분명히 그러한 숫자를 편안하게 수용하려면 몇 가지 비트가 더 있는 레지스터가 필요합니다. 그렇지 않으면 다시 한 번 오버플로 상황이 발생할 위험이 있습니다.
부호 있는 정수의 경우 음의 정수도 저장합니다. 따라서 위와 같이 허용되는 범위보다 작거나 0보다 작은 숫자를 저장하려고 하면 언더플로가 발생합니다.
그러나 계산의 목적은 결정론적인 결과를 얻는 것이므로 이는 기껏해야 짜증스러울 수 있지만 최악의 경우 수백만 달러의 손실을 초래할 수 있습니다. 특히 스마트 계약에서 이러한 정수 오버플로 또는 언더플로 오류가 발생하는 경우.
정수 오버플로와 언더플로는 수십 년 동안 존재해 왔지만 스마트 계약의 버그로 존재해 위험성이 높아졌습니다. 공격자가 이러한 오류를 악용하면 대량의 토큰이 포함된 스마트 계약이 유출될 수 있습니다.
아마도 이러한 유형의 버그가 처음 발생한 것은 3개의 주소에 대해 수십억 개의 비트코인을 생성한 블록 74638에서였을 것입니다. 소프트 포크를 통해 이 오류를 해결하고 블록을 폐기하여 트랜잭션을 무효화하는 데 몇 시간이 걸립니다.
우선, 2,100만 비트코인 이상의 거래는 거부되었습니다. 앞서 언급한 세 계좌에 너무 많은 돈을 보낸 것과 마찬가지로 초과 거래의 경우에도 다르지 않았습니다.
그러나 Ethereum 스마트 계약은 정수 오버플로와 언더플로도 경험했으며, BeautyChain도 대표적인 예입니다.
이 경우 스마트 계약에는 잘못된 코드 줄이 하나 포함되어 있습니다.
결과적으로 공격자는 이론적으로 (2²⁵⁶)-1의 가치에 달하는 BEC 토큰을 무제한으로 받을 수 있었습니다.
이제 정수 언더플로우/오버플로우가 발생하는 스마트 컨트랙트의 또 다른 예를 살펴보겠습니다.
언뜻 보기에 이 예에는 상호 작용하는 두 개의 계약이 있으며 정수 오버플로의 경우 어떤 일이 발생하는지 보여줍니다.
아래에서 볼 수 있듯이 TimeLock 계약을 사용하면 자금을 입금하고 인출할 수 있지만 차이점은 다음과 같습니다. 후자는 일정 기간이 지난 후에만 수행할 수 있습니다. 이 경우 일주일 이내에만 자금을 인출할 수 있습니다.
그러나 공격 계약에서 공격 기능을 호출하면 시간 잠금이 더 이상 유효하지 않으므로 공격자는 즉시 잔액을 인출할 수 있습니다.
즉, type(uint).max+1-timeLock.locktime(address(this)) 문으로 정수 오버플로가 발생하여 시간 잠금이 제거됩니다.
예를 들어, 위 코드를 사용하여 두 스마트 계약을 모두 배포한 후에는 아래와 같이 TimeLock 계약에서 입금 및 출금 기능을 호출하여 시간 잠금이 유지되는지 여부를 테스트할 수 있습니다.
보시다시피, 2 Ether 양을 선택하면 위에 표시된 2 Ether의 스마트 계약 잔액을 얻습니다.
구체적으로, 2 Ether의 잔액을 보유하고 있는 특정 주소는 잔액 기능 필드에 주소를 추가하고 잔액 버튼을 클릭하여 확인할 수 있습니다.
그러나 위에서 언급했듯이 시간 잠금으로 인해 아직 자금을 인출할 수 없습니다. 철회 기능을 누른 후 콘솔을 보면 빨간색 'x' 기호로 표시된 오류를 찾을 수 있습니다. 아래에서 볼 수 있듯이 계약에서 제공하는 이 오류의 원인은 "잠금 시간이 만료되지 않았습니다"입니다.
이제 아래와 같이 배포된 공격 계약을 살펴보겠습니다.
이제 공격 기능을 발동하려면 1이더 이상의 가치를 예치해야 합니다. 따라서 이 경우 아래와 같이 Ether 2개를 선택했습니다.
그 후 '공격'을 누르세요. 아래 2 Ether 잔액에서 알 수 있듯이 입금한 2 Ether가 즉시 인출되어 공격 계약에 추가됩니다.
분명히, 긴 시간 잠금은 입금하자마자 적용되어야 한다는 사실 때문에 이런 일이 발생해서는 안 됩니다. 물론, 우리가 알고 있듯이 type(uint).max+1-timeLock.locktime(address(this)) 문은 증가LockTime 함수를 사용하여 잠금 시간을 줄입니다. 이것이 바로 우리가 Ether 잔액을 즉시 인출할 수 있는 이유입니다.
그렇다면 우리는 다음과 같은 분명한 질문을 갖게 됩니다. 정수 오버플로 및 언더플로 취약점을 해결할 수 있는 방법이 있습니까?
정수 오버플로/언더플로 취약성이 치명적일 수 있다는 점을 인식하여 이 버그에 대한 몇 가지 수정 사항이 출시되었습니다. 이러한 수정 사항과 이러한 오류를 해결하는 방법을 살펴보겠습니다.
조직으로서 Open Zeppelin은 사이버 보안 기술 및 서비스와 관련하여 많은 것을 제공하며 SafeMath 라이브러리는 스마트 계약 개발 저장소의 일부입니다. 이 리포지토리에는 스마트 계약 코드로 가져올 수 있는 계약이 포함되어 있으며 SafeMath 라이브러리도 그 중 하나입니다.
SafeMath.sol 내의 함수 중 하나가 정수 오버플로를 확인하는 방법을 살펴보겠습니다.
이제 a+b 계산이 완료되면 c<a가 발생하는지 확인합니다. 물론 이는 정수 오버플로의 경우에만 해당됩니다.
Solidity의 컴파일러 버전이 0.8.0 이상에 도달하면 이제 정수 오버플로 및 언더플로 검사가 내장되어 있습니다. 따라서 언어와 이 라이브러리를 사용할 때 모두 이 라이브러리를 사용하여 이 취약점을 확인할 수 있습니다. 물론 스마트 계약에 0.8.+ 미만의 컴파일러 버전이 필요한 경우 오버플로 또는 언더플로를 방지하기 위해 이 라이브러리를 사용해야 합니다.
이제 앞서 언급했듯이 스마트 계약에 대해 0.8.0 이상의 컴파일러 버전을 사용하는 경우 이 버전에는 이러한 취약점에 대한 내장 검사기가 있습니다.
실제로 위의 스마트 계약과 작동하는지 확인하기 위해 컴파일러 버전을 “^0.8.0”으로 변경하고 재배포하면 다음과 같은 '되돌리기' 오류가 수신됩니다.
물론 2Ether의 입금은 진행되지 않는데, 이는 Time Lock 값의 오버플로를 확인하기 때문이다. 결과적으로 애초에 입금된 자금이 없기 때문에 출금이 불가능합니다.
의심할 바 없이 여기서는 Attack.attack() 함수 호출이 작동하지 않았으므로 모두 괜찮습니다!
이 긴 블로그 게시물에서 수집해야 할 사항이 있다면 BEC 공격과 마찬가지로 이 취약점을 무시하면 비용이 많이 들 수 있다는 것입니다. 또한 알 수 있듯이 선택하지 않은 채로 두면 악의적이지 않은 오류가 발생하기 쉽습니다. 또는 해커가 이 취약점을 악용하는 것만큼 간단합니다.
BEC 공격이 어떻게 발생했는지에 대한 우리의 이해를 바탕으로 이 취약점을 인식하면 제공되는 수정 사항 덕분에 스마트 계약을 작성할 때 공격을 예방하는 데 큰 도움이 될 수 있습니다. 당신을 넘어뜨리기 위해 기다리고 있는 다른 스마트 계약 취약점이 여러 개 있더라도 말입니다.