ASUS UEFI Update Driver Physical Memory Read/Write

Share this…

A short while ago, slipstream/RoL dropped an exploit for the ASUS memory mapping driver (ASMMAP/ASMMAP64) which was vulnerable to complete physical memory access (read/write) to unprivileged users, allowing for local privilege escalation and all sorts of other problems. An aside to this was that there were also IOCTLs available to perform direct I/O operations (in/out instructions) directly from unprivileged usermode, which had additional interesting impacts for messing with system firmware without triggering AV heuristics.

When the PoC was released, I noted that I’d reported this to ASUSa while beforehand, later clarifying that I’d actually reported it to them in March 2015. To be fair to ASUS, they were initially very responsive via email, and particularly via Twitter DM on the@ASUSUK account, and it seems like they had some bad luck with both the engineer working on the fixes and the customer support advisor handling my tickets leaving the company, resulting in a significant delay in the triage and patch processes. However, promises to keep me in the loop were not kept, and I was always chasing them up for answers.

In addition to the ASMMAP bugs, I also reported the exact same bugs in their UEFI update driver (AsUpIO.sys). This driver is deployed as part of the usermode UEFI update toolset, and exposes almost identical functionality which (as slipstream/RoL pointed out) is likely from an NT3.1 example driver that was written long before Microsoft took steps to segregate malicious users from physical memory in any meaningful way.

One additional piece of functionality which I believe was missed from the original ASMMAP vulnerability release was the ability to read/write Model Specific Registers (MSRs) as an unprivileged user. This was, again, a function exposed as an IOCTL in the driver. For those of you not versed in MSRs, they’re implementation-specific registers which contain control and status values for the processor and supporting components (e.g. SMM). You can read more about them in chapter 35 of the Intel 64 Architecture Software Developer’s Manual Volume 3. MSRs are particularly powerful registers in that they offer the ability to enable or disable all sorts of internal functionality on the processor, and are at least theoretically capable of bricking hardware if you abuse them in the wrong way. One of the most interesting MSRs is the Extended Feature Enable Register (EFER) at index 0xC0000080, which contains the No-eXecute Enable (NXE) and Secure Virtual Machine Enable (SVME) bits. Switching the NXE bit off on a live VM in VirtualBox crashes the VM with a “Guru Mediation” error (there’s an age cutoff on people who will get that reference), which I suppose is a novel anti-VM trick on its own, not to mention the intended behaviour of switching off NX on real-steel hardware.

Rather than just providing you with a bit of PoC code, I thought I’d take the opportunity to go through exactly how I discovered the bugs and what approach I took towards reliable exploitation.

Generally speaking, Windows drivers have a number of interfaces through which usermode code may communicate with them. The most important are I/O Request Packets (IRPs), which are sent to a driver when code performs a particular operation on the driver’s device object. The exposed functions which IRPs are sent to are known as Major Functions, examples of which include open, close, read, write, and I/O control (otherwise known as IOCTL). The descriptor structure for a driver object contains an array of function pointers, each pointing to a dispatch function for a major function. These are fantastic targets for bug-hunting in drivers, since they’re usermode-accessible (and often accessible from non-admin accounts) and can often result in local privilege escalation to kernelmode.

The first key thing to look for is whether or not the driver object is accessible as a low-privilege user. It’s all well and good finding a bug which gets you kernel code execution, but if you’ve got to be an admin to exploit it it’s a bit of a non-issue. When a driver goes through its initialisation steps, it usually names itself and creates a device object using the IoCreateDevice API, and a symbolic link to the DosDevices object directory using the IoCreateSymbolicLinkAPI. An example is as follows:

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath)
{
    NTSTATUS status = STATUS_SUCCESS;
    PDEVICE_OBJECT pDeviceObject = NULL;
    UNICODE_STRING driverName, dosDeviceName;
    
    RtlInitUnicodeString(&driverName, L"\\Device\\Example");
    RtlInitUnicodeString(&dosDeviceName, L"\\DosDevices\\Example"); 
    
    status = IoCreateDevice(pDriverObject, 0,
                            &driverName, 
                            FILE_DEVICE_UNKNOWN,
                            FILE_DEVICE_SECURE_OPEN, 
                            FALSE, &pDeviceObject);
    
    // ...
    
    IoCreateSymbolicLink(&dosDeviceName, &driverName);
    
    // ...
    
    return status;
}

In order to check whether or not the driver’s device object is accessible by low-privilege users, we need to know what name it picked for itself. There are a few approaches to this: we could debug the system and breakpoint on the IoCreateDevice API; we could reverse engineer the driver using a tool such as IDA; or we could simply extract all the strings from the binary and look for any that start with “\\Device\\”.

In the case of AsUpIO.sys, dropping it into IDA shows that it does exactly the above, using the name “AsUpdateio”:

AsUpIO_DriverEntry

This now tells us exactly what we should be looking for. In order to inspect the device object and view its discretionary access control list (DACL), we can use WinObj.

AsUpIO_Object_PoorDACL

As we can see here, the Everyone group is given both Read, Write, and Special permissions, allowing the device object to be directly interacted with from low-privilege usermode. Note that these ACEs are not set by the driver; this is a somewhat “hardened” permissions set applied by an up-to-date Windows 10 install, although it is still accessible by everyone. In Windows 8 and earlier, setting a the DACL to null simply results in it having no ACEs, allowing everyone complete access, unless you apply a hardened security policy. This is because, prior to Win10, the root object namespace had no inheritable ACEs.

The snippet above also gives us the address of the HandleIoControl function which is assigned to handle the Create, Close, and IOCTL major functions. Reverse engineering this shows the IOCTL number which is used for mapping memory:

AsUpIO_JumpMemoryMap

(note: the ASUS_MemMap name was set by me; I renamed it after analysing each function in this set of branches to work out their functions)

Since we now have control over the driver, we now want to exploit the bugs. In this case the IOCTL for doing the memory mapping is 0xA040244Ch, which can be found by reverse engineering the HandleIoControl routine we found above. Just as in the original slipstream/RoL exploit, this IOCTL can be used to map any physical memory section to usermode. The downside, from an exploitation perspective, is that the function covers a wide range of potential memory locations, including addresses where the HAL has to translate to bus addresses rather than the usual physical memory. This is fine if we know a specific location we want to map and access, but it becomes a bit fraught if we want to read through all of physical memory; trying to map and read an area of memory reserved for certain hardware might crash or lock up the system, and that’s not much use for a privesc.

The approach I took was to find a specific location in kernel memory which I knew was safe, then map and read that, and use that single operation to gain the ability to reliably read memory over the lifetime of the system. The ideal object to gain control over is \\Device\PhysicalMemory, as this gives us direct usermode access to the physical memory. The first hurdle is that we need a kernel pointer leak to identify the physical address of that object’s descriptor.

First, we want to know what processes have a handle to this object. By running Process Explorer as and administrator (we don’t need to do this in an actual attack scenario), we can see that the System process keeps a handle to it open:

SystemProcess_PhysicalMemoryHandle

Using an undocumented feature of theNtQuerySystemInformation API, i.e. the SystemHandleInformation information class, we can pull out information about every single handle open on the system. The returned structure for each handle looks like the following:

typedef struct _SYSTEM_HANDLE_INFORMATION
{
    DWORD    dwProcessId;
    BYTE     bObjectType;
    BYTE     bFlags;
    WORD     wValue;
    PVOID    pAddress;
    DWORD    GrantedAccess;
}
SYSTEM_HANDLE;

The pAddress field points to the kernel memory address of the object’s descriptor. By enumerating through all open handles on the system and checking for dwProcessId=4 (i.e. the System process) and bObjectType matching the object type ID of a section (this is different between Windows version), we can find all the sections open by the System process, one of which we know will be \\Device\\PhysicalMemory. In fact, System only has three handles open to sections in Windows 10, so we can just give ourselves access to all of them and not worry too much.

Of course, now that we have the address of the section descriptor in kernel memory, we still need to actually take control of that section object somehow. Let’s take a look at the header structure for the object, 0x30 bytes before the section descriptor, in WinDbg:

0: kd> dt nt!_OBJECT_HEADER 0xffffc001`cca13bd0-0x30
   +0x000 PointerCount     : 0n65537
   +0x008 HandleCount      : 0n2
   +0x008 NextToFree       : 0x00000000`00000002 Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : 0x23 '#'
   +0x019 TraceFlags       : 0 ''
   +0x019 DbgRefTrace      : 0y0
   +0x019 DbgTracePermanent : 0y0
   +0x01a InfoMask         : 0x2 ''
   +0x01b Flags            : 0x16 ''
   +0x01b NewObject        : 0y0
   +0x01b KernelObject     : 0y1
   +0x01b KernelOnlyAccess : 0y1
   +0x01b ExclusiveObject  : 0y0
   +0x01b PermanentObject  : 0y1
   +0x01b DefaultSecurityQuota : 0y0
   +0x01b SingleHandleEntry : 0y0
   +0x01b DeletedInline    : 0y0
   +0x01c Spare            : 0x5000000
   +0x020 ObjectCreateInfo : 0x00000000`00000001 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : 0x00000000`00000001 Void
   +0x028 SecurityDescriptor : 0xffffc001`cca12273 Void
   +0x030 Body             : _QUAD

Now, remember earlier when I said that having a DACL set to null gives everyone access? The SecurityDescriptor field here is, in fact, exactly what gets set to null in such a situation. If we overwrite the field with zeroes, then (theoretically) everyone has access to the object. However, this object is a special case: it has the KernelOnlyAccess flag set. This means that no usermode processes can gain a handle to it. We need to switch this off too, so we set the Flags field to 0x10 to keep the PermenantObject flag but clear the rest:

0: kd> eb (0xffffc001`cca13bd0-0x30)+0x1b 0x10
0: kd> eq (0xffffc001`cca13bd0-0x30)+0x28 0
0: kd> dt nt!_OBJECT_HEADER 0xffffc001`cca13bd0-0x30
   +0x000 PointerCount     : 0n65537
   +0x008 HandleCount      : 0n2
   +0x008 NextToFree       : 0x00000000`00000002 Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : 0x23 '#'
   +0x019 TraceFlags       : 0 ''
   +0x019 DbgRefTrace      : 0y0
   +0x019 DbgTracePermanent : 0y0
   +0x01a InfoMask         : 0x2 ''
   +0x01b Flags            : 0x10 ''
   +0x01b NewObject        : 0y0
   +0x01b KernelObject     : 0y0
   +0x01b KernelOnlyAccess : 0y0
   +0x01b ExclusiveObject  : 0y0
   +0x01b PermanentObject  : 0y1
   +0x01b DefaultSecurityQuota : 0y0
   +0x01b SingleHandleEntry : 0y0
   +0x01b DeletedInline    : 0y0
   +0x01c Spare            : 0x5000000
   +0x020 ObjectCreateInfo : 0x00000000`00000001 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : 0x00000000`00000001 Void
   +0x028 SecurityDescriptor : (null) 
   +0x030 Body             : _QUAD

Now the KernelOnlyAccess and SecurityDescriptor fields are zeroed out, we can gain access to the object from usermode as a non-adminstrative user:

PhysicalMemory_NULL_DACL

In a real exploitation scenario we’d do these edits via the driver bug rather than WinDbg, mapping the page containing the object header and writing to it directly.

Disabling the flags and clearing the security descriptor allows us to map the PhysicalMemory object into any process and use it to gain further control over the system, without worrying about the weird intricacies of how the driver handles certain addresses. This can be done by scanning for EPROCESS structures within memory and identifying one, then jumping through the linked list to find your target process and a known SYSTEM process (e.g. lsass), then duplicating the Token field across to elevate your process. This part isn’t really that novel or interesting, so I won’t go into it here.

One tip I will mention is that you can make the exploitation process much more reliable if you set your process’ priority as high as possible and spin up threads which perform tight loops to keep the processor busy doing nothing, while you mess with memory. This helps keep kernel threads and other process threads from being scheduled as frequently, making it less likely for you to hit a race condition and bugcheck the machine. I only saw this happen once during the hundred or so debugging sessions I did, so it’s not critical, but still worth keeping in mind.

In closing, I hope the teardown of this bug and my exploitation process has been useful to you. While you certainly can directly exploit the bug, it’s not without potential peril, and it’s often safer to pivot to a more stable approach.

Take-home points for security people and driver developers in general:

  • WHQL does not mean the code is secure, nor does it even mean the code is stable or safe. Microsoft happily signed a number of these drivers with vulnerability-as-a-feature code within them. These bugs were trivially identifiable; this indicates that WHQL is likely an automated process to ensure adherence to little more than a “don’t use undocumented / unsupported functions” requirement.
  • Ensure that appropriate DACLs are placed on objects, particularly the device object, via the use of IoCreateDeviceSecure and the Attributes parameters to Create* calls (e.g. CreateMutex, CreateEvent, CreateSemaphore). A null DACL mean anyone can access the object.
  • Drivers should not expose administrative functionality (e.g. UEFI updates) to non-administrative users (e.g. the Everyone group). Ensure that object DACLs reflect this.

Take-home points for ASUS:

  • Implement a security contact mailbox as per the guidance inRFC2142 and ensure that it is checked and managed by someone versed in security. Create a page on your website which lists this contact and outlines your expectations from researchers when reporting security issues.
  • Your Twitter support staff are better at communicating with customers than your support ticket people. You could stand to learn from their more informal and responsive model.
  • Ensure that anything assigned to someone who leaves the company is appropriately reassigned with guidance from that individual. This should help ensure that patches don’t end up delayed by 15 months.
  • Get your code assessed by a 3rd party security contractor before releasing it to customers, and ensure that your developers are given appropriate training on secure development practices. The vulnerable code used was likely copied from examples into a number of your drivers, which indicates that problems may be widespread.

Disclosure timeline:

  • 24th March 2015 – Submitted bug as ticket to ASUS (WTM20150324082900771)
  • 25th March 2015 – Acknowledgement from ASUS
  • 25th March 2015 – Sent reply email with additional information.
  • 27th March 2015 – Reply from “J” from ASUS, says engineer has a fix and is liasing with their own security researcher on the matter.
  • < I forgot about the issue for a long time >
  • 4th September 2015 – Sent email to query status of the issue.
  • 7th September 2015 – Reply from “Anthony” from ASUS, informing me that the agent I’d been interacting with before had left the company, asking for more details on the issue.
  • 7th September 2015 – Sent a response with another full report of the issue.
  • 21st September 2015 – No reply, sent a request for a status update.
  • 22nd September 2015 – Contacted @ASUSUK on Twitter. Had conversation via DM trying to get a status update.
  • 28th September 2015 – Chased up @ASUSUK for an update.
  • 29th September 2015 – Reply informing me that the HQ office in Taipei was closed due to a typhoon.
  • 7th October 2015 – Sent another chase-up message to @ASUSUK.
  • 7th October 2015 – Reply from them; no updates from the office but a promise to let me know when the patch is out.
  • 25th November 2015 – Another chase-up DM to @ASUSUK.
  • 25th November 2015 – HQ were offline, told I’d get a reply the next day. No reply came.
  • 9th May 2016 – Still nothing back from ASUS via email or Twitter, sent another chase-up email and DM informing my intent to disclose within 28 days due to the long delays in releasing.
  • 10th May 2016 – Told that Anthony is OOO until Monday.
  • 12th May 2016 – Told that the delays were due to the project leader at HQ leaving, they’re trying to source someone to fix it and push a fix out ASAP.
  • 12th May 2016 – Sent reply asking to be kept in the loop. ASUS replies saying they’ll keep me informed.
  • 12th June 2016 – Disclosed.

Source:https://codeinsecurity.wordpress.com