bitbank

Hacking Smart Contract Solidity ブロックチェーン

[Solidity-Bug Bounty] Private Dataへのアクセスを1からわかりやすく解説

かるでね

こんにちは!実務でPython、Djangoを使って、機械学習やWebアプリケーション開発などをしているかるでねです!
最近はSolidityを使ってスマートコントラクト開発やバグを見つけたりしています。

今回は「プライベート変数」について取り上げていきます。

Solidityには「public」と「private」の2つの変数が存在します。

名前の通り「public」は公開されているデータで、「private」は公開されていないデータです。

しかし、実は「private」データは見ることができるんです...。

Solidityスキルを1段階あげたい方やバグ・バウンティに参加したいバグハンターを目指している方は必須の知識となるので、この記事で学んでいってください。

バグ・バウンティが何かわからない人は以下の記事を読んでください。

プライベートデータとは?

まずはプライベートデータが何かから確認していきましょう。

プライベートデータを一言で

プレイベートデータとは、一言で言うと「宣言したコントラクト内からのみアクセス可能な変数」のことです。

宣言したコントラクト内からのみアクセスできるため、他のコントラクトや継承先のコントラクトからはアクセスができない。

ストレージデータの保管方法

ストレージデータがどのように格納されているか知っているでしょうか?

スマートコントラクトでは、以下のように32バイトずつのスロットと言われるものが2^256個積み重なっています。

このスロットに宣言順でデータが格納されています。

スロットに格納する時は右から格納され、1スロット内のバイト数が32バイトに収まる場合は複数のデータ同じスロットに格納されます。


言葉だけではわかりにくいので図で確認していきましょう。

以下のように変数が定義されているとします。

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.13;

contract Private {
    string name;
    uint8 age;
    uint8 income;
    bytes32 hash;
    address walletAddress;
    bool engineer;
    bool creator;
}

実際に上記の7つの変数を格納すると以下のようになります。

この図を見ることでなんとなくイメージができたのではないでしょうか?

実際にデータを入れると以下のようになります。

先ほどのコードに何番目のスロットに保存されているかのコメントをつけてみます。

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.13;

contract Private {
    string name;           // スロット0
    uint8 age;             // スロット1
    uint8 income;          // スロット1
    bytes32 hash;          // スロット2
    address walletAddress; // スロット3
    bool engineer;         // スロット3
    bool creator;          // スロット3
}

配列やMappingの格納方法は若干異なるのですが、今回の記事の主題ではないので省かせていただきます(今後まとめます)。

ストレージの詳細

ストレージデータの格納方法を確認できたので、ストレージに保管されるデータの詳細の説明をしていきましょう。

プライベートデータはストレージ内にデータが保存されるため、以下のような条件がつきます。

ストレージ内に保存されたデータブロックチェーンに書き込まれるため、ガス代がかかり永続的に保存される。

32バイト256ビット)のスロットを占有するためのガスコストは20,000ガス。

この際、各スロットがフル稼働(パンパンにデータが詰まっている状態)していなくても、コストは発生します。

ストレージの値を変更するためにかかるガスコストは5,000ガス。

ストレージスロットのクリーンアップ(0以外のバイトを0にすること)時に、一定量のガス代が払い戻される。

これはpublicデータにも共通する内容ですので頭に入れておきましょう。

プライベートデータの閲覧

private変数に格納した値を誰かにみられることはないと直感的には思いますが、実は見ることができます。

private変数に格納した値は、他のコントラクトから変更することは防ぐことはできますが、見ることは誰でも可能です。

ブロックチェーンは透明ということを念頭に置いておくと良いですね。

では本当にprivate変数の値を見ることができるのか確認してみましょう。

Truffleのセットアップ

まずはtruffleのセットアップから行います。

truffleとはスマートコントラクトのコンパイルやテスト、デプロイを自動化するフレームワークです。

truffleを使うことで簡単にprivate変数に格納されたデータを見れてしまうので、早速見ていきましょう。

まずはtruffleをインストールしていきます。

npm install -g truffle

このコマンドを実行する際、Node.jsをインストールしておく必要があるので、まだNode.jsをインストールしていない方は、以下の記事を参考にインストールしてください。

次にディレクトリを作成していきます。

mkdir private_truffle
cd private_truffle

ディレクトリを作成できたら以下を実行してください。

truffle init

これで新たにtruffleプロジェクトが作成できました。

以下のような構成になっているはずです。

.
├── contracts
├── migrations
├── test
└── truffle-config.js

次に以下のコマンドを実行してください。

truffle create contract Private
truffle create test test

このコマンドはContractファイルとテストファイルを作成するコマンドです。

上記コマンドを実行すると以下のような構成になっているはずです。

.
├── contracts
│   └── Private.sol
├── migrations
├── test
│   └── test.js
└── truffle-config.js

では以下のコードをPrivate.solに貼り付けてください。

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.17;

contract Private {
    string private name = "cardene";
    uint8 private age = 30;
    uint8 private income = 30;
    bytes32 private hash = "6a78e3e709447c0b671c573a4b562d6";
    address private walletAddress = 0xC255C784Ac559339A99F91a22E2063ff66B83354;
    bool private engineer = true;
    bool private creator = false;
}

上記貼り付けが完了したら以下のコマンドを実行してください。

truffle compile

上記実行すると以下のような出力が確認できます。

Compiling your contracts...
===========================
> Compiling ./contracts/Private.sololc-bin. Attempt #1
> Artifacts written to /Users/.../build/contracts
> Compiled successfully using:
   - solc: 0.8.17+commit.8df45f5f.Emscripten.clang

ディレクトリの構成を確認すると、buildディレクトリが作られているのが確認できます。

.
├── build
│   └── contracts
│       └── Private.json
├── contracts
│   └── Private.sol
├── migrations
├── test
│   └── test.js
└── truffle-config.js

では次の以下のコマンドを実行してください。

truffle develop

そうすると大量のアドレスが出力され、以下のようにプロンプトが表示されます。

truffle(develop)>

では次にmigrate用のファイルを作成していきましょう。

migrationsディレクトリの中に1_initial_private.jsというファイルを作成してください。

作成できたらそのファイルに以下を追記してください。

const Migrations = artifacts.require("Private");

module.exports = function (deployer) {
  deployer.deploy(Migrations);
};

追記できたら以下のコマンドを実行してください。

truffle(develop)> migrate

そうすると以下が出力されます。

Deploying 'Private'
   -------------------
   > transaction hash:    0xe55cecefb706e6dd3c544a587c2ea3d210d60ff5b8007c23ce9fa9ebc8a7a9da
   > Blocks: 0            Seconds: 0
   > contract address:    0xD4daa96901Ad1112Ec750c84C852D4F6BBc5e83E
   > block number:        1
   > block timestamp:     1671512753
   > account:             0x1F09Ca739ae733511eE0ba49B38Fa72a8557a9A6
   > balance:             99.999414407125
   > gas used:            173509 (0x2a5c5)
   > gas price:           3.375 gwei
   > value sent:          0 ETH
   > total cost:          0.000585592875 ETH

上記の出力のうち「contract address」の部分だけ使用します。

以下を実行してください。

truffle(develop)> data = "contract addressの値"
例)data = "0xD4daa96901Ad1112Ec750c84C852D4F6BBc5e83E"
'0xD4daa96901Ad1112Ec750c84C852D4F6BBc5e83E'

truffle(develop)> web3.eth.getStorageAt(data, 0)
'0x63617264656e650000000000000000000000000000000000000000000000000e'

何やら長い数字が出力されました。

これがストレージ内に保存されているスロットの値になります。

Nameの取得

1つ目の値は「name」だったので、本当に格納されているのか確認してみましょう。

truffle(develop)> name = "0x63617264656e650000000000000000000000000000000000000000000000000e"
'0x63617264656e650000000000000000000000000000000000000000000000000e'

truffle(develop)> web3.utils.toAscii(name)
'cardene\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0E'

先頭に「cardene」という文字がついていますね!

private変数にしていたのにこんなに簡単に値を取得できてしまいました。

cardene」の後ろの\x00は値がないためこのように格納されています。

ストレージの詳細でもお伝えしたように、スロットの一部にしか値を格納しなくても、ガス代が発生するのはこのためです。

Slot1以降の値を取得

ではせっかくなのでprivate変数に格納しているすべての値を出力してみましょう。

truffle(develop)> slot1 = web3.eth.getStorageAt(data, 1)
'0x0000000000000000000000000000000000000000000000000000000000001e1e'
truffle(develop)> slot2 = web3.eth.getStorageAt(data, 2)
'0x3661373865336537303934343763306236373163353733613462353632643600'
truffle(develop)> slot3 = web3.eth.getStorageAt(data, 3)
'0x000000000000000000000001c255c784ac559339a99f91a22e2063ff66b83354'

スロットごとに複数の値が格納されている部分もあります。

Slot1

slot1slot3がそうですね。

早速中身を確認していきましょう。

truffle(develop)> parseInt("0x1e", 16)
30

slot1の末尾の1e0xをつけた値を16進数から10進数に変換すると30と出力されました。

private変数に格納されている値と一致するため、このslot1は以下のようになっています。

0x0000000000000000000000000000000000000000000000000000000000003030

Slot2

slot2は以下の値なので、末尾の00を取り除いて中身を確認します。

0x3661373865336537303934343763306236373163353733613462353632643600

web3.utils.toAscii("0x36613738653365373039343437633062363731633537336134623536326436")
'6a78e3e709447c0b671c573a4b562d6'

指定した値がしっかり格納されていますね。

Slot3

まずはwalletaddressから取得します。

'0x000000000000000000000001c255c784ac559339a99f91a22e2063ff66b83354'

上記のslot3から「000000000000000000000001」を取り除いて取得します。

truffle(develop)>  web3.utils.numberToHex("0xc255c784ac559339a99f91a22e2063ff66b83354")
'0xc255c784ac559339a99f91a22e2063ff66b83354'

指定した通りの値が取得できましたね。

最後にengineercreatorbool値を取得しましょう。

先ほど取り除いた000000000000000000000001の末尾2文字ずつが、それぞれengineercreatorになります。

engineer = 01
creator = 00

この値を使用して以下を実行します。

truffle(develop)> parseInt("0x01", 16)
1
truffle(develop)> parseInt("0x00", 16)
0

1と0が出力され、それぞれ1がTrue、0がFalseなのでしっかり取得できていますね。

これですべてのprivate変数の値を取得できてしまいました。

いかがでしょうか?

例えprivate変数に値を格納しても、このように値が取得できてしまうのです。

対処法

ではこのprivate変数から値を見られないようにする方法はあるのでしょうか?

答えはNoで、どうやっても取得できてしまいます。

そのため、パスワードなど公開されると良くない機密データは変数に格納しないでください。

最後に

今回は「プライベートデータ」について解説してきました。

いかがだったでしょうか?

ポイント

  • プライベートデータが何かわかった!
  • プライベートデータにはアクセスできることと方法がわかった!
  • プライベートデータを扱う際の注意点を理解できた!

上記のどれかに当てはまっていたら嬉しいです!

もし何か質問などがあれば以下のTwitterなどから連絡ください!

普段はSolidityやブロックチェーン、Web3についての情報発信をしています。

Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!

参考

-Hacking, Smart Contract, Solidity, ブロックチェーン
-, ,