首页
社区
课程
招聘
[翻译]task_t指针重大风险预报——PoC task_t considered harmful - many XNU EoPs
发表于: 2016-12-30 12:37 5744

[翻译]task_t指针重大风险预报——PoC task_t considered harmful - many XNU EoPs

2016-12-30 12:37
5744

****

CVE-2016-1757 是由于exec运行期间资源条件竞争导致port失效而产生的安全漏洞
CVE-2016-1757是一个涉及到在exec操作期间,端口结构顺序失效的条件竞争漏洞。
详情:
        当一个suid二进制程序被执行,尽管task struct与执行程序前状态保持一致,但是它执行前的task 和task port确实已失效。
当执行一个suid二进制程序时,虽然这个任务的旧任务以及线程端口已经无效,但是它的任务结构却还保持着相同的状态。

在此期间,执行前task没有自我复制和产生一个新的task。
如果没有fork或者创建新的任务,

这就意味着任何指向之前task struct的指针如今指向一个进程euid为0的进程的task struct。(即拥有root权限的执行环境)

许多IOKit驱动程序都保存着task struct指针作为它们的一部分,可以参考我之前的bug报告中的一些例子。

在这些例子中,我提到了另一个bug,若IOKit驱动程序未引用task struct,则如果杀死相应的task,然后fork和执行一段suid root二进制程序,

我们能够通过一个euid 为0的虚拟内存的task struct指针获得IOKit object交互。

我们就可以得到IOKit对象,并通过task struct指针,与一个euid 为0的进程的虚拟内存进行交互。

(还有一种攻击方式:你也可以通过强制产生一个恢复task struct的服务程序来逃逸沙盒)

(你也可以通过强制launchd生成一个将会重新利用已被释放的task struct的服务,来实现沙盒逃逸。)

反之若这些IOKit驱动程序引用task struct,没关系!
当然,再进一步,即使这些IOKit驱动程序对task struct作了引用,也无所谓!
(至少在没有suid二进制程序运行时)
(至少在suid二进制程序运行时没有问题。)
因为用户端的用户空间客户端在time A拥有发送至task port的权限,但当从task port 传递至IOKit并不意味着仍然有发送权限,仅仅是因为IOKIt驱动程序实际调用的是task struct指针。

就IOSurface而言,这个允许我们方便的发送任意代码至虚拟内存euid为0的读写区域。
以IOSurface为例,这使得我们可以轻松的map euid为0的进程的虚拟内存里的任意可读写区域,并且重新写入。
大量IOKit驱动程序存储taks struct指针,使用它们操作用户空间虚拟内存(如ioacceleratorFamily2,IOthunderboltFamily,IOSurface)或者依赖于taks struct指针去执行权限检测(如IOHIDFamily)

另外一个有趣的例子是stack中的task struct指针
MIG文件中相对应的用户层/内核层中的task port如下格式

  type task_t = mach_port_t
  #if KERNEL_SERVER
      intran: task_t convert_port_to_task(mach_port_t)

convert_port_to_task 如下:

  task_t
  convert_port_to_task(
    ipc_port_t    port)
  {
    task_t    task = TASK_NULL;

    if (IP_VALID(port)) {
      ip_lock(port);

      if (  ip_active(port)         &&
          ip_kotype(port) == IKOT_TASK    ) {
        task = (task_t)port->ip_kobject;
        assert(task != TASK_NULL);

        task_reference_internal(task);
      }

      ip_unlock(port);
    }

    return (task);
  }

task port 转变为相对应的task struct指针,该指针引用于task struct,但仅仅是确保它不被释放,

而非为了执行二进制程序导致它自己的euid不变。
而不是保证它的euid不会变成suid root程序执行的结果。
尽管task port不再有效,但只要port lock解除锁定,task就可以执行标记为suid的二进制程序,task strut指针就依然有效。
这就产生了大量的有趣的条件竞争。
grep所有.defs文件的源代码,需要一个task_t来找到它们;-)
在这个exp中,我将证明最有趣的环节:task_threads
让我们一起来看一下task_threads实际是如何工作的,包括由MIG产生的核心代码。
在 task_server.c(一个自动产生的文件,若找不到该文件,先build XNU)
target_task = convert_port_to_task(In0P->Head.msgh_request_port);

  RetCode = task_threads(target_task, (thread_act_array_t *)&(OutP->act_list.address), &OutP->act_listCnt);
  task_deallocate(target_task);

This gives us back the task struct from the task port then calls task_threads:
(unimportant bits removed)

  task_threads(
    task_t          task,
    thread_act_array_t    *threads_out,
    mach_msg_type_number_t  *count)
  {
    ...
    for (thread = (thread_t)queue_first(&task->threads); i < actual;
          ++i, thread = (thread_t)queue_next(&thread->task_threads)) {
      thread_reference_internal(thread);
      thread_list[j++] = thread;
    }

    ...

      for (i = 0; i < actual; ++i)
        ((ipc_port_t *) thread_list)[i] = convert_thread_to_port(thread_list[i]);
      }
    ...
  }

task_threads利用task struct 指针通过threads列表迭代threads(来遍历线程列表),
然后creates发送指令给task_threads,task_threads发送指令返回给用户空间,
然后赋予它们发送权限,随后在用户空间得到发送返回

过程中出现锁定和解锁,但是锁定和解锁是不相关的。
如果task同时运行suid标记为root的二进程代码会发生什么?
执行代码相关联的两部分主要是ipc_task_reset和ipc_thread_reset
  void
  ipc_task_reset(
    task_t    task)
  {
    ipc_port_t old_kport, new_kport;
    ipc_port_t old_sself;
    ipc_port_t old_exc_actions[EXC_TYPES_COUNT];
    int i;

    new_kport = ipc_port_alloc_kernel();
    if (new_kport == IP_NULL)
      panic("ipc_task_reset");

    itk_lock(task);

    old_kport = task->itk_self;

    if (old_kport == IP_NULL) {
      itk_unlock(task);
      ipc_port_dealloc_kernel(new_kport);
      return;
    }

    task->itk_self = new_kport;
    old_sself = task->itk_sself;
    task->itk_sself = ipc_port_make_send(new_kport);
    ipc_kobject_set(old_kport, IKO_NULL, IKOT_NONE); <-- point (1)

  ... then calls:

  ipc_thread_reset(
    thread_t  thread)
  {
    ipc_port_t old_kport, new_kport;
    ipc_port_t old_sself;
    ipc_port_t old_exc_actions[EXC_TYPES_COUNT];
    boolean_t  has_old_exc_actions = FALSE;
    int      i;

    new_kport = ipc_port_alloc_kernel();
    if (new_kport == IP_NULL)
      panic("ipc_task_reset");

    thread_mtx_lock(thread);

    old_kport = thread->ith_self;

    if (old_kport == IP_NULL) {
      thread_mtx_unlock(thread);
      ipc_port_dealloc_kernel(new_kport);
      return;
    }

    thread->ith_self = new_kport; <-- point (2)

Point (1)从旧的task port清除task struct pointer,然后重新分配一个新的port给task
Point (2)对应的 thread port同上.
调用执行exec的进程B和处理task_threads()的进程A以及imagine
下面是执行过程:
  Process A: target_task = convert_port_to_task(In0P->Head.msgh_request_port); //
得到指向B进程的task struct 指针
  Process B: ipc_kobject_set(old_kport, IKO_NULL, IKOT_NONE); //
B进程使旧的task port失效以至于不(再)拥有task struct的指针
  Process B: thread->ith_self = new_kport //
B进程重新分配一个新的thread ports和激活(并设置)他们
  Process A: ((ipc_port_t *) thread_list)[i] = convert_thread_to_port(thread_list[i]); // A进程读取和转变为新的 thread port 对象!

这里最基本的问题不是这个特殊的资源(条件)竞争,事实上是当最先指定一个task struct指针后,你不能依赖拥有一个相同的euid的task struct 指针。

exploit:

这段利用代码说明一个euid为0进程的thread port竞争资源,
这段poc仅仅利用了这种条件竞争来得到一个euid为0的程序的线程端口。

一旦运行利用代码,我仅仅需要跟随(放置了)一小段ROP payload插入ret-slide。然后使用thread port 设置RIP到gadget添加了大量的rsp、X,随后会弹出shell,只需要运行一段时间,将会出现竞争情况。

测试系统  MacBookAir5,2 OS X 10.11.5(15F34)

在mac os10.12更优化的利用代码,对于内核版本不高于10.12的都有效。


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2019-9-14 20:42 被kanxue编辑 ,原因:
上传的附件:
收藏
免费 1
支持
分享
最新回复 (2)
雪    币: 6
活跃值: (1141)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
实话说我是被图片吸引过来滴
2016-12-30 14:19
0
雪    币: 523
活跃值: (278)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
3
大表哥走的就是二次元套路
2016-12-30 15:07
0
游客
登录 | 注册 方可回帖
返回
//