关于进程有很多可以聊的,也有很多不知道的,所以自然就没法一次性介绍完,希望通过这篇把一些我所知晓的关于进程的事情列一些。
进程的结构
具体什么是进程这里就不多赘述了,毕竟也算是计算机的基础,在windows中,进程定义在一个庞大的结构体中,即struct _EPROCESS
,这个结构体在windows 10下有0xa40字节大小,所以我没法全部列出来。
我们姑且列一部分看一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
struct _EPROCESS
{
struct _KPROCESS Pcb; //0x0
struct _EX_PUSH_LOCK ProcessLock; //0x438
VOID* UniqueProcessId; //0x440
struct _LIST_ENTRY ActiveProcessLinks; //0x448
struct _EX_RUNDOWN_REF RundownProtect; //0x458
...
struct _LIST_ENTRY SessionProcessLinks; //0x4a0
...
UCHAR ImageFileName[15]; //0x5a8
...
struct _LIST_ENTRY JobLinks; //0x5c8
VOID* HighestUserAddress; //0x5d8
struct _LIST_ENTRY ThreadListHead; //0x5e0
...
|
可以看出里面有很多_LIST_ENTRY,这个结构我们在之前有详细展开讲过,这里就不赘述了,具体可以看这个链接https://daliu.net/posts/20241222/#list_entry,由此可见,这个结构体并不是囊括了全部,每一个进程结构体里除了自己,还会通过双链表绑定很多内容,比如进程的结构等等。
既然说到这里,我们首先想要做的事情,自然是知道系统到底有多少进程,我们如何遍历所有的进程。一般呢,我们想要知道进程都是直接打开任务管理器。快捷键:Ctrl + Shift + Esc,(有很多人用Ctrl + Alt + Delete,不知道从windows 7还是windows 10 之后会弹出一个选择页面,然后再选择任务管理器,多了个步骤,所以我都是中上面的组合快捷键)

既然我们可以通过任务管理器直接可视化的看到所有的进程,那我们能不能通过代码的方式在应用层遍历所有的进程呢,答案当然是可以的。
首先,windows给我们封装了系统工具可以直接展示所有的进程,而且放到了系统文件夹里,因此我们可以利用命令行的方式,直接获取。

打开命令行,输入tasklist
,可以看到如图所示:

那我们换到用代码去执行这个命令,依然用我们熟悉的go来实现, 代码如下:
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
|
// getProcessesByConsole
func getProcessesByConsole() {
// 使用 Windows 的任务管理器命令来获取进程信息
cmd := exec.Command("tasklist")
output, err := cmd.Output()
if err != nil {
log.Fatalf("Failed to execute tasklist command: %v", err)
}
// 将输出转换为字符串
outputStr := string(output)
lines := strings.Split(outputStr, "\n")
// 打印表头
fmt.Println("Image Name\tPID\tSession Name\tSession#\tMem Usage")
fmt.Println(strings.Repeat("-", 80))
// 遍历每一行并打印进程信息
for _, line := range lines[3:] { // 跳过表头
if strings.TrimSpace(line) == "" {
continue
}
fmt.Println(line)
}
}
|
执行以下,最终效果跟命令行直接调用差不多

但是仅仅用这种方式执行是不是有点太朴实了点,相当于把tasklist
又给封装了一遍,当然可以再进一步,事实上windows提供了可以遍历的函数,在此先了解一下:
这个函数主要就是用来获取指定进程以及这些进程使用的堆、模块和线程的快照,函数的定义如下:
1
2
3
4
|
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);
|
具体参数的含义比较简单,可以参考这个官方文档:https://learn.microsoft.com/zh-cn/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot,除此,在go的库中也实现了这个函数,我们可以直接调用,这个函数的返回值就是指定快照的打开句柄(是快照,就相当于给进程拍个快照)
然后,我们需要遍历进程,就要用到另外两个函数,Process32First
和Process32Next
,这两个函数分别是用来检索快照中第一个进程,以及记录的下一个进程,函数的定义如下:
1
2
3
4
5
6
7
8
9
|
BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);
BOOL Process32Next(
[in] HANDLE hSnapshot,
[out] LPPROCESSENTRY32 lppe
);
|
其中第二个参数是返回值,返回的是指向ROCESSENTRY32
的指针,而这个结构体定义如下,里面包含了进程相关的一些信息描述:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID;
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
CHAR szExeFile[MAX_PATH];
} PROCESSENTRY32;
DWORD th32ProcessID;// 进程id
DWORD th32ParentProcessID;// 父进程id
CHAR szExeFile[MAX_PATH]; // 进程的可执行文件的名称。
|
接着我们用go的代码来实现这个过程,遍历并打印全部进程的信息:
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
|
// getAllProcessesByCreateToolhelp32Snapshot
func getAllProcessesByCreateToolhelp32Snapshot() {
// 创建进程快照
hSnap, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
if err != nil {
fmt.Printf("Failed to create snapshot: %v\n", err)
return
}
defer syscall.CloseHandle(hSnap)
// 初始化 ProcessEntry32 结构体
var procEntry syscall.ProcessEntry32
procEntry.Size = uint32(unsafe.Sizeof(procEntry))
// 获取第一个进程信息
err = syscall.Process32First(hSnap, &procEntry)
if err != nil {
fmt.Printf("Failed to get first process: %v\n", err)
return
}
// 遍历所有进程
for {
// 将 ANSI 字符串转换为 Go 字符串
processName := syscall.UTF16ToString(procEntry.ExeFile[:])
fmt.Printf("PID: %d, Process Name: %s, Parent PID: %d\n",
procEntry.ProcessID, processName, procEntry.ParentProcessID)
// 获取下一个进程信息
err = syscall.Process32Next(hSnap, &procEntry)
if err != nil {
break
}
}
}
|
执行之后,结果如图,详细的进程信息如下:

当然,如果你想要获取当前进程的信息,还可以直接调用对应的api函数,这个会更直接,就是调用ntdll.dll中的ntQueryInformationProcess
函数,这个api也能返回你所需要的部分信息。
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
|
// getProcessesByNtQueryInformationProcess
func getCurrentProcessesByNtQueryInformationProcess() {
// 获取当前进程的句柄
currentProcess, err := syscall.GetCurrentProcess()
if err != nil {
log.Fatalf("Failed to get current process: %v", err)
}
// 获取进程信息
processInfo := PROCESS_BASIC_INFORMATION{}
processInfoSize := uint32(unsafe.Sizeof(processInfo))
returnLength := int32(0)
ntdll = syscall.NewLazyDLL("ntdll.dll")
ntQueryInformationProcess = ntdll.NewProc("NtQueryInformationProcess")
ret, _, err := ntQueryInformationProcess.Call(
uintptr(currentProcess),
uintptr(ProcessBasicInformation),
uintptr(unsafe.Pointer(&processInfo)),
uintptr(processInfoSize),
uintptr(unsafe.Pointer(&returnLength)),
)
if ret != 0 {
log.Fatalf("NtQueryInformationProcess failed with error code: %d", ret)
}
fmt.Printf("Process ID: %d\n", processInfo.UniqueProcessId)
fmt.Printf("Parent Process ID: %d\n", processInfo.InheritedFromUniqueProcessId)
}
|
调用的结果如图所示:

在应用层我们能够轻易的遍历进程,那试问在驱动层能否做到么,答案是显然如此,驱动层应该是更能轻易实现这个事情,而且是清除的获取对应驱动的每一个结构,首先,我们要知道一个结构体成员,也就是上面介绍的_EPROCESS
中的一个成员,ActiveProcessLinks
。
首先这个成员_LIST_ENTRY
类型的,也就是一个双链表,然后顾名思义可以判断,这个链表上链接的都是活跃的进程,那我们岂不是可以顺着这个链表找到每一个活跃的进程了,既然如此,我们就按照这样的方式编写代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath)
{
pDriverObject->DriverUnload = DriverUnload;
PEPROCESS pCurrentProcess = PsGetCurrentProcess();
if (pCurrentProcess == NULL)
{
return STATUS_SUCCESS;
}
ULONG64 ActiveProcessLinks = (ULONG64)pCurrentProcess + 0x448;
PLIST_ENTRY processHead = (PLIST_ENTRY)ActiveProcessLinks;
while (processHead->Flink != (PLIST_ENTRY)ActiveProcessLinks)
{
char imageName[16] = { 0 };
memcpy(imageName, (ULONG64)processHead + 0x5a8 - 0x448, 15);
DbgPrint("%s\n", imageName);
processHead = processHead->Flink;
}
return STATUS_SUCCESS;
}
|
我们首先要获取自己的进程,所以我们采用PsGetCurrentProcess
这个函数,返回的就是当前进程结构体的指针,然后,我们根据这个地址定位到ActiveProcessLinks
的位置,然后再利用双链表遍历。
其中有两点需要注意,首先,ActiveProcessLinks
的位置是在进程中间,而双链表的每一个节点的位置也是在进程的中间,所以需要进程的地址是需要用ActiveProcessLinks
的每个位置-0x448,其次,为了方便验证我们遍历的是对的,我们需要打印进程的镜像名称,也就是第0x5a8位置的ImageFileName,这是一个不超过16字节的字符数组。
以上驱动最终执行的结果我们在windbg中可以看到:

我们刚刚所确定镜像名称的方式是在知晓_EPROCESS
结构体的结构前提下(window 10),但是,不同的windows版本下,该结构体的结构会有区别,一种勤奋的方法就是把每一个版本的对应获取方式都写上,还一种方法是可以用api直接获取在内核有一个导出函数PsGetProcessImageFileName
但是,当我们在vs 2022中输入这个函数的时候,并没有找到这个符号的定义,所以我们需要用其他的方式。

刚刚已经说了,这个函数是导出函数,用IDA可以验证下,并顺便看下这个函数的构造。

可以看出这个函数确实是导出函数,并且这个函数只有两句就是将rcx+偏移
的地址返回,因为是windows10的内核,也就是我们刚刚说的0x5a8的地址,而rcx
也就是进程结构体的地址了。

所以,我们需要使用另一个函数来帮我们调用PsGetProcessImageFileName
这个函数,另一个函数就是MmGetSystemRoutineAddress
,这个函数的定义如下,入参就是要调用的函数名(是一个UNICODE_STRING
函数名的指针),返回值就是函数指针。
1
2
3
|
PVOID MmGetSystemRoutineAddress(
[in] PUNICODE_STRING SystemRoutineName
);
|
所以,我们可以利用这个函数获取PsGetProcessImageFileName
然后通过这个函数获取镜像名,因此我们更改代码如下:
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
|
#include <ntifs.h>
void DriverUnload(PDRIVER_OBJECT pDriverObject)
{
}
typedef char* (*tPsGetProcessImageFileName)(PEPROCESS pProcess);
tPsGetProcessImageFileName pPsGetProcessImageFileName;
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath)
{
pDriverObject->DriverUnload = DriverUnload;
DbgBreakPoint();
PEPROCESS pCurrentProcess = PsGetCurrentProcess();
if (pCurrentProcess == NULL)
{
return STATUS_SUCCESS;
}
ULONG64 ActiveProcessLinks = (ULONG64)pCurrentProcess + 0x448;
PLIST_ENTRY processHead = (PLIST_ENTRY)ActiveProcessLinks;
while (processHead->Flink != (PLIST_ENTRY)ActiveProcessLinks)
{
char imageName[16] = { 0 };
UNICODE_STRING processName = { 0 };
RtlInitUnicodeString(&processName, L"PsGetProcessImageFileName");
pPsGetProcessImageFileName = MmGetSystemRoutineAddress(&processName);
PEPROCESS processRealHead = (PEPROCESS)((ULONG64)processHead - 0x448);
char* imageTestname = pPsGetProcessImageFileName(processRealHead);
memcpy(imageName, imageTestname, 15);
DbgPrint("%s\n", imageName);
processHead = processHead->Flink;
}
return STATUS_SUCCESS;
}
|
执行最终效果用windbg打印一下,见下图:

其实微软早就给了我们很多丰富的工具来查看进程相关的,这个工具叫做sysinternals,具体可以看下面这个链接,https://learn.microsoft.com/zh-cn/sysinternals/downloads/,其中查看进程的有一个工具叫做Process explorer,打开如图所示。

这个工具很清楚的现实每一个进程的信息,内容比任务管理器更加丰富。具体可以查看这个链接下载,https://learn.microsoft.com/zh-cn/sysinternals/downloads/process-explorer。
除此还有一个推荐,Process Monitor,监控进程的,几乎每一个进程的每个动作都会监控,比如,操作注册表,加载dll文件等等。具体可以参考https://learn.microsoft.com/zh-cn/sysinternals/downloads/procmon这个链接。

今天就介绍这些,后面还会接着继续介绍关于进程的内容,需要一点点整理。