04 月 18 日上午 08:58 开始, 一 DeFi 平台 Uniswap 被黑客利用重入漏洞实施了攻击。大约 24 小时后,于 04 月 19 日(昨天)上午 08:45,又一知名 DeFi 平台 Lendf.Me 被以类似的手段实施了攻击。

攻击的原理是:攻击者通过以太坊上少有 token 使用的 ERC777 的 transferFrom() 回调机制,利用在内部调用 _callTokensToSend() 回调函数劫持交易 , 并在真正更新余额的 _move() 函数之前进行恶意攻击。

在 Uniswap 的攻击案例中,攻击者利用此漏洞消耗尽 Uniswap ETH-imBTC 池约 1,278 个 ETH。而在 Lendf.Me 中,攻击者则是利用它来任意增加内部 imBTC 抵押金额,并通过从其他可用的 Lendf.Me 交易中借入 10 多种资产(总价值约 2,524 万美元)。

详细漏洞攻击细节,我们将在文章后面做详细介绍。

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777

Figure 1: ERC777 transferFrom()

罪恶之源:ERC777 标准

我们首先介绍下 ERC777 标准,该标准是通用 ERC20 标准改进后的一个稀有标准,不仅实现了部分功能扩展还保持着 ERC20 良好的兼容性,愿景是成为 ERC20 标准的有效继承者。

该标准扩展功能提供了“hook”机制, “通过调用“hook”函数来加强智能合约对 Token 转账的通知或控制”。但也正因为此功能的出现给注入代码攻击提供了可能性。

其中最关键的部分是,可以通过注册 from 的 tokensToSend() 来实行回调。我们从下面的代码片段可以看到,ERC777 标准中可以通过 getInterfaceImplementer()(1,054 行)获得攻击者的 tokensToSend() 接口,并在第 1,056 行调用此函数。而此处正是导致被劫持攻击的原因。

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777

Figure 2: ERC777-Compatible tokensToSend() Hijacking

如 2019 年 4 月 OpenZeppelin 发布的帖子以及 2019 年 7 月发布的漏洞利用演示中所述,攻击者可以自己定义函数 tokensToSend(),并通过 setInterfaceImplementer() 来设置合约中的 hook 函数。

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777

Figure 3: OpenZeppelin's Exploit Demo (Hook Setup)

之后攻击者就可以像传统 PC 上的 hook 函数一样,在 tokensToSend() 做任何事情。如下图所示,攻击者可以对同一笔交易进行多次交易。

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777

Figure 4: OpenZeppelin's Exploit Demo (Hook Function)

Uniswap 攻击分析

由于针对 ERC777 的攻击原理在前面已经描述过了,因此我们不再赘述。

就如此恶意交易在 Bloxy 中的截图所示 (hash:0x9cb1d93d6859883361e8c2f9941f13d6156a1e8daa0ebe801b5d0b5a612723c1), 函数的内部调用中又进行一次 tokenToEthSwapInput() 调用。这意味着攻击者可以先通过交易操纵汇率,然后再用另一笔 imBTC 以较低价格兑换更多的 ETH。

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777

Figure 5: Uniswap Hack

Lendf.Me 攻击分析

在 Uniswap 遭攻击约 24 小时后,又一 DeFi 平台 Lendf.Me 也遭到了黑客攻击。下面是其中一个攻击交易的截图。如图所示,supply() 函数中调用真实转账函数 transferFrom() 时,被 hook 的攻击者合约里嵌入了盗用 Lendf.Me 的 withdraw() 的提币操作。

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777

Figure 6: Lendf.Me Hack

在这个交易例子中,攻击者第一次 supply() 时确实向 Lendf.Me 存放了 289.99999999 个 imBTC,而在第二个 supply() 中,攻击者只存放 0.00000001 个 imBTC,但由于攻击者注册了 tokensToSend(),所以在执行 doTransferIn() -> IMBTC :: transferFrom()(第 1,583 行)时,调用了攻击者函数 tokensToSend(),攻击者函数通过调用 Lendf.Me 的 withdraw() 函数把 290 个 imBTC 直接全部提走。

需要注意的是,正常的业务逻辑应该是项目合约中的 Balance 会减去被攻击者提走的 290 个 imBTC,然而当 supply() 执行返回时,余额并未被重置,仍然为 290 imBTC (第 1,599 行)。攻击者就是通过控制修改 Lendf.Me 中攻击者的 imBTC 抵押金额,有了足够大的 imBTC 抵押,攻击就可以从各种流动交易对中借出所有可用的 10 多种资产(资产总值 25,236,849.44 美元)。

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777

Figure 7: Lendf.Me Hack Details

资产流向

攻击者 0x538359 共计从 Lendf.Me 获利 25,236,849.44 美元,其中各个 Token 分布如下:

**
**

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777

如上图,攻击者在获利之后,马上将各个 Token 转移至其关联账号 0xa9bf70 之中,之后攻击者数十次 通过 1inch.exchange, ParaSwap 等平台将其中比较抢手的 WETH, PAX, BUSD 等 Token 换成 ETH, DAI, BAT 代币,另外将其中的 TUSD, USDT 代币存入 Aave 借贷平台。至此为止,攻击者及其关联账号的余额如上所示。

修复建议

PeckShield 安全团队在此建议开发者,可以采用 “Checks-Effects-Interactions”方法来防止这类重入攻击。举个例子,Lendf.Me 的 supply() 里如果是先更新 token 余额,再调用 doTransferIn() 。这将会让攻击在 withdraw() 之后没有重置余额的可能性。

另一方面, ERC777 标准特性会不可避免地启用 hook 机制,因此我们需要检测并防止所有交易功能产生可以重入的风险。例如,如果 supply() 和 withdraw() 同时运行时加个互斥锁,那么攻击者就无法在 supply() 函数内部执行 withdraw() 操作。

最后并不能被忽视的一点是,我们需要认真思考下是否 ERC20 已经足够满足项目需求,我们真的需要 ERC777 么 ?

**
**

PS:此次黑客对 Lendf.Me 的攻击对 DeFi 社区来说无疑是一场灾难,在此建议广大 DeFi 开发者务必注意业务存在的系统性风控风险,应尽可能和第三方安全公司合作排查一切潜在的安全风险。更多详情请点击左下角“阅读原文”查看英文原版分析报告。

推荐阅读:

Fomo3D 式套利模式再现:“聪明”玩家狠狠地薅了一把羊毛!

暗夜临近,DApp 江湖上演现实版 " 狼人杀

资金盘 FairWin 漏洞系统详解:项目方可以撇开“作恶”嫌疑了?

EIDOS 挖矿众生相:“抬价、阻塞、攻击”让 EOS 主网不堪重负

2019 全球数字资产反洗钱(AML)研究报告

0x 协议漏洞原理剖析:恶意挂单可扰乱正常交易秩序

Uniswap 和 lendf.Me 遭攻击始末:愿 DeFi 世界里没有 ERC777