RocketPool 和 Lido 抢跑漏洞修复审查

  • Immunefi
  • 发布于 2023-04-04 23:39
  • 阅读 12

白帽黑客Dmitri Tsumak发现RocketPool和Lido Finance的漏洞,该漏洞允许节点运营者窃取用户存款。Dmitri Tsumak 通过 Immunefi 向 RocketPool 和 Lido 提交漏洞报告,并获得了总计 20 万美元的漏洞赏金。文章分析了漏洞的原理,以及以太坊POS机制下,第三方Staking池的攻击向量。

概要

Whitehat Dmitri Tsumak,StakeWise 的创始人,在 10 月 5 日提交了一个关键的漏洞,该漏洞影响了 RocketPool 和 Lido Finance 的 staking 平台。该漏洞允许节点运营商窃取用户的存款。对于 whitehat 的奖励是来自两个项目的关键 bug 的最高金额(每个项目 100,000 美元),总奖励为 200,000 美元。

该 whitehat 首先披露了 RocketPool 中的漏洞,但在检查了另一个 staking 服务后,他注意到 Lido Finance 中也存在同样的问题,并通过 Immunefi 提交了另一个 bug 报告。智能合约中的 Bug 通常是系统性的。一个影响一个项目的 Bug 在很多情况下会影响其他项目。

披露给 RocketPool 的漏洞也标志着有史以来最快的 bug 赏金支付给黑客之一

时间线

\* 10 月 5 日凌晨 3:27 GMT+2 报告创建

\* 10 月 5 日凌晨 3:35 GMT+2 Immunefi 将报告升级到 RocketPool 团队

\* 10 月 5 日凌晨 3:39 GMT+2 漏洞确认,黑客同意支付报酬

\* 10 月 5 日凌晨 3:45 GMT+2 黑客获得报酬

\* 10 月 5 日凌晨 4:11 GMT+2 Immunefi 获得报酬

Lido 的反应也很快。支付给黑客的报酬于 10 月 9 日发出,因为它必须通过 DAO 批准。

漏洞分析

RocketPool 和 Lido 都是 Ethereum 2.0 的第三方 staking 池。要了解第三方 staking 池的工作原理,我们首先需要讨论 Ethereum 2.0。这是对当前 Ethereum 公共主网的重大升级,旨在通过提高其性能来加速 Ethereum 的使用和采用。新版本的目标不仅仅是以规模处理交易,还包括其他设计目标,例如提高安全性和可扩展性。

Ethereum 2.0 将将其共识模型从当前的 Proof of Work (PoW) 更改为 Proof of Stake (PoS)。PoW 和 PoS 之间的主要区别在于没有矿工。相反,参与者(称为验证者)可以提出新的区块并验证来自另一个验证者的区块,但前提是他们通过将资金存入官方存款合约来 staking 32 ETH。只有这样,验证者才能运行客户端节点来参与 PoS。

目前,staking 相当昂贵。(在撰写本文时,Ether 的价格为 3.5k 美元)。这意味着你需要大约 11.2 万美元才能 staking 32 ETH,而 2019 年首次部署存款合约和启动 Beacon Chain 时,大约需要 6 千美元。

为了克服高昂的入场费问题,创建了第三方 staking 池。它们通过汇集用户资金并在 Beacon Chain 上创建一个可以赚取 staking 奖励的验证者来运作。Lido 和 RocketPool 在实现这一目标的确切方法上有所不同,但其基本思想是相似的。

存款合约

要了解攻击向量,我们首先需要了解存款合约的 deposit 函数及其四个参数。

- Pubkey:Ethereum 2.0 验证者的 BLS 公钥

- Withdrawal credentials:Ethereum 地址的 BLS 公钥,所有提款都将转到该地址。它可以是验证者或分片的另一个地址

- Signature:使用用于创建 Pubkey 的 BLS 私钥签名的 pubkey 和提款

- Deposit data root:以上所有三个参数组合在一个结构中,包含要发送的金额并签名。该签名的哈希用于数据保护,以防止格式错误的 calldata

攻击向量

在将恶意节点运营商包含在 LidoRocketPool 中之后,他们将需要为创建的每个验证者密钥生成额外的存款数据,其中包含提款凭证和 ETH 中的最小存款额(取决于项目)。接下来的步骤对于漏洞利用的发生至关重要。

攻击者等待从池中提交 32 ETH 到存款合约,以用于最初批准的验证者之一。发生这种情况时,恶意节点运营商会使用先前准备好的 deposit data 来抢跑存款,deposit data 包含相同的 validator bls key 的最小所需 deposit value,方法是在存款合约上调用 deposit() 函数。

在 RocketPool 上不需要抢跑的情况下(首先在存款合约上调用 deposit(),然后从 RocketPoolMiniPoolDelegate.sol 调用 stake()),以上步骤允许将池的提款凭证替换为攻击者的提款凭证,并且大部分 staking 的 ETH 来自池用户,而不是攻击者自己的。

发生这种情况是因为信标链客户端的实现方式。

def process_deposit(state: BeaconState, deposit: Deposit) -> None:
  # Verify the Merkle branch
  assert is_valid_merkle_branch(
    leaf=hash_tree_root(deposit.data),
    branch=deposit.proof,
    depth=DEPOSIT_CONTRACT_TREE_DEPTH + 1, # Add 1 for the List length mix-in
    index=state.eth1_deposit_index,
    root=state.eth1_data.deposit_root,
)  # Deposits must be processed in order
  state.eth1_deposit_index += 1  pubkey = deposit.data.pubkey
  amount = deposit.data.amount
  validator_pubkeys = [v.pubkey for v in state.validators]
  if pubkey not in validator_pubkeys:
    # Verify the deposit signature (proof of possession) which is not checked by the deposit contract
    deposit_message = DepositMessage(
      pubkey=deposit.data.pubkey,
      withdrawal_credentials=deposit.data.withdrawal_credentials,
      amount=deposit.data.amount,
)    domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks
    signing_root = compute_signing_root(deposit_message, domain)
    if not bls.Verify(pubkey, signing_root, deposit.data.signature):
      return   # Add validator and balance entries
   state.validators.append(get_validator_from_deposit(state, deposit))
   state.balances.append(amount)else:   # Increase balance by deposit amount
   index = ValidatorIndex(validator_pubkeys.index(pubkey))
   increase_balance(state, index, amount)

上面的代码来自 beacon-chain 的 Ethereum 规范。

当添加验证者时,将在客户端实现上调用 process_deposit()。处理验证者的公共 BLS 密钥,如果公共密钥是用于新验证者的,我们为该验证者创建新的存款数据。所有这些都发生在 if 块中。

if pubkey not in validator_pubkeys

else 块中,我们看到以下内容:

## Increase balance by deposit amountindex = ValidatorIndex(validator_pubkeys.index(pubkey))increase_balance(state, index, amount)

这意味着我们增加了当前验证者的存款。这是因为存款合约至少只需要 1 ETH 才能通过存款。如果我们没有足够的 ETH 并且不想错过在验证者队列中的位置,我们可以稍后发送其余的。但是以上情况为池化 staking 创造了一个问题。

利用该 bug 的分步指南如下:

1. 使用我们准备好的存款数据抢跑有效的存款交易(32 ETH)

2. 恶意存款数据包含相同的验证者 pubkey、存款合约的最小存款(RocketPool 上为 1ETH 或 16ETH)以及我们的提款凭证。

客户端上的 if pubkey not in validator_pubkeys 将首先被调用,因为它之前从未见过验证者的密钥。我们抢跑的第二次存款会将我们的存款增加 32 ETH。提醒一下,这 32 ETH 来自池用户。

漏洞修复

RocketPool 目前在测试网上,并在公开启动之前与其审计师一起修复

Lido 已经实施了临时修复。Lido 决定暂时降低 staking 限制到当前 staking 的金额。这将导致不允许对当前 staking 金额进行下一次存款。与此同时,该团队将致力于此论坛帖子中描述的中期解决方案:https://research.lido.fi/t/mitigations-for-deposit-front-running-vulnerability/1239

致谢

我们要感谢 Dmitri 的重要发现和详细报告。我们还要感谢 RocketPool 和 Lido 的迅速反应以及对问题和报酬的快速处理。

如果你想开始 bug 赏金,我们为你准备好了。查看 Web3 Security Library,并开始在 Immunefi 上赚取奖励 - Immunefi 是 web3 的领先 bug 赏金平台,拥有全球最高的报酬。

要报告其他漏洞,请参阅 Immunefi 的 RocketPoolLido 的 bug 赏金计划。如果你有兴趣使用 bug 赏金来保护你的项目,请访问 Immunefi 服务 页面并填写表格。

  • 原文链接: medium.com/immunefi/rock...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Immunefi
Immunefi
The leading bug bounty platform for blockchain with the world's largest bug bounties.