Gatekeeper One
题目
目标:修改entrant,即:成功调用enter(bytes8 _gateKey)
1 | // SPDX-License-Identifier: MIT |
分析
本道题要我们成功调用enter(bytes8 _gateKey)
,那么就需要通过三个修饰器gateOne
,gateTwo
和gateThree(_gateKey)
的检验。
gateOne
1 | modifier gateOne() { |
这里需要我们理解msg.sender和tx.origin的区别。意思是要求:我们不可以直接调用,需要写一个合约进行调用
gateTwo
1 | modifier gateTwo() { |
gasleft()
:是solidity的内置方法,方法返回此次交易我们还剩余多少gas。returns (uint256)
意思是,当程序执行到require(gasleft() % 8191 == 0)
这一步时,交易所剩的gas取模8191的值为0,那么我们有两个思路来通过这个修饰器:暴力尝试和通过debug来查看gas消耗情况
暴力尝试
gas的剩余量取模8191的结果为0,那么暴力尝试的可能性就只有8191种,那么我们可以做一个循环,从0gas开始到8191gas进行调用方法,总有一次可以通过此修饰器,很有意思的是,网络上有大佬优化了这个暴力尝试的过程(attack方法是网上大佬的思路,attack_是真正的暴力破解)
【网络版本】
1 | // 为什么这么写呢?因为有玩家通过测试,本关这里的gas大概在210次 |
【未知情况,真正暴力破解版本】
1 | for (uint256 i = 0; i < 8191; i++) { |
debug查看
目的是通过require(gasleft() % 8191 == 0);
的检验,那么我们可以debug到gasleft()操作码,它的操作码是GAS,GAS执行完之后剩余的gas就是 X % 8191
的值X。我们的需求就是X是8191的倍数。
solidity代码和交易中,执行同一个方法,操作码是不变的,那么我们代码执行到GAS操作码所消耗的gas就是定值。我们可以把这个定值找出来,然后加上8191的倍数,就可以通过此处的检验。
所以我们先设置一个较大的gas执行,看看执行到GAS操作码之后,消耗了多少gas
但是遇到了问题,我的remix页面debug不到GAS操作码。原因:remix需要Ethersccan测试网的API,且追踪一个已经验证的合约,才可以进入debug。如下图设置一下:
【1】gas设置为100000
71874=8191*8+6346
100000-6346=93654
65627=8191*8+99
93654-99=93555
65529=8191*8+1
93555-1=93554
通过
奇怪的是:我的代码从始至终都没有变,gas设置为100000。然后交易的时候gas设置为100000=>93654=>93555=>93544。最终在93544成功。我保证我每一次计算都是正确的,然而每次发送交易的时候,执行到GAS操作码的时候remain gas的值都会变化。只有93555=>93544的时候固定消耗gas才保持不变。
因此,我认为,就算同一个代码,发送交易的时候设置的gas不一致,那么程序执行的时候操作码消耗的gas也会产生变化?只有一些特定的不同gas值才会消耗相同的gas。即:gas上限的设置也会影响操作码的gas消耗,只是有些时候比较幸运不会产生变化【就比如本题的93555和93554固定消耗gas相同,而100000、93654、93555他们三者不同的gas上限设置也会导致固定消耗不同】
因此,我推断:交易的时候gas上限的设置也会影响实际gas的消耗情况,只是这个情况比较特殊,而有些gas就不会影响gas消耗。而这个特殊的情况就是我们本题需要找的值
而且,在call语句设置的gas似乎是没用的,程序只看交易的时候设置的gas上限。本题我的代码的call的gas一直是100000,然后交易的时候,gas交易上限不断改变,到了GAS操作码的时候remain gas也会产生变化
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
录制了一个debug解题的视频,里面阐述了做题过程和疑问
gateThree(_gateKey)
想要通过这个修饰器检验,我们得先了解一下solidity中类型截断、保留、补位的规则
1 | modifier gateThree(bytes8 _gateKey) { |
空讲很难理解,用一个例子来解释吧。传入的是bytes8,那么就假如传入的是:0x11112222aaaabbbb
- 第一个require:
uint32(uint64(_gateKey))
从低位截取,变成0xaaaabbbb。uint16(uint64(_gateKey))
从低位截取,变成0xbbbb。根据solidity的规则,uint32和uint16在比较的时候,较小的类型uint16会在高位补0至位数和较大类型uint32一致,即:0x0000bbbb和0xaaaabbbb比较。因此,我们的参数_gateKey
得是一个xxxxxxxx0000xxxx类型的数值。 - 第二个require:
uint64(_gateKey)
是保留所有位,而uint32(uint64(_gateKey))
保留低32位。两者低32位是一模一样的,要通过require,则需要高32位任意一位不一致即可,因为uint32(uint64(_gateKey))
高32位全部为0,那么我们传入的参数高32位至少需要一位数不为0。因此,我们的参数_gateKey
可以是一个FFFFFFFF0000xxxx类型的数值。 - 第三个require:目前我们确定参数
_gateKey
可以是一个FFFFFFFF0000xxxx类型的数值。那么uint32(uint64(_gateKey))
之后的结果就是0000xxxx。uint16(uint160(tx.origin))
是对钱包地址进行操作,数值类型从低位开始截取,即uint16(uint160(tx.origin))
的结果是我们钱包地址的后16位,就是后面4个数,对于我来说为97c6。那么这个_gateKey
就确定下来了,可以为:FFFFFFFF000097c6。(前8个F是可变的,0000是雷打不动的,97c6根据自己的钱包而定)
攻击合约
1 | contract Hack{ |
做题
两次获取题目,attack和attack_都试一次,看看是不是都可以暴力破解。答案:都成功
通过