• 文章/ARTICLE
  • DYLD_ROOT_PATH dyld本地提权漏洞分析
  • 360NirvanTeam 2015-12-23 7446

  DYLD_ROOT_PATH是dyld中进行处理的一个环境变量,但由于处理不当,可导致的本地提权。该漏洞存在于Mac OS X 10.10.5中,在El Capitan(10.11)中被修补。Luis Miras(@_luism)在其博客上放出了漏洞分析以及提权利用代码。本文将对此漏洞进行简单分析,并说明提权所利用的攻击方式。 

 

一、漏洞描述

 


        dyld在处理DYLD_ROOT_PATH环境变量时,对于恶意构造的MachO文件dyld_sim的映射处理不当,导致dyld的代码段能够被重新映射,造成任意代码执行。若通过DYLD_ROOT_PATH漏洞运行setuid的程序,就能够实现本地提权。

 

二、dyld

 



        dyld是Mac OS X以及iOS上的动态链接器,和系统的加载器共同负责准备一个进程的启动过程。进程启动过程的基本步骤为:
      1. 系统加载器将可执行文件页面和dyld映射进内存。
      2. 系统加载器将控制权转交给dyld,由dyld负责将可执行文件依赖的库映射到进程地址空间中,并进行链接。
      3. 加载过程完成后,就从可执行文件的在内存中的入口点开始运行。

 

三、漏洞分析

 


 

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.


        根据dyld的man page,DYLD_ROOT_PATH环境变量是一个用冒号分隔的目录的列表,dyld在准备进程的过程中,在处理每一个image文件时,会把DYLD_ROOT_PATH中每一个目录路径插入到image文件路径之前,直到这个文件被找到。然而,通过阅读dyld源码,发现DYLD_ROOT_PATH有未文档化的处理流程。dyld::_main中相关源码如下:

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
    uintptr_t result = 0;
    sMainExecutableMachHeader = mainExecutableMH;
    #if __MAC_OS_X_VERSION_MIN_REQUIRED
    // if this is host dyld, check to see if iOS simulator is being run
    const char* rootPath = _simple_getenv(envp, “DYLD_ROOT_PATH"); //获取环境变量的值
    if ( rootPath != NULL ) {
        // look to see if simulator has its own dyld
        char simDyldPath[PATH_MAX]; 
        strlcpy(simDyldPath, rootPath, PATH_MAX);
        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);  //调用useSimulatorDyld
            if ( !result && (*startGlue == 0) )
                halt("problem loading iOS simulator dyld");
            return result;
        }
    }
    #endif
…
}


        从源码中可以看到,在dyld::_main函数在开始运行时,就会从DYLD_ROOT_PATH环境变量中取出值,进行路径的拼接。例如,DYLD_ROOT_PATH=/tmp时,dyld就会尝试打开文件/tmp/usr/lib/dyld_sim文件,若打开成功,就以/tmp/usr/lib/dyld_sim为参数调用useSimulatorDyld函数,漏洞就发生在这个函数中。useSimulatorDyld函数详情见源码文件dyld.cpp,简要流程如下:


      1. 读入MachO文件头;
      2. 遍历Load Command中所有LC_SEGMENT,计算得到总共需要映射的内存空间大小;
      3. 调用vm_allocate函数分配内存;
      4. 再次遍历所有Load Command,调用mmap映射每一个LC_SEGMENT到进程地址空间;(漏洞触发)
      5. 检查代码签名,验证通过则跳转到dyld_sim的入口点。


        在上述流程中,对于恶意构造的MachO文件的Load Command处理不当,使得在调用mmap进行段映射的时候,能够用包含任意代码的段替换原先映射的dyld的代码段,实现任意代码执行。而且,漏洞代码在检查dyld_sim文件的签名之前就被触发,因此能够绕过代码签名的检查。下面通过具体的代码来分析漏洞原因。


        useSimulatorDyld中有两个循环来处理Load Command。第一个循环的代码用于计算需要映射的Segment的总大小,代码如下。如果当前LC_SEGMENT的file offset值为0,那么preferredLoadAddress就设置为当前Segment的vmaddr;如果所有LC_SEGMENT命令的file offset都不为0,那么preferredLoadAddress就保持默认值0(构造恶意MachO文件时需要令所有file offset都不为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;  //mappingSize是需要申请的映射空间的总大小
            if ( seg->fileoff == 0 )
                preferredLoadAddress = seg->vmaddr;
        }
        break;
    }
    cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}


        接着,dyld会调用在进程地址空间中申请内存进行映射,调用vm_allocate,代码如下,loadAddress即为dyld_sim在进程地址空间中的起始地址。

vm_address_t loadAddress = 0;
uintptr_t entry = 0;
if ( ::vm_allocate(mach_task_self(), &loadAddress, mappingSize, VM_FLAGS_ANYWHERE) != 0 )
    return 0;


        然后进入第二个循环,根据Load Command的类型进行不同的处理,对于LC_SEGMENT_COMMAND调用mmap进行映射,这里也就是可以触发任意代码执行的位置,漏洞代码相关的代码包括下面两部分:

(1)requestedLoadAddress = seg->vmaddr – preferredLoadAddress + loadAddress
通过构造MachO文件,可以使preferredLoadAddress为0,那么requestedLoadAddress = seg->vmaddr + loadAddress,其中seg->vmaddr攻击者可控。

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;
       
       //requestedLoadAddress是当前段将要映射到的地址
       //在preferredLoadAddress=0的情况下,requestedLoadAddress = vmaddr + loadAddress
       //seg->vmaddr可以在构造MachO文件时指定,因此requestedLoadAddress在一定范围内可控
       uintptr_t requestedLoadAddress = seg->vmaddr - preferredLoadAddress + loadAddress;
       
       //mmap的标志位为MAP_FIXED
       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:
    ...
    case LC_CODE_SIGNATURE:
    ...
    }
    cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}

        loadAddress是否可控呢?以crontab为例,crontab运行时的内存布局如下,crontab ASLR偏移为0x612e000。对于crontab,在没有ASLR的情况下,dyld_sim的loadAddress为0x106137000 – 0x612e000 = 0x100009000。在启用了ASLR的情况下,ASLR的偏移范围是[0, 0xffff000],因此dyld_sim的loadAddress的范围是[0x100009000, 0x110008000],即loadAddress范围已知。requestedLoadAddress也因此处于一个可控的范围内。

sudo vmmap -interleaved crontab
==== regions for process 72384  (non-writable and writable regions are interleaved)
REGION TYPE              START - END             [ VSIZE] PRT/MAX SHRMOD  REGION DETAIL
mapped file    000000010612e000-0000000106133000 [   20K] r-x/rwx SM=COW  /usr/bin/crontab
mapped file    0000000106133000-0000000106134000 [    4K] rw-/rwx SM=COW  /usr/bin/crontab
mapped file    0000000106134000-0000000106137000 [   12K] r--/rwx SM=COW  /usr/bin/crontab
<———————loadAddress会紧跟着crontab的映射页—————————>
STACK GUARD   00007fff55ad2000-00007fff592d2000 [ 56.0M] ---/rwx SM=NUL  stack guard for thread 0
Stack         00007fff592d2000-00007fff59ad1000 [ 8188K] rw-/rwx SM=ZER  thread 0
Stack         00007fff59ad1000-00007fff59ad2000 [    4K] rw-/rwx SM=COW  thread 0
__TEXT        00007fff69c9a000-00007fff69ca7000 [   52K] r-x/rwx SM=COW  /usr/lib/dyld
__TEXT        00007fff69ca7000-00007fff69ca8000 [    4K] r-x/rwx SM=PRV  /usr/lib/dyld
__TEXT        00007fff69ca8000-00007fff69cd1000 [  164K] r-x/rwx SM=COW  /usr/lib/dyld
__DATA        00007fff69cd1000-00007fff69cd4000 [   12K] rw-/rwx SM=COW  /usr/lib/dyld
__DATA        00007fff69cd4000-00007fff69d0a000 [  216K] rw-/rwx SM=NUL  /usr/lib/dyld
__LINKEDIT    00007fff69d0a000-00007fff69d1e000 [   80K] r--/rwx SM=COW  /usr/lib/dyld
VM_ALLOCATE   00007fffffe00000-00007fffffe01000 [    4K] r--/r-- SM=SHM
shared memory 00007fffffe94000-00007fffffe95000 [    4K] r-x/r-x SM=SHM

(2)mmap的标志位为MAP_FIXED。根据man page,当MAP_FIXED的映射请求成功时,将会替换原来进程空间的已经映射的任意内存页。这就表示能够替换堆页、栈页,甚至是可执行代码的页。

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.

        综合上面的分析,攻击者可以通过构造恶意的MachO文件,控制Segment的requestedLoadAddress,使其指向某个可执行代码页,然后通过mmap映射,用包含攻击代码的页替换原有页,然后再设法运行被替换的代码页,就实现了任意代码执行。

 

四、利用方法简单分析

 


        根据上文漏洞利用的分析,需要进行页面替换,并且需要设法执行被替换的页面。那么什么样的代码页作为目标比较合适?Luis Miras在文章中以dyld中执行mmap syscall的代码页作为目标,很巧妙的思路。

•  正常情况下,当syscall执行完成后,rax寄存器指向映射的首地址,并将会执行0x00007FFF5FC26DC6处的指令。

•  如果攻击者替换了当前的代码页,在页首放置shellcode(因为页属性攻击者可控,设置成可执行,就不用通过ROP来绕过DEP),并将0x00007FFF5FC26DC6处的指令替换成jmp rax,就会导致程序流程直接跳转到shellcode中,实现任意代码执行。

__text:00007FFF5FC26DBC  ___mmap         proc near               ; CODE XREF: _mmap+31p
__text:00007FFF5FC26DBC B8 C5 00 00 02   mov     eax, 20000C5h
__text:00007FFF5FC26DC1 49 89 CA         mov     r10, rcx
__text:00007FFF5FC26DC4 0F 05            syscall
__text:00007FFF5FC26DC6 73 08            jnb     short locret_7FFF5FC26DD0
__text:00007FFF5FC26DC8 48 89 C7         mov     rdi, rax
__text:00007FFF5FC26DCB E9 4A F9 FF FF   jmp     _cerror_nocancel

        由于mmap是在dyld检查dyld_sim的代码签名之前调用,在通过mmap触发shellcode运行后,直接进入shellcode的流程,就不会检查dyld_sim的签名!

 

References

 



1. http://luismiras.github.io/muymacho-exploiting_DYLD_ROOT_PATH/
2. http://www.opensource.apple.com/source/dyld/dyld-353.2.3/
3. https://github.com/luismiras/muymacho

微信 扫一扫

分享到