見出し画像

Ethereumスマートコントラクトのハニーポットについて

この記事では、「The Art of The Scam: Demystifying Honeypots in Ethereum Smart Contracts」という論文からスマートコントラクトのハニーポットについて紹介したいと思います。

https://www.usenix.org/system/files/sec19-torres.pdf

本論文では、Ethereum ネットワーク上のハニーポット・スマートコントラクトの普及率、挙動、Ethereum ブロックチェーンへの影響を調査し、ハニーポット・スマートコントラクトの体系的分析を提示しています。また、論文の著者らはハニーポット技術の分類法を開発し、これを用いて、ハニーポットを公開するために記号的実行と明確に定義されたヒューリスティックを採用したツールである「HONEYBADGER」を構築しています。

Ethereum はこれまでに脆弱なスマートコントラクトを狙った攻撃に何度も直面しています。最も顕著なものは、2016 年の DAO ハッキングと 2017 年の Parity Wallet ハッキングで、合わせて 4 億ドルを超える損失をもたらしました。


これらの攻撃に対応して、学会では、ブロックチェーン上にコントラクトを展開する前に、コントラクトの脆弱性をスキャンすることを可能にする多数の異なるツールを提案しました。残念ながらこれらのツールは、脆弱なコントラクトを簡単に見つけ、それを悪用するために攻撃者によっても使用される可能性があります。これは、攻撃者が脆弱なコントラクトのためにブロックチェーンを積極的にスキャンすることによって、反応的なアプローチに従うことを可能にします。

また、攻撃者は、被害者を罠に誘い込むことで、より積極的なアプローチを取ることもできます。言い換えれば

被害者が自分のところに来るように仕向けることができるのなら、なぜ被害者を探すのに時間をかけなければならないのか?

この新しいタイプの詐欺は、「ハニーポット」としてコミュニティによって紹介されています。ハニーポットはスマートコントラクトの設計に明らかな欠陥があるように見えるもので、ユーザが一定量の Ether をコントラクトに送金すると、任意のユーザがコントラクトから Ether を排出できるようになるものです。しかし、一旦この脆弱性を利用しようとすると、まだ知られていない第二のトラップドアが出現し、Ether の流出が成功しないようになっています。つまり、ユーザは見かけの脆弱性だけに注目し、第 2 の脆弱性がコントラクトに隠されている可能性を考慮しないのです。

イーサリアム・ハニーポット

定義 1:ハニーポット
ハニーポットとは、任意のユーザ(被害者)が追加で資金を送ることを条件に、その資金を漏らすふりをするスマートコントラクトのことです。ただし、ユーザから提供された資金はトラップされ、せいぜいハニーポット作成者(攻撃者)が回収することができる程度です。

下図は、ハニーポットの登場人物とフェーズの違いを表しています。ハニーポットは一般的に 3 つのフェーズで動作します。

  1. 攻撃者は、一見脆弱に見えるコントラクトをデプロイし、餌としてアセット(資金)を置く

  2. 被害者は、少なくとも必要な額のアセットを送金することによってコントラクトを悪用しようとするが、失敗する

  3. 攻撃者は、被害者が失った資金と一緒に餌を引き出す

攻撃者は、ハニーポットを設置するために特別な能力を必要としません。実際、攻撃者は通常の Ethereum ユーザと同じ能力を持っています。攻撃者は、スマートコントラクトをデプロイし、餌を配置するために必要な資金のみを必要とします。

ハニーポットの分類

論文の著者らは、様々な技術を分類学的に整理しました(下図参照)。また、異なる技術をそれらが動作するレベルに従って、以下の 3 つの異なるクラスにグループ化しています。

  1. Ethereum Virtual Machine (EVM)

  2. Solidity コンパイラ

  3. Etherscan

最初のクラスは、EVM の異常な挙動を利用してユーザーをだまします。EVM は厳密かつパブリックに知られたルールセットに従っていますが、それでもユーザは、不適合な動作を示唆する悪意のあるスマートコントラクトの実装によって、誤解や混乱を招く可能性があります。2 つ目のクラスは、Solidity コンパイラによってもたらされる問題から利益を得るハニーポットに関連しています。コンパイラの問題はよく知られていますが、他の問題はまだ文書化されておらず、ユーザがスマートコントラクトを注意深く分析しないか、実世界の条件でテストしない場合、気づかない可能性があります。3 番目のクラスは、Etherscan のウェブサイトに表示される情報が限られていることに関連する問題を利用します。Etherscan はおそらく最も著名な Ethereum ブロックチェーンエクスプローラであり、多くのユーザはそこに表示されるデータを完全に信頼しています。

以下の章節では、簡単な例を通して、それぞれのハニーポット技術を説明します。また、次のことを仮定しています。1) 攻撃者は、スマートコントラクトを悪用しようとするユーザのインセンティブとして、スマートコントラクトに Ether という餌を仕込む。2) 攻撃者は、ハニーポットに含まれる Ether の量を取得する方法を持っている。

EVM

1 contract Ownable {
2 ...
3   function multiplicate(address adr) payable {
4     if (msg.value >= this.balance)
5       adr.transfer(this.balance+msg.value);
6   }
7 }

残高障害

Ethereum のスマートコントラクトは、残高を保有しています。上のコントラクトは、残高障害と呼ぶ手法を利用したハニーポットの例です。multiplicate 関数は、この関数の呼び出し元がスマートコントラクトの現在の残高以上の値を含んでいる場合、コントラクトの残高(this.balance)とこの関数呼び出しへのトランザクションに含まれる値(msg.value)を任意のアドレスに転送することを示唆するものです。従って、だまされやすいユーザは、現在の残高以上の値でこの関数を呼び出すだけで、「自分が投資した」値とコントラクトの残高が得られると考えるでしょう。しかし、そうすると、4 行目の条件が成立しないので、5 行目が実行されないことにすぐ気がつきます。

Solidity コンパイラ

継承障害

Solidity は is キーワードによる継承をサポートしています。あるコントラクトが複数のコントラクトを継承する場合、ブロックチェーン上には 1 つのコントラクトのみが作成され、すべてのベースコントラクトのコードは作成されたコントラクトにコピーされます。下のコードは継承障害と呼ぶ手法を利用したハニーポットの例です。

1 contract Ownable {
2   address owner = msg.sender;
3   modifier onlyOwner {
4     require(msg.sender == owner);
5     _;
6   }
7 }
8 contract KingOfTheHill is Ownable {
9   address public owner;
10  ...
11  function() public payable {
12    if(msg.value >jackpot)owner=msg.sender;
13    jackpot += msg.value;
14  }
15  function takeAll() public onlyOwner {
16    msg.sender.transfer(this.balance);
17    jackpot = 0;
18  }
19 }

Ownable コントラクトを継承するコントラクト KingOfTheHill があります。1)関数 takeAll は、変数 owner に格納されたアドレスにのみコントラクトの残高を引き出すことを許可しています。2)owner 変数は、現在のジャックポットよりも大きなメッセージ値でフォールバック関数を呼び出すことによって変更できます(12行目)。ここで、ユーザが自分をオーナーに設定するためにこの関数を呼び出そうとすると、トランザクションは成功します。しかし、その後、残高を引き出そうとすると、トランザクションは失敗します。これは、9 行目で宣言された変数 owner が、2 行目で宣言された変数と同じではないからです。9 行目のオーナーは 2 行目のオーナーに上書きされると考えるのが普通だが、そうではありません。Solidity コンパイラは 2 つの変数を別の変数として扱うため、9 行目の owner に書き込んでも、Ownable コントラクトで定義された owner が変更されることはありません。

空文字リテラルスキップ

上のコントラクトでは、ユーザはコントラクト関数 invest に最低額の Ether を送ることで投資を行うことができます。投資家は、関数 divest を呼び出すことで、投資を引き出すことができます。ここで、コードをよく見てみると、投資家が元々投資した金額よりも大きな金額を売却することを禁止するものは何もないことに気づきます。このため、だまされやすいユーザは、divest という関数が悪用される可能性があると信じてしまうのです。しかし、このコントラクトには、「空文字リテラルスキップ」と呼ばれるバグがあります。関数 loggedTransfer(14行目)の引数として与えられる空の文字列リテラルは、Solidityコンパイラのエンコーダによってスキップされます。このため、この引数に続くすべての引数のエンコーディングが 32 バイト左にシフトし、関数呼び出しの引数 msg には target の値が与えられ、target には currentOwner の値が与えられ、currentOwner にはデフォルト値 0 が与えられます。したがって、loggedTransfer 関数の最後で target の代わりに currentOwner に転送が実行されます。スマートコントラクトの明らかな脆弱性を利用しようとするユーザは、実質的にはコントラクトの owner に資産を転送しているだけです。

型推論オーバーフロー

Solidity では var という変数を宣言すると、コンパイラは型推論を用いて、変数に代入される最初の式から自動的に最小の型を推論します。上図のコントラクトは、型推論オーバーフローと呼ぶ手法を利用したハニーポットの一例です。このコントラクトでは、まず、投資額が 2 倍になることが示唆されています。しかし、型は最初の代入からしか推論されないので、7 行目のループは無限大となります。変数 i は uint8 型で、この型の最高値は 255 であり、2 * msg.value よりも小さくなります。したがって、ループの停止条件には決して到達しません。それでも、変数 multi が amountToTransfer よりも小さければ、ループは停止します。ループが終了すると、コントラクトは呼び出し元に値を送り返しますが、その値は最大でも255wei(1ether = 10^18wei)であり、ユーザがもともと投資した値よりはるかに小さくなります。

非初期化構造体

Solidity は、構造体という形で新しいデータ型を定義する手段を提供します。上図は非初期化構造体ハニーポットの例です。コントラクトの残高を引き出す代わりに、ユーザは最低額のベットを行い、コントラクト内に格納された乱数を当てる必要があります。しかし、ブロックチェーンに保存されたデータはパブリックに利用可能なので、その乱数は誰でも簡単に得られます。まず考えられるのは、コントラクト作成者が単にプライベートと宣言された変数を秘密と思い込む、よくある間違いを犯したということです。無実のユーザは、ブロックチェーンから乱数を読み取り、ベットを行い正しい数字を提供することで関数 guessNumber を呼び出すだけです。その後、コントラクトはユーザの参加状況を追跡しているように見える構造体を作成します。しかし、この構造体は new キーワードで適切に初期化されていません。その結果、Solidity コンパイラは、構造体に含まれる最初の変数 (player) の格納場所をコントラクトに含まれる最初の変数 (randomNumber) の格納場所にマップします。これにより、乱数が呼び出し元のアドレスで上書きされ、14 行目の条件が失敗することになります。注目すべきは、ハニーポット作成者は、ユーザが上書きされた値を推測しようとする可能性があることを認識していることです。そのため、1 から 10 までの数字に限定し(10行目)、ユーザーがこの条件を満たすアドレスを生成する可能性を極端に減らしています。

Etherscan

「隠れ」状態更新

通常のトランザクションに加えて、Etherscan はいわゆる内部メッセージも表示します。これは、ユーザアカウントではなく他のコントラクトから発信されたトランザクションです。ただし、ユーザビリティの観点から、Etherscanは空のトランザクション値を含む内部メッセージは表示しません。

上図のコントラクトは、「隠れ」状態更新のハニーポット手法の一例です。この例では、保存されたハッシュを計算するために使われた正しい値を推測できる人に残高を送金します。だまされやすいユーザは passHasBeenSet が false に設定されていると仮定し、保護されていない SetPass 関数を呼び出そうとします。これは、少なくとも 1ether がコントラクトに転送されていれば、ハッシュを既知の値で書き換えることができます。Etherscan の内部メッセージを分析すると、ユーザは PassHasBeenSet 関数を呼び出した証拠を見つけることができず、したがって passHasBeenSet が false に設定されていると仮定します。しかし、Etherscan によるフィルタリングは、ハニーポットの作成者によって悪用され、別のコントラクトから PassHasBeenSet 関数を呼び出し、空のトランザクション値を使用することによって、変数 passHasBeenSet の状態を静かに更新させることが可能です。このように、Etherscan に表示される内部メッセージを見るだけで、何も知らないユーザは変数が false に設定されていると信じ、自信を持って SetPass 関数に ether を転送してしまうのです。

「隠れ」転送

Etherscan は、検証済みのスマートコントラクトのソースコードを表示するウェブインターフェースを提供しています。Validated とは、提供されたソースコードが関連するバイトコードに正常にコンパイルされたことを意味します。Etherscan はしばらくの間、HTML の textarea 要素内にソースコードを表示し、大きなコード行はある幅までしか表示されませんでした。そのため、コードの残りの行は隠され、水平方向にスクロールすることで初めて表示されるようになっていました。

上図のコントラクトでは、この「機能」を利用して、関数 withdrawAll の 4 行目に長い空白の列を導入し、その後に続くコードを効果的に隠しています。この隠されたコードは、関数の呼び出し元が所有者でない場合に作動され、それによって関数の呼び出し元へのその後の残高移動を防止しています。また、4 行目のチェックでは、ブロック番号が 5,040,270 より大きくなければならないことに注意してください。これにより、ハニーポットがメインネットワークにデプロイされたときに、単に資金を盗むことを保証します。テストネットワーク上のブロック番号はより小さいので、そのようなネットワーク上でこのコントラクトをテストすると、すべての資金が被害者に転送され、コントラクトはハニーポットではないと信じさせることができます。このようなハニーポットを「隠れ」転送と呼びます。

Straw Man Contract

上図では、Straw Man Contract ハニーポットの一例です。一見すると、このコントラクトの CashOut 関数はリエントラント攻撃に対して脆弱であるように見えます(14 行目)。リエントラント攻撃を行うには、まず Deposit 関数を呼び出し、最小限の ether を送金する必要があります。その後、CashOut 関数を呼び出し、TransferLog に保存されているコントラクトアドレスの呼び出しを実行します。Log というコントラクトはロガーとして機能することになっています。しかし、ハニーポットの作成者は、このロガーコントラクトのバイトコードを含むアドドレスでコントラクトを初期化しませんでした。代わりに、同じインターフェースを実装するコントラクトを指す別のアドレスで初期化されているが、関数 AddMessage が文字列「CashOut」で呼び出され、呼び出し元がハニーポットの作成者でない場合は例外が作動します。したがって、ユーザが行う再入可能性攻撃は、必ず失敗します。もう一つの方法は、残高の移動の直前に delegatecall を使用することです。delegatecall は、呼び出し先コントラクトが呼び出し元コントラクトのスタックを変更することを可能にします。したがって、攻撃者は、スタックに含まれるユーザのアドレスを自分のアドレスと交換するだけで、delegatecall から戻るときに、残高がユーザではなく攻撃者に転送されます。

この記事が気に入ったらサポートをしてみませんか?