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
credential objects. Then I run emulation for preprocessing such as
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|
|Network Kernel Extension||ctl_register()||ctl_deregister()||socket object and mbuf data|
|Network Filter||sflt_register()||sflt_unregister()||raw network packet|
I have some demonstration about emulating a famous rootkit on MacOS: