This page looks best with JavaScript enabled

Windows Hide Process

 ·  ☕ 2 min read · 👀... views

Sometimes, we may need hide our process from various programs in order to achieve specific objectives. For instance, we may want to hide our virus from antivirus software, or conceal our cheat from anti-cheat programs. There are lots of different approaches introduced by blogs in internet. In this article, I will introduce a few methods that I believe are both effective and relatively straightforward.

First and foremost, I want to emphasize at the beginning of this section that this method is not effective. In fact, it may even lead to various problems. EPROCESS, which stands for PCB (Process Control Block) in windows system, has led some individuals to believe that by unlinking EPROCESS, a specific process can be hidden. While this might have been true in older versions of Windows, attempting to hide any process using this approach in newer versions will almost certainly result in BSOD (Blue Screen of Death).

 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

void MyRemoveListEntry(PLIST_ENTRY curNode)
{
  PLIST_ENTRY preNode, nextNode;

  nextNode = curNode->Flink;
  preNode = curNode->Blink;

  if(preNode != NULL)
    preNode->Flink = curNode->Flink;
  if(nextNode != NULL)
    nextNode->Blink = curNode->Blink;

  curNode->Flink = curNode->Blink = curNode;
}

NTSTATUS HideProcess::unlink_process(HANDLE pid)
{
    NTSTATUS nStatus = STATUS_INVALID_PARAMETER;

    if (pid <= (HANDLE)4)
        return nStatus;

    nStatus = Utils::AttachProcess(pid);
    if (!NT_SUCCESS(nStatus))
        return nStatus;

    PCHAR pEProcess = (PCHAR)PsGetCurrentProcess();

    PLIST_ENTRY curNode = (PLIST_ENTRY)((PUCHAR)pEProcess + KernelOffsets::ActiveProcessLinks);
    MyRemoveListEntry(curNode);
    unlink_process_list.push_back(std::make_pair(pid, curNode));

    Utils::DetachProcess();

    return STATUS_SUCCESS;
}

And you have to recover this unlinked node before process exits. You can call this function by Process Callback.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void HideProcess::recover_unlink_process(HANDLE pid) {
    for (auto& item : unlink_process_list) {
        if (pid == item.first) {

            if (!NT_SUCCESS(Utils::AttachProcess((HANDLE)4)))
                KeBugCheck(INVALID_PROCESS_ATTACH_ATTEMPT);

            PLIST_ENTRY head = reinterpret_cast<PLIST_ENTRY>((PUCHAR)PsGetCurrentProcess() + KernelOffsets::ActiveProcessLinks);
            InsertTailList(head, item.second);
            item.first = 0;
            item.second = 0;

            Utils::DetachProcess();
        }
    }
}

Deny Open Process

This approach, using the hook of NtOpenProcess, is an outdated method like unlink eprocess. Althouth this method may not completely remove a specific process from taskmgr, many people deem that denying requests to open any process is another form of hide process. This is because no process can open the specific process or retrieve information about it. The implementation is relatively simple, but it requires bypassing PatchGuard, because this approach denies open process requests through inlinehook of NtOpenProcess.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
NTSTATUS HookedNtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId) {

    // Hide Process
    HideProcess& instance = HideProcess::Instance();
    if (std::find(instance.deny_open_process_list.begin(), instance.deny_open_process_list.end(), ClientId->UniqueProcess) != instance.deny_open_process_list.end()) {
        *ProcessHandle = 0;
        return STATUS_ACCESS_DENIED;
    }

    return o_NtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}

Set Process PID

Not long ago, I discovered a method to hide process from a cheat sample. I couldn’t find this process from taskmgr, and I had seen this result by Ydark and NoOneArk.

Ydark
NoOne

Both Ydark and NoOneArk discovered that csrss.exe had been hidden, and they identified the real pid of this process. The question is: why can these ark-tools identify that this process has been hidden, but still retrieve its real pid.

The implementation is simple, too. This sample clears the pid of the process’s EPROCESS.pid variable. The following demo is the implementation method of the sample.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
nStatus = Utils::AttachProcess(pid);
if (!NT_SUCCESS(nStatus))
    return nStatus;

KIRQL Irql;
KeAcquireSpinLock(&spin_lock, &Irql);

PCHAR pEProcess = (PCHAR)PsGetCurrentProcess();
HANDLE now_pid = *reinterpret_cast<HANDLE*>(reinterpret_cast<PUCHAR>(pEProcess) + KernelOffsets::UniqueProcessId);
seted_pid_process_list.emplace_back(std::make_pair(pid, reinterpret_cast<uintptr_t>(pEProcess)));

KeReleaseSpinLock(&spin_lock, Irql);

*reinterpret_cast<HANDLE*>(reinterpret_cast<PUCHAR>(pEProcess) + KernelOffsets::UniqueProcessId) = new_pid;

Utils::DetachProcess();

And we have two points to pay attention to:

  1. Setting EPROCESS.pid = 0 is not advisable, and it can somtimes cause the system to BSOD. Instead, I recommand considering assigning EPROCESS.pid a random value or using the pid of an existing process.
  2. We must recover EPROCESS.pid before the process exits.

Now, the question is that can we improve this approach to prevent ark-tools from retrieving the real pid, and even prevent detecting the presence of this hidden process? Before we can enhance this approach, it is crucial to understand how ark-tools can retrieve the real pid from a hidden process. As we are aware, each process consists of one or more threads, with these threads being represented by objects called ETHREAD in the kernel. ETHREAD records the thread’s pid and associated EPROCESS. Ark-tools enum all the threads and retrieve their pid and EPROCESS, and then compare the ETHREAD.pid with ETHREAD.EPROCESS.pid.

After comprehending the underlying principle behind how Ark-tools discover hidden process and retrieve their real pid, we can improve this approach to thwart Ark-tools from detecting our concealed process. One method to achieve this is by modifying the ETHREAD.Cid.UniqueProcess after setting EPROCESS.pid.

 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
nStatus = Utils::AttachProcess(pid);
if (!NT_SUCCESS(nStatus))
    return nStatus;

KIRQL Irql;
KeAcquireSpinLock(&spin_lock, &Irql);

PCHAR pEProcess = (PCHAR)PsGetCurrentProcess();
HANDLE now_pid = *reinterpret_cast<HANDLE*>(reinterpret_cast<PUCHAR>(pEProcess) + KernelOffsets::UniqueProcessId);
seted_pid_process_list.emplace_back(std::make_pair(pid, reinterpret_cast<uintptr_t>(pEProcess)));

KeReleaseSpinLock(&spin_lock, Irql);

*reinterpret_cast<HANDLE*>(reinterpret_cast<PUCHAR>(pEProcess) + KernelOffsets::UniqueProcessId) = new_pid;

// Clear Thread pid info
if (KernelOffsets::Cid != 0) {
    for (int i = 4; i < 0x40000; i += 4)
    {
        PETHREAD ethrd;
        if (NT_SUCCESS(PsLookupThreadByThreadId((HANDLE)i, &ethrd)))
        {
            if (PsGetThreadProcessId(ethrd) == pid)
                *reinterpret_cast<HANDLE*>((PUCHAR)ethrd + KernelOffsets::Cid) = new_pid;		// ETHREAD.Cid.UniqueProcess
            ObDereferenceObject(ethrd);
        }
    }
}

Utils::DetachProcess();

Using this method, Ydark can not find our hidden process and NoOneArk discover two processes which have the same pid.

3

Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer