注意! イーサリアムのSはセキュリティの略です。 パート3.実際の堅牢性







Solidity言語のスマートコントラクトに固有の典型的な脆弱性、攻撃、問題領域、およびEthereumプラットフォーム全体に特化したシリーズの第3部を紹介します。 ここでは、Solidityの機能と、それらが熟練者の手に渡る脆弱性について説明します。







前半では、フロントランニング攻撃、さまざまな乱数生成アルゴリズム、および権限証明コンセンサスを使用したネットワークの復元力について説明しました。 2番目は、整数オーバーフロー、ABIエンコード/デコード、初期化されていないストレージポインター、タイプの混乱、およびバックドアの作り方について話しました。 このパートでは、Solidityのいくつかの特徴的な機能について説明し、契約で発生する可能性のある論理的な脆弱性について説明します。







エーテルの進化



まず、スマートコントラクトが値とユーザーアドレスを相互に交換する方法。 初めに、エーテルは別の契約を呼び出すことによって送信されました:







msg.sender.call.value(42) //    msg.sender.call.value(42)()
      
      





ただし、署名を指定せずにコントラクトを呼び出すと、フォールバック関数が呼び出されます。この関数には、任意のコードが存在する場合があります。 仕事のこのような異常な論理は、 TheDAOがハッキングされた助けを借りて、有名な再入をもたらしました。







その後、 send



関数が登場しました。これも単なる構文上の砂糖です。ボンネットの下では同じ呼び出しが行われ、ガスの量が制限されているため、リエントラントは機能しません。







 msg.sender.send(42) // msg.sender.call.value(42).gas(2300)() -  , ?
      
      





ただし、何らかの問題が発生してブロードキャストを送信できない場合、送信は実行の流れを中断しません。 この動作も重要です。 たとえば、ブロードキャストは送信されず、契約のステータスはすでに変更されています。 誰かはエーテルなしで残されるでしょう







そのため、転送が発生し、何か問題が発生した場合は例外がスローされます。







 msg.sender.transfer(42) // if (!msg.sender.send(42)) revert()
      
      





しかし、彼女は特効薬ではありません。 ブロードキャストの送信先となるアドレスの配列があり、 transfer



を使用する場合、操作全体の成功は各受信者に依存することを想像してください-ブロードキャストを受信しない場合、すべての変更は完全にロールバックされます。







そして、放送の最後の瞬間はselfdestruct



機能です。







 selfdestruct(where)
      
      





実際、これは契約を破棄するための関数ですが、契約に残っているすべての空気は、引数として示されたアドレスに送信されます。 さらに、これは決して回避することができません-受信アドレスが契約であってもエーテルは離脱し、そのフォールバック機能はpayable



ません(フォールバックは単に呼び出されません)。 空気はまだ作成されいない契約にも送信されます!







継承



Solidityでは、多重継承を可能にするために、 C3線形化アルゴリズムが使用されます(たとえば、Pythonの場合と同じ)。 そして、多重継承の熊手を踏まない幸運な人々にとって、最終的なグラフはおそらく明白ではないように思われます。 例を考えてみましょう:







 contract Grandfather { bool public grandfatherCalled; function pickUpFromKindergarten() internal { grandfatherCalled = true; } } contract Mom is Grandfather { bool public momCalled; function pickUpFromKindergarten() internal { momCalled = true; } } contract Dad is Grandfather { bool public dadCalled; function pickUpFromKindergarten() internal { dadCalled = true; super.pickUpFromKindergarten(); } } contract Son is Mom, Dad { function sonWannaHome() public { super.pickUpFromKindergarten(); } }
      
      





Son.sonWannaHome()からコールグラフを続けます。







答え

お父さん、そしてお母さんと呼ばれます。 合計継承は次のとおりです。

息子->お父さん->お母さん->おじいさん







多重継承に関するバグとの多かれ少なかれ妥当な契約の例は、 Underhanded Solidity Coding Contestで発表されました。







論理的



スマートコントラクトは人々によって書かれ、人々はしばしば変数、 コンストラクタの名前に間違いを犯します。 また、特定の機能(たとえば、 Parity Multisigなど)へのアクセスを制限することを忘れています。また、スマートコントラクトの機能はいつでもどのアドレスからでも呼び出すことができるため、開発者は競合状態の発生の可能性を注意深く監視する必要があります。 スマートコントラクトが呼び出しの順序を制御できるように、彼自身が必要な同期プリミティブとアクセス修飾子を実装する必要があります。 さらに、コードアナライザーが見つけられないものがあります-ドメインエラー。 したがって、このセクションでは著作権の脆弱性について説明します。







暗黙の数学



たとえば、ユーザーがブロードキャストで受け取るトークンの数を計算するなど、数学を扱う必要がある契約の大部分では、 SafeMathライブラリが使用されます。 ただし、名前は誤解を招く可能性があります-実際、SafeMathはオーバーフローのみを考慮します。 以下の契約を検討することを提案します。







 contract Crowdsale is Ownable { using SafeMath for uint; Token public token; address public beneficiary; uint public collectedWei; uint public tokensSold; uint public tokensForSale = 7000000000 * 1 ether; uint public priceTokenWei = 1 ether / 200; bool public crowdsaleFinished = false; function purchase() payable { require(!crowdsaleFinished); require(tokensSold < tokensForSale); require(msg.value >= 0.001 ether); uint sum = msg.value; uint amount = sum.div(priceTokenWei).mul(1 ether); uint retSum = 0; if(tokensSold.add(amount) > tokensForSale) { uint retAmount = tokensSold.add(amount).sub(tokensForSale); retSum = retAmount.mul(priceTokenWei).div(1 ether); amount = amount.sub(retAmount); sum = sum.sub(retSum); } tokensSold = tokensSold.add(amount); collectedWei = collectedWei.add(sum); beneficiary.transfer(sum); token.mint(msg.sender, amount); if(retSum > 0) { msg.sender.transfer(retSum); } LogNewContribution(msg.sender, amount, sum); } }
      
      





何か疑わしいことに気づきましたか? ほとんどの場合そうではなく、これは絶対に正常です。 正しくしましょう。 式sum.div(priceTokenWei).mul(1 ether)



注意してsum.div(priceTokenWei).mul(1 ether)



-ロジックの観点から、ここではすべてが非常にスムーズです。必要な単位に変換するには、1エーテルを掛けます。」











=/50000000000000001000000000000000000







しかし、ニュアンスがあります。 各ライブラリ呼び出し(およびそれらの2つ)は2つのuintを受け取り、uintを返します。これは、最初の操作の小数部分が完全に正当に破棄されることを意味します。







 //     SafeMath function div(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a / b; return c; }
      
      





したがって、このクラウドセール契約に整数ではないエーテルを送信することにより、投資家はトークンを失い、ICOは予想よりも多くを収集できます。D完全な契約はsolidity_tricksにあります。







円形マイニング履歴操作による複数投票



このような長い名前は、PoAネットワーク契約の監査中に発見された面白い脆弱性を隠しています。 ネットワークのルールによると、12個以上のバリデーターがあり、バリデーターのキー(およびそれに応じてアドレス)を変更するなど、さまざまな票を保持できます。 バリデーターがキーを変更して2回投票できないようにするために、スマートコントラクトはすべてのキーの履歴を保持します。 そして、投票を検証するとき、有権者の中に彼の祖先がいないことをチェックします。







そのため、キーが変更されるたびに、以前のキーを参照するマッピングに配置されます。 したがって、新しい変更のたびに、契約にはキーの履歴を確認する機会があります。 ただし、この構成では、追加のチェックなしで、バリデーターはキー履歴をループして、古いキーを切り捨てることができます。







1)キーAのバリデーターは投票Xを登録し、キーの変更を要求します。 その後、キーBを保持します。今すぐ新しいキーで投票しようとすると、キーAが履歴Bにあるため失敗します。

History(B): B => A => 0x









2)したがって、バリデーターは再度キーの変更を要求し、キーCを受け取ります。再び、今でも同じ理由でトリックは機能しません。

History(C): C => B => A => 0x









3)次に、バリデーターはキーCからキーBへの変更を要求します。その後、キーの履歴はBとCの間でループされ、Aは含まれません。

History(B): B => C => B => C => B => ...









バリデーターは、キーBまたはCを使用して、2回目のX投票に投票できるようになりました。 修正および元のレポートおよびその他の脆弱性。







現時点では、次の2つの質問があります。









 function areOldMiningKeysVoted(uint256 _id, address _miningKey) public view returns(bool) { VotingData storage ballot = votingState[_id]; IKeysManager keysManager = IKeysManager(getKeysManager()); for (uint8 i = 0; i < maxOldMiningKeysDeepCheck; i++) { address oldMiningKey = keysManager.miningKeyHistory(_miningKey); if (oldMiningKey == address(0)) { return false; } if (ballot.voters[oldMiningKey]) { return true; } else { _miningKey = oldMiningKey; } } return false; }
      
      





いずれの場合でも、変数iがuint8として定義されているため、ループサイズは256回以下の繰り返しになります。







この脆弱性を悪用する本当の可能性は作者からの質問を提起しますが、stackoverflowのチャットで配列が高価であることがわかった後、マッピングで単方向リストを保存しようとする人にとっては依然として有用です:)







寛大な払い戻し



次の脆弱性は、むしろグローバル変数の値の無知/誤解に関連しています。 コミット-公開スキームの可能な実装の1つをご覧ください。







 pragma solidity ^0.4.4; import 'common/Object.sol'; import 'token/Recipient.sol'; /** * @title Random number generator contract */ contract Random is Object, Recipient { struct Seed { bytes32 seed; uint256 entropy; uint256 blockNum; } /** * @dev Random seed data */ Seed[] public randomSeed; /** * @dev Get length of random seed data */ function randomSeedLength() constant returns (uint256) { return randomSeed.length; } /** * @dev Minimal count of seed data parts */ uint256 public minEntropy; /** * @dev Set minimal count of seed data * @param _entropy Count of seed data parts */ function setMinEntropy(uint256 _entropy) onlyOwner { minEntropy = _entropy; } /** * @dev Put new seed data part * @param _hash Random hash */ function put(bytes32 _hash) { if (randomSeed.length == 0) randomSeed.push(Seed("", 0, 0)); var latest = randomSeed[randomSeed.length - 1]; if (latest.entropy < minEntropy) { latest.seed = sha3(latest.seed, _hash); latest.entropy += 1; latest.blockNum = block.number; } else { randomSeed.push(Seed(_hash, 1, block.number)); } // Refund transaction gas cost if (!msg.sender.send(msg.gas * tx.gasprice)) throw; } /** * @dev Get random number * @param _id Seed ident * @param _range Random number range value */ function get(uint256 _id, uint256 _range) constant returns (uint256) { var seed = randomSeed[_id]; if (seed.entropy < minEntropy) throw; return uint256(seed.seed) % _range; } }
      
      





シードの次の部分をコミットするときに、スマートコントラクトが使用済みガスを返すことに気付きましたか(put関数を参照)。 それ自体、費やした手数料を返還したいという願望はイーサリアムプラットフォームのパラダイムに収まりませんが、これは最悪ではありません。 ここでの脆弱性は、msg.gas値が送信者によって制御され、 残りのガスを意味することです。 したがって、攻撃者は、取引のガスとその価格を操作することにより、契約からすべての資金を引き出すことができます。







結論の代わりに



この記事では、スマートコントラクトを作成するときにミスを犯す可能性のある場所について読者の直感を形成するために、いくつかの論理的な脆弱性のみを検証しました。 実際、契約にはこうした論理的(著作権)脆弱性のほとんどが存在します。 まず、ビジネスロジックまたはサブジェクトエリアに接続されます。 これは、少なくともユーザーが「不適切な行動」の基準を説明できるようになるまで、契約のほとんどの脆弱性を自動的に検出できないことも示唆しています。 ところで、次のパートでは、現在の状態でどのようなツールが存在し、どのようなツールが適しているかを検討します。







PS寛大な払い戻しの例については、 Raz0rに感謝します :)








All Articles