macOS IPC Study Notes

1. 方式

  • MIG

  • XPC

  • DO

    ….

-w303

-w843

然而一切都是在Mach Msg的基础之上的。

2. 一些基础概念

2.1 什么是Port

个人理解就是类似Windows上handle的概念。
用户态经过处理是一个类似socket的整数,内核态(namep)索引到与之对应的消息队列,IPC时通过Port传递数据到消息队列,或者从消息队列取出数据。

2.2.2 port name

2.2.3 (port) right

一个port和对这个port的访问权限,有对应的权限才能做对应的操作,比如recv,接收数据;send,发送数据。

1
2
3
4
5
6
7
#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)

2.2 创建流程

示例代码:

1
2
mach_port_t p;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);

mach_port_allocate

函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
kern_return_t
mach_port_allocate(
ipc_space_t space,
mach_port_right_t right,
mach_port_name_t *namep)
{
kern_return_t kr;
mach_port_qos_t qos = qos_template;

kr = mach_port_allocate_full (space, right, MACH_PORT_NULL,
&qos, namep);
return (kr);
}

mach_port_allocate_full
根据不同的right走不同的分配逻辑:
-w680

ipc_port_alloc /ipc_port_alloc_name
-w614

两个函数区别只是是否指定了name。

ipc_object_alloc
-w596

348行通过一个宏,把port转name的方式获取namep,之后对ipc entry的关键结构进行初始化。

ipc_port_init

初始化port结构

-w641

至此,port初始化完成,namep初始化完成,可以根据namep索引到对应的内核中的消息队列。

port与ipc entry的关系,来自Mac OS X Internals:

其中的一些概念:

  • 用户态 mach_port_t

port在用户态表示,类似socket的一个整数

  • 内核态 mach_port_name_t

    1
    typedef natural_t mach_port_name_t;

    port在内核态表示

  • qos

    1
    Structure used to pass information about port allocation requests.Must be padded to 64-bits total length.
  • ipc space

1
2
Each task has a private IPC spacea namespace for portsthat is represented by the ipc_space structure in the kernel. 
Mac OS X Internals

每个task都有自己的唯一一个ipc sapce,

其中 ipc_entry_t is_table; /* an array of entries */ 字段是放着所有的ipc entry。

task的结构体实在是太大了,从task的struct ipc_space *itk_space;字段索引到其对应的ipc space。

  • ipc entry

看源码发现,图中的ipc_tree_entry结构没了:

1
2
3
4
5
6
7
8
9
10
11
12
struct ipc_space {
lck_spin_t is_lock_data;
ipc_space_refs_t is_bits; /* holds refs, active, growing */
ipc_entry_num_t is_table_size; /* current size of table */
ipc_entry_num_t is_table_free; /* count of free elements */
ipc_entry_t is_table; /* an array of entries */
task_t is_task; /* associated task */
struct ipc_table_size *is_table_next; /* info for larger table */
ipc_entry_num_t is_low_mod; /* lowest modified entry during growth */
ipc_entry_num_t is_high_mod; /* highest modified entry during growth */
int is_node_id; /* HOST_LOCAL_NODE, or remote node if proxy space */
};
  • port的user reference计数是啥
    一个port的user reference只表示了某个entry在task的space中被多少个地方使用,和entry实际指向哪个port没有关系

2.3 发送MACH MSG

mach msg的结构不再赘述,这部分直接看message.h头文件里的定义即可,下面着重看发送和接收过程。

其实是一个把mach msg转换成kmsg结构,然后入队(目标消息队列)的操作,目标进程获取就是一个出队的操作。

用户态(client) <--> 内核态 <--> 用户态(server)

收/发都是用mach_msg,使用options参数区别是收还是发。

2.3.1 流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mach_msg(...)
mach_msg_trap(...)
mach_msg_overwrite_trap(...)
option & MACH_SEND_MSG --> mach_msg_send
// mach_msg --> kmsg
// alloc a kernel msg buffer(kmsg), and copy user mach_msg to kernel msg buffer
ipc_kmsg_get(msg_addr, send_size, &kmsg);
ipc_kmsg_copyin
ipc_kmsg_copyin_hearder(kmsg, space, override, optionp)
ipc_kmsg_copyin_body(kmsg, space, map, optionp)
ipc_kmsg_send
ipc_voucher_send_preprocessing(kmsg);
ipc_mqueue_send(&port->ip_messages, kmsg,option,send_timeout);
option & MACH_RCV_MSG //TODO
ipc_mqueue_copyin(space, rcv_name, &mqueue, &object);
mach_msg_rcv_link_special_reply_port(...)
ipc_mqueue_receive
mach_msg_receive_results

2.3.2 发送

  • mach_msg_send( mach_msg_header_t *msg, mach_msg_option_t option, mach_msg_size_t send_size, mach_msg_timeout_t send_timeout, mach_msg_priority_t override)

    根据消息大小重新分配了内存,并且把消息拷贝进来,并且消息尾部增加了一些字段:

1
2
3
4
5
trailer = (mach_msg_max_trailer_t *) ((vm_offset_t)kmsg->ikm_header + send_size);
trailer->msgh_sender = current_thread()->task->sec_token;
trailer->msgh_audit = current_thread()->task->audit_token;
trailer->msgh_trailer_type = MACH_MSG_TRAILER_FORMAT_0;
trailer->msgh_trailer_size = MACH_MSG_TRAILER_MINIMUM_SIZE;

之前审服务的时候遇到过,还以为这部分是可控的,造成乌龙。 囧

  • ipc_kmsg_copyin(kmsg, space, map, override, &option);

    此时kmsg是新分配的内存,里面放的是要发送的mach msg, space和map都是当前task的space和map,直接获取,这部分有个图,可以看Macos Internals

    • ipc_kmsg_copyin_header(kmsg, space, override, optionp);
      拷贝 port rights,成功的话,原本消息中(kmsg)的port name都会被替换成对应的对象的指针。
    • ipc_kmsg_copyin_body( kmsg, space, map);
      拷贝msg body部分,中间验证了size、desc部分的size,类型等字段。
      desc_count < 0x3fff 。
      descriptor_size部分,必须是desc*16 == descriptor_size,不满足会为了对齐而调整。
      最终完成拷贝,把用户态的mach msg拷贝到了kmsg中。
  • ipc_kmsg_send(kmsg, option, send_timeout);
    到这里的时候port right拷贝了,消息内容也拷贝了,该直接发送了。把消息发送到dst的消息队列里。
    对于发送给内核的消息和非内核的消息分开处理
    内核:kmsg = ipc_kobject_server(kmsg, option);
    其他:ipc_mqueue_send(&port->ip_messages, kmsg, option, send_timeout);

2.3.3 接收

  • ipc_mqueue_copyin(space, rcv_name, &mqueue, &object);
    Convert a name in a space to a message queue.
    根据这个recv_name在space里找到ipc_entry结构,从而找到其中ipc_object->ipmessage结构。
    -w532

  • mach_msg_rcv_link_special_reply_port(…)

  • ipc_mqueue_receive(mqueue, option, rcv_size, msg_timeout, THREAD_ABORTSAFE);
    Receive a message from a message queue
    之前得到了消息队列mqueue,这个函数就是从这个消息队列中取出消息。

    • ipc_mqueue_receive_on_thread
      使用指定thread从消息队列中接收消息。
      接受分port set(imq_is_set()) 和 单个port(imq_is_queue()),这部分看message queue的结构体也能看出来必须要这么处理。

      消息队列是一个循环双向链表,取消息的过程就是一个 unlink的过程:
      -w629

  • mach_msg_receive_results
    Receive a message, copy out的操作,把之前“解链”的消息拷贝出来。

3. 引用

再谈Mach-IPC
Mac OS X Internals
MOXil
Auditing and Exploiting Apple IPC – Ianbeer
bazad