1. 変数の保存/代入/削除:
副題
1. 機密情報をチェーン上に保存しないでください
脆弱性の詳細:ブロックチェーンベースの透明性、チェーン上にデプロイされた契約データは透明で可視です
、プライベートに変更された変数であっても。プライベートの可視性は関数と外部コントラクトに対してのみであるため、どのユーザーもチェーン上のデータを取得することでこれらの値を取得できます。この場合、オンチェーンのプライベート変更によって保証されると期待される機密性操作は安全ではありません。
contract Eocene{
mapping(address => bytes 32) candidate;
uint private seed = 0x 12341 3d;
function select() public{
bytes 32 result = keccak 256(abi.encodePacked(seed));
if(result == candidate[msg.sender]){
payable(msg.sender).transfer( 1 ether);
}
}
}
次のような単純な懸賞コードが存在するとします。
シードがプライベート変数として宣言されている場合でも、誰でもコントラクト アドレスとシードのスロット位置を通じてチェーン上のシード値を取得し、対応する値を計算して eth を取得できます。
コントラクトには検証用のキー値を保存せず、キー値をオフチェーンに保存し、対応する検証ロジックのみをチェーン上に実装します。
副題
2. 変数のデフォルト値に注意する
脆弱性の詳細:
Solidity では、変数の初期値は 0/false です。この場合、ある変数に基づいて判断する際に、変数の初期値の影響を考慮しないと、それ相応のセキュリティ上の問題が発生する可能性があります。
contract Eocene{
mapping(address => bool) unlocked;
uint averageDrop;
address token;
function setAverageDrop() public {
averageDrop = 1000;
}
function drop() public {
if(unlocked[msg.sender] == false){
ERC 20(token).transfer(msg.sender, averageDrop);
}
}
}
次のエアドロップのロック解除コードを考えてみましょう。
コントラクトの本来の目的は、ロックされていないすべてのアドレスにトークンを配布することですが、Solidity ではすべての変数の初期値が 0/false であることが無視されます。マッピング タイプでは、キーはスロットとのスプライシングにのみ使用され、ストレージ内のキーに対応するアドレスは keccak 256 を通じて計算されます。つまり、初期化されているかどうかに関係なく、どのアドレスにもそれに対応するストレージがあることになります。 、初期値は通常 0/false です。
いかなる状況でも、特にマッピング型に基づく変数では、変数のデフォルト値に基づいて重要な判断を行わず、そのような問題を厳重に防止してください。
副題
3. 使用しなくなった構造体型の値を削除する
脆弱性の詳細:
どのマッピング タイプでも、値フィールドのタイプが構造体で、対応する値が不要になった場合は、削除設定を使用して値を削除する必要があります。それ以外の場合、値は対応するスロットに残ります。
contract Eocene{
struct Stake{
uint amount;
uint needReceive;
uint startTime;
}
mapping(address => Stake) stakes;
mapping(address => bool) staker;
function getStake() public{
Stake memory _stake = stakes[msg.sender];
(msg.sender).transfer(_stake.needReceive);
staker[msg.sender] = false;
// delete stakes[msg.sender] // need do but don't
}
function calReceive() public{
require(staker[msg.sender],'not staker');
stakes[msg.sender].needReceive = stakes[msg.sender].amount * (block.time - stakes[msg.sender].startTime);
stakes[msg.sender].amount = 0;
}
}
次の形式のコードを考えてみましょう。
上記のコントラクトコードは、プレッジ金額とプレッジ時間に基づいて取得されるethの量を計算しますが、プレッジ完了後はstaker[msg.sender]の値のみがfalseに設定され、対応するstakes[msg.sender]はまだ存在しています。したがって、攻撃者は制限なく getStake() 関数を呼び出して eth を取得できます。
修復措置:もちろん、上記のコードには、脆弱性の存在につながるその他の補助的な問題もいくつかありますが、次のことに注意する必要があります。それ以外の場合、値のペアは対応するスロットに常に存在します。
2. 関数の定義:
副題
1. 表示する必要がある宣言された関数の可視性
脆弱性の詳細:関数のデフォルトの可視性は public です。どの関数でも、その可視性を明示的に宣言する必要があります
、特に基礎となる関数を呼び出すために関数が複数のレイヤーにネストされている場合、過失によって基礎となる関数に誤って可視性が与えられることを防ぐため、過失によって引き起こされる抜け穴の存在を防ぎます。
contract Eocene{
mapping(address => bool) whitelist;
function _a() {
payable(msg.sender).transfer( 1 ether);
}
function a() public{
require(whitelist[msg.sender],'not in whitelist');
_a();
}
}
次の脆弱なコードの例を考えてみましょう。
a() 関数は、require によってホワイトリストのアドレスを制限し、渡した後に対応するアドレスに送金します。通常の状況では、_a() は外部から呼び出されるべきではありませんが、ここでは _a() の可視性が明示的に宣言されていないため、パブリックとみなされるため、外部から直接呼び出すことができます。
すべての関数の可視性を明示的に宣言します。特に外部から直接呼び出すことができない関数の場合は、明示的に protected または private として宣言する必要があります。
副題
2. 関数リエントラント攻撃
脆弱性の詳細:
他の関数と同様に、再入後に発生する可能性のある問題を考慮する必要があります。ここでの再入可能性には、転送/送信/呼び出し/静的コールなどの外部呼び出しによって引き起こされるすべての再入可能性の問題が含まれます。
contract Fund {
mapping(address => uint) shares;
function withdraw() public {
if (payable(msg.sender).send(shares[msg.sender]))
shares[msg.sender] = 0;
}
}
次のコード形式を考えてみましょう。
異議申し立てコントラクトの場合、msg.sender が悪意のある場合、msg.sender が現在のすべてのコントラクトの残高を無制限に抽出する可能性があります。ただし、transfer/send/call/staticall および外部コントラクト関数を呼び出す場合、再入可能の問題が発生する可能性があることにも注意する必要があります。
コントラクトの具体的な実装の詳細に従って、キー変数の実装を最初に変更できます。たとえば、アピールコントラクトでは、最初にshares[msg.sender]の値を記録し、その後送信操作を実行できます。 share[msg.sender]を0に設定した後。もちろん、グローバル変数とデコレータの組み合わせでも実現できます。
3. 外部とのやり取り
副題
1. 外部呼び出しのアドレスと関数リストを制限する
脆弱性の詳細:
外部関数の呼び出しについては、合理的な状況の下で、呼び出されるコントラクト アドレスとコントラクト関数を制限する必要があります。
contract Eocene{
function callExt(address _target, bytes calldata data) public{
_target.call(data);
}
function delegateCallExt(address _target, bytes calldata data) public{
_target.delegatecall(data);
}
}
次のコード形式を考えてみましょう。
callExt 関数は、任意の関数の任意のアドレスを呼び出すために使用されます。この場合、再エントリの問題が発生しやすくなります。コントラクトが任意のウォレットにトークン資産を保持すると、対応するトークンの転送関数を直接呼び出すことができます。この機能を使って歩きます。
ただし、delegateCallExt 関数の呼び出し時に制限がない場合、コントラクトが直接破棄され、コントラクトのアドレス残高全体が転送され、コントラクトが破棄される可能性があります。
外部コントラクトへの呼び出しについては、まずアドレスがホワイトリストで制限できるかどうかを検討し、さらに指定したアドレスの関数名が制限できるかどうかを検討します。
副題
2. call、send、delegatecall、staticcallを使用する場合、外部呼び出しの判定は例外だけでなく戻り値でも判定する必要がある
脆弱性の詳細:
アピール関数は内部エラーによる復帰を引き起こすのではなく、単に復帰を返すだけです。これらを使用するときは常に、関数の戻り値を使用して、実行が成功したかどうかを判断する必要があります。
contract Eocene{
address token; //any token address
function deposit(uint amount) public{
token.call(abi.EncodeWithSignature("transferfrom(address from, address receipt, uint amount)"), msg.sender, address(this), amount);
mint(msg.sender, amount);
}
}
次のサンプルコードを考えてみましょう。
デポジット() 関数では、コントラクトはまず msg.sender の指定されたトークンを現在のアドレスに転送しようとします。転送が成功した後、msg.sender の現在のコインを鋳造します。ただし、.call 関数は失敗してもトランザクション全体を元に戻さないため、msg.sender から現在のアドレスへの通貨の転送に失敗した場合でも、msg.sender に金額の現在の通貨が鋳造されます。
call、send、delegatecall、staticcallの実行結果の判定は、元に戻るかどうかではなく、戻り値で判断する必要があります。
4 番目に、アクセス制御:
副題
1. tx.origin に基づく ID 認証がない
脆弱性の詳細:tx.origin に基づいて認証しない
, tx.origin はトランザクション全体のイニシエーターであり、コントラクトの再帰呼び出しによって変更されません。tx.origin に基づく認証では、tx.origin が msg.sender であることは保証できません。 tx.origin ベースの認証により、ユーザー アカウントのセキュリティも強化されます。
contract Eocene{
mapping(address=>bool) whitelist;
function freeDeposit() public{
require(whitelist[tx.origin],'not in whitelist');
payable(msg.sender).transfer( 1 ether);
}
}
次の脆弱性の例を考えてみましょう。
ホワイトリスト内のアドレスが何らかのフィッシング リンクによって誘導され、一見無害な悪意のあるコントラクトのアドレスと関数を呼び出し、その悪意のあるアドレスがサンプル コードの freeDeposit 関数を呼び出すと、ホワイトリストのアドレスに属するはずの資産が、悪意のある契約アドレス。
修復措置:tx.origin に基づく認証はありません。または、アピールコードを payable(tx.origin).transfer( 1 ether) に変更しても問題ありません。, ただし、require(whitelist[msg.sender],'not inwhitelist'); を使用して判断します。
副題
2. extcodesizeの戻り値でeosアカウントを判断しないでください。
脆弱性の詳細:コントラクトコードの初期化フェーズでは、アドレスがコントラクトアドレスであってもextcodesizeの戻り値は0となります。
、戻り値に基づいて判断すると、得られる結果は不正確になります。
contract Eocene{
function withdraw() public{
uint size;
assembly {
size := extcodesize(caller())
}
require(size== 0,"not eos account");
msg.sender.transfer( 1 ether);
}
}
次のコード形式を考えてみましょう。
異議申し立てコントラクトの撤回関数では、EOS アカウントのみが extcodesize 戻り値を通じてトークンを取得できることが望まれますが、コントラクトが初期化されるとき、コントラクト アドレスの extcodesize 戻り値も 0 であることは無視されます。その結果、判定は不正確となり、どのアドレスでもコントラクトからトークンを取得できるようになります。
いつでも外部アドレスが契約アドレスであるかどうかに基づいて判断せず、どの種類のアカウントでも契約コードが正常に機能することを確認するようにしてください。
5. 算術演算
副題
1. 数値演算を行う場合、オーバーフロー問題を考慮する
脆弱性の詳細:
オーバーフロー問題とは、コントラクトが整数演算を行うときに発生するオーバーフロー問題を指します。その主な理由は、数値型には最大長があり、2 つの整数の演算がその最大値を超えると、超えた部分が切り捨てられて問題が発生するためです。
contract Eocene{
mapping(address=>uint) balanceof;
function withdraw(uint amount) public{
payable(msg.sender).transfer(amount);
balanceof[msg.sender] = balanceof[msg.sender]-amount;
require(balanceof[msg.sender] >= 0,'not enough balance');
}
}
次のコード形式を考えてみましょう。
アピール関数では、balanceof[msg.sender] < amountの場合、balanceof型が符号なし整数に限定されるため、合計の計算結果がint型の負の値となり、uint型に変換すると、非常に大きな正の値の場合、この時点で require の制限がバイパスされ、攻撃者はコントラクト サマリーから任意の数のトークンを盗むことができます。
修復措置:、最終的な計算結果がオーバーフローを引き起こさないようにするため。
副題
2. 整数演算を行う場合は、int 型を慎重に使用してください。
脆弱性の詳細:
整数型を使用して計算を行う場合は、この種の操作が必要でない限り、計算のために uint 型を int 型に変換するように注意してください。 uint型の整数をint型に変換する際、uint型のオーバーフローが発生する場合があり、int型では無効となるためです。
contract Eocene{
int public result;
uint public uresult;
function cal(uint _a, uint _b) public{
result = int(_a)-int(_b);
uresult = uint(result);
}
}
次のコード形式を考えてみましょう。
Solidity バージョン 0.8.0 以降でコンパイルする場合、 `cal( 0, 1)` を呼び出すと、 uint では ` 0-1` がオーバーフローを起こしても、 int 型 revert の計算ではオーバーフローしません ( 0-1 の結果は int 型の範囲内にあります)。また、結果値を uint 型に変換すると、実際の uint 型の計算がオーバーフローした後の結果値となり、隠れたオーバーフロー問題が発生します。
ただし、ここで cal(type(int).min, type(int).max) を呼び出した場合、この時点の整数計算も int 型の範囲を超えているため、依然として revert がトリガーされることに注意してください。
修復措置:進行中。整数演算自体がオーバーフローする必要がある場合は、uncheck を使用して uint 型演算の実装をラップすることを検討してください。
副題
3. 精度が失われる可能性のある操作では、拡張することで許容できない精度の損失を防ぎます。
脆弱性の詳細:
整数演算を行う場合、精度の低下によって生じる可能性のある問題が考慮され、精度が拡張されます。
contract Eocene {
uint totalsupply;
mapping(address=>uint) balancesof;
uint BasePrice = 1 e 16;
function mint() public payable {
uint tokens = msg.value/BasePrice;
balancesof[msg.sender] += tokens;
totalsupply += tokens;
}
}
次のフォームのコードを考えてみましょう。
アピールコントラクトを考慮すると、mintでは取得すべきトークン数はmsg.value/basePriceで計算されますが、`/`の計算精度の都合上、msg.valueが1 e 16未満の部分は全てロックされてしまいます。契約の過程で、これはイーサの無駄につながるだけでなく、ユーザーエクスペリエンスにも非常に悪影響を及ぼします。
精度が欠落している可能性がある整数計算の場合は、最初に整数を `* 1 eN` だけ拡張します (N は必要な精度サイズです)。
6. ランダムな値:
副題
1. 推測可能な/操作可能なオンチェーンデータを乱数シードとして使用しないでください。
脆弱性の詳細:
ブロックチェーンの特殊性により、チェーン上に真のランダムな値は存在せず、チェーン上のデータを乱数値や乱数のシードとして使用すべきではなく、オフチェーンから乱数値を取得することを検討してください。
contract Eocene{
function winner(bytes 32 value) public payable{
require(msg.value > 0.5 ether,"not enough value");
if(value == keccak 256(abi.encodePacked(block.timestamp))){
msg.sender.transfer( 1 ether);
}
}
}
コード例は次のとおりです。
異議申し立てコントラクトの場合、現在のブロック時間タグを使用してランダム値を計算し、それをユーザーが送信したランダム値と比較し、同じランダム値でユーザーに報酬を与えます。時間に基づくランダムな状況のように見えますが、実際には、keccak 256 (abi.encodePacked(block.timestamp)) を使用するユーザーは誰でもコントラクト呼び出しを通じて値を計算し、その値を勝者関数のコントラクト コードに送信して、 eth を取得します。さらに、block.timestamp の値はマイナーによって悪意を持って改ざんされる可能性があり、必ずしも公平ではないことも理解する必要があります。
修復措置:、チェーンリンクを使用してオフラインのランダム値を取得することを検討してください。
セブン、DOS:
副題
1. 状態変数配列全体をメモリ変数にコピーする操作は禁止されています。
脆弱性の詳細:
関数の利用可能なメモリ サイズに対する Solidity の制限はストレージ (0x ffffffffffffffff) よりもはるかに低いため、動的配列全体をメモリにコピーする動作は利用可能なメモリ サイズを超え、元に戻される可能性があります。
contract Eocene{
uint[] id;
function pop(uint amount) public{
require(amount>0,'not valid amount');
uint[] memory _id=id; // this may be revert because of memory space limit
for(uint i= 0;i<_id.length;i++)
{
if(amount==_id[i]){
id[i] = 0;
}
}
}
function push(uint amount) public{
require(amount>0,'not valid amount');
id.push(amount);
}
}
次のコード形式を考えてみましょう。
上記のコードでは、`uint[]memory_id=id;`はストレージにある`uint[]id;`の変数値をメモリに置き、push関数は`uint[]idに値を挿入することができます。 ;`、そして Solidity にはメモリ空間に制限があるため、`uint[] id;` の長さが `(0x ffffffffffffffff-0x 40)/0x 20-1` を超えると、過剰なメモリ使用量が発生して元に戻ります。これは、このコントラクトの Pop 関数が正常に実行できないこと、または `uint[]memory _id=id;` 操作を伴う関数が正常に実行できないことを意味します。
修復措置:可変動的配列を常にメモリにコピーしないでください。メモリ使用量がこの値を超える関数は正常に実行できません。。
副題
2. どの for ループでも、外部変更可能な変数に基づいてループ判定を行うことはできません。
脆弱性の詳細:
for ループの判定を外部変更変数に基づいて行う場合、外部変更変数が大きすぎてガス消費量が多すぎるという問題が発生する可能性があります。 DOS 攻撃は、ガス消費量が各契約発信者が耐えられるほど高い場合に発生します。
contract Eocene{
uint[] id;
function pop(uint amount) public{
require(amount>0,'not valid amount');
for(uint i= 0;i
{
if(amount==id[i]){
id[i] = 0;
}
}
}
function push(uint amount) public{
require(amount>0,'not valid amount');
id.push(amount);
}
}
次のコードを考えてみましょう。
ここでは、ストレージからメモリに配列をコピーする操作を削除しますが、このコードのもう 1 つの問題は、for ループが `uint[] id;` の長さに基づいており、id の長さは増やすことしかできず、増やすことはできないことです。つまり、pop()関数で消費されるガスがどんどん大きくなっていき、そのガスが大きすぎてpop関数の実行で許容できる最大ガス消費量を超えてしまうと、pop関数を実行する人が少なくなってしまいます。 、DOS攻撃が実現します。
修復措置:、dos 問題の存在を防ぐために、ループ操作はその実行の最大長を判断できる必要があります。
副題
3. ループ内で、try/catch を使用して未確定の例外をキャッチします。
脆弱性の詳細:
contract Eocene{
address[] candidates;
mapping(address=>uint) balanceof;
function claim() public{
for(uint i= 0;i
{
address candidate = candidates[i];
require(balanceof[candidate]>0,'no balance');
payable(candidate).transfer(balanceof[candidate]);
}
}
}
ループ内の外部アドレスによって引き起こされる可能性のある復帰がある場合は、復帰のキャプチャを考慮する必要があります。そうしないと、内側のループの実行が失敗すると、それまでのすべてのガス消費が無意味になってしまいます。ただし、ループ内の実行失敗を外部アドレスで制御できる場合、起こり得る例外をキャッチするための try/catch がないと、ループの判定が完全に完了せず、DOS 攻撃が発生する可能性があります。
このコードでは、for ループを使用して各候補に送金しますが、フォールバックまたは受信関数で候補が直接元に戻されるとループが正常に実行されず、DOS 攻撃が発生することが考慮されていません。
どの for ループでも、外部呼び出しがあり、その呼び出しが元に戻るかどうか判断できない場合は、元に戻すことによって引き起こされる DOS 攻撃を防ぐために、try/catch を使用して例外を補充する必要があります。。
8. 上位バージョンのコンパイラを使用します。
副題
1. 0.8.17 以降のコンパイラを使用してコントラクトをコンパイルします
最初のレベルのタイトル
- Solidity
私たちについて
At Eocene Research, we provide the insights of intentions and security behind everything you know or don't know of blockchain, and empower every individual and organization to answer complex questions we hadn't even dreamed of back then.
