杀死Filecoin节点 - Trail of Bits博客
Simone Monica
2024年11月13日
区块链, 漏洞披露
今年1月,我们在Filecoin网络的Lotus和Venus客户端中发现并报告了一个漏洞,允许攻击者远程崩溃节点并触发拒绝服务。该问题由索引验证不正确引起,导致索引越界恐慌。
该漏洞展示了我们在区块链节点审计中经常观察到的不安全实践:使用有符号整数的危险性。这篇博客文章详细介绍了我们发现的问题、修复方法,以及为什么你应该尽可能使用无符号整数来防止代码库中的类似问题。Lotus和Venus都通过转换为无符号整数修复了该漏洞。
Filecoin工作原理
Filecoin是一个允许存储和检索文件的网络,构建在IPFS协议之上。Filecoin是由tipsets组成的链,其中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中的方法。该函数在同步阶段过程中调用,当节点尝试获取包含头和消息的所有tipsets时。
当调用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
}
|
正如我们之前看到的,用户控制这两个值。由于预期长度未正确验证,它可能导致索引越界恐慌,如下视频所示:
这种验证缺失也可以通过Hello协议利用,该协议在两个对等方首次相遇时执行。该协议允许对等方交换关于其最重tipsets的信息。如果另一个对等方的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、rollups和桥接。我们的客户利用我们在Go和Rust方面的熟练程度来构建强大的软件。如果你需要支持,请联系我们。
如果你喜欢这篇文章,请分享:
Twitter
LinkedIn
GitHub
Mastodon
Hacker News
页面内容
- Filecoin工作原理
- 利用问题
- 不正确的数组长度验证
- 越界访问
- 修复
- 预防
- 保护你的区块链节点
近期文章
- 构建安全消息传递很难:对Bitchat安全辩论的细致看法
- 使用Deptective调查你的依赖项
- 系好安全带,Buttercup,AIxCC的评分回合正在进行中!
- 使你的智能合约超越私钥风险
- Go解析器中意想不到的安全隐患
© 2025 Trail of Bits.
使用Hugo和Mainroad主题生成。