Hackit

Posted on Sep 05, 2022Read on Mirror.xyz

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

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

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

一、漏洞成因

图1

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

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

二、逆向分析

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

图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

由图中可以发现,黑客的操作手法和我们推测的差不多,计算随机数结果、批量循环 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

四、实战演练

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

$ anvil

图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

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

相关代码

https://github.com/0xNezha/luckyHack

关于作者

https://twitter.com/0xNezha