Ethereum(44) - 重要な情報の取り扱い①(実装編)

スマートコントラクトは、ステートに情報を保持し他の誰かと情報を連携することができます。

ただこのステートに保持する情報の扱いには気を付ける必要があります。

ソースコード

privateなステート(secret)に文字を格納するだけのサンプルソースを準備します。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.11;
contract Secret {
string private secret; // 秘密の文字列

/// コンストラクタ
function Secret(string _secret) {
secret = _secret;
}

/// 秘密の文字列を設定
function setSecret(string _secret) public {
secret = _secret;
}
}

このコントラクトを生成する場合に、秘密の文字列を設定します。(6-8行目)

またこの秘密の文字列はsetSecret関数(11-13行目)を使って更新することはできますが、その文字列を参照する関数はなく、privateで宣言している(3行目)ためその内容を確認することはできない・・・はずです。

本当にprivateで宣言していれば、その情報を参照することはできないのでしょうか。

次回は、このスマートコントラクトの動作確認を行います。

Ethereum(43) - Timestamp Dependence②(実行編)

前回作成した抽選を行うスマートコントラクトの動作確認を行います。

アカウントごとの役割

アカウントの役割は次の通りです。

  • MAIN ACCOUNT (eth.accounts[0])
    コントラクト生成者。抽選会の主催者。
  • ACCOUNT1 (eth.accounts[1])
    抽選申込者1。
  • ACCOUNT2 (eth.accounts[2])
    抽選申込者2。
  • ACCOUNT3 (eth.accounts[3])
    抽選申込者3。
  • ACCOUNT4 (eth.accounts[4])
    マイナー。

デプロイ

まずはデプロイを行います。デプロイアカウントはMAIN ACCOUNTです。

デプロイするのはLotteryです。

[デプロイ]


デプロイ後のコントラクトの状態を確認します。

[コントラクト状態]

主催者(Owner)がMain accountで、応募者の数(Num applicants)が0であることが確認できます。

抽選会に応募

アカウント1,アカウント2、アカウント3から抽選会に応募します。

各アカウントからEnter関数を指定しトランザクションを発行します。

[アカウント1から抽選会に応募]

[アカウント2から抽選会に応募]

[アカウント3から抽選会に応募]


応募したあとのコントラクトの状態を確認します。

応募者(Applicants)は入力欄の右端にある小さい三角ボタンをクリックすると、インデックスを変えて応募者を確認することができます。

[応募1の確認]

[応募2の確認]

[応募3の確認]

応募者の人数(Num applicants)が3で、各応募者(Applicants)がアカウント1,アカウント2、アカウント3であることが確認できます。

抽選(当選者を決める)

抽選の開催者(Main Account)から、Hold関数をコールして当選者を決定します。

[開催者が当選者を決める]


コントラクトの状態を参照し、当選者を確認します。

[当選者の確認]

当選者(Winner address)が、アカウント2であることが確認できます。

また、当選者を決めるパラメータとなったTimestampは1627990495であることが分かります。

抽選時のトランザクション確認

抽選を行った時のトランザクションを確認します。

[当選者を決めた際のトランザクション]

このトランザクションは15909番目のブロックに取り込まれています。

このブロックのタイムスタンプをgethコンソールから確認します。

1
2
> eth.getBlock(15909).timestamp
1627990495

15909番目のブロックのタイムスタンプは1627990495となっています。

この値は、コントラクトで抽選を行った時のタイムスタンプと同じです。

つまり、スマートコントラクトで参照しているblock.timestampはトランザクションが発行されたときのタイムスタンプではなく、ブロックに取り込まれた時のタイムスタンプだということになります。

ブロックのタイムスタンプ

ブロックのタイムスタンプはマイナーがブロックを生成する際に設定するもので、どんな値が設定されるのかはマイナー次第となります。

抽選会にマイナーが応募していたとすると、マイナーは抽選時のトランザクションを検知し自分自身が当選者となるようなタイムスタンプを設定し、ブロックを作成するということが可能です。

今回のサンプルソースは、抽選結果がタイムスタンプにより決定されるといった仕様のため、タイムスタンプを操作され当選者を自由に決められてしまうという脆弱性があったといことになります。

タイムスタンプを操作されると困るような場合は、タイムスタンプだけに依存した仕様にしないように気を付けましょう。

Ethereum(42) - Timestamp Dependence①(実装編)

ブロックのタイムスタンプに依存した処理(Timestamp Dependence)を行うと脆弱性となる可能性があります。

抽選を行うスマートコントラクトを作成し、この問題を検証してみます。

実装

抽選を行うサンプルソースは次の通りです。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
pragma solidity ^0.4.11;
contract Lottery {
mapping (uint => address) public applicants; // 応募者を管理
uint public numApplicants; // 応募者数を管理
address public winnerAddress; // 抽選者情報
uint public winnerInd; // 当選者のインデックス
address public owner; // オーナー
uint public timestamp; // タイムスタンプ

/// ownerチェック用のmodifier
modifier onlyOwner() {
require(msg.sender == owner);
_;
}

/// コンストラクタ
function Lottery() {
numApplicants = 0;
owner = msg.sender;
}

/// 抽選に申し込む関数
function enter() public {
// 応募者数は3人まで
require(numApplicants < 3);
// すでに応募済みであれば処理を終了
for(uint i = 0; i < numApplicants; i++) {
require(applicants[i] != msg.sender);
}
// 応募を受け付ける
applicants[numApplicants++] = msg.sender;
}

/// 抽選を行う
function hold() public onlyOwner {
// 応募者が3人に達していない場合は処理を終了
require(numApplicants == 3);
// タイムスタンプを設定
timestamp = block.timestamp;
// 抽選
winnerInd = timestamp % 3;
winnerAddress = applicants[winnerInd];
}
}

処理のポイントを解説します。

  • enter関数(23行目)
    抽選に申し込む関数です。
    申し込みができるのは3人までとします。
  • hold関数(35行目)
    抽選を行い当選者を決める関数です。
    当選者はタイムスタンプを3で割った余りのインデックスの応募者とします。

非常にシンプルな処理ですが、当選者がタイムスタンプに依存していることが脆弱性となりえます。


次回は、このスマートコントラクトの動作確認を行います。

Ethereum(41) - Transaction-Ordering Dependence④(自動実行編)

今回は、トランザクション発行を検知して、そのトランザクションより優先して処理を実行してみます。

アカウントごとの役割

アカウントの役割は次の通りです。

  • MAIN ACCOUNT (eth.accounts[0])
    コントラクト生成者。売り手。自動で攻撃を行う。
  • ACCOUNT1 (eth.accounts[1])
    買い手。
  • ACCOUNT2 (eth.accounts[2])
    マイナー。

コントラクトの状態確認

現状のコントラクトの状態を確認します。

[コントラクトの状態]

価格は20 etherであることが確認できます。

自動実行処理を設定(イベント検知)

トランザクションを検知して、priceを自動で20 etherから25 etherに更新する処理を設定します。

コマンドの全体は下記の通りで、gethコンソール上で実行します。

[コマンド]

1
2
3
4
5
6
7
8
9
10
11
12
var filter = web3.eth.filter('pending');
filter.watch(function(error, result){
var tx = web3.eth.getTransaction(result);
if(!error && tx.to.toUpperCase() === mpt.address.toUpperCase() && tx.from !== eth.accounts[0]) {
console.log('Tx Hash:' + result);
var _gasPrice = parseInt(tx.gasPrice, 10) + 1;
console.log('Gas Price:' + _gasPrice);
var attackTx = mpt.updatePrice.sendTransaction(25,{from:eth.accounts[0], gas:5000000, gasPrice:_gasPrice});
console.log('Attack Tx Hash:' + attackTx);
console.log('done');
}
});

上記コマンドの説明は以下の通りです。

  • 1行目
    マイニングされていないトランザクションのみを抽出するためにフィルタをかけます。
  • 2行目
    watch関数で監視を開始します。
  • 4行目
    下記の条件を全て満たしているかどうかを判定します。
    ①エラーでないこと。
    ②検知したトランザクションの宛先アドレスとmpt(コントラクト)のアドレスが一致すること。
    ③fromアドレスが自分でないこと。(自分が発行したトランザクションを対象外とするため)
  • 6行目
    検知したトランザクションのgasPriceを取得して、それよりも1wei大きい値を_gasPriceに格納します。
  • 8行目
    検知したトランザクションよりも高いgasPriceでupdatePrice処理(価格更新)を呼び出します。

購入トランザクション発行

アカウント1から購入トランザクション(buy関数)を発行します。

[コマンド]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> personal.unlockAccount(eth.accounts[0])
Unlock account 0xec3b01f36b44182746ca109230567c4915512e35
Passphrase:
true

> personal.unlockAccount(eth.accounts[1])
Unlock account 0x63f25b9bbd974fdfa07477cb99c60d2073cfe560
Passphrase:
true

> mpt.buy.sendTransaction(10, {from:eth.accounts[1], gas:5000000, gasPrice:50000000000, value:web3.toWei(500, "wei")});
"0xc9d81f9a6a65554a963ca178fc0e0026bb95c88ed05892d7ffbec4786fe5b5b1"

[↓↓↓自動で実行される]
> Tx Hash:0xc9d81f9a6a65554a963ca178fc0e0026bb95c88ed05892d7ffbec4786fe5b5b1
Gas Price:50000000001
Attack Tx Hash:0xfca59c8a842abb5154a3953a7a3092d2ecf43d83b1c74db4ddcf863817a7e454
done

14行目以降が、イベントが検知され自動で実行された処理のログになります。


発行されたトランザクションの内容を確認します。

[トランザクションの確認]

アカウント1としてはpriceが20 etherで購入(buy)したはずですが、イベント検知処理により価格更新のトランザクションが優先的に実行されたために25 etherでの購入することになっていることが確認できます。

まとめ

今回のサンプルでは攻撃者がスマートコントラクトのオーナーという前提で行いましたが、逆のパターンもありえます。

自分が作成したスマートコントラクトに対して、TODを突いた攻撃を受ける可能性もあります。

また、作成したスマートコントラクトがTODの影響を受けた場合に問題になるかどうかは、攻撃意思がなくても発生する可能性があります。

スマートコントラクトを実装する際にはTODの影響を受けるかどうかを必ず確認するようにしましょう。

Ethereum(40) - Transaction-Ordering Dependence③(実行編)

前回は、Transaction-Ordering Dependence問題を検証するためのコントラクトをデプロイし、geth上で操作できるようにコントラクトの変数宣言を行いました。

今回はこのスマートコントラクトの動作確認を行います。

アカウントごとの役割

アカウントの役割は次の通りです。

  • MAIN ACCOUNT (eth.accounts[0])
    コントラクト生成者。売り手
  • ACCOUNT1 (eth.accounts[1])
    買い手。
  • ACCOUNT2 (eth.accounts[2])
    マイナー。

発行順通りに実行

動作確認は2つのトランザクを間髪入れずに連続で行う必要があるため、gethコンソール上から行います。

最初に買い手からbuy関数を呼び出し、2つ目でオーナーからupdate関数をコールしpriceを更新します。

buy関数のgasPriceを高く設定しているのがポイントとなります。

送金を行う前に各アカウントのロックを解除しています。

[コマンド]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> personal.unlockAccount(eth.accounts[0])
Unlock account 0xec3b01f36b44182746ca109230567c4915512e35
Passphrase:
true

> personal.unlockAccount(eth.accounts[1])
Unlock account 0x63f25b9bbd974fdfa07477cb99c60d2073cfe560
Passphrase:
true

> mpt.buy.sendTransaction(10, {from:eth.accounts[1], gas:5000000, gasPrice:90000000000, value:web3.toWei(500, "wei")});
"0xb3e313756c83d6dc25384c3edb45dd173cd7382f2ace5a7e91e059f424a4be1d"

> mpt.updatePrice.sendTransaction(15, {from:eth.accounts[0], gas:5000000, gasPrice:50000000000});
"0xc67339009a3350ebdc93aef77079a3b35ed7cfcf1b594febd1e546f26e690074"

実行結果を確認します。

[結果]

先にbuyを発行しているため、updatePriceでpriceが15 etherに更新される前のpriceで購入できているのは当たり前のように思えます。

しかし実際には発行順とは違う順番でトランザクションが実行されてしまうことがあります。

発行順と異なる実行

買い手はbuyトランザクションを発行する時に参照していたpriceではなくupdatePrice後のpriceで購入してしまうケースがあります。

コマンド発行順は同じですが、updatePrice関数のgasPriceを高く設定して実行してみます。

[コマンド]

> personal.unlockAccount(eth.accounts[0])
Unlock account 0xec3b01f36b44182746ca109230567c4915512e35
Passphrase:
true

> personal.unlockAccount(eth.accounts[1])
Unlock account 0x63f25b9bbd974fdfa07477cb99c60d2073cfe560
Passphrase:
true

> mpt.buy.sendTransaction(10, {from:eth.accounts[1], gas:5000000, gasPrice:50000000000, value:web3.toWei(500, "wei")});
"0x75840755d8756c7918b925329b83ddba5d62c43caeb7c80f9144b15c5594c173"

> mpt.updatePrice.sendTransaction(20, {from:eth.accounts[0], gas:5000000, gasPrice:90000000000});
"0x4583af1ec7929ba834d493482e57f0474349dba2d09cea8d8c9376d16409708f"```

<br>

実行結果を確認します。

[結果]
<img src="/img/geth/tod3b.png" alt="" />

買い手がbuyトランザクションを発行する時に参照していたpriceは<b>15 ehter</b>でしたが、<b>20 ehter</b>での購入となってしまいました。

<br>

1回目と結果が変わったのは意図的にこの順番になるようにオーナーが<b>トランザクションの順番をコントロールした</b>からです。

Ethereumではマイニングする際に、<b>gasPriceが高いトランザクションを優先して実行</b>するようになっています。

そのため、後者のトランザクションのgasPriceを前者のgasPriceより高く設定すれば、トランザクションの発行順によらず後者が優先されてしまいます。

次回は、トランザクションのイベントを検知しこの操作を自動化してみます。

Ethereum(39) - Transaction-Ordering Dependence②(デプロイ編)

前回は、Transaction-Ordering Dependence問題を検証するためのサンプルコードを準備しました。

今回はそのスマートコントラクトをデプロイし、gethコンソール上でそのコントラクトを操作できるように準備します。

アカウントごとの役割

アカウントの役割は次の通りです。

  • MAIN ACCOUNT (eth.accounts[0])
    コントラクト生成者。売り手
  • ACCOUNT1 (eth.accounts[1])
    買い手。
  • ACCOUNT2 (eth.accounts[2])
    マイナー。

デプロイ

まずはデプロイを行います。デプロイアカウントはMAIN ACCOUNTです。

デプロイするのはMarket Place TODです。

[デプロイ]


デプロイ後のコントラクトの状態を確認します。

[コントラクト状態]

コントラクトのアドレスインターフェースは、geth上のコントラクト定義の際に必要になりますので、コピーしておいて下さい。

geth上でコントラクト変数定義

geth上でコントラクトを操作できるようにmptという変数名で定義します。

コントラクトを定義する際は次のようなコマンドを実行します。

[コントラクトの定義方法]

1
var mpt = eth.contract(インターフェース).at('アドレス')

インターフェースとアドレスには、Mist Walletから取得したものを設定します。

[コマンド]

1
var mpt = eth.contract([ { "constant": true, "inputs": [], "name": "stockQuantity", "outputs": [ { "name": "", "type": "uint256", "value": "100" } ], "payable": false, "type": "function" }, { "constant": false, "inputs": [ { "name": "_price", "type": "uint256" } ], "name": "updatePrice", "outputs": [], "payable": false, "type": "function" }, { "constant": true, "inputs": [], "name": "owner", "outputs": [ { "name": "", "type": "address", "value": "0xec3b01f36b44182746ca109230567c4915512e35" } ], "payable": false, "type": "function" }, { "constant": true, "inputs": [], "name": "price", "outputs": [ { "name": "", "type": "uint256", "value": "10" } ], "payable": false, "type": "function" }, { "constant": false, "inputs": [ { "name": "_quantity", "type": "uint256" } ], "name": "buy", "outputs": [], "payable": true, "type": "function" }, { "inputs": [], "payable": false, "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "_price", "type": "uint256" } ], "name": "UpdatePrice", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "_price", "type": "uint256" }, { "indexed": false, "name": "_quantity", "type": "uint256" }, { "indexed": false, "name": "_value", "type": "uint256" }, { "indexed": false, "name": "_change", "type": "uint256" } ], "name": "Buy", "type": "event" } ]).at('0x350aC614C66c28dF49B9791A59c2b388b3253F18')

定義したコントラクトの内容はgeth上で確認できます。

[定義したコントラクトの内容確認]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
> mpt
{
abi: [{
constant: true,
inputs: [],
name: "stockQuantity",
outputs: [{...}],
payable: false,
type: "function"
}, {
constant: false,
inputs: [{...}],
name: "updatePrice",
outputs: [],
payable: false,
type: "function"
}, {
constant: true,
inputs: [],
name: "owner",
outputs: [{...}],
payable: false,
type: "function"
}, {
constant: true,
inputs: [],
name: "price",
outputs: [{...}],
payable: false,
type: "function"
}, {
constant: false,
inputs: [{...}],
name: "buy",
outputs: [],
payable: true,
type: "function"
}, {
inputs: [],
payable: false,
type: "constructor"
}, {
anonymous: false,
inputs: [{...}],
name: "UpdatePrice",
type: "event"
}, {
anonymous: false,
inputs: [{...}, {...}, {...}, {...}],
name: "Buy",
type: "event"
}],
address: "0x350aC614C66c28dF49B9791A59c2b388b3253F18",
transactionHash: null,
Buy: function(),
UpdatePrice: function(),
allEvents: function(),
buy: function(),
owner: function(),
price: function(),
stockQuantity: function(),
updatePrice: function()
}

以上で、geth上でコントラクトの操作を行う準備ができました。


次回は、このスマートコントラクトの動作確認を行います。

Ethereum(38) - Transaction-Ordering Dependence①(実装編)

トランザクションがブロックに取り込まれる順番はマイナーに依存するため、意図した順番でトランザクションが実行されないことがあります。

この問題はTransaction-Ordering Dependenceと呼ばれています。

サンプルコードを作成し、この問題を検証していきます、

実装

Transaction-Ordering Dependence問題を確認するための、サンプルコードは下記の通りです。

マーケットプレイスを表現したスマートコントラクトになります。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
pragma solidity ^0.4.11;

contract MarketPlaceTOD {
address public owner;
uint public price; // 1個あたりの金額
uint public stockQuantity; // 在庫数

modifier onlyOwner() {
require(msg.sender == owner);
_;
}

event UpdatePrice(uint _price);
event Buy(uint _price, uint _quantity, uint _value, uint _change);

/// コンストラクタ
function MarketPlaceTOD() {
owner = msg.sender;
price = 10;
stockQuantity = 100;
}

/// 1個あたりの金額を更新
function updatePrice(uint _price) public onlyOwner {
price = _price;
UpdatePrice(price);
}

/// 購入処理
function buy(uint _quantity) public payable {
// 購入金額と在庫数を確認
if (msg.value < _quantity * price || _quantity > stockQuantity) {
throw;
}

// お釣りを返す
if(!msg.sender.send(msg.value - _quantity * price)) {
throw;
}

stockQuantity -= _quantity; // 在庫を減らす
Buy(price, _quantity, msg.value, msg.value - _quantity * price);
}
}

このコードのポイントは次の通りです。

  • 売り手がコントラクトを生成します。
  • 買い手がコントラクトにトランザクションを発行することで購入が成立します。
  • コントラクトは1個あたりの値段在庫数をステートに保持しています。
  • 値段はupdatePrice関数を実行することで、売り手が更新します。
  • 買い手はbuy関数をetherの送金を伴う形で呼び出すことで購入可能です。
  • 送金額がbuy関数の引数の購入数1個あたりの値段をかけた値以上で、在庫がある場合に購入できます。

次回は、このスマートコントラクトをデプロイし、geth上で動作確認できるようにコントラクト変数を定義します。

Ethereum(37) - Reentrancy問題⑤(実行編 - 改善版)

今回は、Reentrancy問題を改善したスマートコントラクトに対して攻撃を行います。

アカウントごとの役割

アカウントの役割は次の通りです。

  • MAIN ACCOUNT (eth.accounts[0])
    攻撃される側のコンストラクト(Victim Balance)生成者。
  • ACCOUNT1 (eth.accounts[1])
    攻撃される側のコントラクトに送金を行う通常のユーザ。
  • ACCOUNT2 (eth.accounts[2])
    攻撃する側のコンストラクト(Evil Receiverコ)生成者。
  • ACCOUNT3 (eth.accounts[3])
    マイナー。

攻撃される側のコントラクト(改善版)をデプロイ

まずはデプロイを行います。デプロイアカウントはMAIN ACCOUNTです。

デプロイするのは、前回改善したVictim Balanceコントラクトになります。

[Victim Balanceコントラクトのデプロイ]


デプロイ後のコントラクトの状態を確認します。

[攻撃される側のコントラクト状態]

デプロイ直後なので、コントラクトの残高が0 ehterであることが確認できます。

攻撃する側のコントラクトをデプロイ

次は攻撃する側のコントラクトのデプロイを行います。デプロイアカウントはACCOUNT2です。

デプロイするのは、Evil Receiverコントラクトになります。

targetには、攻撃される側のコントラクト(改善したVictim Balance)のアドレスを設定します。

[Evil Receiverコントラクトのデプロイ]


デプロイ後のコントラクトの状態を確認します。

[攻撃する側のコントラクト状態]

ターゲットのコントラクトがVictim Balanceとなっていることが確認できます。

通常のユーザから送金

ACCOUNT1から、攻撃される側のコントラクト(Victim Balance)に20 ether送金します。

Add To Balance関数を使用します。

[送金]


送金後のコントラクトの状態を確認します。

[攻撃される側のコントラクト状態]

残高が20 etherになっていることが確認できます。

攻撃する側のコントラクトに送金

ACCOUNT2から、攻撃する側のコントラクト(Evil Receiver)に10 etherを送金します。

Add Balance関数を使用します。

[送金]


送金後のコントラクトの状態を確認します。

[攻撃する側のコントラクト状態]

残高が10 etherになっていることが確認できます。

攻撃する側のコントラクトから送金

攻撃する側のコントラクト(Evil Receiver)から攻撃される側のコントラクト(Victim Balance)に10 etherを送金します。

Send Eth To Target関数を使用します。

[送金]


攻撃される側のコントラクトの状態を確認します。

[攻撃される側のコントラクト状態]

10 ether追加されて、残高が30 etherになりました。


攻撃する側のコントラクトの状態を確認します。

[攻撃する側のコントラクト状態]

10 ether送金したので、残高が0 etherになりました

攻撃する側のコントラクトから返金要求

攻撃する側のコントラクト(Evil Receiver)から攻撃される側のコントラクト(Victim Balance)に返金要求を行います。Withdraw関数を使用します。

10 ether送金したので、返金額は10 etherとなるはずです。

[返金]


攻撃された側のコントラクト状態を確認します。

[攻撃される側のコントラクト状態]

改善前は、20 ether送金されてしまい10 etherしか残っていませんでしたが、今回は問題なく10 etherが返金され残高が20 etherとなっています。


攻撃した側のコントラクト状態も確認します。

[攻撃する側のコントラクト状態]

前回は攻撃の結果20 ether引き出せましたが、今回は本来の10 ehterの返金となり攻撃に失敗しています。

以上で、Reentrancy問題のあったらスマートコントラクトの改善を確認することができました。

Ethereum(36) - Reentrancy問題④(実装編 - 改善版)

Reentrancy問題を解決するためのソース修正を行います。

処理フロー検討

問題のあったコントラクトの返金処理フローは下記のようになっていました。

 ① 残高を確認する。
 ② 残高の全額を引き出す(呼び出し元へ送金)
 ③ 管理している残高を0にする。

問題が発生した原因は、②の処理中に再度返金処理をコールされる可能性があるからでした。

この場合、③の残高を0にする前に①と②が再度実行されてしまい、再び送金処理が実行されてしまいます。

ということは②の送金処理の前に③の残高を0にする処理を実行すれば問題が解決するような気がします。

処理フロー(改善版)

処理フローを次のように改善します。

 ① 残高を確認する。
 ② 残高更新前に送金額を退避する。
 ③ 管理している残高を0にする。
 ④ 残高の全額を引き出す(呼び出し元へ送金)

④の送金する前に③の管理残高を0にしているところが改善点です。

また、③の残高更新前に②の返金額を退避しています。

実装(改善版)

改善版のソースコードは下記の通りです。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
pragma solidity ^0.4.11;
contract VictimBalance {
// アドレス毎に残高を管理
mapping (address => uint) public userBalances;
event MessageLog(string); // メッセージ表示用のイベント
event BalanceLog(uint); // 残高表示用のイベント

/// コンストラクタ
function VictimBalance() {
}

/// 送金される際に呼ばれる関数
function addToBalance() public payable {
userBalances[msg.sender] += msg.value;
}

/// etherを引き出す時に呼ばれる関数
function withdrawBalance() public payable returns(bool) {
MessageLog("== withdrawBalance started. ==");
BalanceLog(this.balance);

// ①残高を確認
if(userBalances[msg.sender] == 0) {
MessageLog("No Balance.");
return false;
}

// ②残高更新前に送金額を退避
uint amount = userBalances[msg.sender];

// ③残高を更新
userBalances[msg.sender] = 0;

// ④呼出し元に返金
if (!(msg.sender.call.value(amount)())) { throw; }

MessageLog("=== withdrawBalance finished. ==");
return true;
}
}

修正箇所は23行目から35行目の返金フローです。


次回は、このコントラクトに対して再度同じ攻撃を行い問題が解決しているかどうかを確認します。

Ethereum(35) - Reentrancy問題③(実行編)

今回は、Reentrancy問題のあるスマートコントラクトに攻撃をしてみます。

アカウントごとの役割

アカウントの役割は次の通りです。

  • MAIN ACCOUNT (eth.accounts[0])
    攻撃される側のコンストラクト(Victim Balance)生成者。
  • ACCOUNT1 (eth.accounts[1])
    攻撃される側のコントラクトに送金を行う通常のユーザ。
  • ACCOUNT2 (eth.accounts[2])
    攻撃する側のコンストラクト(Evil Receiverコ)生成者。
  • ACCOUNT3 (eth.accounts[3])
    マイナー。

攻撃される側のコントラクトをデプロイ

まずはデプロイを行います。デプロイアカウントはMAIN ACCOUNTです。

デプロイするのは、Victim Balanceコントラクトになります。

[Victim Balanceコントラクトのデプロイ]


デプロイ後のコントラクトの状態を確認します。

[攻撃される側のコントラクト状態]

デプロイ直後なので、コントラクトの残高が0 ehterであることが確認できます。

攻撃する側のコントラクトをデプロイ

次は攻撃する側のコントラクトのデプロイを行います。デプロイアカウントはACCOUNT2です。

デプロイするのは、Evil Receiverコントラクトになります。

targetには、攻撃される側のコントラクトのアドレスを設定します。

[Evil Receiverコントラクトのデプロイ]


デプロイ後のコントラクトの状態を確認します。

[攻撃する側のコントラクト状態]

ターゲットのコントラクトがVictim Balanceとなっていることが確認できます。

通常のユーザから送金

ACCOUNT1から、攻撃される側のコントラクト(Victim Balance)に20 ether送金します。

Add To Balance関数を使用します。

[送金]


送金後のコントラクトの状態を確認します。

[攻撃される側のコントラクト状態]

残高が20 etherになっていることが確認できます。

攻撃する側のコントラクトに送金

ACCOUNT2から、攻撃する側のコントラクト(Evil Receiver)に10 etherを送金します。

Add Balance関数を使用します。

[送金]


送金後のコントラクトの状態を確認します。

[攻撃する側のコントラクト状態]

残高が10 etherになっていることが確認できます。

攻撃する側のコントラクトから送金

攻撃する側のコントラクト(Evil Receiver)から攻撃される側のコントラクト(Victim Balance)に10 etherを送金します。

Send Eth To Target関数を使用します。

[送金]


攻撃される側のコントラクトの状態を確認します。

[攻撃される側のコントラクト状態]

10 ether追加されて、残高が30 etherになりました。


攻撃する側のコントラクトの状態を確認します。

[攻撃する側のコントラクト状態]

10 ether送金したので、残高が0 etherになりました

攻撃する側のコントラクトから返金要求

攻撃する側のコントラクト(Evil Receiver)から攻撃される側のコントラクト(Victim Balance)に返金要求を行います。Withdraw関数を使用します。

10 ether送金したので、返金額は10 etherとなるはずです。

[返金]


攻撃された側のコントラクト状態を確認します。

[攻撃される側のコントラクト状態]

問題が発生しました。

10 ether返金されたはずなので、20 etherの残高のはずが10 etherしかありません。


攻撃した側のコントラクト状態も確認します。

[攻撃する側のコントラクト状態]

本来は攻撃する側のコントラクトからは10 ehterしか引き出せないはずですが、20 etherを引き出せています。

これは、攻撃される側のコントラクト(Victim Balance)で返金処理後に残高更新していることに問題があります。

Fallback関数から再度攻撃される側の返金用関数(withdraw関数)が呼び出さると残高があるという判定になり、再返金処理が実行されてしまっているのです。

(イベントログにて、処理フローを確認したかったのですが、ログが時系列に出力されずにとても確認しづらかったので省略します😥😥)


次回はこのReentrancy問題を解決するためのソース修正を行います。