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