Rust 스마트 컨트랙트 개발일지 (4)
BlockSec
2022-03-29 10:30
本文约6467字,阅读全文需要约26分钟
계약 안전 정수 오버플로.

관련 기사:

Rust 스마트 컨트랙트 개발일지 (1) 컨트랙트 상태 데이터 정의 및 메소드 구현

그림

Rust 스마트 컨트랙트 개발일지 (1) 컨트랙트 상태 데이터 정의 및 메소드 구현

Rust 스마트 계약 개발 일기 (2) Rust 스마트 계약 단위 테스트 작성

Rust 스마트 계약 개발 일기 (2) Rust 스마트 계약 단위 테스트 작성

Rust 스마트 컨트랙트 개발일지 (3) Rust 스마트 컨트랙트 배포, 함수 호출 및 Explorer 활용

Rust 스마트 컨트랙트 개발일지 (3) Rust 스마트 컨트랙트 배포, 함수 호출 및 Explorer 활용

첫 번째 레벨 제목

1. 정수 오버플로 취약점 개요

그림

대부분의 프로그래밍 언어에서 정수 값은 일반적으로 고정 길이 메모리에 저장됩니다. 정수는 부호 없는 것과 부호 있는 두 가지 유형으로 나눌 수 있습니다. 이들의 차이점은 최상위 비트를 정수의 부호를 나타내는 데 사용되는 부호 비트로 사용하는지 여부입니다. 예를 들어, 32비트 메모리 공간은 0에서 4,294,967,295 사이의 부호 없는 정수(uint32) 또는 -2,147,483,648에서 2,147,483,647 사이의 부호 있는 정수(int32)를 저장할 수 있습니다.

하지만 uint32 범위에서 4,294,967,295 + 1을 계산하고 그 결과를 해당 정수 유형의 최대값보다 크게 저장하려고 하면 어떻게 될까요?

   0xFFFFFFFF
+ 0x00000001
------------
= 0x00000000

이 실행 결과는 특정 프로그래밍 언어 및 컴파일러에 따라 다르지만 대부분의 경우 계산 결과는 "오버플로"를 표시하고 0을 반환합니다. 동시에 대부분의 프로그래밍 언어와 컴파일러는 이러한 유형의 오류를 확인하지 않고 간단한 모듈로 연산만 수행하며 정의되지 않은 다른 동작도 있습니다.

정수 오버플로의 존재는 종종 프로그램이 런타임에 예기치 않은 결과를 생성하게 만듭니다. 블록체인 스마트 계약 작성 시, 특히 분산 금융 분야에서 정수 수치 계산의 사용 시나리오는 매우 일반적이므로 정수 오버플로 취약점의 가능성에 특별한 주의를 기울여야 합니다.

   0x00000000
- 0x00000001
------------
= 0xFFFFFFFF

금융 기관이 부호 없는 32비트 정수를 사용하여 주가를 표시한다고 가정합니다. 그러나 이 정수 유형을 사용하여 유형이 표현할 수 있는 최대값보다 큰 숫자를 나타낼 때 컴퓨터는 32비트 메모리 범위 외부에 추가로 1개 이상의 비트를 배치하고(즉, 오버플로) 마지막으로 숫자를 오버플로 비트 이외의 값은 잘리고 $429,496,7296은 가능한 한 0으로 읽습니다. 이때 누군가 그 가치로 계속해서 거래를 하면 주가는 0이 되어 온갖 혼란을 야기하게 된다. 따라서 정수 오버플로 취약점 문제는 우리가 주목해야 할 문제입니다.

Rust 언어로 스마트 계약을 작성할 때 정수 오버플로를 피하는 방법은 이 기사의 후속 논의의 초점이 될 것입니다.

2. 정수 오버플로 정의

https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

값이 변수 유형이 표현할 수 있는 범위를 초과하면 오버플로가 발생합니다. 오버플로는 크게 정수 오버플로(오버플로)와 언더플로(언더플로)의 두 가지 경우로 나눌 수 있습니다.

1. function batchTransfer(address[] _receivers, uint256 _value) public whenNotPausedreturns (bool) {
2.     uint cnt = _receivers.length;
3.     uint256 amount = uint256(cnt) * _value;
4.     require(cnt > 0 && cnt <= 20);
5.     require(_value > 0 && balances[msg.sender] >= amount);
6. 
7.     balances[msg.sender] = balances[msg.sender].sub(amount);
8.     for (uint i = 0; i < cnt; i++) {
9.         balances[_receivers[i]] = balances[_receivers[i]].add(_value);
10.        Transfer(msg.sender, _receivers[i], _value);
11.   }
12.    return true;
13. }

2.1 정수 오버플로

즉, 위의 정수 오버플로 취약점 개요에서 설명한 것과 유사하게, 예를 들어 Solidity에서 uint32로 표현할 수 있는 부호 없는 정수의 범위는 다음과 같습니다. 0 ~ 2^32 - 1, 2^32 - 1이 표현됩니다. 16진수에서 0xFFFFFFFF로 2^32 - 1 더하기 1은 오버플로를 유발합니다.

2.2 정수 언더플로

부호 없는 정수 uin32의 표현 범위에도 하한이 있습니다. 즉, 최소값은 0입니다. 0에서 1을 빼면 uint32 정수의 언더플로가 발생합니다.

3. 정수 오버플로 인스턴스

BeautyChain 팀은 2018년 4월 22일 BEC 토큰이 4월 22일 비정상적으로 변동했다고 발표했습니다. 공격자는 정수 오버플로로 인한 취약점을 악용하여 10^58 BEC를 성공적으로 획득했습니다.

[profile.release]
overflow-checks = true
panic = "abort"

본 컨트랙트의 공격 이벤트에서 공격자는 정수 오버플로 취약점이 있는 "batchTransfer" 함수를 실행하여 트랜잭션을 생성했습니다.

다음은 이 함수의 구체적인 구현입니다.

이 기능은 여러 주소(수취인)에게 송금하는 데 사용되며 각 주소의 송금 금액은 가치입니다.

위 코드의 세 번째 줄 uint256 amount = uint256(cnt) * _value는 이체해야 하는 전체 금액을 계산하는 데 사용되지만 이 코드 줄에는 정수 오버플로가 발생할 가능성이 있습니다. 값 = 0x8000000000000000000000000000000000000000000000000000이고 수신자의 길이가 2일 때 세 번째 코드 줄의 곱셈 연산 중에 정수 오버플로가 발생하여 amount = 0이 됩니다. amount = 0이 사용자의 balances[msg.sender]보다 작기 때문에 5행에서 컨트랙트 발신자 msg.sender의 잔액이 이체할 금액보다 큰지 확인하는 것은 쉽게 통과된다. 이러한 방식으로 공격자는 후속 전송 작업을 수행하여 이익을 얻을 수 있습니다.

4. 정수 오버플로 방지 기술

이 섹션에서는 정수 오버플로를 방지하기 위해 Rust 언어의 기능과 결합된 몇 가지 일반적인 방법을 사용하는 방법을 소개합니다.

Rust 언어에서: 릴리스 버전의 대상 파일을 컴파일할 때 구성되지 않은 경우 Rust는 기본적으로 정수 오버플로를 확인하지 않습니다. 8비트 부호 없는 정수(uint8)의 경우와 같이 정수가 오버플로되면 Rust의 일반적인 접근 방식은 값 256을 0으로, 257을 1로 만드는 식입니다. 이때 Rust는 Panic을 트리거하지 않지만 변수의 값이 예상한 값이 아닐 수 있습니다. 따라서 우리는 Rust 프로그램의 컴파일 옵션을 약간 구성하여 프로그램이 릴리스 모드에서 정수 오버플로를 확인하고 패닉을 트리거하여 정수 오버플로로 인한 프로그램 예외를 방지할 수 있도록 해야 합니다."0.9.1"릴리스 모드에서 정수 오버플로를 확인하도록 Cargo.toml을 구성합니다.

[dependencies]
이 구성을 사용하여 프로그램에서 정수 오버플로에 대한 처리 전략을 설정할 수 있습니다.
uint = { version = "0.9.1", default-features = false }

4.1 더 큰 정수를 지원하기 위해 Rust Crate 단위 사용(현재 최신 버전은 0.9.1)

use uint::construct_uint;

Solidity가 지원할 수 있는 가장 큰 정수형이 u256인 것에 비해 Rust의 현재 표준 라이브러리가 제공할 수 있는 가장 큰 정수형은 u128에 불과합니다. Rust 스마트 계약에서 더 큰 정수 작업을 더 잘 지원하기 위해 Rust 단위 크레이트를 사용하여 확장을 도울 수 있습니다.

construct_uint! {
    pub struct U1024(16);
}
construct_uint! {
    pub struct U512(8);
}
construct_uint! {
    pub struct U256(4);
}

4.1.1 Rust 단위 상자 소개

Rust 단위 크레이트를 사용하여 큰 부호 없는 정수 유형을 제공하고 Rust의 원래 정수 유형과 매우 유사한 API에 대한 내장 지원을 제공하면서 성능과 크로스 플랫폼 사용성을 고려하세요.

// (2^1024)-1 = 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215 
let p =U1024::from_dec_str("179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215").expect("p to be a good number in the example");

4.1.2 Rust uint crate 사용법

   #[test]
   fn test_uint(){
     let p = U1024::from_dec_str("179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215").expect("p to be a good number in the example");
     assert_eq!(p,U1024::max_value());
   }

먼저 Rust 프로젝트의 Cargo.toml에서 uint 크레이트에 대한 종속성을 추가하고 버전 번호를 최신으로 지정합니다.

running 1 test
test tests::test_uint ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s

버전.

# near-sdk, near-contract-standard 등과 같은 기타 종속성

   #[test]
   fn test_overflow(){
그런 다음 Rust 프로그램에서 크레이트를 가져와서 사용할 수 있습니다.
       let amounts: u128 = 340282366920938463463374607431768211455;
     
다음 명령문을 사용하여 원하는 부호 없는 정수 유형을 생성할 수 있습니다.
       let amount_u256 = U256::from(amounts) * U256::from(amounts);
       println!("{:?}",amount_u256);

4.2 uint 유형 변환 기능을 사용하여 정수 오버플로 감지
       let amount_u256 = U256::from(amounts) + 1;
       println!("{:?}",amount_u256);
       
다음 방법을 사용하여 먼저 변수 p를 정의하고 U1024에 대해 uint crate에서 정의한 from_dec_str 방법을 사용하여 변수 p에 값을 할당할 수 있습니다.
      let amount_u128 = amount_u256.as_u128();
       println!("{:?}",amount_u128);
   }

단위 테스트 1: uint가 U1024가 표현할 수 있는 최대값을 지원할 수 있는지 확인하는 데 사용됩니다.

running 1 test
115792089237316195423570985008687907852589419931798687112530834793049593217025
340282366920938463463374607431768211456
thread 'tests::test_overflow' panicked at 'Integer overflow when casting to u128', src/lib.rs:16:1

단위 테스트 결과:

변수 p:U1024는 U1024가 표현할 수 있는 최대값을 정확히 저장하고 있음을 알 수 있다.

단위 테스트 2: 정수 오버플로 테스트

// u128이 표현할 수 있는 최대값, 즉 2^128 -1<_>// U256은 일반적으로 (2^128 -1)*(2^128 -1)의 연산 결과를 오버플로 없이 표현할 수 있습니다.

// 여기서 (2^128 -1) + 1 = 2^128

#[test]
fn test_underflow(){
   let amounts= U256::from(0);
   let amount_u256 = amounts.checked_sub(U256::from(1));
   println!("{:?}",amount_u256);
}

// u128 unsigned integer로 표현할 수 있는 0에서 2^128 -1 범위를 오버플로하므로 Panic이 트리거됩니다.

running 1 test
None
test tests::test_underflow ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s

단위 테스트 결과는 다음과 같습니다.

#[test]
fn test_underflow(){
    let amounts= U256::from(0);
-    let amount_u256 = amounts.checked_sub(U256::from(1));
+    let amount_u256 =amounts.checked_sub(U256::from(1)).expect("ERR_SUB_INSUFFICIENT");
    println!("{:?}",amount_u256);
}

uint crate에서 제공하는 타입 변환 함수 .as_u128() 기능에 따르면 amount_u256이 타입별로 u128로 변환되면 u128 unsigned integer가 표현할 수 있는 범위를 초과하여 Painc가 트리거됩니다. 이때 러스트가 정수 오버플로를 감지할 수 있음을 알 수 있습니다.

running 1 test
thread 'tests::test_underflow' panicked at 'ERR_SUB_INSUFFICIENT', src/lib.rs:126:62

4.3 안전한 수학을 사용하여 정수 오버플로 및 언더플로 확인

Rust 언어는 또한 정수 연산에서 발생할 수 있는 정수 오버플로에 대해 다양한 연산 동작을 제공합니다. 정수 오버플로의 동작을 더 세밀하게 제어해야 하는 경우 표준 라이브러리에서 Wrapping_*, saturating_*, checked_* 및 overflowing_* 시리즈 함수를 호출할 수 있습니다. 이 섹션에서는 checked_* 함수에 중점을 둘 것입니다. 독자는 위의 키워드를 검색할 수 있습니다. 정수 오버플로를 제어하는 ​​방법에 대해 자세히 알아보세요.

checked_*가 반환하는 유형은 옵션입니다.

BlockSec
作者文库