CertiK:HopeLend 遭借贷攻击事件分析
攻击的漏洞在于在销毁存款凭证时错误的整数除法问题,但黑客没想到“螳螂捕蝉,黄雀在后”。
作者: CertiK
北京时间 2023 年 10 月 18 日 19:48:59,Hope.money 的借贷池受到了基于闪电贷实施的攻击。
Hope.money 包括了借贷平台 HopeLend、去中心化交易所 HopeSwap、稳定币 HOPE 和治理代币 LT,为用户提供去中心化金融全栈服务。
本次攻击涉及的协议是 HopeLend,是一个去中心化借贷平台,用户可以为协议提供流动性或者超额抵押借贷赚取收益。
事件始末
在 HopeLend 的代码实现中,借贷池存在可被利用的漏洞,由于在销毁存款凭证时,出现了错误的整数除法问题,导致小数点部分被截断,实现了销毁比预期少的凭证数量,获得和预期一致的价值代币。
攻击者利用这个缺陷掏空了 Hope.money 上存在资金的多种借贷池。
其中 hEthWbtc 借贷池于 73 天前部署,但是其中没有资金,因此黑客通过往该借贷池注入大量资金来达到让贴现率戏剧性地暴涨,从而实现了在一个区块交易内快速掏空了所有其他借贷池子的资金。
更戏剧性的是,实现利用的黑客没有获得漏洞利用的资金,他的攻击交易被抢跑者发现,抢跑者模仿其攻击行为并成功抢走了所有攻击收益资金(527 ETH),最终有 50% 的攻击收益资金 (263 ETH) 被抢跑者用于贿赂打包区块的矿工(payload)。
发现漏洞的初始黑客,在区块 18377039 创建了攻击合约,并在区块 18377042 进行了攻击合约的调用,此时抢跑者监控到内存池里的交易,并将其攻击合约进行模拟,作为抢跑合约的输入,在同样的 18377042 区块进行利用,而初始黑客在 18377042 区块的交易由于排序在抢跑者后面,从而执行失败了。
资金去向
抢跑者在获得收益后的一小时,将资金转移到:0x9a9122Ef3C4B33cAe7902EDFCD5F5a486792Bc3A
在 10 月 20 日 13:30:23,疑似官方团队联系了该地址,允许抢跑者留下 26 ETH(10% 的获利)作为奖励,并得到抢跑者的答复。
最终资金在沟通一个小时后,转移到 Gnosis Safe 的多签金库中。
下面我们将展示真实的漏洞和黑客进行利用的细节。
前置信息
HopeLend 的借贷协议实现 Fork 自 Aave,因此涉及到漏洞相关的核心业务逻辑参考自 Aave 的白皮书。
0x00 存款和借贷
Aave 是一个纯粹的 DeFi,借贷业务通过流动性池实现,用户在 Aave 存款提供流动性时,期望获得借贷所获得的收益。
贷款收益并不会完全分配给用户,有少部分利息收入会被计入风险储备金,此部分比例较少,大部分贷款收益会分发给提供流动性的用户。
在 Aave 中进行存款放贷时,Aave 是通过贴现的方式,将不同时间点的存款数量转化成流动性池初始时间点的存款数量份额,因此每数量份额的底层资产对应的本息和,就可以直接用 amount(份额) * index(贴现率)算出来,大大方便了计算和理解。
可以理解为类似购买基金的过程,基金的初始净值是 1,用户投入 100 块钱获得 100 的份额,假设经过一段时间获得收益,净值变成 1.03,此时用户再次投入 100 块钱,获得的份额是 97,用户的总份额是 197。
这其实是将该资产按照 index(净值)进行贴现处理。之所以这么处理,是因为用户实际的本息和是用 balance 去乘以当前的 index。当第二次存款的时候,用户正确的本息和应该是 100 * 1.03 + 100 = 203,如果不做贴现处理,第二次用户存入 100 后的本息和就变成了 (100+100) * 1.03 = 206,是错误的,如果进行了贴现,本息和就变成了(100 + 100 / 1.03) * 1.03 = 103 + 100 = 203,203 的结果是正确的。
攻击过程
0x25126......403907(hETHWBTC pool)
0x5a63e......844e74(攻击合约 - 套现)
借出初始闪电贷资金,进行质押
攻击者首先从 Aave 闪电贷借入 2300WBTC,将其中 2000 枚 WBTC 质押(deposit)到 HopeLend,资金将会被转移至 HopeLend 的 hEthWbtc 合约(0x251…907),同时获取相应的 2000 枚 hETHWBTC。
借助空借贷池操纵初始贴现率(liquidityIndex)
从 HopeLend 进行闪电贷借入 2000 枚 WBTC。
目前的价值是 1 hETHWBTC = 1 WBTC。
按照正常的存取 ETHWBTC 换回 WBTC 的操作,是不会影响兑换比例(只有当收入了利息才会影响兑换比例,1 hETHWBTC 会获得更多 WBTC)。
此时黑客开始通过一系列复杂操作,操纵贴现率:
- 黑客直接又将得到的 2000 枚 WBTC 通过直接转账(transfer)的方式转移资金至 HopeLend 的 hEthWbtc 合约 ( 0x251…907),这一步并不是还贷。
- 黑客随后取出(withdraw)之前步骤 1 中质押(deposit)的绝大部分 WBTC(1999.999…),所以上一步才需要转回 WBTC 以补充池子内的资产。
- 最后黑客手上仅保留最小单位 (1e-8) 的 hEthWbtc,这里不能完全提完,是因为需要留下一点点,作为计算贴现率(liquidityIndex)时,会基于现有的加上新增的,如果清零的话,导致贴现率(liquidityIndex)变成 0,就不能让池子里的比例失衡。
- 把上一步销毁掉绝大部分 hEthWbtc 换回来的 wBTC,加上之前闪电贷剩余的 wBTC,归还向 HopeLend 池子借出的闪电贷,共支付 2001.8 枚 WBTC( 其中包含利息 1.8 枚 wBTC)。
- 上面的过程销毁掉大部分的 hEthWbtc,只留下 1 最小单位(1e-8)的 hEthWbtc 在黑客账户,这样一来 hETHWBTC 总量就减少了,而借贷池里却有 2001.8 枚 wBTC,此时的贴现率(liquidityIndex)达到惊人的 126,000,000。
这里涉及到一个知识,存款用户的利息根本上来自池中流动性的增长,借贷池会根据存款率和使用率,动态调节借款和存款利率。
此处,当池子从闪电贷利息 (1.8WBTC) 获得额外流动性时,百分之七十 (126,000,000) 被计入 liquidityIndex(liquidityIndex),这个数值用来计算每单位存款 (hEthWbt) 的贴现价值。
由于池子在黑客操作前为空,还款后 totalLiquidity 仅为 1,amount 是 126000000,初始 liquidityIndex 为 1,得出结果为 126000001。
继续放大贴现率
黑客继续从 HopeLend 进行闪电贷借入 2000 枚 WBTC,并每次归还额外的 1.8 枚 WBTC,使得每次 LiquidityIndex 得以累加 126,000,000。
黑客重复执行了 60 次该过程,最终 liquidityIndex 达到 7,560,000,001,攻击者持有的 1 个最小单位的 hEthWBTC 贴现价值可达 75.6WBTC(约为 214 万美元)。
这也就使得黑客操控了 hEthWBTC,使之价值失真。
掏空其他存在资金的借贷池,形成收益
攻击者接着将 1 个最小单位的 hEthWBTC 为抵押,从 HopeLend 的其他五个代币池借出了大量资产。
包括:
- 175.4 - WETH
- 145,522.220985 - USDT
- 123,406.134999 - USDC
- 844,282.284002229528476039 - HOPE
- 220,617.821736563540747967 - stHOPE
这些代币被作为收益通过 Uniswap 兑换为 WBTC 和 WETH,扣除各种费用后,最终黑客获利为约 263 枚 WETH(除去贿赂 payload 的 263.9 枚 WETH)。
为什么黑客可以从其他池子借走大量资金:
借款或取走存款时,借贷合约会检验用户的抵押资产状况,确保借出不超过抵押。
由于之前贴现率已被黑客操纵且贴现率会以 normalizedIncome 乘数计入抵押价值计算,其手中的一单位 hEthWBTC 抵押价值高达 75.6WBTC。
每一次从其他池借款,黑客都轻松通过了抵押资产校验。
此时, 攻击者总共在 HopeLend 投入了 2000+1.8*60 枚 WBTC 用于操纵 liquidityIndex,只留存了 1 单位的 hEtthWBTC。
利用关键漏洞点(整数除法错误)套现
为了取出之前的投入 wBTC,攻击者部署了另一个攻击合约:0x5a63e......844e74,并调用其中的 withdrawAllBtc() 方法:
漏洞过程如下:
- 首先存入 151.20000002 枚 wBTC,根据当前的 liquidityIndex(1 最小单位 hEthWBTC=75.6wBTC),攻击者获得 2 个最小单位的 hEthWBTC。
- 取出(withdraw)113.4 个 wBTC,反算出其对应的 hEthWBTC 份额,对 hEthWBTC 进行 burn 操作。
- 113.4 个 wBTC 需要销毁 1.9999999998 最小单位的的 hEthWBTC,但是由于 div 函数精度问题,仅一个最小单位的 hEthWBTC 被销毁,因此变成可被利用的漏洞,黑客仍可保留 1 个最小单位的 hEthWBTC。
关键漏洞
hEthWBTC 的 burn 方法调用了高精度除法 rayDiv。
此处:
a=11340000000(打算取出的 WBTC)
b=7560000001000000000000000009655610336(贴现率)
虽然 (a*1e27+b/2)/b = 1.9999999998,solidity 自带的 div 方法截断返回 1, 相当于 11340000000 / 7560000001 除法后小数位被截断了。
0x5a63( 攻击合约 - 套现 ) 继续存入 75.60000001WBTC 恰好又获得 1 个最小单位 hEthWBTC,从而继续持有 2 个最小单位 hEthWBTC。
如此循环取出 113.40000000wBTC,存入 75.60000001wBTC 的操作,每次攻击者可以凭空获取 37.8 枚 wBTC。
循环 58 次后,攻击者取出了所有前期投入的 wBTC ,并顺利归还 Aave 的闪电贷。
结论
由于 hEthWBTC 借贷池未被初始化,攻击者得以轻易操纵 liquidityIndex,将其增至极大,提现率作为除数极大放大后,由于整数除法的截断误差,使得取出之前的投入更容易在一个区块内实现。
在运转良好的借贷池中,由于池中已有流动性,不容易因为少量的贷款利息增加而极大增加贴现率。
免责声明:文章中的所有内容仅代表作者的观点,与本平台无关。用户不应以本文作为投资决策的参考。