关于进程有很多可以聊的,也有很多不知道的,所以自然就没法一次性介绍完,希望通过这篇把一些我所知晓的关于进程的事情列一些。
进程的结构
具体什么是进程这里就不多赘述了,毕竟也算是计算机的基础,在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 这个链接。
今天就介绍这些,后面还会接着继续介绍关于进程的内容,需要一点点整理。