macOS Rootkit Emulation

CyStack image

Trung Nguyen

CEO @CyStack|June 24, 2022

Kernel rootkit is considered the most dangerous malware that may infect computers. Operating at ring 0, the highest privilege level in the system, this super malware has unrestricted power to control the whole machine, thus can defeat all the defensive and monitoring mechanisms. Unfortunately, dynamic analysis solutions for kernel rootkits are severely lacking; indeed, most existing dynamic tools are just built for userspace code (ring 3), but not for Operating System (OS) level. This limitation forces security researchers to turn to static analysis, which however proved to be very tricky & time consuming. In this research, I use Qiling Framework as the main emulator ( give him stars on Github ? ).

This research is a part of our publication at BlackHat USA 2020. The presentation is now available here.

First, let see how a rootkit is loaded into the kernel. The macOS kernel is officially known as XNU, which is a hybrid kernel combined from Mach kernel and BSD kernel. The kernel format also belongs to the MachO executable file. The kernel usually exposes its interface, a.k.a KPI, to let users use its functionality, and all of them are implemented inside the kernel code. Like other operating systems, macOS needs drivers to control devices, they are called Kernel Extensions or Kexts. The kernel extension is a bundle of files, and the kernel loads it from external space. Some interesting information can be gathered from Info.plist inside the bundle. In general, rootkit plays a role as a driver then it gets full power features from KPI.

In order to emulate the driver, I decided to load both kernel and kernel extensions together. Because the kernel is also a MachO executable binary, as well as the main component of KEXT, so I can load all of their Segment64 to emulator engine. Now I can access all implemented code of KPIs from the kernel. Besides, like a normal application, I also have to resolve local symbols and some other dynamic symbols of KEXT. Note that kernel releases its KPIs through some dependencies, so It is necessary to create junk code as a kind of indirection calling.


Next, a driver will run from its initial function, and this entry address can be extracted from the binary symbol, for IOKit driver is ::start method, and for the generic driver is the address stored in __realmain symbol. Before emulating the driver, some objects/context under kernel space should be initialized. I setup mac_policy_list by allocating a new object in the emulator engine and fill the address in target on kernel space. The same thing I will do for allproc symbol on kernel space. I also create some vnode and credential objects. Then I run emulation for preprocessing such as ::attach, ::probe, or kmod_info. Finally, I can go into the entry of the driver.

Regarding instrumentation, I map all KPIs exported from kernel to user-defined methods. It helps to simplify some features or just pass through and use the native function.

On the other hand, I also hook thread-related KPIs to disable multi-thread functionality. I give the driver a chance to interact with the real machine with some specific KPIs. For example, getattrlistbulk is a function to retrieve every entry in a directory ( which is called inside the application ls ). So I just scan all files and folders in the directory and pack them using vfs_attr_pack function from the kernel. To emulate syscall, I just find the sysent symbol on loaded kernel space, assign arguments to registers and run the entry address.

In some cases, we may want to call a native KPI from the hooked KPI. Normally, we have to save the current state and run another emulator. But it may screw up in some complicated situations. So I have a workaround here: I create a junk code and push its address to stack as saved rip. This junk code has three main missions: prepare arguments to registers and stacks, clear stack after calling, and jump to native KPI directly.

Under kernel, there are many events and callbacks that need to be emulated. So I build an Event Management System to listen to a register request from the driver and emulate the interaction from the user. That means I hook into KPI used to register callbacks to create a new event on EMS, then when user wants to trigger callbacks, I just emulate the corresponding address from EMS. In details,

Event Hook function to register Hook function to unregister Additional parameters when trigger event
SYSCTL sysctl_register_oid() sysctl_unregister_oid() sysctlbyname_args objects
Network Kernel Extension ctl_register() ctl_deregister() socket object and mbuf data
Network Filter sflt_register() sflt_unregister() raw network packet
MAC Policy mac_policy_register() mac_policy_unregister()  
KAuth kauth_listen_scope() kauth_unlisten_scope()  

I have some demonstration about emulating a famous rootkit on MacOS: Rubilyn

Related posts

Stored XSS leads to account takeover in Flarum
Stored XSS leads to account takeover in Flarum
November 19 2022|Advisories

CyStack Advisory ID CSA-2022-01 CVE IDs CVE-2022-41938 Severity Critical CVSS v3 Base 9.0 Synopsis CyStack’s researchers recently discovered a Stored XSS vulnerability in the Flarum platform version 1.5.0 to 1.6.1 which can lead to an account takeover attack. Flarum is a widely used simple and open-source forum platform. At the time of this post, we […]

Cyclos < 4.14.15 – Remote code execution
Cyclos < 4.14.15 – Remote code execution
June 24 2022|Advisories

CyStack Advisory ID CSA-2021-01 CVE IDs CVE-2021-44832 Severity Critical CVSS v3 Base 10.0 Synopsis Cyclos is a payment software created for banks, barters, remittances, and innovative currency systems. Cyclos is used by more than 1500 payment systems worldwide. CyStack recently found that Cyclos versions prior to 4.14.15 are vulnerable to the remote code execution vulnerability. […]

Cesanta Mongoose 6.16 &#8211; Integer overflow
Cesanta Mongoose 6.16 – Integer overflow
April 5 2023|Advisories

CyStack Advisory ID CSA-2019-04 CVE IDs CVE-2019-19307 Severity Critical CVSS v3 Base 9.8 Synopsis CyStack Security discovered an integer overflow vulnerability in the implementation of MQTT protocol in the Cesanta Mongoose Library version 6.16. By exploiting the vulnerability, a remote, unauthenticated attacker can perform a DoS attack to broker server with an infinite loop or […]