Windows x64

进程隐藏:

断开进程活动链,抹除全局句柄表自己的内容,修改EPROCESS的PID貌似也行……(未完待续)

通过进程活动链

偏移是从内核.exe查出来的,多少有点拙劣,不是很想用API,怕Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VOID Traversal_Process()
{
PEPROCESS currentProcess = PsGetCurrentProcess();
PLIST_ENTRY currentEntry = (PLIST_ENTRY)((ULONG_PTR)currentProcess + 0x188); // ActiveProcessLinks 的偏移量,WinDbg找出来的

PLIST_ENTRY Temp_PList_Entry = currentEntry->Flink;
CHAR* FileName = *(CHAR**)((ULONG64)currentProcess + 0x2e0);
KdPrint(("进程:%s\n", FileName));
while (Temp_PList_Entry!=currentEntry)
{
FileName = (CHAR*)((ULONG64)Temp_PList_Entry - 0x188 + 0x2e0);
KdPrint(("PID:0x%x 进程:%s\n", *(ULONG64*)((ULONG64)Temp_PList_Entry - 0x8), FileName));
Temp_PList_Entry = Temp_PList_Entry->Flink;
}
}

效果如下:

1725971176400

优点:

  • 简单直接:该方法比较标准,通过遍历 ActiveProcessLinks 链表可以很方便地枚举所有进程。
  • 效率高:双向链表结构比较紧凑,遍历开销较小。
  • 进程管理相关性强:Windows 内核使用这个链表来维护进程,所有正常运行的进程都会在链表中。

缺点:

  • 容易被Rootkit隐藏:一些恶意软件或Rootkit可以通过从 ActiveProcessLinks 链表中移除特定进程的节点,来实现进程隐藏。这种情况下,隐藏的进程无法被这种方法检测到。

遍历PspCidTable

PspCidTable 是一个内核句柄表,存放进程和线程的内核对象(EPROCESS 和 ETHREAD),并通过 PID 和 TID 进行索引(所以进程ID和线程ID不可能相同),ID 号以 4 递增。

然后我查阅资料发现
PsLookupProcessByProcessId 不是通过 EPROCESS 的双向链表(ActiveProcessLinks)去获取进程,而是使用全局的进程/线程句柄表( PspCidTable),在内核态有很多优点,特别是在性能、并发安全性和系统一致性方面。

所以首先去WDK看看这个函数在低版本的实现:

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
NTSTATUS
PsLookupProcessByProcessId(
__in HANDLE ProcessId,
__deref_out PEPROCESS *Process
)

/*++

Routine Description:

This function accepts the process id of a process and returns a
referenced pointer to the process.

Arguments:

ProcessId - Specifies the Process ID of the process.

Process - Returns a referenced pointer to the process specified by the
process id.

Return Value:

STATUS_SUCCESS - A process was located based on the contents of
the process id.

STATUS_INVALID_PARAMETER - The process was not found.

--*/

{

PHANDLE_TABLE_ENTRY CidEntry;
PEPROCESS lProcess;
PETHREAD CurrentThread;
NTSTATUS Status;

PAGED_CODE();

Status = STATUS_INVALID_PARAMETER;

CurrentThread = PsGetCurrentThread ();
KeEnterCriticalRegionThread (&CurrentThread->Tcb);

CidEntry = ExMapHandleToPointer(PspCidTable, ProcessId);
if (CidEntry != NULL) {
lProcess = (PEPROCESS)CidEntry->Object;
if (lProcess->Pcb.Header.Type == ProcessObject &&
lProcess->GrantedAccess != 0) {
if (ObReferenceObjectSafe(lProcess)) {
*Process = lProcess;
Status = STATUS_SUCCESS;
}
}

ExUnlockHandleTableEntry(PspCidTable, CidEntry);
}

KeLeaveCriticalRegionThread (&CurrentThread->Tcb);
return Status;
}

发现是通过 ExMapHandleToPointer 这个函数实现的

然后又在 ExMapHandleToPointer 函数发现是在 ExpLookupHandleTableEntry 中获取EPROCESS

1726027403040

于是我们打开win7的 ntoskrnl.exe,分析下win7 x64内核是如何实现PsLookupProcessByProcessId拿到进程信息的

1726027521937

1726027542154

1726027816001

我们发现几个有用的点:

  1. PID必然是4的倍数
  2. 看PspCidTable的最后两个字节,一共有三种处理方式,如图展示

于是我们便可以编写代码,去把这个进程遍历出来

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

VOID Traversal_Process_ByCid()
{

ULONG64 Handle_Table = *(ULONG64*)0xfffff8a000004890;
ULONG64 Temp = Handle_Table;
for (ULONG64 i = 0; i < 65535; i =i+ 4)
{
Handle_Table = Temp;
ULONG64 PEprocess = 0;
if ((Handle_Table & 3) == 0)
{
PEprocess = *(ULONG64*)(Handle_Table + i * 4);
}
else if ((Handle_Table & 3) == 1)
{
Handle_Table = Handle_Table - 1;
PEprocess = *(ULONG64*)((((i & 0xFFFFFFFFFFFFFFFC) - (i & 0x3FC)) >> 7) + Handle_Table) + 4 * (i & 0x3FC);
}

else if ((Handle_Table & 3) == 2)
{
Handle_Table = Handle_Table - 2;
ULONG64 v6 = i & 0xFFFFFFFFFFFFFFFC;
PEprocess = *(ULONG64*)(*(ULONG64*)(((((v6 - (v6 & 0x3FF)) >> 7) - (((v6 - (v6 & 0x3FF)) >> 7) & 0xFFF)) >> 9) + Handle_Table) + (((v6 - (v6 & 0x3FF)) >> 7) & 0xFFF)) + 4 * (v6 & 0x3FF);
}


ULONG64 EPROCESS;
if (MmIsAddressValid((PVOID)PEprocess)&& PEprocess!=NULL) {
EPROCESS = *(ULONG64*)PEprocess & 0xFFFFFFFFFFFFFFFE;
if (MmIsAddressValid((PVOID)EPROCESS) && EPROCESS != NULL) {
if (*(unsigned char*)EPROCESS == 3)
{
CHAR* FileName = (CHAR*)(EPROCESS + 0x2e0);
KdPrint(("进程:%s\n", FileName));
}
}
}

}
}

效果:

1726065985742

暴力内存搜索

我们可以观察发现,EPROCESS所处的内存空间貌似都是集中的,那么我们就可以去扫这一块内存

1726065408184

那么这就需要我们找特征:

例如EPROCESS的第一个字节,必然是3,代表应用程序

1726065505692

还有其他的,可以去用MmIsAddressValid检测是否是合法地址辅助判断

总之这种方法,进程是几乎不可能隐藏的,可以说是无解,但是效率较低