$30 off During Our Annual Pro Sale. View Details »

有名なスマートコントラクト脆弱性・ハックについての復習 / Widely known vul...

Nao Hanamura
September 26, 2018
1.2k

有名なスマートコントラクト脆弱性・ハックについての復習 / Widely known vulnerability of smart contract

Nao Hanamura

September 26, 2018
Tweet

More Decks by Nao Hanamura

Transcript

  1. 2 これまでやってきたこと 自己紹介:2017年5月頃からブロックチェーンのエンジニアをしており、特に 仮想通貨監査などに関わってきました 開発・研究 •  ブロックチェーンデータの分析基盤構築 •  ブロックチェーン領域のモニタリングサービス開発 • 

    トレーサブルリング署名研究 コミュニティ活 動 •  DashのWhitePaper翻訳 •  技術ブログ「Neutrinoで働くブロックチェーンエンジニアのブログ」 (www.blockchainengineer.tokyo/) 仮想通貨 監査 •  仮想通貨監査の技術アドバイザリー ✓ 特に取引所の監査手続開発 ✓ 特定通貨の監査手法などのリサーチ 花村直親(Naochika Hanamura) @naomasabit
  2. 6 Reentrancy Attack(再入国攻撃)によってDAO事件では当時の価値約65 億円が被害を受けた %"0 $POUSBDU )BDLFS $POUSBDU 1.攻撃者はあらかじめ 少額のETHをdepositしておく

    %"0 $POUSBDU )BDLFS $POUSBDU 2.攻撃者は1で預けた分、withdrawコントラクトを呼び出す が、前提4のFallback関数の動きより、被害者が残高帳を 更新する前に出金が何度も行われる 1.被害者のコントラクトには - deposit関数(預金) - withdraw関数(出金) が定義されており、 銀行的にユーザーの残高を預かったり引き出し たりできる 2.withdraw関数(出金)は、先にETHの出金をして から、残高帳の預金数字を減らす 3.攻撃者は自身にコントラクトを持つアドレスであ る 4.攻撃者は被害者のwithdrawコントラクトを呼び 出し、ETHの出金がされた直後に何度もwithdraw するような処理をFallback関数に既定しておく deposit࣮ߦ withdraw × αճ Reentrancy AttackʢDAOʣ લఏ
  3. 7 Reentrancy Attack(再入国攻撃) - DAO事件の攻撃サンプルとハッキン グされたコードを示す #Ϣʔβʔͷ࢒ߴா mapping (address =>

    uint) private userBalances; #Ҿ͖ग़ؔ͠਺ function withdrawBalance() public { # Ҿ͖ग़͠Մೳֹۚͷऔಘ uint amountToWithdraw = userBalances[msg.sender]; # require͔Β߈ܸऀ͸FallbackΛ࣮ߦͯ͠Ҿ͖ग़͠Λ ϧʔϓ࣮ߦ͢Δ require(msg.sender.call.value(amountToWithdraw) ()); # Ϣʔβʔͷ࢒ߴாߋ৽Λߦ͏ userBalances[msg.sender] = 0; } int private count; function Attacker() public { count = 0; } # Fallbackؔ਺ function () payable public { count += 1; # Ҿ͖ग़͠ΛԿ౓΋ߦ͏ॲཧΛنఆ͓ͯ͠ ͘ if (count < 10000) { DaoContract daoContract = DaoContract(msg.sender); daoContract.withdrawBalance(); } } DAOContract AttackContractExample
  4. Reentrancy Attackを防ぐためには、繰り返しに耐える処理を実装する 必要があった #Ϣʔβʔͷ࢒ߴா mapping (address => uint) private userBalances;

    #Ҿ͖ग़ؔ͠਺ function withdrawBalance() public { # Ҿ͖ग़͠Մೳֹۚͷऔಘ uint amountToWithdraw = userBalances[msg.sender]; #͜͜Λى఺ʹ߈ܸऀ͸FallbackΛ࣮ߦͯ͠Ҿ͖ग़͠ Λϧʔϓ࣮ߦ͢Δ require(msg.sender.call.value(amountToWithdraw)()); # Ϣʔβʔͷ࢒ߴாߋ৽Λߦ͏ userBalances[msg.sender] = 0; } Original DAOContract #Ϣʔβʔͷ࢒ߴா mapping (address => uint) private userBalances; #Ҿ͖ग़ؔ͠਺ function withdrawBalance() public { # Ҿ͖ग़͠Մೳֹۚͷऔಘ uint amountToWithdraw = userBalances[msg.sender]; # Ϣʔβʔͷ࢒ߴாߋ৽ΛɺҾ͖ग़͠લʹߦ͏ ͜ͱͰReentrancy߈ܸʹରԠ͢Δ userBalances[msg.sender] = 0; require(msg.sender.call.value(amountToWithdraw) ()); } Protected DAOContract 8
  5. 10 BECトークンの不正な引き出しを起こすために、オーバーフローを利 用して所持残高チェックを擦り抜けた //࢓༷ɿෳ਺ͷΞυϨεʹରͯ͠Ұ౓ʹಉ਺ͷτʔΫϯΛૹ Δ //Ҿ਺ɿ1,ΞυϨεͷ഑ྻ 2,ૹΔτʔΫϯྔ function batchTransfer(address[] _receivers,

    uint256 _value) public whenNotPaused returns (bool) { //① ૹΔτʔΫϯྔʹҾ͖౉͞ΕͨΞυϨεͷ਺Λ͔͚ ͯɺૹΔ૯ྔΛࢉग़͢Δ uint cnt = _receivers.length; // ͜͜ͰΦʔόʔϑϩʔΛى͜͢ uint256 amount = uint256(cnt) * _value; //② ૹΔର৅ΞυϨε͕1Ҏ্20ҎԼ͔ɺૹΓݩʹे෼ͳ τʔΫϯྔ͕͋Δ͔Λ൑ఆ require(cnt > 0 && cnt <= 20); require(_value > 0 && balances[msg.sender] >= amount); //③ ֤ΞυϨεʹରͯ͠ಉ਺ͷτʔΫϯΛૹΔ balances[msg.sender] = balances[msg.sender].sub(amount); for (uint i = 0; i < cnt; i++) { balances[_receivers[i]] = balances[_receivers[i]].add(_value); Transfer(msg.sender, _receivers[i], _value); } return true; } BEC Token Contract オーバーフローを起こした方法 •  引数はアドレス(_receivers)を2つ、送 るトークン量(value)が2^255 •  攻撃者のBECトークン所持量は0 •  amount = 2^255 *2 となり、uint256 型の限界値2^256が巻き戻って0にな る •  引数(value)に設定された2^255BEC トークンが引数のアドレスにそれぞれ 送られる
  6. •  攻撃によるトークン供給量 (攻撃によるトークン供給量) / (単位補正※) =2^256 / (10^18) =115,792,089,237,316,000,000,000,000,000,0 00,000,000,000,000,000,000,000,000,000

    ≒ 1,158阿僧祇(あそうぎ) ※BECトークンの1単位が10^18なので補正す る •  倍率 (攻撃によるトークン供給量 + 元の供給量) / (元の供給量) = (2^256 / (10^18) + 7,000,000,000) / 7,000,000,000 =16,541,727,033,902,300,000,000,000,000,00 0,000,000,007,000,000,000 ≒ 16.5極 結果、元の供給量70億に対して1,158阿僧祇(あそうぎ)量が供給さ れ、16.5極(ごく)倍のインフレーションが起きて価値が崩壊した ୯Ґ ಡΈํ େ͖͞ ԯ ͓͘ 10^8 ஹ ͪΐ͏ 10^12 ژ ͚͍ɺ͖ΐ͏ 10^16 ᆇ ͕͍ 10^20 䉾 ͡ΐɺ͠ 10^24 য় ͡ΐ͏ 10^28 ߔ ͜͏ 10^32 ׾ ͔Μ 10^36 ਖ਼ ͍ͤ 10^40 ࡌ ͍͞ 10^44 ۃ ͘͝ 10^48 ߃Տࠫ ͝͏͕͠Ό 10^52 Ѩૐᷫ ͋ͦ͏͗ 10^56 ಹ༝ଞ ͳΏͨ 10^60 ෆՄࢥٞ ;͔͗͠ 10^64 ແྔେ਺ ΉΓΐ͏͍ͨ͢͏ 10^68 ࢀߟɿ਺ͷ୯Ґ 11
  7. 攻撃を防ぐためにはオーバーフローを考慮した四則演算を行う必要 があった OpenZeppelinは安全にsolidityを書くためのフレームワークを提供している function mul(uint256 _a, uint256 _b) internal pure

    returns (uint256) { // Gas optimization: this is cheaper than requiring 'a' not being zero, but the // benefit is lost if 'b' is also tested. // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 if (_a == 0) { return 0; } uint256 c = _a * _b; // ݕࢉͯ͠ΦʔόʔϑϩʔରԠΛ͍ͯ͠Δ require(c / _a == _b); return c; } OpenZeppelin SafeMath.sol 12
  8. Parity Multisig Walletでは、オーナー権限の取得が外部から呼び出せる 状態にあったことで、コントラクトが使用不能になった // throw unless the contract is

    not yet initialized. modifier only_uninitialized { if (m_numOwners > 0) throw; _; } //①ϋοΧʔ͸ॳظԽΛͯ͠ΦʔφʔݖݶΛखʹೖΕ ͨ //WalletͷॳظԽؔ਺ initWallet(address[] _owners, uint _required, uint _daylimit) only uninitialized { //೔࣍ͷૹ্ۚݶֹͳͲΛઃఆ͢Δؔ਺ initDaylimit(_daylimit); //ΦʔφʔΛઃఆ͢Δؔ਺ initMultiowned(_owners, _required); } // constructor - stores initial daily limit and records the present day's index. function initDaylimit(uint _limit) only_uninitialized { m_dailyLimit = _limit; m_lastDay = today(); } 13 Parityͷར༻ ϥΠϒϥϦ 0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4 // constructor is given number of sigs required to do protected "onlymanyowners" transactions // as well as the selection of addresses capable of confirming them. function initMultiowned(address[] _owners, uint _required) only_uninitialized { m_numOwners = _owners.length + 1; m_owners[1] = uint(msg.sender); m_ownerIndex[uint(msg.sender)] = 1; for (uint i = 0; i < _owners.length; ++i) { m_owners[2 + i] = uint(_owners[i]); m_ownerIndex[uint(_owners[i])] = 2 + i; } m_required = _required; } // kills the contract sending everything to `_to`. //② ΦʔφʔݖݶΛ࣋ͬͨϋοΧʔ͸killΛݺͼग़ͯ͠ແޮ Խͨ͠ // Φʔφʔ͕ݺͼग़ͤΔίϯτϥΫτͷແޮԽ function kill(address _to) onlymanyowners(sha3(msg.data)) external { suicide(_to); }