I was recently on an engagement where we phished in and ran into UAC which gave me more trouble than I expected. When a user logs onto Windows, a logon session is created and the credentials are tied into an authentication package inside of the logon session. Whenever a process wants to act as a user or use the user’s credentials, it uses a token. These tokens are tied to the logon sessions and ultimately determine how the credential is used. In the case of User Access Control (UAC) and Administrative users, the token is effectively split into two levels. Tokens have different integrity levels:
- Low – Very restrictive, commonly used in sandboxing
- Medium – Regular user
- High – Administrative privileges
- System – SYSTEM privileges
UAC splits the Administrative user’s token into a medium and a high integrity token. When that user tries to run something as an administrator, a prompt is shown which they must accept, which then the high integrity token is then applied to that process or thread.

A UAC bypass is going from the Administrative user’s medium integrity token to high integrity token without having to interact with the prompt. In Cobalt Strike, this can be observed in a medium integrity context by typing get privs
.In addition, the user will not have an asterisk * next to their name.


UAC Bypasses are not anything new and have existed for quite sometime, an exhaustive list can be found here: https://github.com/hfiref0x/UACME#usage
Tons of PowerShell scripts and some C# tools exist around using some of these techniques in order to bypass UAC. There’s even Cobalt Strike Aggressor scripts to automate it for you. A lot of the UAC bypasses in the aforementioned page have been remediated, however there’s still a few that exist. One that I’m particular of is my from my co-worker’s, @enigma0x3 & @mattifestation, research which DLL hijacks the ‘SilentCleanup’ scheduled task, which more can be read about here: https://enigma0x3.net/2016/07/22/bypassing-uac-on-windows-10-using-disk-cleanup/
The problem I ran into, was these tools and scripts leveraged the built-in payload generator in Cobalt Strike, which always immediately get picked up by AV and EDRs. My goal then was to find out a way to generate a DLL that can run shellcode and not be picked up by AV. Finding a specific shellcode runner that spits out a DLL turned up short, however since EXEs and DLLs are both PEs, I figured I could just modify an existing shellcode runner to compile into a DLL.
After a bit of tinkering with quite a few shellcode runners, I ended up using one of my co-worker’s, @djhohenstein, projects, CSharp SetThreadContext. which stemmed from @_xpn_‘s work here. The beauty of this project is that it automatically determines an executable to spawn into, which avoids Get-InjectedThread.
After struggling for a bit on how to get the project to compile a working .DLL instead of an .EXE, I (once again) stumbled upon @_xpn_‘s work here. The TL;DR of it, is there is a NuGet package that will automatically export a function from the project so an entry point is available when called with rundll32.exe
. Keep in mind this is for 64-bit architecture.
First, generate the payload using Cobalt Strike (or whatever C2 you prefer).

Follow Dwight’s instructions on generating the encrypted.bin file here. Build the .EXE, which is the default output type, and ensure that it successfully works and establishes a beacon. Next, install the NuGet package ‘DllExport’ by right clicking on the solution an selecting ‘Manage NuGet Packages for Solution…’

Click on ‘Browse’ and search for ‘DllExport’, then install the package. Once it finishes installing, it will run a .bat file to set up the DLL Export type. Ensure that the ‘Runner’ project is selected, then ensure the settings match the ones pictured below.

Once setup, it will ask to reload the project, which choose ‘Yes’ to. Next, add the DllExport
attribute right before the Main function. At the same time, clear the Main function’s argument.

The next steps are import to ensure the DLL is generated in the correct architecture or else the code will not run. I probably spent more time on this part than what was necessary.
At the top of Visual Studio, click the drop down menu where it says ‘Any CPU’ and click on ‘Configuration Manager’

Change the ‘Active Solution platform’ to x64

To the left of the configuration manager menu, change the build to ‘Release’.

Next, right click on ‘Runner’ in the solution explorer and click on ‘Properties’. Change the Output type to ‘Class Library’.

Next, click on the ‘Build’ tab on the left, and check “Optimize code”

Now you can right click on ‘Runner’ in the solution explorer and select ‘Build’ to build the .DLL.
The working DLL will be placed in \CSharpSetThreadContext\Runner\bin\Release\x64
\Runner.dll
Author’s Note: I’m not sure why it requires so much finessing, but I’m open to any optimizations or explanations if anyone knows. Specifically, only the DLL in the \x64\ directory will work, for some reason the one that’s under \Release\ does not contain the entrypoint that should be generated by [DllExport], even though it’s built at the same time as the one in \x64\.
You can then run the DLL (ensure it’s the one in the x64 directory)
rundll32.exe .\Runner.dll,Main
Now that we have a working DLL shellcode runner, we can run it through ConfuserEx to perform basic AV evasion on it.
With a working DLL shellcode runner that will bypass AV (Defender at least), we can then use it for a UAC Bypass. For the actual bypass, I use @chryzsh‘s Aggressor script here which includes an edited version of the C# binary located here. I once again use ConfuserEx on that binary to evade AV (again, at least Defender). The last step is to edit the Aggressor script to not create the built-in Cobalt Strike Payload and upload it. Also change the function after \\temp.dll from ‘Start’ to ‘Main’.

Rename Runner.dll to temp.dll (Or edit the Aggressor script to execute whatever name you want) and upload it to “C:\Users\[User]”. Finally, string it all together to form a UAC bypass.
Credits & Acknowledgements
- @_xpn_ for the blog posts and answering my dumb questions
- @djhohenstein for his C# Shellcode Runner
- @enigma0x3 & @mattifestation for the UAC Bypass technique
- @chryzsh for the Aggressor Script