はじめに(Introduction)
以下をGoogle翻訳で翻訳します。
イーサリアムプロキシの悪意のあるバックドア
Malicious backdoors in Ethereum Proxies
スマート コントラクトのアップグレード可能性のためのプロキシ パターンを活用する方法についての詳細な説明。
Patricio Palladino
Published in Nomic Foundation
5 分で読めます。 2018年6月2日
最近、ZeppelinOS の初期リリースを監査し、事実上すべてのアップグレード可能なスマート コントラクトを実装するために使用されるプロキシ パターンの脆弱性を発見しました。
この脆弱性により、攻撃者は、Solidity とプロキシ パターンの仕組みを深く理解していなければ、発見するのが非常に困難な悪意のあるコードを隠すことができます。
この問題は ZeppelinOS ではすでに修正されています。
Solidity 関数呼び出しの内部構造
Solidity function calls’ internals
contract SimpleContract {
uint256 private someData;
function set(uint256 value) public {
someData = value;
}
function get() public view returns (uint256) {
return someData;
}
function() public payable {
}
}
Solidity で書かれたシンプルなスマート コントラクト
あなたがイーサリアム用に構築している開発者であれば、Solidity の観点からスマート コントラクトをコード化し、考える可能性が高くなりますが、ネットワークはそのように動作しません。
ネットワークの観点から見ると、スマート コントラクトは、コードの単一のチャンクが関連付けられたアカウントです。
他のアカウントがコントラクトにメッセージ1 を送信すると、そのコードが EVM 上で実行されます。
では、コントラクトに連続したコードが 1 つしかない場合、どのようにしてさまざまな関数を呼び出すことができるのでしょうか?
Ethereum は、そのコンポーネント間で通信する標準的な方法、つまり Application Binary Interface (略して ABI) を定義しています。
これは、システムでどの機能が利用できるかだけでなく、通常当たり前だと思っていることがどれだけ機能するかを指定する、低レベルの API と考えることができます。
関数の呼び出し方法、関数に引数を渡す方法、関数が値を返す方法などがあります。
Ethereum ABI では、トランザクションの data
パラメータは、呼び出そうとしているメソッドを識別する関数セレクターで開始する必要があると規定されています。
セレクターを使用すると、コントラクトのコードは、呼び出そうとしている関数を実装するコード自体の部分にジャンプします。
関数セレクターは、関数のシグネチャの sha3
ハッシュの最初の 4 バイトにすぎません。
たとえば、get
のセレクターは sha3(“get()”)[0:4]
として計算され、0x6d4ce63c
が得られます。
同様に、set
のものは sha3(“set(uint256)”)[0:4]
の結果です。
関数セレクターには例外が 1 つだけあり、それはすべてのスマート コントラクトに存在するフォールバック関数であり、セレクターがありません。
これは、data
パラメーターが指定されていない場合、または指定されたセレクターがコントラクトのメソッドのいずれにも一致しない場合に呼び出されるという特別な動作を持っています。
プロキシ パターンの再考
The Proxy Pattern revisited
プロキシ パターン、そのさまざまなバリエーション、およびそれらのトレードオフについては、多くのことが書かれています。
選択したプロキシ パターンに関係なく、そのコア機能は同じです。つまり、受信したすべてのメッセージをコントラクトの現在の実装に転送します2。
これがどのように機能するかを見てみましょう。
contract Proxy {
function proxyOwner() public view returns (address);
function setProxyOwner(address _owner) public returns (address);
function implementation() public view returns (address);
function setImplementation(address _implementation) internal returns (address);
function upgrade(address _implementation) public {
require(msg.sender == proxyOwner());
setImplementation(_implementation);
}
function() payable {
address _impl = implementation();
assembly {
calldatacopy(0, 0, calldatasize);
let result := delegatecall(gas, _impl, 0, calldatasize, 0, 0)
returndatacopy(0, 0, returndatasize)
switch result
case 0 { revert(0, returndatasize) }
default { return(0, retundatasize)}
}
}
}
プロキシコントラクトの実装
心配しないでください。この恐ろしい組み立てブロックがどのように機能するかを理解する必要はありません。
現在のメッセージを実装に転送し、受信したのとまったく同じ data
パラメータを送信します。
転送ロジックをフォールバック関数に配置すると、おそらく、あらゆる呼び出しを Proxy
へ転送できるようになります。
実は、これは必ずしも起こるわけではありません。
Proxy
はアップグレード可能である必要があるため、独自のメタ機能も必要です。
したがって、implementation()
や proxyOwner()
などの関数は、それらが存在し、フォールバック関数が実行されない限り、転送されません。
プロキシ セレクターのクラッシュ
Proxy selector clashing
賢いイーサリアム開発者であれば、セレクターが実装コントラクト内の関数と一致するプロキシ コントラクト内の関数はすべて、実装コードを完全にスキップして直接呼び出されることに気づいたかもしれません。
関数セレクターは固定バイト数を使用するため、常に競合が発生する可能性があります。
Solidity コンパイラーがコントラクト内のセレクターの衝突を検出するため、これは日常の開発では問題になりませんが、セレクターがコントラクト間の対話に使用される場合に悪用可能になります。
クラッシュを悪用して、一見行儀の良いコントラクトを作成することができますが、実際にはバックドアが隠蔽されています。
Rust コードをいくつか使用したところ、clash550254402()
には proxyOwner()
と同じセレクターがあることがわかりました。
新しい Macbook Pro でそれを見つけるのに 15 分もかかりませんでした。
やる気のあるハッカーはプロセスを最適化し、離散的に見える関数名を見つけるためにより多くのリソースを費やすことができます。
悪用可能性
Exploitability
プロキシ パターンは、スマート コントラクトをアップグレード可能にするためイーサリアム エコシステム全体で使用されている現在のアプローチであり、セレクター クラッシュ攻撃により、それを使用するプロジェクト、またはアップグレード メカニズムへのアクセス権を取得した攻撃者が、悪意のある機能を隠蔽するコードを展開することが可能になります。
たとえば、ほとんどのアップグレード可能実装には、コントラクトのストレージをアップグレードする機能である状態移行という概念があります。
これらは、コミット番号などの自動生成文字列がこれらの関数の名前として受け入れられるため、セレクターのクラッシュを偽装するのに特に役立ち、セレクターのクラッシュ攻撃を簡単に偽装できます。
ZeppelinOS に対して実施したセキュリティ監査のコンテキストで、ネットワークの任意のユーザーに他のユーザーが使用できる実装をデプロイさせることを意図していることを考慮すると、プロキシの所有者だけでなく誰でもこれを悪用できる可能性があることがわかりました。
別の例として、資金を正常に移動するように見える関数呼び出しが、実際にはまったく呼び出されず、誰かのお金を盗む可能性があります。
提案されたソリューション
Proposed solution
この脆弱性が発見される前に、Zeppelin の Francisco Giordano はすでに Transparent Proxies に取り組んでいました。
これは、セレクターの衝突の可能性なしに、実装コントラクトで Proxy
と同じ関数名を使用できるようにすることを目的とした改良された手法です。
これにより攻撃が排除されます。
これらの新しいプロキシは、プロキシの所有者からの通話でない限り、あらゆる通話を転送することで機能します。
衝突は依然として存在しますが、発信者がプロキシ所有者以外の場合、通話は転送されます。
これにより、プロキシ所有者が競合に陥る可能性がある唯一のアカウントとなり、ユーザーが隠蔽の危険にさらされることがなくなります。
唯一の欠点は、他のユーザーが ABI を使用して Proxy
自身の状態 (つまり、所有者と実装) を読み取ることができないことです。
代わりに web3.eth.getStorageAt()
を使用する必要があります。
これは、アップグレード可能なコントラクトが実装ソース コードに示されているとおりに正確に実行されることを確認するために支払う、かなり小さな代償です。
読者のための演習
Exercise for the reader
この脆弱性が悪用される方法をさらに詳しく知りたい人のために、小さな演習をまとめました。
あなたの仕事は、このコントラクト内の Ropsten-ETH を盗み、何が起こっているのかを解明することです。
これは Proxy
コントラクトであるため、その実装も確認する必要があることに注意してください。
これらのコントラクトでは何をしても構いませんが、他の人もプレイできるように残高を完全に空にしないでください。
ノート
Notes
- メッセージは、アカウントが相互に通信する方法です
トランザクションを送信すると、別のアカウントにメッセージが送信されます。
送信者が契約である場合、これらは通常、内部トランザクションと呼ばれます。 - メッセージは、従来のプロキシのように実際には転送されません
何が起こっているかというと、実装のコードがdelegatecall
を介してプロキシであるかのように実行されるということです。
Nomic Labs から高品質のスマート コントラクト監査を受けてください。
まとめ(Conclusion)
過去の記事ですが、プロキシアップデートパターンの苦労が垣間見える記事でした。
現在のプロキシアップデートパターンはこういった歴史を経ているので現在では安心して使えるように見えます。