# 以太坊智能合约逆向分析与实战：（4）复刻黑客的恶意合约


By [Hackit](https://paragraph.com/@hackbot) · 2022-09-05

---

据路边社消息，前几天一个刚出校门的“Web3 创业者”发布了一个彩票项目，通过付费 mint NFT 的方式及 “实时开奖” 的玩法，用户在mint NFT 时有 1/2 的概率获得 1.9 倍的费用返还。（中奖率高达 50% ？！实际数学期望E(x)=0.5\*1.9 = 0.95，所以说久赌必输啊兄弟们！）

可能项目方对链上项目的运行机制和潜在风险不够了解，导致项目刚一发布便惨遭黑客攻击， 0.6 eth 的奖池被无情撸走，只好宣布创业失败。我们在严正谴责黑客的无良行径之余，不禁会想：这个项目究竟是哪里出问题了呢？今天我们通过阅读项目源码和反编译黑客的恶意合约，来分析此次攻击事件：

一、漏洞成因
------

![图1](https://storage.googleapis.com/papyrus_images/3930e85e9cc4fab2f61dcc3512920b44c5c2baa02bf11ebe64a231ff3d03add3.png)

图1

从以上源码中我们可以发现，开发者使用了“**当前区块难度** （block.difficulty）”和“**当前区块时间戳**（block.timestamp）”作为随机数种子，并将生成的随机数对 2 进行取模运算，如果结果是偶数则表明未中奖，是奇数则为中奖。

“区块难度”和“时间戳”是两个重要的区块属性，他们的数值很难被人为控制，而且一旦生成便无法修改。乍一看确实很适合当作随机数种子。然而开发者犯下的错误在于：他在不恰当的场景下（即时开奖）使用了这种生成方法。由于区块属性是公开可读的，攻击者完全有可能在 mint NFT 的前一刻，读取这两个区块属性并计算随机数，如果运算结果不符合中奖条件，则不发起 mint 或让 mint 中止；如果随机数结果符合中奖条件，则立即发起 mint 甚至在同一区块内大量 mint ，最终抽空奖池。漏洞利用的方法很简单，但我们这次并不直接动手写攻击工具，而是准备从逆向的角度来分析，看看这位黑客的操作是否和我们推测的一样。

二、逆向分析
------

这是黑客在链上的[攻击记录](https://etherscan.io/tx/0x804ff3801542bff435a5d733f4d8a93a535d73d0de0f843fd979756a7eab26af)，黑客从布署合约到合约充值，再发起攻击然后卷款跑路，一气呵成。[该笔tx](https://etherscan.io/tx/0x804ff3801542bff435a5d733f4d8a93a535d73d0de0f843fd979756a7eab26af)在一个区块内 mint 了 50 个 NFT 之多，而且都是中奖的。这就意味着攻击者不仅没为这些 NFT 付费，还获取了额外的奖金。那么，这个邪恶的合约都干了点什么呢？

![图2](https://storage.googleapis.com/papyrus_images/d8f977857a4d1d16239f37b70f3f4e7e528f11219c2e93d11f533f0abb77da8b.png)

图2

很遗憾，合约并没有开源（当然不会开源……）我们只好通过逆向的方式去研究了。这次我们使用一款叫做 [**Panoramix decompiler**](https://github.com/palkeo/panoramix) 的工具，它也被很多区块浏览器上所集成，但我选择在本地运行，因为更方便一些。

（ 如果你没有安装的话： `$ pip install panoramix-decompiler` ）

然后指定合约所在的链的 RPC 。我们分析的合约在 ETH 链上，所以使用以下 RPC：

`$ export WEB3_PROVIDER_URI = https://rpc.ankr.com/eth`

再给出合约地址，软件就开始自动下载二进制文件并开始反编译了：

`$ panoramix 0x880df6cc30bb7934d065498ed9163a6e3b5aa67d`

过了一会就能看到编译结果。此次反编译的结果比较清楚明了，没有什么需要深入分析的。只有个别没有识别出哈希对应的函数签名，也可以结合 https: //www. 4byte.directory/ 、https: //sig. eth. samczsun.c om/ 等工具来查询。为了节省篇幅。在这里就只画些重点，大家自行阅读吧:

![图3](https://storage.googleapis.com/papyrus_images/1437db33605064bf8912ff6d0bc52812fe78e87840fd7db2cef373c60eeece32.png)

图3

由图中可以发现，黑客的操作手法和我们推测的差不多，计算随机数结果、批量循环 mint NFT 这些功能该有的都有，还写了转移 NFT 的方法。需要注意的是，按照 ERC721 标准的要求，如果用合约来调用 NFT 的 \_safemint() 方法，该合约要实现 onERC721Received() 才可以成功mint。

三、代码实现
------

原理和逻辑既然已经搞清楚，那就只剩下编码实现了。这次我们使用 **Foundry** 来进行编写和测试。这是一款用 Rust 编写的构建工具，与其他基于 js 的构建工具相比速度更快一些。

Foundry 由三个不同的命令行工具（CLI）组成，包括 **forge**（用于构建、测试、部署和验证合约），**cast**（用于进行RPC调用和合约交互），和 **anvil**（用于运行本地EVM区块链节点）。详情请戳[官方文档](https://book.getfoundry.sh/)。

如果你还没有安装 Foundry ,需要：

`$ curl -L https://foundry.paradigm.xyz | bash`

`$ foundryup`

如果已经安装，就直接新建项目：

`$ forge init luckyHack && cd luckyHack`

删掉项目示例合约、测试合约：

`$rm src/Contract.sol`

`$rm test/Contract.sol`

在 src/下新建 **luckyTiger.sol** (作为测试目标)、**luckyHack.sol** (作为攻击工具)。

其中luckyTiger.sol 是照抄项目方的合约，luckyHack.sol 由我们自己编写，核心代码如图：

![图4](https://storage.googleapis.com/papyrus_images/5e52041ecc51da4f9138f69740a18eff405909defcda02201fe08b7a7167ad34.png)

图4

四、实战演练
------

这次测试正好把 Foundry的三大件（forge，cast，anvil）用上一遍，十分方便。把我们先回到项目根目录，用anvil启动本地节点：

`$ anvil`

![图5](https://storage.googleapis.com/papyrus_images/09a2234f233621a8a7a45281b283c2087205bed8f6d88d32f98a590ec025821d.png)

图5

本地节点会给出 10 个地址及私钥用于开发测试。这里我们假设地址0 是项目方、地址1是黑客，分别用二者的私钥来部署 NFT 项目和攻击合约。

我们开启另一个终端，用 forge 编译和部署合约。先后编译测试目标 luckyTiger.sol、攻击工具luckyHack.sol：

`$ forge build`

编译通过之后我们开始部署，luckyTiger.sol 的构造函数需要传递 tokenURI 等参数，记得用 --constructor-args ：

`$ forge create src/luckyTiger.sol:luckytiger --private-key=测试私钥0 --constructor-args "AAA" "BBB"`

`$ forge create src/luckyHack.sol:luckyHack --private-key=测试私钥1`

一切准备完毕后，测试环境各参数如下：

项目方地址：0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

NFT地址：0x5FbDB2315678afecb367f032d93F642f64180aa3

黑客地址：0x70997970C51812dc3A010C7d01b50e0d17dc79C8

攻击合约地址：0x8464135c8F25Da09e49BC8782676a84730C318bC

**测试流程**：

\*\*1、\*\*项目方调用 **addBonusPool()** 向合约奖池注资，彩票项目开始运行。本例设置为 5 eth。我们使用 cast 与链上合约进行交互：

`$ cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 “addBonusPool()” --value 5ether --private-key=测试私钥0`

查看一下合约余额,返回5000000000000000000：

`$ cast balance 0x5FbDB2315678afecb367f032d93F642f64180aa3`

\*\*2、\*\*攻击者调用 **sendEther()** 向攻击合约注资，作为mint NFT 的成本。本例设置为3 eth:

`cast send 0x8464135c8F25Da09e49BC8782676a84730C318bC “sendEther()” --value 3ether --private-key=测试私钥1`

查看一下合约余额,返回3000000000000000000：

`$ cast balance 0x5FbDB2315678afecb367f032d93F642f64180aa3`

\*\*3、\*\*攻击者先通过调用攻击合约的 **getRandom()**，查询当前区块参数是否符合中奖条件。(uint256)约定了返回值的格式，如果不写，会默认返回一长串十六进制字符：

`$ cast call 0x8464135c8F25Da09e49BC8782676a84730C318bC "getRandom()(uint256)"`

\*\*4、\*\*如果返回值为 1， 说明当前区块参数符合中奖条件。此时攻击者调用 **hack(uint256)** 向NFT项目发起攻击，由于是测试，我们先搞它 50 次 ：

`$ cast send 0x8464135c8F25Da09e49BC8782676a84730C318bC "hack(uint256)" "50" --gas-limit 5000000 --private-key=测试私钥1`

如此往复几次，我们查看下奖池余额和攻击合约余额：

奖池余额 ：4500000000000000000 （减少了0.5 eth）

攻击合约余额：3450000000000000000 （增加了0.45 eth）

什么？你非要问之间差的0.05 eth到哪去了？NFT 里面写的有啊：

`$ cast balance 0x511604E18d63D32ac2605B5f0aF0cF580D21FA49`

你看，在项目方的钱包里……

**补充说明：**

在以上实战演练中，为了研究方便和保证测试的全面性，我们搭建了整个测试环境。其实在我们日常测试时，完全不必大费周章地设置整个环境，可以利用 Foundry 这个便利的功能，将主网进行分叉，创造一个真实的链上场景进行演练：

`$ anvil --fork-url https://rpc.ankr.com/eth --fork-block-number 15403398`

![图6](https://storage.googleapis.com/papyrus_images/2ca84d29f51ec3cbfd5ff5ffade74977cb91585a05c4941514612a92fd3c73a6.png)

图6

如上图所示，我们从以太主网的区块高度 `15403398` 分叉出了本地测试网，之后的操作与上面一样，但我们只需要专注编写攻击合约就可以了。

**相关代码**

[

GitHub - 0xNezha/luckyHack: 针对一起攻击事件的分析及复现
------------------------------------------

针对一起攻击事件的分析及复现. Contribute to 0xNezha/luckyHack development by creating an account on GitHub.

https://github.com

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

](https://github.com/0xNezha/luckyHack)

**关于作者**

---

*Originally published on [Hackit](https://paragraph.com/@hackbot/4)*
