Side-by-Side with HelloJackHunter: Unveiling the Mysteries of WinSxS

This post explores Windows Side-by-Side (WinSxS) and DLL hijacking, deep-diving some tooling I've written and some of the fun along the way.

Side-by-Side with HelloJackHunter: Unveiling the Mysteries of WinSxS
Chasing Sunset, photos.zsec.uk

This post on Windows Side-by-Side loading is a deeper dive into research I picked up at the tail end of 2023, in my free time between Christmas and New Year's.

As we know, Dynamic-link library(DLL) Side loading / DLL Hijacking is nothing new, nor is Windows Side-by-Side (WinSxS); however, side loading is handy from an adversarial tradecraft perspective, be it for establishing initial access, persistence, privilege escalation, or execution within an environment.

I picked up the research after reading a great post from John Carroll about ExpLoading, which is a technique for hijacking search orders from the current directory. At the time, I was also looking into WinSxS, as someone had mentioned in a blog post, but the blog lacked any proof of concept code, leading me down my rabbit hole.

Thus, a lot of this tooling and blog post were born; during my research, I identified work by another researcher who had dove into DLL hijacking and function export and  proxying in the past:

While doing this research, I leveraged Aaron's code and rewrote it in Python3, which I've forked here. Still, the original intention of this tooling is similar to what HelloJackHunter does, so the rewrite was warranted. Building upon others' work is how we learn and improve.

Side Loading / Hijacking 101

Dynamic-Link Library (DLL) search order hijacking, often shortened to DLL hijacking, exploits an application's execution flow via external DLLs. By hijacking the search order used to load legitimate content, it is possible to force an application to load a malicious DLL.

When a vulnerable application(I've found a few in the wild that are non-WinSxS binaries, including a CVE I got in 2020 for Nvidia) is set to run with elevated privileges, any malicious DLL loaded into it inherits those elevated privileges, thus enabling privilege escalation. The application's behaviour often remains undisturbed since malicious DLLs are designed to seamlessly load the legitimate ones they replace or in cases where DLL paths aren't explicitly defined.

This discreet DLL launching capability presents myriad opportunities. In scenarios where the use of Rundll32 is impractical, diverting the execution flow of a trusted binary, adhering to the principle of living off the land (LOLBINS), offers a means to deploy malicious DLLs from diverse locations and inject them into legitimate processes.

What is WinSxS?

WinSxS, standing for "Windows Side by Side", is a directory located in C:\Windows\WinSxS, where Windows keeps files essential for installing the operating system alongside backups or different versions of those files. There are a bunch of sub-folders and copies of just about every binary on a Windows environment, which makes it ripe for exploitation. As a result, lots of LOLBINS may bypass execution policies.

WinSxS plays a crucial role in handling updates. When you install updates, new versions of system files are added to the folder. This ensures that applications' latest versions are available, contributing to overall system security and performance.

What this also means is we can leverage legitimate binaries and multiple versions of said binaries for malicious purposes, taking DLL hijacking out of the equation for a moment; what it means is there are several copies of PowerShell and cmd.exe, too; here's an example from a W10 VM:

  • c:\Windows\WinSxS\amd64_microsoft-windows-commandprompt_31bf3856ad364e35_10.0.22621.2506_none_6ae72c5495fc1170\cmd.exe
  • c:\Windows\WinSxS\amd64_microsoft-windows-commandprompt_31bf3856ad364e35_10.0.22621.2506_none_6ae72c5495fc1170\f\cmd.exe
  • c:\Windows\WinSxS\amd64_microsoft-windows-commandprompt_31bf3856ad364e35_10.0.22621.2506_none_6ae72c5495fc1170\r\cmd.exe
  • c:\Windows\WinSxS\amd64_microsoft-windows-powershell-exe_31bf3856ad364e35_10.0.22621.2506_none_48f0644b7dd22b85\powershell.exe
  • c:\Windows\WinSxS\amd64_microsoft-windows-powershell-exe_31bf3856ad364e35_10.0.22621.2506_none_48f0644b7dd22b85\f\powershell.exe
  • c:\Windows\WinSxS\amd64_microsoft-windows-powershell-exe_31bf3856ad364e35_10.0.22621.2506_none_48f0644b7dd22b85\r\powershell.exe

Now, in your environment, these paths may vary; therefore, investigating the version of WinSxS of binaries is always worth doing and may present additional options for execution within an environment.  What I have seen in specific environments is the ability to leverage alternate paths to bypass weak application allowlisting implementations that C:\Windows\System32 might be blocked WinSxS may come to your aid.

Putting the Two Together

Now that we're more clued up on WinSxS and DLL Hijacking, what about combining the two to form an automated identification and hunt workflow? The typical workflow looks like the following:

  1. Hunt out binaries in WinSxS
  2. Map out DLLs being called from $currentdir
  3. Run HelloJackHunter and point it in a for loop at the DLLs

Hunting available binaries is relatively easy and just requires some PowerShell to start; the advice would be to do the research on your own dev system and replicate more in the target environment because none of the one-liners supplied are meant to be opsec safe and are more for highlighting quick paths to get the stuff you need/want.

Mapping out Available Binaries

There are many ways to hunt out binaries. Simply searching in Explorer for *.exe will give you a GUI list, or using something like everything.exe will allow you to copy and export the paths, or merely use PowerShell like so and point it at the WinSxS directory:

GCI -Path C:\Windows\WinSxS -Recurse -Filter *.exe | Select -First 20 | Select Name, FullName, @{l='FileVersion';e={[System.Version]($_.VersionInfo.FileVersion)}} | Group Name | ForEach-Object { $_.Group | Sort-Object -Property FileVersion -Descending | Select-Object -First 1 }

I ran this on a Windows 10 VM and generated a nice list of available binaries, which you can download and play with here(https://github.com/ZephrFish/HelloJackHunter/blob/main/WinSxSBins.txt). Or, if you want to run the above yourself, I'll explain what the command does:

  1. GCI -Path C:\Windows\WinSxS -Recurse -Filter *.exe: This part of the command uses the GCI alias for the Get-ChildItem cmdlet. Which searches recursively (-recurse) for all files with the .exe extension in the C:\Windows\WinSxS directory and child directories.
  2. | Select -first 20: This command part uses the Select-Object Module/cmdlet to select the first 20 items from the list of files obtained from the previous step; you could remove this option to dump them all out and use something like Out-String export to a file.
  3. | select name,fullname,@{l='FileVersion';e={[SYSTEM.version]($_.versioninfo.fileversion)}}: The Select-Object cmdlet is again used to select specific properties from each item in the list. It selects the Name and FullName properties directly. Additionally, it creates a custom property called FileVersion using a calculated expression (@{l='FileVersion';e={[SYSTEM.version]($_.versioninfo.fileversion)}}). This expression extracts the file version information from each file using ($_.versioninfo.fileversion), and [SYSTEM.version] sets it to a System.Version object.
  4. | group Name: This part of the command groups the items based on their Name property. This means that files with the same name will be grouped together.
  5. | %{$_.Group | sort -descending fileVersion | select -first 1}: This part of the command uses the % alias for the ForEach-Object cmdlet to iterate over each group of files. Within each group, it sorts the files in descending order based on their FileVersion property (sort -descending fileVersion), and then selects the first file from each group (select -first 1). This effectively selects the file with the highest version number from each group.

We can modify the above a little bit to store just the path to an object, which we can call in a DLLHiJackChecker script; the object would be as follows:

$LatestBinaries = GCI -Path C:\Windows\WinSxS -Recurse -Filter *.exe | 
    Select -First 20 | 
    Select Name, FullName, @{l='FileVersion';e={[System.Version]($_.VersionInfo.FileVersion)}} | 
    Group Name | 
    ForEach-Object { $_.Group | Sort-Object -Property FileVersion -Descending | Select-Object -First 1 } |
    Select-Object -ExpandProperty FullName

While this is limited to just 20 binaries, the Select -First can be modified to include as many as you want when hunting. Here's an output I ran before to show the object storing many paths:

Once we have a usable list, the next step is to hunt out DLLHiJacking at scale. To do this manually, Process Monitor can be leveraged to search through running processes for all DLLs loaded and called by running applications. The following steps can be taken:

1) Start Process Monitor with the following filters, to identify potentially vulnerable applications and binaries.

  • Result contains NOT FOUND
  • Path ends with .dll

2) Export the running list to a CSV, then the CSV can be parsed with some simple PowerShell:

$logData = Import-Csv C:\PathToFile\log.csv
$logData | Where-Object { $_.Path -like "*\CurrentDirectory\*" } | Select TimeOfDay, Path, Result

Once a list has been achieved of pwnable DLLs, the next step is to mass-build malicious DLLs; this is where HelloJackHunter comes into play. If doing it manually isn't your style, you could try automating procmon too, but it gets a bit dicey with PowerShell, I took some code from StackOverflow/Google/Git and bastardised them together, modified it slightly to work in this scenario:

 # Start ProcMon in the background
 Start-Process "C:\Users\User\procmon.exe" -ArgumentList "/Quiet /Minimized /LoadConfig DLLHijacking.pmc /BackingFile log.pml"

 # Start your application from a text file list of binaries
 $binaryPaths = Get-Content -Path "C:\Users\User\WinSxSBins.txt"
 foreach ($binaryPath in $binaryPaths) {
     if (Test-Path $binaryPath) {
         Start-Process $binaryPath -PassThru
     } else {
         Write-Host "Binary path does not exist: $binaryPath"
     }
 }
 

 Start-Sleep -Seconds 60
 
# Kill Procmon So we can check the results
 Start-Process "C:\Users\User\procmon.exe" -ArgumentList "/Terminate"
 
 # Convert the ProcMon log to a CSV for easier analysis
 & "C:\Users\User\procmon.exe" /OpenLog log.pml /SaveAs log.csv /SaveApplyFilter
 
 
 $logData = Import-Csv log.csv
 $result = $logData | Where-Object { $_.Path -like "*\CurrentDirectory\*" } | Select TimeOfDay, Path, Result
 
 # Display the results
 $result
  
 

To create DLLHijacking.pmcthe following steps can be taken:

  1. Open ProcMon
  • Configure Filters:
  • Go to Filter -> Filter... or press Ctrl+L.
    Add a new filter with the following settings:
  • Field: Operation
  • Condition: is
  • Value: Load Image
  • Action: Include

If you are hunting for non WinSxS binaries, then you might want to add additional filters to exclude standard and trusted directories from which DLLs are loaded, such as C:\Windows\ or any other directory that you consider safe.

  • Field: Path
  • Condition: excludes
  • Value: C:\Windows\
  • Action: Exclude
  • Apply the filter.

2. Set Up Additional Options:

  • You can specify columns to display relevant information like Path, Result, and Detail which shows the specific DLL path that is being accessed.

3. Save Your Configuration:

  • Go to File -> Save Filter....
  • Save the configuration as a .pmc file, naming it appropriately, such as DLLHijacking.pmc.

HelloJackHunter - Automating Identification

Automating things is always nice to make life a bit easier in hunting; enter a tool I wrote and released today:  HelloJackHunter, a wordplay on HijackHunter. The tool scans available DLLs and leverages dumpbin.exe to extract exported functions, associating each with an example message box. This mapping facilitates the enumeration of functions that can be leveraged. Additionally, it supports various methods for function hooking and execution.

Running through the workflow, the first step is to build the list of available binaries, check if they're vulnerable, and then execute hellojackhunterto export the relevant functions.

As a high-level proof of concept, the following code can be used to prove DLL hijacking; again, this is nothing new.

#include <windows.h>
#include <iostream>

BOOL APIENTRY DllMain(HMODULE hModule,
                      DWORD  ul_reason_for_call,
                      LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        std::cout << "DLL loaded by a process.\n";
        break;
    case DLL_THREAD_ATTACH:
        std::cout << "A thread is created in the current process.\n";
        break;
    case DLL_THREAD_DETACH:
        std::cout << "A thread is exiting cleanly.\n";
        break;
    case DLL_PROCESS_DETACH:
        std::cout << "DLL unloaded from a process.\n";
        break;
    }
    return TRUE;
}

The above is a simple DLLMain function; each of the case options specifies actions undertaken by the DLL when loaded:

  1. DLL_PROCESS_ATTACH:
  • Triggered when the DLL is loaded into the process's memory. This occurs either when the process starts or when the DLL is loaded dynamically using the LoadLibrary function.
  • Typical uses in this case include initializing global or static data, setting up hooks, or allocating needed resources while the DLL is loaded in the calling process.

2. DLL_THREAD_ATTACH:

  • This notification occurs when a new thread is created in a process that has already loaded the DLL. If a DLL is loaded at the start of a process, each new thread that starts up will trigger any code in this condition.
  • This is often used to allocate or initialize data specific to the new thread, such as thread-local storage.

3. DLL_THREAD_DETACH:

  • Triggered when a thread exits cleanly. This does not necessarily occur when a process is terminating, as threads may end before the process concludes.
  • This is an opportunity to clean up resources allocated in DLL_THREAD_ATTACH, like releasing thread-specific data to avoid memory leaks.

4. DLL_PROCESS_DETACH:

  • Occurs when the DLL is unloaded from the process's memory, which can happen either because the process is terminating or because the DLL is being unloaded dynamically by a call to FreeLibrary.
  • This case is typically used for cleanup tasks such as releasing shared resources, unregistering hooks, and deallocating memory used by the DLL to ensure a clean exit with no resource leaks.

The various functions are nothing new about DLL hijacking. The new part where HelloJackHunter comes into play is mapping the available functions to trigger when the DLL is executed.

Known Vulnerable Binaries

So, from my research into WinSxS, I found four consistently vulnerable binaries and several others, but the easiest to demo are shown in the table below. There are hundreds more to test, but instead of doing it all myself, I'd rather share with the community and have people have a look themselves!

(Because Andy can't work out Ghost and it's own magical implementation of markdown, you'll need to scroll horizontally in the table)

Binary Name Path DLL Name / Path
ngentask.exe C:\Windows\WinSxS\amd64_netfx4-ngentask_exe_b03f5f7f11d50a3a_4.0.15912.0_none_d5e7146d665097c0\ngentask.exe mscorsvc.dll
explorer.exe C:\Windows\WinSxS\amd64_microsoft-windows-explorer_31bf3856ad364e35_10.0.22621.3235_none_31b295f9f540d278\explorer.exe cscapi.dll
aspnet_wp.exe C:\Windows\WinSxS\amd64_netfx4-aspnet_wp_exe_b03f5f7f11d50a3a_4.0.15912.0_none_107a08446d17dcf2\aspnet_wp.exe webengine.dll, webengine4.dll
aspnet_regiis.exe c:\Windows\WinSxS\amd64_netfx4-aspnet_regiis_exe_b03f5f7f11d50a3a_4.0.15912.0_none_833013222f03235e\aspnet_regiis.exe webengine4.dll

Each can be exploited with a DLL in the local directory and called to execute functions in search order hijacks. They also serve as technical living off the land binaries as the path will be slightly different per system due to the GUIs in the path name, but the binaries will still be present. To find them easily, the following PowerShell can be used:

(Get-Command binaryname.exe -All).Path

The above command works similarly to the where binary on Windows, and syntax was found in this post.

Detecting This Activity

Ultimately, researching is fun, but how does one detect this activity? Well, I've written a simple Yara rule to detect WinSxS binaries that load DLLs from outside the C:\Windows\WinSxS. This rule would primarily focus on matching patterns in the binary that indicate it is loading DLLs, specifically looking at the paths from which these DLLs are loaded.

rule WinSxS_Outside_DLL_Loading
{
    meta:
        description = "Detect binaries loading DLLs from outside the C:\\Windows\\WinSxS directory"
        author = "ZephrFish"
        reference = "Internal Analysis"
        date = "2024-05-12"

    strings:
        $suspect_path = /LoadLibrary(Ex)?\s*\(.*[^\WinSxS\\][A-Za-z]:\\.*\.dll.*/ nocase wide ascii

    condition:
        $suspect_path
}

However, it's important to note that YARA does not directly monitor system behaviour or real-time DLL loading activities. Instead, YARA is used to scan static files for signatures and patterns. To effectively monitor DLL loading paths in real-time, you would typically use other tools or system monitoring techniques in combination with something like Sysmon or similar.