スマートコントラクトのセキュリティ Part 1

ーSolidityの6つの脆弱性とその対策ー

本記事は、How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 1)(Georgios Konstantopoulos) の翻訳です。万一誤訳などありましたらPrivate Note機能でお知らせ下さい。

前回の記事では、Devcon3で提示されたコンセプトを分析し、イーサリアムのスケーラビリティの将来について論じた。では、スケーラビリティの課題が全て解決され、イーサリアムのスマートコントラクトが問題なく動いているとちょっと想像してみよう。

そのユーザーは善良となるのか、それともコントラクトのスムーズな機能を妨げる敵となり得るのか?

スマートコントラクトは「不変」なものだ。いったんデプロイされるとコードを変更することは不可能であり、バグが見つかっても修正することはできない。

組織全体がスマート・コントラクトのコードによって管理されている未来では、適切なセキュリティがかなり必要とされる。 TheDAOや2017年のParityハック事件(7月11月)といった過去のハッキングは開発者の意識を高めたが、まだまだ道のりは長い。

“スマート・コントラクトはハッカーのディズニーランドだ”

この記事では、よく知られたセキュリティ・ホールとその対策についてやっていこう。

1. オーバーフローとアンダーフロー

オーバーフローとは、数値が最大値を超えてインクリメントされてしまうことだ。Solidityは256ビットの数値まで扱えるので (2²⁵⁶-1まで)、1ずつインクリメントしていくと結果0となる。

0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+ 0x000000000000000000000000000000000001
----------------------------------------
= 0x000000000000000000000000000000000000
最大値到達後、オドメーターやトリップメーターは0から再スタートする。オドメーター・ロールオーバーと呼ばれる。[出典]

この逆の場合も同様に、数値が符号なしである場合デクリメントすると数値がアンダーフローしてしまい、最大値の結果となる。

0x000000000000000000000000000000000000
- 0x000000000000000000000000000000000001
----------------------------------------
= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

下部『Compile and Run』をクリックして、バグをテストしてみよう:

どちらの場合も同じくらい危険であるが、アンダーフローのケースの方がより発生しやすい。例えばトークンホルダーがXトークンを所有しているのに、X + 1を使おうとする場合などである。コードがそれをチェックしないければ、攻撃者は彼が持っていたよりも多くのトークンを使うことを許されてしまい、残高の上限を超えてしまう

対策: 現在OpenZeppelinのSafeMathライブラリの使用がスタンダードとなっている。

バグが修正されているのをテストしてみよう:

2. 可視性とデリゲートコール

2017年7月にこの界隈にいた人々にとって、このバグはよく知られたものであろう。ユーザーが3000万ドルを失った、あのParityウォレットハック事件だ。

Solidityの可視性修飾子とその相違

Public 関数は誰でも呼び出すことができる(コントラクト内の関数から、派生コントラクトの関数から、または外部ユーザーから)

External 関数は外部からのみアクセス可能で、つまりコントラクトの別関数から呼び出すことはできない。以下のソースコードはコンパイルされない。なぜならcannotBeCalledのexternal修飾子は、コントラクトの関数から呼び出されることを許可しないためだ(ただし、別コントラクトからは呼び出すことができる)

Externalはより安価に使用できる。なぜならここで説明されているように、public修飾子が全ての引数をmemoryにコピーする必要があるが、externalはcalldataのオペコード を使用するからである。

Privateinternal はよりシンプルだ: private は関数がコントラクト内でのみ呼び出されるという意味だが、internal はよりゆるい制限を示し、親から継承されたコントラクトにその関数を使用することを許可する。

つまり、外部のやりとりが必要でない限り、関数はprivateまたはinternalにしておかなくてはならない。

デリゲートコール

solidityドキュメントからわかりやすく言い換えると:

“デリゲートコールは、ターゲットアドレスのコードが呼び出し元のコントラクトのコンテキストで実行され、msg.sendermsg.valueの値が変わらないという点を除いて、メッセージコールと同じものだ。
これは、コントラクトが実行時に異なるアドレスから動的にコードをロードできることを意味する。ストレージ、現在のアドレスと残高は依然として呼び出し元のコントラクトを参照しているため、呼び出されたアドレスからコードのみが取得される。”

この低レベル関数は、ライブラリの実装やコードのモジュール化のためのバックボーンとしてとても便利なものである。しかし基本的にあなたのコントラクトは誰にでもしたいことをさせてしまうので、このことが脆弱性の扉となる。

下の例では攻撃者はデリゲートのパブリック関数pwnを呼び出すことができるが、その呼び出しはDelegationのコンテキスト内にあるのでコントラクトの所有権を主張することができる。

Parityハック事件には、安全でない可視性修飾子と、delegateコールの不正データ誤用の両方が関係していた。脆弱なコントラクトの関数はdelegatecallを実装していて、所有権を変更できる別コントラクトの関数はpublicのままだったのだ。こうして、攻撃者はmsg.dataフィールドを作成して脆弱な関数を呼び出すことができてしまった。

msg.dataフィールドに含まれるのは、呼び出したい関数の署名である。ここでの署名とは、関数プロトタイプのsha3 (alias for keccak256)ハッシュの最初の8バイトを意味する。

In this case:
web3.sha3("pwn()").slice(0, 10) --> 0xdd365b8b
If the function takes an argument, pwn(uint256 x):
web3.sha3("pwn(uint256)").slice(0,10) --> 0x35f4581b

3. リエントラント性 (TheDAOハッキング事件)

Solidityのcall 関数は、 valueとともに呼び出されると受け取った全てのガスを転送する。下のスニペットでは、センダーの残高を減らすより前にコールが行われている。このことが、The DAOハック事件の際にredditのコメントで非常によく説明されていたように脆弱性へと繋がるのだ。:

“これを簡単にいうと、 銀行の窓口係が依頼された全額をあなたに渡すまで、口座の残高を変えないようなものだ。「500ドルおろせますか?あっちょっと待って、おろす前に500ドルおろせますか?」
このスマート・コントラクトは、あなたが500ドルを所有していることをはじめに一度チェックするだけに設計されているので、インタラプトをコントラクト自身が可能にしてしまっている」

ここで詳しく説明されているように、valueのトランスファーを行うより前にセンダーの残高を減らすよう修正しなくてはならない。 また並列プログラミングを扱った経験がある人向けだと、ミューテックスを使用してどんな種類の競合状態も完全に緩和する別の解決策がある。

こういった状況を処理するにはrequire(msg.sender.transfer(_value)) とするのが現在のベストな方法だ。


パート1はここまでとしよう。パート2では、あまり知られていないエクスプロイト、ワークフローに追加すべきツール、そしてスマート・コントラクトのセキュリティの将来について説明していこう。

プロジェクトのアップデートやDApp開発に興味がありますか?
情報を見逃さないよう、メーリングリストに参加しましょう!↓


Loom Network は、イーサリアムのハイスケーラブルなDPoSサイドチェーン構築のためのプラットフォームで、大規模ゲームやソーシャルアプリにフォーカスしています。

さらなる情報は こちらから.

あなたがブロックチェーンゲームのファンであれば、 Zombie Battlegroundをチェック!世界初・独自のブロックチェーン上でフルに稼働するPC & モバイルカードゲームです。

そしてもしこの記事をお楽しみいただけ、最新情報の受け取りをご希望であれば、私たちの プライベートメーリングリストへの登録や、TelegramTwitterGithubQiitaのフォローをお願いします!

Thanks to James Martin Duffy.