
関連記事:
Rustスマートコントラクト開発日記(1)コントラクト状態データの定義とメソッド実装
Rustスマートコントラクト開発日記(1)コントラクト状態データの定義とメソッド実装
Rust スマートコントラクト開発日記 (2) Rust スマートコントラクトの単体テストを書く
Rust スマートコントラクト開発日記 (2) Rust スマートコントラクトの単体テストを書く
Rustスマートコントラクト開発日記(3) Rustスマートコントラクトのデプロイ、関数呼び出し、エクスプローラーの使い方
Rustスマートコントラクト開発日記(3) Rustスマートコントラクトのデプロイ、関数呼び出し、エクスプローラーの使い方
最初のレベルのタイトル
1. 整数オーバーフローの脆弱性の概要
写真
ほとんどのプログラミング言語では、整数値は通常、固定長メモリに格納されます。整数は、符号なしと符号付きの 2 つのタイプに分類できます。それらの違いは、最上位ビットが整数の符号を示すために使用される符号ビットとして使用されるかどうかです。たとえば、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
値が変数の型で表現できる範囲を超えるとオーバーフローが発生します。オーバーフローは主に整数オーバーフロー(overflow)とアンダーフロー(underflow)の2つに分けられます。
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トークンが異常変動したことを発表しました。攻撃者は、整数オーバーフローによって引き起こされる脆弱性を悪用して、10^58 BEC の取得に成功しました。
[profile.release]
overflow-checks = true
panic = "abort"
このコントラクトの攻撃イベントでは、攻撃者は整数オーバーフローの脆弱性を備えた関数「batchTransfer」を実行してトランザクションを実行しました。
この関数の具体的な実装は次のとおりです。
この機能は複数のアドレス(受取人)に送金するために使用され、各アドレスの送金金額が価値となります。
上記のコードの 3 行目 uint256 amount = uint256(cnt) * _value は、転送する必要がある全体の量を計算するために使用されますが、このコード行では整数オーバーフローが発生する可能性があります。値 = 0x80000000000000000000000000000000000000000000000000000 の場合、レシーバーの長さが 2 の場合、コードの 3 行目の乗算演算中に整数オーバーフローが発生し、量 = 0 になります。 amount = 0はユーザーの残高[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 uint を使用してより大きな整数をサポートする (現在の最新バージョンは 0.9.1)
use uint::construct_uint;
Solidity がサポートできる最大の整数型は u256 であるのに対し、Rust の現在の標準ライブラリが提供できる最大の整数型は u128 のみです。 Rust スマート コントラクトでより大きな整数演算をより適切にサポートするために、Rust uint クレートを使用してスケーリングを支援できます。
construct_uint! {
pub struct U1024(16);
}
construct_uint! {
pub struct U512(8);
}
construct_uint! {
pub struct U256(4);
}
4.1.1 Rust uint クレートの概要
Rust uint クレートを使用して、パフォーマンスとクロスプラットフォームの使いやすさを考慮しながら、大きな符号なし整数型と、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 クレートの使用方法
#[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 crate への依存関係を追加し、バージョン番号を最新のものとして指定します。
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
バージョン。
# 他の依存関係 (sdk に近い、契約標準に近いなど)。
#[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
単体テストの 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 符号なし整数で表現できる範囲 0 から 2^128 -1 をオーバーフローするため、パニックがトリガーされます。
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 符号なし整数が表現できる範囲をオーバーフローするため、Painc がトリガーされます。この時点で Rust が整数オーバーフローを検出できることがわかります。
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_* 関数に焦点を当てます。読者は上記のキーワードで検索できます。整数オーバーフローを制御する方法の詳細をご覧ください。
selected_* によって返されるタイプは Option です