2022年10月7日,由于IAVL Merkle proof验证系统存在缺陷,Binance Bridge遭到黑客攻击,损失200万BNB(约6亿美元)。文章分析了攻击者如何生成恶意payload和proof,利用该漏洞窃取资金,并探讨了利用该漏洞耗尽token Hub合约的可能性。文章还提供了重现攻击概念验证(POC) 的方法,并总结了从这次事件中获得的教训。
2022年10月7日,由于IAVL Merkle proof 验证系统的一个缺陷,Binance Bridge 遭受黑客攻击,导致恶意黑客窃取了 200 万 BNB,相当于当时约 6 亿美元。
为了应对这次攻击,币安暂停了该系统几个小时,并引入了黑名单机制。
在本文中,我们将详细介绍恶意 payload 和 proof 是如何生成的,这使得黑客能够窃取 200 万 BNB。我们还将更进一步,制作一个可以用来耗尽 token Hub 合约的 payload。
本文由 Immunefi 智能合约分诊员 Omik 撰写。
Merkle 树
通过 Merkle 树结构,我们可以验证数据是否包含在数据库中,而无需通过哈希其值并将其与 Merkle 树的根哈希进行比较来公开该数据库中的所有数据。
Merkle 树中每个节点的数据是每个子节点连接值的哈希值。如果是叶节点,则数据是底层数据的哈希值。因此,P1 中的数据是 P3 和 P4 的哈希值(即 P1=h(P3, P4)),而 P3 中的数据是数据的哈希值(即 P3=h(data))。
为了能够根据 Merkle 树验证数据,我们必须首先知道我们要验证的数据的路径节点是什么。路径节点是位于从叶节点到 Merkle 树的根节点的路径上的节点。路径节点本质上是一个指导节点,它将引导我们的数据到达其根节点。
例如,假设我们在 P3 中有一些数据,并且我们想验证此数据是否包含在 Merkle 树中。首先,我们必须对我们的数据进行哈希处理以生成 P3 值。然后,我们必须确定 P3 的路径节点。在这种情况下,P3 的路径节点是 P4 和 P2。因此,仅提供 P4 和 P2 值,我们就可以到达根节点。P4 和 P2 的值可以被视为我们在 P3 中拥有的数据的 Merkle 证明。这些证明可以用来验证 P3 中的数据。
你可能会问:为什么 P1 不包含在路径节点中?这是因为我们可以通过连接 P3 和 P4 的值来自己生成 P1 值,这等于 P1 的值。
自然地,哈希中的一个字符差异会产生完全不同的输出。因此,连接节点以生成 P1 或任何非叶节点的顺序至关重要。并且有多种方法可以进行此排序。在本例中,我们将介绍 Merkle 树中的常见排序以及币安使用的排序,不幸的是,这导致了漏洞利用。
常见排序
Merkle 树中 Merkle 验证的常见连接排序是通过比较其子节点的值来完成的。如果子节点的值大于另一个子节点的值,则将其连接到具有较大值的子节点。
例如,节点 P1 具有 P3 和 P4 节点作为其子节点。要获得 P1 的值,我们必须连接 P3 和 P4 值,然后对连接后的值进行哈希处理。如果 P3 值大于 P4,则排序为 (P3, P4),如果 P4 值大于 P3,则排序为 (P4, P3)。
IAVL 验证中的排序
IAVL Merkle 树
IAVL 验证通过向其路径节点添加附加属性来执行其连接排序。这些属性是 right 属性和 left 属性。这样做是为了确定是否应将其哈希到节点的左侧或右侧。
例如,我们可以查看节点 P4,该节点的 left 属性设置为 null,right 属性设置为 P4。这意味着我们可以通过将子值与作为数据右侧的 P4 值进行哈希来获得值 P1,或 P1=H(P3, P4)。
此漏洞的根本原因是该节点的 right 属性未用于计算树的根哈希。由于 proof 是 用户可控的,因此黑客能够传递恶意的 right 属性(即恶意 payload 的哈希),并且该 proof 将被认为是有效的。
if !bytes.Equal(derivedRoot, lpath.Right) {
return nil, treeEnd, false, cmn.ErrorWrap(ErrInvalidRoot, "intermediate root hash %X doesn't match, got %X", lpath.Right, derivedRoot)
}
一旦我们理解了此问题的根本原因,我们如何设置 proofs 中的 right 属性,以及攻击者如何提出该 proof?
Emiliano Bonassi 在他的其中一条 推文 中解释说,攻击者使用的是第一次跨链交易的 proof。该 proof 取自此 交易。
让我们尝试解构 proof,使其更易于阅读(这受到了 Emiliano 的 脚本 的启发)
我们从上述交易中获取合法的 proof。
以其原始形式读取和修改 proof 对我们来说很困难。这就是为什么我们需要解组 proof。这将使我们能够在保持其对象类型的同时读取和修改数据。
解码 proof。
打印出合法 proof 的 rangeProof
。
哈希处理恶意 payload。
制作 package 序列。
向 proof 添加新的 leave。
向 proof 添加空的内部节点。
将新的恶意 leave 哈希到 proof 的 right 属性。
打印出恶意 proof。
编组恶意 proof。
验证恶意 proof,以确认该恶意 proof 有效。
相关函数 handlePackage()
只能由 relayer 调用。要成为 relayer,你必须向 relayer 合约存入 100 BNB,这正是黑客在 利用该合约 之前所做的。
由于 precompile 合约已经更新,我们无法通过在本地 fork 来测试 PoC。因此,我们将尝试在不 fork 主网的情况下在本地测试 PoC。为了验证我们的攻击是否有效,我们可以使用 IAVL 库中的验证函数。
我们可以通过制作 payload,然后将 payload 传递给 Go 代码来开始攻击,以便为恶意 payload 生成 proof。
可以通过调用 craftPayload()
并传递 token 地址、金额、接收者和交易的过期时间来制作 payload。要获取原生 token (BNB),请将 token 地址作为 address(0) 传递。
一旦我们有了 payload,我们就可以将 payload 设置为 main.go
文件中的 hackerPayloadOri
变量,并运行 main.go
。
如果 main.go
的输出为合法 proof 和恶意 proof 提供了相同的根哈希,并且在验证 proof 时没有返回错误,则意味着我们的 PoC 有效。
LEGIT ROOT HASH = e09159530585455058cf1785f411ea44230f39334e6e0f6a3c54dbf069df2b62
EVIL ROOT HASH = e09159530585455058cf1785f411ea44230f39334e6e0f6a3c54dbf069df2b62
error computing root hash? <nil>
error verifying proof? <nil>
Binance Bridge 黑客攻击是 2022 年最大的黑客攻击之一,它突出了与其他代码库集成时安全性的重要性。在本例中,我们了解到在计算 proof 的根哈希时,必须考虑 属性的两侧。
币安通过在属性两侧都设置时完全恢复交易来 修复了此问题。这就是为什么我们无法通过在本地 fork 主网来运行 POC。
所有 payload 组件的完整源代码如下。
main.go
craftPayload.sol
- 原文链接: medium.com/immunefi/hack...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!