Threat Research

Analysis: Inspecting Mach Messages in macOS Kernel-Mode Part II: Sniffing the received Mach messages

By Kai Lu | October 26, 2018

In this blog, we will review how to inspect the received Mach messages by setting up a kernel inline hooking for function mach_msg_receive_results().

In part I of this blog, we discussed how to inspect the sending of Mach messages in kernel-mode perspective. In part II, I will continue to define how to inspect received Mach messages by setting up a kernel inline hook. Let’s get started!

Receiving Mach messages

As mentioned in Part I, in the section “Messaging Implementation”, the functions mach_msg() and mach_msg_overwrite() can also both be used to receive Mach message. Let’s take a look at their execution flow:

Figure 1. Receiving the Mach message

You can read the source code of the function mach_msg_overwrite_trap(), which is implemented in xnu-4570.71.2/osfmk/ipc/mach_msg.c. The function mach_msg_overwrite_trap() can invoke the function mach_msg_receive_results() to receive a Mach message. The following is a code snippet that handles received messages in the function mach_msg_overwrite_trap():

Figure 2. A code snippet that handles the received messages in the function mach_msg_overwrite_trap()

The definition of the function mach_msg_receive_results() is shown below:

Figure 3. The definition of the function mach_msg_receive_results()

We can see that this function first invokes the function current_thread(), which returns a pointer to the thread structure. The thread structure is defined in xnu-4570.71.2/osfmk/kern/thread.h. It contains many fields, so we need to find the field that stores the received message. 

Figure 4. The field that stores the received message in the thread structure

We can see here that the field saved.receive.kmsg of the thread structure stores the received message.

The following are some macro definitions related to the received message in xnu-4570.71.2/osfmk/kern/thread.h:

Figure 5. The macro definitions related to the received message

Going back to Figure 3, the code “self->ith_kmsg” is used to get a pointer to the ipc_kmsg structure that stores the received Mach message.  So we need to implement an inline hook on the function mach_msg_receive_results(),get a reference of the ipc_kmsg structure, and then record the received Mach message.  

Kernel Inline Hooking

Let’s go into the assembly world of the function mach_msg_receive_results():

Figure 6. The assembly of the function mach_msg_receive_results()

We can see that the function starts with a prologue. Here I used the following inline hook with a length of 12 bytes.

Figure 7. The inline hooking instructions for the function mach_msg_receive_results()

Let’s look at the assembly instructions of the hooked mach_msg_receive_results(). It is able to jump to the memory address 0xFFFFFF7F9BE6B100, which is actually the address of its trampoline.

Figure 8. The hooked mach_msg_receive_results()

So far, we have picked the opcode instructions for inline hooking. We next need to construct the trampolines. The following is the trampoline of hooking mach_msg_receive_results().

Figure 9. The trampoline of hooking mach_msg_receive_results()

The first 12 bytes of instructions are copied from the prologue of the function mach_msg_receive_results(). In this trampoline, it first executes the prologue of the target function, and it then invokes its handler that is used to log the received Mach messages. Finally, it jumps back to the target function to execute the remaining instructions. Let’s calculate the indirect jumping address:

Figure 10. Calculating the indirect jumping address

We can see that the trampoline finally jumps to 0xffffff801816f07c(mach_msg_receive_results+12) to execute its instructions.

Next, let’s dive into the handler(mach_msg_receive_results_prologue_handler). The following is a snippet of the function mach_msg_receive_results_prologue_handler().

Figure 11. The code snippet of the function mach_msg_receive_results_prologue_handler()

It first gets a pointer to the current thread structure from gs:0x8. The field saved.receive.kmsg in the thread structure is located at offset 0x2E0. The field saved.receive.state in the thread structure is located at offset 0x2B0. This information allows us to get the pointer to the ipc_kmsg structure and the received state via their corresponding offsets. Once we get the ipc_kmsg structure, we can parse and record the received Mach messages. The variable recv_state represents the state of receiving a Mach message.

So far, we defined how to sniff the received Mach messages by implementing an inline hook of the function mach_msg_receive_results().

Finally, the following picture depicts the workflow of the kernel inline hooking.

Figure 12. The workflow of the kernel inline hooking

Sniffing the Mach Messages

In the previous sections, I detailed the implementation of inline hooking for the function mach_msg_receive_results(). In this section, I will present the sniffed received Mach message. It can record a detailed structure of Mach messages sent by a specific process. Some screenshots are shown below:

Figure 13. The process WindowServer successfully receives a Mach message
Figure 14. The process logd successfully receives a Mach message
Figure 15. The process opendirectoryd successfully receives a Mach message
Figure 16. The process WindowServer fails to receive a Mach message due to MACH_RCV_TIMED_OUT

Conclusion

In this blog, I presented how to sniff the received Mach message by setting up a kernel inline hook for the function mach_msg_receive_results(). It’s able to record all fields of the ipc_kmsg structure and every field of a Mach message. Implementing some kernel inline hooks on kernel APIs is a very interesting project. You can do a ton of fascinating things by setting up some inline hooks on specific kernel APIs, depending on your interests. The only limit is your imagination!

Note: All hooks were set up in macOS High Sierra 10.13.6 (17G65). For the latest OS version, macOS Mojave 10.14, you might need to tweak some hard-coded offsets. It’s easy to get the correct offsets through some reverse engineering.

Download our latest Fortinet Global Threat Landscape Report to find out more detail about recent threat landscape trends.

Sign up for our weekly FortiGuard Threat Brief.

Know your vulnerabilities – get the facts about your network security. A Fortinet Cyber Threat Assessment can help you better understand: Security and Threat Prevention, User Productivity, and Network Utilization and Performance.

 

Tags: