本文由“合天智匯”公眾號首發,作者:b1ngo
Ripple 20:Treck TCP/IP協議漏洞技術分析
Ripple20是一系列影響數億台設備的0day(19個),是JSOF研究實驗室在Treck TCP/IP協議棧中發現的【1】,該協議棧廣泛應用於嵌入式和物聯網設備。而由於這些漏洞很底層,並且流傳版本很廣(6.0.1.67以下),通過供應鏈的傳播,使得這些漏洞影響十分廣泛和深遠。這也是這個漏洞代稱Ripple的由來:The damaging effects of a these vulnerabilities has been amplified like a ripple effect to a dramatic extent due to the supply chain factor. (Ripple意思就是漣漪,連鎖作用) 我們在本文中,重點分析了Ripple20中的CVE-2020-11896(CVSS V3 10.0)漏洞原理和利用。
背景知識
IP 分片
IP分片的概念是為了解決數據包最大長度的限制,例如以太網最大傳輸單元MTU為1500字節。那么如果發送的IP packet超過這個長度,就需要將數據包分為小的片段fragments。分片之后,想要將分片的數據包,在接收端重組,還需要一些標志位,用於表示接下來是否還有分片,該分片在整個數據包的偏移offset和是否是同一數據包等信息。在IP頭部有4個字節(4-8字節)是用於存儲這些分片信息的:

其中Flags字段有3部分:Reverse位、DF(Don't Fragment)和MF(More Fragments)。
IP隧道
IP隧道是將IP報文封裝在另一個IP報文中的技術,如果外層和內存協議都是IP協議,那么我們將之稱為ip-in-ip,結構如下圖所示:

Treck TCP/IP的內部實現
Treck TCP/IP中,通過tsPacket結構表示數據包Packet:
struct tsPacket { ttUserPacket pktUserStruct; ttSharedDataPtr pktSharedDataPtr; // Point to corresponding sharable ttSharedData struct tsPacket * pktChainNextPtr; // Next packet (head of a new datagram in a queue) struct tsDeviceEntry * pktDeviceEntryPtr; // pointer to network Device struct union anon_union_for_pktPtrUnion pktPtrUnion; tt32Bit pktTcpXmitTime; tt16Bit pktUserFlags; tt16Bit pktFlags; tt16Bit pktFlags2; tt16Bit pktMhomeIndex; tt8Bit pktTunnelCount; // Number of times this packet has been decapsulated. Initially setto zero. tt8Bit pktIpHdrLen; // Number of bytes occupied by the IP header. tt8Bit pktNetworkLayer; // Specifies the network layer type of this packet (IPv4, IPv6, ARP, etc). tt8Bit pktFiller[1]; };
該結構重要字段包括: tsShareData字段:指向存儲處理數據包所需信息的Buffer的指針 pktChainNextPtr字段:指向下一個Packet pktUserStruct字段:表示數據包分片 pktUserStruct字段是另外一個重要的數據結構ttUserPacket(typedef struct tsUserPacket):
struct tsUserPacket { void * pktuLinkNextPtr; // Next tsUserPacket for fragmented data 指向下一個分片 ttUser8BitPtr pktuLinkDataPtr; // Pointer to data ttPktLen pktuLinkDataLength; // Size of data pointed by pktuLinkDataPtr 當前buffer的大小 ttPktLen pktuChainDataLength; // Total packet length (of chained fragmented data). Valid in first link only. 整個packet的長度,如果沒有分片,等於pktuLinkDataLength int pktuLinkExtraCount; // Number of links linked to this one (not including this one). Valid in first link only. };
該結構重要字段包括: pktuLinkNextPtr字段:指向下一個分片的指針 pktuLinkDataPtr字段:指向數據buffer的指針 pktuLinkDataLength字段:表示當前分片長度 pktuChainDataLength字段:表示整個Packet長度,當不存在分片的時候,這個長度與pktuLinkDataLength長度相等
數據包在不同層級中進行處理的的時候,需要調整pktuLinkDataPtr指針。例如一個ICMP請求包,該數據包有3層:Ethernet、IPv4和ICMP。在以太網層處理過程中(tfEtherRecv),pktuLinkDataPtr字段指向的是以太網頭部,當下一層處理的時候,該字段和一些其他字段將會做以下調整:

在這個例子中,以太網包頭長度為0xE,所以pktuLinkDataPtr指針向前調整0xE,而長度字段則減少0xE。 在接下來,IPv4層(tfIpIncomingPacket)開始處理數據包,pktuLinkDatatr此時指向的是IP頭部。Treck協議棧通過在tfIpIncomingPacket調用tfIpReassemblePacket函數來重組分片,每接受到一個分片,該函數將該分片插入到鏈表中,鏈表之間通過pktuLinkNextPtr指針連接起來:

如果分片有遺漏,那么最終返回Null;如果沒有,那么該函數將會把分片鏈表交給下一層去處理。
CVE-2020-11896漏洞原理
下圖是IP頭部的結構:

其中4字節到8字節表示的是IP分片相關信息,包括表示,Flags和Fragment Offset。 而前4字節則有兩個重要字段: IP Header Length:IP頭部長度 Total Length:IP數據包總長度
接着輸入理解一下tfIpIncomingPacket函數的實現,該函數首先對數據包進行一些簡單的check:

接下來檢查IpTotalLength是否小於等於pktuChainDataLength字段,這意味着實際接收到的數據比IP頭部標識的IP數據包總長度要長,在這種情況下,就對多余數據進行裁剪:

裁剪(trimming)的方式很簡單,就是將pktuChainDataLength和pktuLinkDataLength設置為ipTotalLength的長度。注意,漏洞就在這個裁剪這里。 回顧一下先前所說:pktuChainDataLength代表的是整個數據包的長度,pktuLinkDataLength代表的是當前分片的長度,如果上述操作成功,那么就會出現: pktuLinkDataLength = pktuChainDataLength = IpTotalLength 可是如果這個時候,如果pktuLinkNextPtr還是指向其他的分片呢,那么就會存在不一致現象,這種不一致的現象將會導致后續處理數據包產生錯誤。但是還是有問題,因為在tfIpIncomingPacket函數處理中,首先進行的是trimming操作,接着調用tfIpReassemblePacket對根據pktuChainDataLength大小,建立分片鏈表,而在tfIpReassemblePacket函數中並不會將分片數據包復制到buffer中。也就是說,先進行trimming操作,接着調用tfIpReassemblePacket建立分片鏈表,最終將分片鏈表返回給下一層進行處理,而下一層並不會再次調用tfIpIncomingPacket。這種情況下,上述不一致現象實際上並不能利用。
使用IP隧道
為了使得分片在IP層被處理,並且可以到達脆弱代碼位置,我們使用IP隧道。 tfIpIncomingPacket函數在處理內層IP數據包時,將之作為沒有分片的數據包進行處理,也就是說MF標志位=0,此時將不會調用tfIpReassemblePacket函數進行處理。這個tfIpIncomingPacket函數將會在兩個地方被調用,一次是在內層的IP packet(沒有分片,只調用一次),一次是在外層的ip packet(多次,每個分片調用一次)。在這個處理過程中,tfIpIncomingPacket首先將會接收所有的外層ip分片,對於每個分片,都會調用tfIpReassemblePacket函數。在接收全部的分片之后,將會進入下一網絡層的處理,這里由於ip-in-ip,tfIncomingPacket函數將會被本身所遞歸調用,去處理內層ip數據。ip-in-ip結構如下圖所示:

考慮如下這個場景: 內層IP數據包:IPv4{len=32, proto=17}/UDP{checksum=0, len=12} payload為:’A’*1000 外層IP數據包(分片1):IPv4{frag offset=0, MF=1, proto=4, id=0xabcd} 外層IP數據包(分片2):IPv4{frag offset=40, MF=0, proto=4, id=0xabcd} 整體數據包及分片情況如下圖所示:

當tfIpIncomingPacket函數(該函數被處理外層數據包的tfIpIncomingPacket所遞歸調用)處理內層IP數據包的時候,此時已經完成了分片的重組,這兩個ip分片被tsUserPacket->pktuLinkDataPtr所連接起來。 那么接下來在該函數接下來的流程中,內層IP數據包的total length(32),是小於pktuChainDataLength(1000+ 8 + 20 = 1028)的,進入trimming分支進行裁剪操作,將pktuChainDataLength設置為32。
if ((uint)ipTotalLength <= pkt->pktuChainDataLength) { if ((uint)ipTotalLength != pkt->pktuChainDataLength) { pkt->pktuChainDataLength = (uint)ipTotalLength; pkt->pktuLinkDataLength = (uint)ipTotalLength; } }
現在我們新的問題出現了:如何將這個不一致問題(inconsistency)轉為一個內存破壞(memory corruption)?
UDP 2.3.2中的heap overflow
在UDP數據處理中,可以確定的是,至少有一條代碼路徑是將IP分片復制到自定義的buffer里面,那么就存在漏洞的可能性。這個過程需要malloc一個堆空間,堆空間的大小取決於pktuChainDataLength字段,然后將ip分片復制到heap中。做復制這個事情的函數是tfCopyPacket,該函數邏輯抽象如下:

可以看到,這個memcpy的過程並不考慮長度。而堆塊本身的大小是取決於pktuChainDataLength,這個字段在先前漏洞trimming被觸發后,實際上是小於實際IP數據包總大小的,heap overflow就這樣出現了。 接下來是一些UDP本身字段的校驗,這部分就不再詳細敘述了,為了解決接收隊列非空的要求,還需要快速的發數據包保持接受隊列中存在數據包。
CVE-2020-11896漏洞利用
接下來作者利用Digi Connect ME 9210進行了驗證,證明可以做到遠程代碼執行。這個設備如下圖所示:

這是一個極小的嵌入式設備,用於完成串口到以太網的轉化,串口常包括SPI、I2C和CAN等,里面有嵌入式的ARM CPU,有一個NET+OS操作系統。本次實驗的目標是通過遠程執行shellcode,將開發板上面的LED燈點亮。
exploit編寫策略
heap overflow的利用大概有兩種方式: 1. 覆蓋堆中的meta-data信息。所謂meta-data就是堆中的元信息,包括堆塊的大小、空閑位的標志等等堆本身自帶的信息,這種利用方式和libc pwn中的off by one原理類似,溢出到下一個堆塊的重要信息。 2. 覆蓋堆上面的數據結構。這種攻擊方式是希望堆上面存着一些數據結構,該結構中存着一些函數指針可以被覆蓋。
第二種利用方式與特定應用程序相關,第一種則適用面更加廣泛,本文選擇第一種利用方式。
理解Treck heap的內部實現
Treck中實現了一套自己的堆分配機制,在內部使用的是固定大小的堆分配模式,一個分配單元被稱之為bucket。在Treck實現中,有如下幾種固定大小:

而釋放后的bucket,有自己對應的free buckets list,每次釋放一個bucket后,就將其插入到對應大小list的頭部,這里可以類比於ptmalloc中的tcache機制。在Treck協議棧中通過tfGetRawBuffer,該函數參數為一個4字節大小的size,返回一個指向分配內存的指針,這個分配的內存我們稱之為raw buffer,raw buffer的釋放通過tfFreeRawBuffer。分配的過程就是從對應空閑鏈表中取出一個堆塊,然后轉為ttRawBufferPtr類型指針返回給用戶:

如果空閑鏈表中沒有空閑的bucket,那么就調用tfBufferDoubleMalloc分配一個新堆塊返回給用戶:


釋放的時候將其插入到對應的空閑鏈表中去

可以看到,空閑bucket插入到空閑鏈表后,將會利用rawNextPtr指針鏈接起來,rawNextPtr指向上一個空閑bucket,結構如下圖所示:

到目前為止,我們搞清楚了Trec中的堆結構,堆的分配和釋放過程,特別是空閑bucket也是類似ptmalloc一樣通過鏈表利用堆中的指針鏈接起來的。那么我們自然可以想到,如果可以覆蓋掉rawNexPtr指針為可控值,就可以做到任意地址分配了:

就像上圖所示,通過兩次分配之后,我們就可以在棧上分配一個可控的堆塊。這種攻擊方式,在libc Pwn中是非常常見的,例如fastbin attack打malloc_hook改為one_gadget。 這個嵌入式設備是ARM v9,並且這個堆實現過程基本沒有check,甚至沒有開NX,最后白皮書中采取的方法也是通過ROP跳到shellcode來做到RCE的。 目前筆者手頭還沒有實際運行Treck的設備,所以沒法實際動手調試和寫exp,運行效果可以看JSOF官方的視頻演示效果【1】,啟明星辰ADLab也做了一個視頻【2】。
總結
-
Ripple 20存在於Treck TCP/IP協議棧中,該協議棧廣泛存在於嵌入式設備中,波及的行業包括:工業設備、電力設備、企業網絡設備、交通、能源等等,而上述設施我們稱之為:關鍵基礎設施
-
Ripple 20不好修補,首先由於供應鏈的廣泛傳播,漏洞存在形式又很底層,所以排查設備是否運行Treck TCP/IP協議棧容易疏漏。其次雖然Treck官方已經提供了最新穩定版本6.0.1.67,但是這種基礎協議棧軟件的更新還是比較麻煩的
-
從白皮書披露來看,漏洞利用技術並不十分復雜,所以漏洞利用門檻可能不會很高,例如這個漏洞,即使無法做到RCE,至少遠程令設備crash是比較容易做到的,而關鍵基礎設施的crash也已經算是大問題了
相關實驗:
TCP攻擊實例分析
(通過該實驗了解SYN-Flooding攻擊,RST攻擊,TCP會話劫持,並通過會話劫持拿到服務器shell 權限。)
聲明:筆者初衷用於分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為后果自負,與合天智匯及原作者無關!