深入解析Filecoin节点崩溃漏洞:索引验证缺陷与修复方案

本文详细分析了Filecoin网络Lotus和Venus客户端中的远程崩溃漏洞,该漏洞由于索引验证错误导致节点拒绝服务,涵盖漏洞原理、利用方式、修复方案及预防措施。

攻击Filecoin节点 - Trail of Bits博客

Simone Monica
2024年11月13日
区块链, 漏洞披露

今年1月,我们在Filecoin网络的Lotus和Venus客户端中发现并报告了一个漏洞,攻击者可通过该漏洞远程崩溃节点并触发拒绝服务。此问题由索引验证错误引起,导致索引越界恐慌。

该漏洞展示了我们在区块链节点审计中经常观察到的不安全实践:使用有符号整数的危险性。本篇博客详细说明我们发现的问题、修复方式,以及为何应在代码库中尽可能使用无符号整数来预防类似问题。Lotus和Venus均通过转换为无符号整数修复了此漏洞。

Filecoin工作原理

Filecoin是一个基于IPFS协议的文件存储和检索网络。Filecoin由一系列tipset组成,每个tipset是一组具有相同高度和父tipset的区块。存在三个主要客户端:Lotus(官方Go实现)、Venus(另一个Go实现,部分代码库与Lotus共享)和Forest(实验性Rust实现)。我们的漏洞影响Lotus和Venus,但为简化起见,仅以Lotus为例说明。

Lotus有一个数据结构CompactedMessages,用于保存tipset的所有消息以节省空间。

1
2
3
4
5
6
7
type CompactedMessages struct {
    Bls         []*types.Message
    BlsIncludes [][]uint64

    Secpk         []*types.SignedMessage
    SecpkIncludes [][]uint64
}

CompactedMessages结构

我们将以Bls(CompactedMessages)的消息类型作为参考;代表签名消息的Secpk在我们的问题背景下工作方式相同。该结构体包含一个Bls字段(包含所有消息)和一个BlsIncludes字段(将Bls字段中的消息与tipset中的块匹配)。第一个索引是块索引,第二个是消息索引。例如,如果要获取块1中的消息5,我们将使用BlsIncludes[1][5]返回的值来索引Bls切片。

利用问题

当处理来自对等节点包含tipset消息的响应时,消息索引BlsIncludes值未正确验证是否在Bls切片范围内。

此问题包括两部分:validateCompressedIndices中错误的数组长度验证和由此导致的越界访问。

错误的数组长度验证

validateCompressedIndices函数中,消息索引(无符号整数)被转换为有符号整数,然后验证是否小于Bls长度;否则函数返回错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func (c *client) validateCompressedIndices(chain []*BSTipSet) error {
    resLength := len(chain)
    for tipsetIdx := 0; tipsetIdx < resLength; tipsetIdx++ {
        msgs := chain[tipsetIdx].Messages
        blocksNum := len(chain[tipsetIdx].Blocks)

        if len(msgs.BlsIncludes) != blocksNum {
            return xerrors.Errorf("BlsIncludes (%d) does not match number of blocks (%d)",
                len(msgs.BlsIncludes), blocksNum)
        }

        for blockIdx := 0; blockIdx < blocksNum; blockIdx++ {
            for _, mi := range msgs.BlsIncludes[blockIdx] {
                if int(mi) >= len(msgs.Bls) {
                    return xerrors.Errorf("index in BlsIncludes (%d) exceeds number of messages (%d)",
                        mi, len(msgs.Bls))
                }
            }
                        ...
}

错误的索引验证

然而,由于消息索引由发送消息的对等节点控制,对等节点可以通过将索引设置为大于有符号整数最大值的值来绕过验证,导致索引在转换为有符号整数时变为负数。

越界访问

有多种方式可以利用此错误的数组长度验证,但我们重点关注checkMsgMeta中的一种。此函数在同步阶段调用,当节点尝试获取包含头部和消息的所有tipset时。

当调用checkMsgMeta(ts, cm.Bls, cm.Secpk, cm.BlsIncludes, cm.SecpkIncludes)时:

  • cm.Bls / allbmsgs是包含消息的CompactedMessages结构体的切片
  • cm.BlsIncludes / bmi包含用于匹配Bls切片中特定消息的索引
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func checkMsgMeta(ts *types.TipSet, allbmsgs []*types.Message, allsmsgs []*types.SignedMessage, bmi, smi [][]uint64) error {
    for bi, b := range ts.Blocks() {
        if msgc := len(bmi[bi]) + len(smi[bi]); msgc > build.BlockMessageLimit {
            return fmt.Errorf("block %q has too many messages (%d)", b.Cid(), msgc)
        }

        var smsgCids []cid.Cid
        for _, m := range smi[bi] {
            smsgCids = append(smsgCids, allsmsgs[m].Cid())
        }

        var bmsgCids []cid.Cid
        for _, m := range bmi[bi] {
            bmsgCids = append(bmsgCids, allbmsgs[m].Cid())
        }

                         ...

    return nil
}

checkMsgMeta函数

如前所述,用户控制这两个值。由于预期长度未正确验证,可能导致索引越界恐慌,如下视频所示:

这种验证缺失也可以通过Hello协议利用,该协议在两个对等节点首次相遇时执行。协议允许对等节点交换关于其最重tipset的信息。如果另一个对等节点的tipset更新,且请求节点没有该tipset,后者可以请求它。与同步类似,当解压缩接收到的消息以形成tipset时,会发生索引越界恐慌。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 解压缩消息并用它们形成完整的tipset。头部也需要被请求。
func (res *validatedResponse) toFullTipSets() []*store.FullTipSet {
    if len(res.tipsets) == 0 || len(res.tipsets) != len(res.messages) {
        // 只有在响应中返回头部和消息时才能进行此解压缩。
        // (第二次检查已由`validatedResponse`的保证隐含,此处仅为完整性添加。)
        return nil
    }
    ftsList := make([]*store.FullTipSet, len(res.tipsets))
    for tipsetIdx := range res.tipsets {
        fts := &store.FullTipSet{} // 修复:应使用`NewFullTipSet` API。
        msgs := res.messages[tipsetIdx]
        for blockIdx, b := range res.tipsets[tipsetIdx].Blocks() {
            fb := &types.FullBlock{
                Header: b,
            }
            for _, mi := range msgs.BlsIncludes[blockIdx] {
                fb.BlsMessages = append(fb.BlsMessages, msgs.Bls[mi])
            }
            for _, mi := range msgs.SecpkIncludes[blockIdx] {
                fb.SecpkMessages = append(fb.SecpkMessages, msgs.Secpk[mi])
            }

            fts.Blocks = append(fts.Blocks, fb)
        }
        ftsList[tipsetIdx] = fts
    }
    return ftsList
}

索引越界

修复

要修复此问题,需要在validateCompressedIndices函数中将Bls/Secpk切片的长度转换为无符号整数,并在无符号整数上进行比较。一个潜在的替代修复方法是检查有符号消息索引是否大于或等于零;然而,我们认为这种方法更直接,因为它只需要对无符号整数进行单一条件检查,而不是对有符号整数进行两个条件检查。

使用此方法,Lotus在版本1.25.2(PR #11565)中修复了问题,Venus在版本1.14.3(PR #6258)中修复了问题。

预防

此类问题在处理有符号整数时很常见。应尽可能:

  • 使用无符号整数,它们更不容易出错。
  • 在从较大类型转换为较小类型或从无符号整数转换为有符号整数时注意。
  • 实施检查或不变量,确保起始变量的域可以正确表示在目标类型的域中(即,不可能发生下溢或溢出)。

此外,以下Semgrep规则可帮助避免犯同样的错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
rules:
  - id: check-int-comparison
    patterns:
      - pattern-either:
          - pattern: |
              if int($X) >= len($Y) {
                return ...
              }
          - pattern: |
              if int($X) > len($Y) {
                return ...
              }
          - pattern: |
              if len($Y) > int($X) {
                return ...
              }
          - pattern: |
              if len($Y) >= int($X) {
                return ...
              }

    message: |
      避免将整型转换值与切片长度比较。可能导致索引越界错误。
    severity: WARNING
    languages:
      - go

保护您的区块链节点

构建区块链节点具有挑战性,需要在共识、网络、虚拟机和各种相关组件之间平衡风险。这一挑战也强调了在此类项目中传统应用安全考虑的重要性。

在Trail of Bits,我们在审查区块链节点(涵盖L1、L2、rollup和桥接)方面积累了深厚专业知识。我们的客户利用我们在Go和Rust方面的熟练技能构建稳健软件。如果您需要支持,请联系我们。

如果您喜欢这篇文章,请分享:
Twitter LinkedIn GitHub Mastodon Hacker News

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计