With Microsoft continuously improving kernel mitigations and raising the bar for exploiting native kernel components, third-party kernel drivers are becoming a more appealing target for attackers and an important area of research for security analysts. A vulnerability in a signed third-party driver could have a serious impact: it can be abused by attackers to escalate privileges or, more commonly, bypass driver signature enforcement—without the complexity of using a more expensive zero-day kernel exploit in the OS itself.
Computer manufacturers usually ship devices with software and tools that facilitate device management. These software and tools, including drivers, often contain components that run with ring-0 privileges in the kernel. With these components installed by default, each must be as secure as the kernel; even one flawed component could become the Achilles’ heel of the whole kernel security design.
We discovered such a driver while investigating an alert raised by Microsoft Defender Advanced Threat Protection’s kernel sensors. We traced the anomalous behavior to a device management driver developed by Huawei. Digging deeper, we found a lapse in the design that led to a vulnerability that could allow local privilege escalation.
We reported the vulnerability (assigned CVE-2019-5241) to Huawei, who responded and cooperated quickly and professionally. On January 9, 2019, Huawei released a fix: https://www.huawei.com/en/psirt/security-advisories/huawei-sa-20190109-01-pcmanager-en.
In this blog post, we’d like to share our journey from investigating one Microsoft Defender ATP alert to discovering a vulnerability, cooperating with the vendor, and protecting customers.
Detecting kernel-initiated code injections with Microsoft Defender ATP
Starting in Windows 10, version 1809, the kernel has been instrumented with new sensors designed to trace User APC code injection initiated by a kernel code, providing better visibility into kernel threats like DOUBLEPULSAR. As described in our in-depth analysis, DOUBLEPULSAR is a kernel backdoor used by the WannaCry ransomware to inject the main payload into user-space. DOUBLEPULSAR copied the user payload from the kernel into an executable memory region in lsass.exe and inserted a User APC to a victim thread with NormalRoutine targeting this region.
Figure 1. WannaCry User APC injection technique schematic diagram
While the User APC code injection technique isn’t novel (see Conficker or Valerino’s earliest proof-of-concept), detecting threats running in the kernel is not trivial. Since PatchGuard was introduced, hooking NTOSKRNL is no longer allowed; there’s no documented way drivers could get notification for any of the above operations. Hence, without proper optics, the only sustainable strategy would be applying memory forensics, which can be complicated.
The new set of kernel sensors aim to address this kind of kernel threat. Microsoft Defender ATP leverages these sensors to detect suspicious operations invoked by a kernel code that might lead to code injection into user-mode. One such suspicious operation, though not related to WannaCry, DOUBLEPULSAR, or other known kernel threats, triggered this investigation that led to our discovery of a vulnerability.
Investigating an anomalous code injection from the kernel
While monitoring alerts related to kernel-mode attacks, one alert drew our attention:
Figure 2. Microsoft Defender ATP kernel-initiating code injection alert
The alert process tree showed an abnormal memory allocation and execution in the context of services.exe by a kernel code. Investigating further, we found that an identical alert was fired on another machine around the same time.
To get a better understanding of the observed anomaly, we looked at the raw signals we got from the kernel sensors. This analysis yielded the following findings:
- A system thread called nt!NtAllocateVirtualMemory allocated a single page (size = 0x1000) with PAGE_EXECUTE_READWRITE protection mask in services.exe address space
- The system thread then called nt!KeInsertQueueApc to queue User APC to a services.exe arbitrary thread with NormalRoutine pointing to the beginning of the executable page and NormalContext pointing to offset 0x800
The payload copied from kernel mode is divided into two portions: a shellcode (NormalRoutine) and a parameter block (NormalContext). At this point, the overall behavior looked suspicious enough for us to proceed with the hunting. Our goal was to incriminate the kernel code that triggered the alert.
Incriminating the source
In user-mode threats, the caller process context could shed light on the actor and link to other phases in the attack chain. In contrast, with kernel-mode threats, the story is more complicated. The kernel by nature is asynchronous; callbacks might be called in an arbitrary context, making process context meaningless for forensics purposes.
Therefore, we tried to find an indirect evidence to third-party code loaded into the kernel. By inspecting the machine timeline, we found that several third-party drivers were loaded earlier that day.
We concluded based on their file path that they are all related to an app from Huawei called PC Manager, a device management software for Huawei MateBook laptops. The installer is available on Huawei website, so we downloaded it for inspection. For each Huawei driver we used dumpbin.exe to examine imported functions.
And then we had a hit:
Figure 3. dumpbin utility used to detect user APC injection primitives
HwOs2Ec10x64.sys: Unexpected behavior from a driver
Hunting led us to the kernel code that triggered the alert. One would expect that a device management software would perform mostly hardware-related tasks, with the supplied device drivers being the communication layer with the OEM-specific hardware. So why was this driver exhibiting unusual behavior? To answer this question, we reverse-engineered HwOs2Ec10x64.sys.
Our entry point was the function implementing the user APC injection. We found a code path that:
- allocates RWX page in some target process;
- resolves CreateProcessW and CloseHandle function pointers in the address space of the target process;
- copies a code area from the driver as well as what seemed to be a parameter block to the allocated page; and
- performs User APC injection targeting that page
The parameter block contains both the resolved function pointers as well as a string, which was found to be a command line.
Figure 4. User APC injection code
The APC normal routine is a shellcode which calls CreateProcessW with the given process command line string. This implied that the purpose of the code injection to services.exe is to spawn a child process.
Figure 5. User shellcode performing process creation
Inspecting the xrefs, we noticed that the injection code originated from a create-process notify routine when Create = FALSE. Hence, the trigger was some process termination.
But what command does the shellcode execute? Attaching a kernel debugger and setting a breakpoint on the memcpy_s in charge of copying the parameters from kernel to user-mode revealed the created process: one of Huawei’s installed services, MateBookService.exe, invoked with “/startup” in its command line.
Figure 6. Breakpoint hit on the call to memcpy_s copying shellcode parameters
Why would a valid service be started that way? Inspecting MateBookService.exe!main revealed a “startup mode” that revived the service if it’s stopped – some sort of watchdog mechanism meant to keep the Huawei PC Manager main service running.
Figure 7.MateBookService.exe /startup code path
At this point of the investigation, the only missing piece in the puzzle was making sure the terminated process triggering the injection is indeed MateBookService.exe.
Figure 8. Validating terminated process identity
The code path that decides whether to inject to services.exe uses a global list of watched process names. Hitting a breakpoint in the iteration loop revealed which process was registered: it was MateBookService.exe, as expected, and it was the only process on that list.
Figure 9. Breakpoint hit during process name comparison against global list
HwOs2Ec10x64.sys also provided process protection against external tampering. Any attempt to force MateBookService.exe termination would fail with Access Denied.
Abusing HwOs2Ec10x64.sys process watch mechanism
The next step in our investigation was to determine whether an attacker can tamper with the global watched process list. We came across an IOCTL handler that added an entry to that list. MateBookService.exe process likely uses this IOCTL to register itself when the service starts. This IOCTL is sent to the driver control device, created from its DriverEntry.
Figure 10. HwOs2Ec10x64.sys control device creation with IoCreateDevice
Since the device object is created with IoCreateDevice, Everyone has RW access to it. Another important observation was that this device isn’t exclusive, hence multiple handles could be opened to it.
Nevertheless, when we tried to open a handle to the device \\.\HwOs2EcX64, it failed with Last Error = 537, “Application verifier has found an error in the current process”. The driver was rejecting our request to open the device. How is access enforced? It must be on the CreateFile path; in other words, in HwOs2Ec10x64.sys IRP_MJ_CREATE dispatch routine.
Figure 11. IRP_MJ_CREATE dispatch routine
This function validates the calling process by making sure that the main executable path belongs to an allow list (e.g., C:\Program Files\Huawei\PCManager\MateBookService.exe). This simple check on the initiating process name, however, doesn’t guarantee the integrity of the calling process. An attacker-controlled instance of MateBookService.exe will still be granted access to the device \\.\HwOs2EcX64 and be able to call some of its IRP functions. Then, the attacker-controlled process could abuse this capability to talk with the device to register a watched executable of its own choice. Given the fact that a parent process has full permissions over its children, even a code with low privileges might spawn an infected MateBookService.exe and inject code into it. In our proof-of-concept, we used process hollowing.
Figure 12. Procmon utility results showing POC process start/exit & IL
Because watched processes are blindly launched by the watchdog when they’re terminated, the attacker-controlled executable would be invoked as a child of services.exe, running as LocalSystem, hence with elevated privileges.
Figure 13. Procexp utility process-tree view showing LPE_POC running as LocalSystem
Responsible disclosure and protecting customers
Once we had a working POC demonstrating the elevation of privilege from a low-integrity attacker-controlled process, we responsibly reported the bug to Huawei through the Microsoft Security Vulnerability Research (MSVR) program. The vulnerability was assigned CVE-2019-5241. Meanwhile, we kept our customers safe by building a detection mechanism that would raise an alert for any successful privilege escalation exploiting the HwOs2Ec10x64.sys watchdog vulnerability as we described.
Figure 14. Microsoft Defender ATP alerting on the privilege escalation POC code
Abusing a second IOCTL handler
Having been able to freely invoke IOCTL handlers of the driver from user-mode, we looked for other capabilities that can be abused. We found one: the driver provided a capability to map any physical page into user-mode with RW permissions. Invoking this handler allowed a code running with low privileges to read-write beyond the process boundaries—to other processes or even to kernel space. This, of course, means a full machine compromise.
We also worked with Huawei to fix this second vulnerability, which was assigned CVE-2019-5242. Huawei addressed the flaw in the same security advisory: https://www.huawei.com/en/psirt/security-advisories/huawei-sa-20190109-01-pcmanager-en.
We presented our research at the Blue Hat IL Conference in February. Watch the video recording here, and get the slide deck here.
While the original alert turned out to be benign, in the sense that it didn’t detect an actual kernel threat like DOUBLEPULSAR, it did trigger an investigation that eventually led us to finding vulnerabilities. The two vulnerabilities we discovered in the driver prove the importance of designing software and products with security in mind. Security boundaries must be honored. Attack surface should be minimized as much as possible. In this case, the flaws could have been prevented if certain precautions were taken:
- The device object created by the driver should be created with a DACL granting SYSTEM RW access (since only the vendor’s services were communicating directly with the driver)
- If a service should persist, developers should check that it’s not already provided by the OS before trying to implement a complex mechanism
- User-mode shouldn’t be allowed to perform privileged operations like writing to any physical page; if needed, the driver should do the actual writing for well-defined, hardware-related scenarios
Microsoft’s driver security checklist provides some guidelines for driver developers to help reduce the risk of drivers being compromised.
Our discovery of the driver vulnerabilities also highlights the strength of Microsoft Defender ATP’s sensors. These sensors expose anomalous behavior and give SecOps personnel the intelligence and tools to investigate threats, as we did.
Anomalous behaviors typically point to attack techniques perpetrated by adversaries with only malicious intent. In this case, they pointed to a flawed design that can be abused. Nevertheless, Microsoft Defender ATP exposed a security flaw and protected customers before it can even be used in actual attacks.
Not yet reaping the benefits of Microsoft Defender ATP’s industry-leading optics and detection capabilities? Sign up for free trial today.
Amit Rapaport (@realAmitRap)
Microsoft Defender Research team
Talk to us
Questions, concerns, or insights on this story? Join discussions at the Windows Defender ATP community.
Follow us on Twitter @MsftSecIntel.