Cover photo

简单CTF:BetToken,链上随机数

起源

今天早上有个群友发了这题目,简单的提供了一下思路。晚上来重新看了一下这题目,虽然是简单题目,考察的知识点也不多,主要考验是否能串联思路的能力。

关键代码如下。

post image

对未来准备踏入web3的朋友可以尝试分析一下这段代码的可攻击性和出问题的点。

补充信息:

post image

分析

1,这段代码最核心的问题其中 rand 值的生成,这种纯链上随机数生成是100%会被攻击的,无论用什么方式,都无法解决这个问题,只不过有不同的复杂度和攻击收益权衡利弊。

2,可以看到代码中 // not contract 部分,这里是一种有漏洞的EOA检测方式。合约地址中的特点是extcodesize不为0,但是在合约部署时,这个值会返回0。正确的检测方式应该为 tx.origin == msg.sender

3,lasttime在执行过一次bet后,会被设定为当前时间,也就是说,一个区块中只能执行一次这个调用。因为这个现在的存在,让这道题有了一个小小的难点。

4,在计算rand的时候,需要知道nonce值,但是合约中nonceprivate无法直接读取,这里其实是考察storageLayout的小知识点。

解题思路

第一问

首先是第一个点,rand值即然是链上生成的,再配合mod如果为12的时候收益最大,所以说我们只需要直接计算出 rank % 12 即可,将此值作为value,进行调用,那么结果一定是成功。

可以写出如下代码:

post image

这段代码需要放在我们的地址合约中,因此使用address(this)

第二问

因为需要在创建合约的时候进行调用,所以我们需要在constructor函数中编写攻击代码,代码逻辑比较简单,调用计算后调用bet即可。不过这里需要先领取一下空投,所以我们判断一下balance为0时,调用airdrop函数。

post image

这里和上一部分的代码结合一下,用作于地址合约的代码。不完整代码如下。

post image

第三问

因为一个区块只能执行一次合约,所以如果我们在合约中不断创建新合约去调用,那么地址将会一直变换,我们一直无法达到胜利条件:连胜20次。

所以我们需要用一个方法,不断的在一个地址上调用constructor函数。这需要两个功能配合。create2 + selfdestruct

我们修改一下CAddress合约的constructor函数。

post image

其中IExp是我们的主合约,因为create2的时候,不同的参数会影响initcode,所以需要传递相同的参数才能保证selfdestruct后,create2的合约地址在相同的位置,也就是说这里的bet地址其实可以作为参数传入,不过为了方便,我这里直接使用接口调用Exp合约。

Exp合约

现在要编写负责主控的Exp合约,他负责create2创建地址合约,所以他的功能其实并没有多少代码非常简单。

post image

其中代码负责两部分:

constructor合约计算了我们生成的地址合约的地址,并且保存了一下betToken的地址

attack函数用于创建地址合约,在较高的solidity版本中,已经支持了new Contract{salt:salt}()通过传递salt值来进行确定性生成。

第四问

到最后,我们只需要调用exp.attack(nonce)即可完成攻击。这里的问题是如何获得private变量,其实这个也很简单,只需要通过getStorageAt函数即可得到指定slot上的内容。关于存储布局的相关知识这里不再展开,有兴趣的朋友可以留言或者私信我。

源代码

https://github.com/nishuzumi/ctf_betToken

小插曲

最早的时候我是用foundry进行测试开发的,后来发现怎么都不行,经过我长时间的思考后,我坚定的认为是foundry的bug,于是后面换成了hardhat进行测试开发,果然成功了。