# 简单CTF：BetToken,链上随机数

By [Box ⛩️](https://paragraph.com/@box) · 2022-06-25

---

起源
==

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

关键代码如下。

![](https://storage.googleapis.com/papyrus_images/e8ecdee27f008e344dd4d2aca18cebfb75bd72819c64e64ca8f9f3b74ae8593e.png)

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

补充信息：

![](https://storage.googleapis.com/papyrus_images/4dd253bc70a0356b20f22800322c38d5533bba05a7de0267b41c54aa4e91d907.png)

分析
--

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

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

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

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

解题思路
----

### 第一问

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

可以写出如下代码：

![](https://storage.googleapis.com/papyrus_images/b1100a207acceb2e02b35039239f78e2bb28f9b628e4de9a97b7e2ba4d54c8c5.png)

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

### 第二问

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

![](https://storage.googleapis.com/papyrus_images/842e87a245713c07ea551ca315c27ef9fb1d0317259008e88972dfff9886f229.png)

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

![](https://storage.googleapis.com/papyrus_images/a07dd03768657533191e92ce99610c8cd3f1fd2703d35a4b32baadea9d1451b1.png)

### 第三问

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

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

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

![](https://storage.googleapis.com/papyrus_images/cb07cb3ac9746baa1bf5f6caa2ee50d79ce57cb71c0bacaec93039be59197089.png)

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

#### Exp合约

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

![](https://storage.googleapis.com/papyrus_images/dc0b601fc007d190343bc364de99c7eecf3bc88ec140c6ce085788e29449b4aa.png)

其中代码负责两部分：

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

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

### 第四问

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

源代码
---

[https://github.com/nishuzumi/ctf\_betToken](https://github.com/nishuzumi/ctf_betToken)

小插曲
---

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

---

*Originally published on [Box ⛩️](https://paragraph.com/@box/ctf-bettoken)*
