关于进程的一些故事

简介:介绍关于windows进程的一些零碎的知识点。

前言

关于进程有很多可以聊的,也有很多不知道的,所以自然就没法一次性介绍完,希望通过这篇把一些我所知晓的关于进程的事情列一些。

进程的结构

具体什么是进程这里就不多赘述了,毕竟也算是计算机的基础,在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 之后会弹出一个选择页面,然后再选择任务管理器,多了个步骤,所以我都是中上面的组合快捷键)

2

应用层获取

既然我们可以通过任务管理器直接可视化的看到所有的进程,那我们能不能通过代码的方式在应用层遍历所有的进程呢,答案当然是可以的。

首先,windows给我们封装了系统工具可以直接展示所有的进程,而且放到了系统文件夹里,因此我们可以利用命令行的方式,直接获取。

3

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

4

那我们换到用代码去执行这个命令,依然用我们熟悉的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)
    }
}

执行以下,最终效果跟命令行直接调用差不多

5

但是仅仅用这种方式执行是不是有点太朴实了点,相当于把tasklist又给封装了一遍,当然可以再进一步,事实上windows提供了可以遍历的函数,在此先了解一下:

CreateToolhelp32Snapshot

这个函数主要就是用来获取指定进程以及这些进程使用的堆、模块和线程的快照,函数的定义如下:

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的库中也实现了这个函数,我们可以直接调用,这个函数的返回值就是指定快照的打开句柄(是快照,就相当于给进程拍个快照)

然后,我们需要遍历进程,就要用到另外两个函数,Process32FirstProcess32Next,这两个函数分别是用来检索快照中第一个进程,以及记录的下一个进程,函数的定义如下:

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
        }
    }

}

执行之后,结果如图,详细的进程信息如下:

6

当然,如果你想要获取当前进程的信息,还可以直接调用对应的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)
}

调用的结果如图所示:

7

驱动层遍历

在应用层我们能够轻易的遍历进程,那试问在驱动层能否做到么,答案是显然如此,驱动层应该是更能轻易实现这个事情,而且是清除的获取对应驱动的每一个结构,首先,我们要知道一个结构体成员,也就是上面介绍的_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中可以看到:

8

优化定位方式

我们刚刚所确定镜像名称的方式是在知晓_EPROCESS结构体的结构前提下(window 10),但是,不同的windows版本下,该结构体的结构会有区别,一种勤奋的方法就是把每一个版本的对应获取方式都写上,还一种方法是可以用api直接获取在内核有一个导出函数PsGetProcessImageFileName

但是,当我们在vs 2022中输入这个函数的时候,并没有找到这个符号的定义,所以我们需要用其他的方式。

9

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

10

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

11

所以,我们需要使用另一个函数来帮我们调用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打印一下,见下图:

12

微软小工具

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

13

这个工具很清楚的现实每一个进程的信息,内容比任务管理器更加丰富。具体可以查看这个链接下载,https://learn.microsoft.com/zh-cn/sysinternals/downloads/process-explorer

除此还有一个推荐,Process Monitor,监控进程的,几乎每一个进程的每个动作都会监控,比如,操作注册表,加载dll文件等等。具体可以参考https://learn.microsoft.com/zh-cn/sysinternals/downloads/procmon这个链接。

14

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

updatedupdated2025-02-222025-02-22