Know Your Enemy
In the previous post (Part 1), we covered several rootkit technique implementations. Now we will focus on kernel rootkit analysis, looking at two case studies of rootkits found in the wild: Husky Rootkit and Mingloa/CopperStealer Rootkit.Through these case studies, we’ll share our insights about rootkit analysis techniques and methodology.
Before we dive into the analysis, here are several guidelines about how we approached this Windows kernel driver and some prior knowledge that will assist in understanding the purpose of key functions in the binary.
Let’s start with the binary’s entry point. In the case of a Windows kernel driver, it is DriverEntry.
The DriverEntry usually includes the following blocks of code:
- Calls to IoCreateDevice and IoCreateSymbolicLink.
- Initialization of the Major Function array with function pointers to various handler functions.
- Assignment of the DriverUnload routine with a function pointer to a handler function.
The following snippet (Snippet 1) showcases how a DriverEntry for a simple Windows kernel driver would be implemented in C language.
Snippet 1: An example of a DriverEntry implementation in C.
The next snippet (Snippet 2) showcases how the disassembly of the same DriverEntry would look.
Snippet 2: Disassembly of DriverEntry.
DriverUnload is a function that is invoked when the driver is unloaded.
The purpose of this handler function is to clean up any resources that were created by the driver during its initialization and execution — for example, deleting both the device and symbolic link that were created in the DriverEntry.
It would also be a great strategic function to call ExFreePoolWithTag to de-allocate any pool memory that was allocated in the DriverEntry function.
Snippet 3: An example of a DriverUnload implementation in C.
Windows Kernel Structures
To fully understand the disassembly of a Windows kernel driver, we should also be familiar with a few of the kernel structures used by the object manager and other components in the kernel.
For example, the following structure is the DRIVER_OBJECT (Snippet 4).
Snippet 4: A breakdown of the DRIVER_OBJECT structure.
It is useful to map out the IRP major functions used by the driver when reverse engineering it.
For instance, by looking at the structure offsets (Snippet 4) and the disassembly (Snippet 2), we can determine that sub_1400014B0 is the DriverUnload.
We can also use the IRP major functions code values described in wdm.h/ntddk.h to conclude that sub_140001280 (in Snippet 2) is the function handler for IRP_MJ_CREATE by checking what the major function of the code is that would give us the result of 0x70 from the offset of MajorFunction (0x70) in the DRIVER_OBJECT structure. That is obviously 0x00*PointerSize (8 in x64 architecture); thus, we are dealing with IRP_MJ_CREATE.
In the same manner, we can determine what the function handlers are for IRP_MJ_CLOSE, IRP_MJ_READ, IRP_MJ_WRITE and IRP_MJ_DEVICE_CONTROL.
Snippet 5: An excerpt from wdm.h defining the constant values for all IRP major functions.
Some other kernel structures we should be familiar with when performing our analysis are the IRP and IO_STACK_LOCATION structures.
An IRP, also known as I/O Request Packet, is the structure that represents an I/O request during its creation, while moving between different drivers in the device stack, and until the point of the request’s completion.
An IRP is created when DeviceIoControl with a certain IOCTL operation is called from user-mode on a handle of a device object acquired by the user.
Snippet 6: A breakdown of the IRP structure.
Additionally, the IO_STACK_LOCATION represents the current location of an IRP in the device stack (and thus the CurrentLocation field in the IRP structure is a pointer to an IO_STACK_LOCATION).
The IO_STACK_LOCATION structure contains a union-typed Parameters field that specifies the different parameters to be used by different major functions in the driver.
For example, in case the current operation is IRP_MJ_DEVICE_CONTROL, the parameters of type DeviceIoControl would be used, containing OutputBufferLength, InputBufferLength, IoControlCode and Type3InputBuffer.
Snippet 7: A breakdown of the IO_STACK_LOCATION structure.
Armed with our new understanding of Windows kernel drivers and how to find key functions in Windows drivers, let’s look at some real-world, in-the-wild examples.
Case Study #1: APT29 Brute Ratel C4 Campaign Drops “Husky” Rootkit
This research originated from looking at samples associated with a campaign that was also mentioned in a blog by Palo Alto Networks Unit 42 about Brute Ratel C4. Unfortunately, they did not provide a technical analysis of this sample, so we decided to dig deeper ourselves.
File size 284.92 KB (291760 bytes)
The sample is a kernel driver signed with a leaked NVIDIA certificate from the LAP$US group. It uses the Heresy’s Gate method found by zerosum0x0 (Figure 1), which is a technique used for injecting code to user-mode from a kernel-mode driver, bypassing SMEP.
Figure 1: Disassembly of the signed driver using Heresy’s Gate method by zerosum0x0.
The injected shellcode uses classic techniques like traversing the InLoadOrderModuleList to find library handles and resolving API functions such as LoadLibraryA and GetProcAddress, which can be used to resolve any other API.
The injected shellcode is also quite long to analyze (Figure 2) and looks very similar to the shellcode described in the aforementioned Unit 42 blog, since it uses multiple push instructions to store data on the stack. The data stored in the stack includes:
- Base64-encoded config data for Brute Ratel C4
- Brute Ratel C4 payload
- Portable Executable (PE) 64 binary that is a VMProtect packed kernel driver, which is loaded later
Figure 2: An excerpt from the shellcode, pushing many values to the stack and forming a Base64 blob.
The Brute Ratel C4 config can be decrypted using the following short script (Snippet 8):
Snippet 8: A code snippet used to decode and decrypt the config from a Base64 blob extracted from the stack.
After decrypting the config, we get the following output:
Snippet 9: An example of the decrypted config.
The decrypted config data (Snippet 9) includes some basic configuration for the Brute Ratel C4 payload, including a C2 server address and port to start communication with, a Base64-encoded template of what a request to the C2 should look like and different paths on the C2 for various functionality and options.
Figure 3: A breakdown of the attack scenario.
We found the x64 rootkit installed along with the Brute Ratel C4 sample on the infected machine to be more interesting, as it was completely ignored by other vendors covering this same sample.
As we mentioned, the x64 rootkit, which we dubbed the “Husky” Rootkit, was dropped along with the Brute Ratel payload.
The kernel driver was packed with VMProtect and signed with a certificate issued to “SHANGMAO CHEN” (Figure 4).
Figure 4: The certificate used by the rootkit.
Since this DriverEntry (Figure 5) function is packed and obfuscated, it is hard to gather any information from it. It starts with a series of unconditional branch instructions (jmp) and basically leads to the VMProtect unpacking stub.
Figure 5: A VMProtected DriverEntry showing an unconditional branch instruction as its first instruction.
But after unpacking it, we found functions like GsDriverEntry that contain much more information, as well as important strings (Figure 6) that we can use in our analysis.
Figure 6: Disassembly of a branch from GsDriverEntry containing strings of URLs with thpt (mixed up version of HTTP) as its URL protocol.
The rootkit interacts directly to and from \\Device\Tcp in order to communicate. For that reason, connections are hidden from user-mode tools such as netstat and tcpview running on the infected machine.
An alternative is to use Wireshark on the VM host machine to tap into the shared network interface of the guest machine in order to monitor all of the communication traffic of the infected VM (Figures 7 and 8).
Figure 7: Wireshark network capture of the traffic initiated by the rootkit.
The malware communicates with several domains and relative paths for each domain.
Figure 8: Web request and response from the server to the /xccdd path in the URL shows the response payload.
The specific HTTP traffic that caught our attention were some images (JPEG – JFIF Header) that were downloaded from the following URL: http://pic.rmb.bdstatic.com//bjh/.jpeg.
The JPEG files (Figure 9) contained pictures of dogs that look quite innocent, so I named the rootkit “Husky” after those images. I must add a disclaimer that this is evidence that I have no idea about dog species since I was later told that none of these images are actually of a husky.
Figure 9: A picture of a dog that looked to me like a husky and contained a piggybacked payload.
Each JPEG also had a steganographic payload in the form of data concatenated to the end of the picture at offset 0x1769 after a separator of multiple 0’s (Figure 10).
Figure 10: Hexview of the separator between the end of the picture and the beginning of the piggybacked payload in the .jpg with a picture of a dog.
By looking at the data, we can see that the first 32 bytes are the same as the server response from the previous request to hxxp://rxeva6w.com:10100/xccdd in hexlified format (Snippet 10).
Snippet 10: First 32 bytes of the payload similar across different payloads.
Ironically, the domain rxeva6w.com has 0/88 detections (Figure 11).
Figure 11: VirusTotal shows 0/88 detection rate on the rveva6w.com domain.
The Encryption/Decryption algorithm used by the HTTP payloads is a slightly modified DES algorithm with the key “j_k*a-vb” (Figure 12).
Figure 12: The decryption key is passed to the DES decryption function.
Apart from communicating over HTTP and hiding connections, this rootkit is also able to load new modules downloaded from different URLs.
Obviously, this rootkit packs additional functionality that we do not cover in this blog, so we may publish in a follow-up blog post or further update about this in the future as we continue our analysis.
Case Study #2: Mingloa (CopperStealer) Rootkit
Mingloa malware was first discovered and named by ESET in 2019.
It was later covered by Proofpoint in this blogpost and was also dubbed CopperStealer.
It is believed that Mingloa has Chinese origins, hence its name. This is due to a short routine in the user-mode component that checks if the locale is not Simplified Chinese (Figure 13) or else exits.
Figure 13: Simplified Chinese locale check.
The original blogpost by Proofpoint states the following: “The analyzed sample also can drop and load a kernel driver. The purpose of this driver is currently unknown.“ Of course, this statement led us to investigate.
As noted in the Proofpoint research, the malware contains the ability to find and steal saved browser passwords. In addition to the saved browser passwords, the malware uses stored cookies to retrieve a User Access Token from Facebook.
This is one of many cases where blanket credential and security token protection techniques like those included in CyberArk Endpoint Privilege Manager can significantly limit the impact of credential stealers such as CopperStealer. If these techniques are used, CopperStealer would fail to scrape the data from the infected machine (for more details on scraping of passwords from browsers, see a previous blog from CyberArk Labs).
File size 21.27 KB (21784 bytes)
This malicious kernel module was compiled for both x86 and x64 architectures.
Figure 14: Breakdown of the malware attack scenario.
The driver is signed with a certificate that was issued to 大连纵梦网络科技有限公司 (Figure 15), which translates to “Dalian Longmeng Network Technology Co. Ltd” or “Dalian Morningstar Network Technology.” It is possible this certificate was stolen from an infected machine or leaked by an employee.
Figure 15: The certificate issued to “Dalian Longmeng Network Technology” used to sign the driver.
The Setup From User-Mode
Let’s first look at the user-mode malware infection routine that is supposed to deploy the driver (Figure 16).
Figure 16: Disassembly of the user-mode component execution-flow to install the driver.
Looking at this snippet, we can see that the InstallDriver function receives a single argument and is first called with the argument value of 0. The second time, it is called with an argument value of 1.
If we look closely at InstallDriver, we see that it first tries to create a semaphore (Figures 17 and 18), then checks the Windows version. If any of these calls fail, it will exit without doing anything.
Figure 17: Disassembly of the beginning of the InstallDriver function in the binary, where it calls the CreateSemaphoreWrapper.
If the previous checks succeed, then the malware will proceed, stopping and deleting any services with the same name and finally comparing the shouldInstallDriver argument to 0.
Figure 18: Disassembly of the CreateSemphoreWrapper function.
If the value of shouldInstallDriver is equal to 0, the function will return without any more instructions executed. Otherwise, it will proceed with installing the appropriate driver (Figure 19) embedded into the binary, according to the system architecture.
Figure 19: Disassembly of the InstallDriver function describing the flow of installing a driver on the system.
This part of the code also contains a logic bug that prevents this driver from ever being loaded.
The first call to InstallDriver, which is supposed to only delete any existing driver, would also create a semaphore.
The second call, which is supposed to also install the driver, would exit prematurely before ever installing the driver since the semaphore already exists.
This logic bug is somewhat of a mystery since malware is usually tested for these types of errors. In this case, it was either deployed in haste without any testing or was not meant to be deployed yet to any infected machines.
The kernel-mode component of this malware is a Legacy File-System Filter Driver, which, unlike the more modern mini-filter driver, can modify system behavior without the use of callback filtering functions such as pre-operation callback routine or post-operation callback routine.
Legacy File-System Filter Drivers can modify file-system behavior directly and are called for every I/O operation such as CREATE, READ and WRITE.
By looking at the DriverEntry (Figure 20), we see that two major functions routines are assigned IRP_MJ_READ and IRP_MJ_SET_INFORMATION. Additionally, it registers two callback functions — one by using CmRegisterCallback and the other by using IoRegisterFsRegistrationChange.
Figure 20: Disassembly of the DriverEntry of the Mingloa rootkit driver.
When IoRegisterFsRegistrationChange is called, a function pointer to DriverNotificationRoutine, whose purpose is to either attach or detach the filter driver depending on whether the file-system is active or not, is passed to it (Figure 21).
Figure 21: Disassembly of the DriverNotificationRoutine function.
The malware authors have created the driver for the following functionality that is based on the filter driver:
- Self-defense: Protection against removal
- Registry Key deletion prevention (Windows Service)
- Reading prevention for a denylist of files (except for an allowlist of processes)
Self-defense: Protection Against Removal
By attaching the driver as a filter driver to the file-system and implementing the IRP_MJ_SET_INFORMATION (Figure 22), the authors can check the filename that is meant to be deleted within the denylist.
Figure 22: Disassembly of the IrpMjSetInformationHandler function.
If the filename is denylisted, the handler will return STATUS_ACCESS_DENIED and will halt the processing of the IRP. Otherwise, it will pass it on to the underlying driver in the device stack (Figure 23).
Figure 23: Disassembly of the IrpMjGenericHandler function.
Registry Key Deletion Prevention
The Registry Key Deletion Prevention feature prevents the deletion of the registry keys and values associated with the Windows service for the kernel driver.
The way this feature works is by registering a RegistryCallback routine that is triggered for every registry change and comparing the registry path with the service’s path.
Prevent Reading of Denylisted Files (Except for Allowlisted Processes)
This feature uses the same file-system filter driver mechanism described in the Self-Deletion Prevention for IRP_MJ_READ (Figure 24).
Figure 24: Disassembly of the IrpMjReadHandler function.
Basically, it first checks the name of the file being accessed, then checks whether it contains or ends with one of the following denylisted strings:
- \\Login Data\x00
If the string is not denylisted, then the filter function will forward the IRP to the underlying driver in the device stack. But if the string is denylisted, it will first check whether the process attempting to access the file is an allowlisted process from the following list:
If the process name is allowlisted again, the filter function will forward the IRP to the underlying driver in the device stack. But if it is not, it will block the request by returning STATUS_ACCESS_DENIED, causing the read request to fail (Figure 25).
Figure 25: An example of an attempt to output the contents of the cookies.db file when the rootkit is loaded.
In multiple instances, the rootkit hides important strings such as the filename denylist or the process name allowlist with the following obfuscation. It initializes a string with REGISTRY\MACHINE\SOFTWARE and uses different bitwise arithmetic manipulations (Figure 26) to uncover the multiple strings, such as:
Figure 26: Disassembly view of the string de-obfuscation technique.
Although we would have liked to create a script to uncover these obfuscated strings, unfortunately, the authors made it hard for us to do so by randomizing the bitwise operations and values used for every string.
Hunting For Rootkits
Unlike user-mode malware, which imports mainly from libraries such as kernel32.dll and ntdll.dll, kernel-mode rootkits import their API functions almost exclusively from ntoskrnl.exe, which is the kernel itself. This fact is useful while hunting for rootkits in VirusTotal (VT) since it makes it easy to find drivers with malicious intent.
For instance, we can use the following query (Snippet 11):
Snippet 11: An example of a VirusTotal query to find malicious drivers.
The query will look for PE format files that are not signed or trusted and import them from ntoskrnl.exe.
Another option is to use a Yara rule when looking for a more specific set of files.
We could also employ a unique API usage with an additional binary pattern or strings to find new samples of our malicious driver or rootkit.
Just like when we’ve already analyzed a sample and want to find similar files (older or newer), we could use some properties of the code, such as the tag used in ExAllocatePoolWithTag and .pdb symbols to find related files to our initial binary.
An example of such a rule would be as follows (Snippet 12):
Snippet 12: An example for a Yara rule to hunt for malicious drivers (a.k.a. rootkits).
Conclusion: “Rootkits Are Not a Thing of the Past”
As we have seen in the case studies in this blog, rootkits are still active and targeting modern versions of Windows, including Windows 10 and 11 in both x86 and x64 architectures.
We have seen that rootkits have evolved from Hooking and DKOM-based techniques, which we covered in the last blog, to other techniques like file-system filter drivers and signed drivers by stolen certificates to avoid triggering PatchGuard and “bypass” DSE mitigations, as well as EDR (endpoint detection and remediation) solutions.
Products such as CyberArk Endpoint Privilege Manager can prevent such threats from succeeding by using least privilege controls or by just removing the administrator account from the system and thus preventing new drivers from being installed, as no unprivileged user on the system has the permissions to install a driver.
原文始发于Rotem Salinas：Fantastic Rootkits and Where to Find Them (Part 2)
转载请注明：Fantastic Rootkits and Where to Find Them (Part 2) | CTF导航
你是最广泛的人 深入解释了一些东西 非常有用 非常感谢