深入解析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时,会发生索引越界恐慌。

 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{} // FIXME: 我们应该使用`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方面的熟练程度来构建健壮的软件。如果你需要支持,请联系我们。

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