致命索引漏洞:如何远程崩溃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
}

我们将以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 / allbmsgsCompactedMessages结构的切片,包含消息
  • 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
}

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

这种验证缺失也可以通过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
29
30
31
// Decompress messages and form full tipsets with them. The headers
// need to have been requested as well.
func (res *validatedResponse) toFullTipSets() []*store.FullTipSet {
    if len(res.tipsets) == 0 || len(res.tipsets) != len(res.messages) {
        // This decompression can only be done if both headers and
        // messages are returned in the response. (The second check
        // is already implied by the guarantees of `validatedResponse`,
        // added here just for completeness.)
        return nil
    }
    ftsList := make([]*store.FullTipSet, len(res.tipsets))
    for tipsetIdx := range res.tipsets {
        fts := &store.FullTipSet{} // FIXME: We should use the `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: |
      Avoid comparing an integer converted value with the length of a slice. It may lead to index out of range errors.
    severity: WARNING
    languages:
      - go

保护您的区块链节点

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

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

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

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