ERC223トークン CUSTOM_CALLバグ攻撃の解説
ブロックチェーンがもたらす非中央集権の力の強さは仮想通貨・トークンだけでなく分権という文脈で様々な広がりを見せています。EthereumトークンスタンダードのERC20におけるトークン消失問題を解決するERC223ですが、トークン実装において外部コールバックによる攻撃が可能となる方法が公開されていました。
ATNトークンのハッキング事件
2018年5月11日、ATN(AI Technology Network)の発行するERC223実装のトークンにおいて約1100万のATNトークンが攻撃者のアドレス宛に不正送信されました。攻撃者はコントラクト内のトークン送信を行う関数で外部コールバックを利用し、複数の攻撃者のアドレス宛にトークン発行を行っていたようです。ATNは攻撃者のアドレスの凍結およびトークン供給量の回復を早急に実施しました。[1]
github上のERC223の実装コードがこの攻撃を可能とするコードとなっているため、実装の際はセキュアなコーディングへと改良する必要があります。ERC223の仕様の策定自体に問題はありませんが、ERC223のgithubにこの実装が推奨として掲載されているため、実装がセキュアでないトークンは非常に多そうです。プロキシーを経由していない場合、スマートコントラクトの処理は変更できないため注意が必要です。
また今回の攻撃はATNが実装に使用していたと思われるスマートコントラクトのフレームワークDappsysのDS-Authの認証機能と組み合わせて攻撃が行われていました。全く同じ攻撃が可能となるケースは非常に少いと思われますが、攻撃の中で対象となった他のコントラクトへの外部コールバックはリエントラント(再入可能)の攻撃を可能にするため対策が必要だと思います。
トークンを実装する際にはSolidityやConsenSysのベストプラクティス集を参考にしてください。
ERC223およびERC827のgithub実装例において同様の攻撃が発生する可能性のあるコードが掲載されています。これらのリンクの通りにERC223/ERC827のトークンを実装すると攻撃の対象となる可能性があります。
またERC827についてはセキュアなスマートコントラクトのライブラリ集であるOpenZeppelinから既に取り除かれています。[6]
CUSTOM_CALLバグ攻撃手法
攻撃手法の解説メモです。まずは攻撃の被害にあったATNの実装です。コード内_custom_fallbackへsetOwner(address)と攻撃者のアドレスを外部コールバックとして渡すことで、ATNコントラクトの所有者を変更することができます。権限奪取後はトークンコントラクトの所有者となることができるためトークン発行など所有者として関数実行が可能です。低レベルのcallがバイトコードの実行、fallback関数の呼び出しを可能にするために推奨されておらず、呼び出し先コントラクトのfallback関数で再度トークンコントラクトの該当関数を呼び出すことで相互再帰が発生するリエントラント(再入可能)の問題も発生し得ます。
byte4(keccak256(_custom_fallback))の部分において対象の関数シグネチャが見つからない場合、呼び出し先コントラクトの無名関数がfallback関数として実行されることも要注意ポイントです。今回の攻撃はトークンコントラクト自体に対してsetOwner(address)を再帰的に呼び出しています。
// CUSTOM_CALL abusing
function transferFrom(
address _from,
address _to,
uint256 _amount,
bytes _data,
string _custom_fallback
)
public returns (bool success)
{
...
ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
receiver.call.value(0)(byte4(keccak256(_custom_fallback)), _from, _amount, _data);
...
}
トランザクションのcalldata(ここでは_data)は通常バイトコード展開されて渡されますが、setOwer(address)を表す4バイトの関数シグネチャとその引数のaddress型の展開後、関数に必要なバイト数でパディングされ、EVM(Ethereum Virtual Machine)上で実行されます。したがって外部コールバックから呼び出し先コントラクトに存在する関数シグネチャを呼び出す際、攻撃者は自由に組み立てたパラメータをトークンコントラクトへ渡すことができてしまいます。
receiver.call.value(0)(byte4(keccak256(_custom_fallback)), _from, _amount, _data)
setOwner(address)はDS-Authの関数ではmodifierとしてisAuthorizedを持ちますが、送信者がトークンコントラクト自身のアドレスの場合は認証をパスできるようです。以下、DS-AuthのGitHubサイトから引用。[7]
Usage
The
auth
modifier provided by DSAuth triggers the internalisAuthorized
function to require that themsg.sender
is authorized ie. the sender is either:1. the contract
owner
;2. the contract itself;
3. or has been granted permission via a specified
authority
CUSTOM_CALLバグ攻撃への対策
安全にコーディングするには、以下のようにtokenFallback関数を使用して受け取り側コントラクトの動作を定義します。トークン送信対象がコントラクトでtokenFallbackが定義されていない場合は、必ずrevertするように処理する必要があります。
// Correct draft code sample
contract ERC223 {
function transfer(address to, uint value, bytes data) {
uint codeLength;
assembly {
codeLength := extcodesize(_to)
}
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
if(codeLength>0) {
// Require proper transaction handling.
ERC223Receiver receiver = ERC223Receiver(_to);
receiver.tokenFallback(msg.sender, _value, _data);
}
}
}
以下はあるトークンの実装ですがモジュール化されており可読性が高く参考になると思いました。送信先がコントラクトかどうかを判定するisContractをprivateで定義してコントラクトである場合とそうでない場合で呼び出す関数を分岐させています。呼び出し先がコントラクトの場合には仕様通りtokenFallbackを外部コールバックして処理するようにすると安全です。
function isContract(address _to) private view returns (bool) {
uint length;
assembly {
length := extcodesize(_addr)
}
return (length > 0);
}function transfer(address _to, uint _value) public returns (bool) {
...
if (isContract(_to)) {
return transferToContract(_to, _value);
} else {
return transferToAddress(_to, _value);
}
}function transferToAddress(address _to, uint _value) private returns (bool) {
...
Transfer(msg.sender, _to, _value);
...
}function transferToContract(address _to, uint _value) private returns () {
...
ContractReceiver receiver = ContractReceiver(_to);
receiver.tokenFallback(msg.sender, _value, _data);
...
}
またERC223についてはgithub上のEIPsおよび‘Ethereum Improvement Proposals’に掲載されていないため実装するかどうか十分に検討が必要です。現段階ではERC223がEthereumのトークン標準になるかどうか全く未知数です。
ERC223実装における他の問題
ケースがまだオープンなため詳細はリンク先を参照して頂きたいのですが、ERC223は以下のような問題も報告されているようです。
1、call処理におけるbytesデータが引き起こす不整合の問題 [8]
// ERC223_Token.sol#L70
assert(_to.call.value(0)(bytes4(keccak256(_custom_fallback)), msg.sender, _value, _data));
2、イベント処理Transferにおいてindexed bytes変数が引き起こすエラーの問題 [9]
// ERC223_Interface.sol#L18
event Transfer(address indexed from, address indexed to, uint value, bytes indexed data);
まとめ
- ERC223およびERC827のgithub実装例には攻撃が可能となるコードが含まれている
- ERC223の仕様に準拠したトークンが攻撃を受けトークンが不正送信された
- callのような低レベルな呼び出しには様々なバグの可能性があるために使用しない、もしくは使用する際には十分に注意する
- トランザクションのcalldataの処理には攻撃者に自由に関数のパラメータを組み立てられる可能性がある
References
- [1] ERC232 Smart Contract Breach and Resolution by ATN
- [2] Solidity Security Considerations
- [3] A Guide to Smart Contract Security Best Practices by ConsenSys
- [4] Risky ERC223 implementation
- [5] Risky ERC827 implementation
- [6] Removed ERC827 token by OpenZeppelin
- [7] dapphub/ds-auth
- [8] Bug: low-level calls have problems of transmitting bytes variables
- [9] Issue of using bytes indexed in event definition