02 月 15 日,bZx 团队在官方电报群上发出公告,称有黑客对 bZx 协议进行了漏洞攻击,且已暂停除了借贷外的其他功能。对于攻击细节,bZx 官方并没有进行详细披露。

PeckShield 安全人员主动跟进 bZx 攻击事件,发现这起事件是针对 DeFi 项目间共享可组合流动性的设计进行攻击,特别在有杠杆交易及借贷功能的 DeFi 项目里,该问题会更容易被利用。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

Figure 1: Five Arbitrage Steps in bZx Hack

漏洞的攻击细节如下:

此攻击事件发生在北京时间 2020-02-15 09:38:57 (块高度#9484688)。攻击者 的 transaction 信息可以在 etherscan 上查到。此攻击过程可以分为以下五个步骤:

第一步:闪贷获取可用资金

攻击者通过在部署的合约中调用了 dYdX 闪贷功能借入了 10,000 个 ETH。这部分是已知的 dYdX 的基本借贷功能,我们不做进一步解释。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

Figure 2: Flashloan Borrowing From dYdX

当第一步操作过后,如下表中攻击者资产,此时并没有收益:

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

第二步:囤积 WBTC 现货

通过第一步闪贷获得 ETH 后,攻击者将其中的 5,500 ETH 存入 Compound 作为抵押品,贷出 112 WBTC。这也是正常的 Compound 借贷操作,贷出的 WBTC 将在第四步中被抛售。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

Figure 3: WBTC Hoarding From Compound

在此步骤操作后,我们可以看到关于攻击者控制的资产发生了改变,但此时仍然没有获益:

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

第三步:杠杆拉盘 WBTC 价格

利用 bZx 的杠杆交易功能,做空 ETH 购入大量 WBTC。具体步骤是:攻击者存入 1,300 ETH 并调用 bZx 杠杆交易功能,即接口 mintWithEther(),在内部会继续调用接口 marginTradeFromDeposit()。接下来,攻击者将从 bZx 5 倍杠杆获得的 5,637.62 个 ETH,通过 KyberSwap 兑换成 51.345576 WBTC。请注意,此处做空 ETH 是借来的 5 倍。本次交易导致将 WETH / WBTC 的兑换率提高到 109.8 ,大约是正常兑换率(~38.5 WETH / WBTC)的 3 倍。

为了完成此交易,KyberSwap 基本上会查询其储备金并找到最优惠的汇率,最终只有 Uniswap 能提供这样的流通性,因此这个交易从本质上推动了 Uniswap 中 WBTC 价格上涨了 3 倍。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

Figure 4: Margin Pumping With bZx (and Kyber + Uniswap)

应该注意的是,这步操作在合约内部实现有个安全检查逻辑,但是实际上在交易之后并没有验证锁仓值。也就是说,当攻击发生时,此检查没有启用,我们在后面会有一节详细介绍此合约中的问题。

在这一步之后,我们注意到关于黑客控制的资产有以下改变。不过,在这一步之后仍然没有获利。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

第四步:抛售 WBTC 现货

在 Uniswap 中 WBTC 价格飙升后(价格为 61.4 WETH / WBTC),攻击者将第二步中通过 Compound 借的 112 WBTC 全部卖给 Uniswap 并返还了相应的 WETH。这次交易攻击者共计获得 6,871.41 个 ETH 的净额作为回报。在这一步之后,可以看到攻击者已经获得不少利润。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

Figure 5: WBTC Dumping With Uniswap

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

第五步:闪贷还款

攻击者从抛售的 112 WBTC 中获得的 6,871.41 个 ETH,将闪贷的 10,000 个 ETH 偿还给 dYdX,从而完成闪贷还款。

在这一步之后,我们重新计算了以下资产详情。结果显示,攻击者通过此次攻击获得 71 ETH,加上这两个锁仓:Compound (+5,500weth/-112WBTC)和 bZx (-4,337WETH/+51WBTC)。bZx 锁仓处于违约状态,Compound 的锁仓是有利可图的。显然,在攻击之后,攻击者就开始偿还 Compoud 债务(112BTC)以赎回抵押的 5,500 个 WETH。由于 bZx 锁仓已经处于违约状态,攻击者也不再感兴趣了。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

参考 1WBTC=38.5WETH (1WETH=0.025BTC)的平均市场价格,若攻击者以市场价格购入 112 WBTC 花费约需 4,300 个 ETH。此 112 WBTC 用以清偿 Compond 债务并取回抵押品 5,500 ETH,则最终攻击者总共获利为 71 WETH +5,500 WETH -4,300 ETH=1,271 ETH,合计大约 $355,880 (当前 ETH 价格 $280)。

硬核解析:bZx 可规避风险代码逻辑缺陷

通过前面攻击者在合约中实现的步骤可以看出,问题的核心原因是在第三步调用 marginTradeFromDeposit() 通过借贷的 1,300 ETH,加 5 倍杠杆来实现做空 ETH/WBTC 交易的,于是我们进一步审查合约代码,发现这是一个「可避免的套利机会」,但因为代码存在的逻辑错误造成可用于规避风险的代码逻辑没有生效。具体代码追踪如下:

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

首先是 marginTradeFromDeposit( ) 调用_borrowTokenAndUse( ),此处由于是以存入的资产作杠杆交易,第四个参数为 true (第 840 行)。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

在_borrowTokenAndUse( ) 里,当 amountIsADeposit 为 true 时,调用_getBorrowAmountAndRate( ) 并且将 borrowAmount 存入 sentAmounts1 (第 1,348 行)。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

在 1,355 行,sentAmounts6 被设置为 sentAmounts1 并且于第 1,370 行调用_borrowTokenAndUseFinal( )

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

经由 IBZx interface 进入 bZxContract 的 takeOrderFromiToken( ) 函数。

bZxContract 属于另一个合约 iTokens_loanOpeningFunctions 于是我们我们继续分析合约代码,在函数中发现有一个关键的逻辑判断:

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

在第 148 行,bZx 事实上尝试利用 oracle 合约的 shouldLiquidate( ) 检查这个杠杆交易的仓位是否健康。然而,因为第一个条件(第 146~147 行)已经为 true,则继续执行,而忽略了 shouldLiquidate() 的逻辑判断。

事实上,在合约 BZxOracle 的 shouldLiquidate( ) 中实现了对 getCurrentMarginAmount( ) <= loanOrder.maintenanceMarginAmount 判断,如果执行到 shouldLiquidate( ) 就可以有效避免这个攻击的发生。

PeckShield:硬核技术解析,bZx 协议遭黑客漏洞攻击始末

如前所述,这是一次很有意思的攻击,它结合了各种有趣的特性,如贷款、杠杆交易和拉高价格等。 之所以可能发生这种攻击,是因为当前项目共享可组合流动性的设计。特别是,5 倍杠杆交易允许用户以相对较低的成本借入大量代币,加上 DeFi 项目间共享的流动性,导致交易价格更容易被操控。

来源链接:mp.weixin.qq.com