主要是将链表结构的使用,在内核开发中使用起来比较方便的一种数据结构【LIST_ENTRY】。
一、内存的分配
主要是学习一些基本操作。现在推荐使用的动态分配函数【ExAllocatePoolWithTag】
PVOID tempbuffer = ExAllocatePoolWithTag(NonPagedPool, 0x1000, 'xxaa');
这样可以通过工具来找到标签对应的内存地址,我没找到视频里的【PoolMonEX.exe】,但是找到了【PoolMonX.exe】这个程序也可以用🤣
可以看到实际上的内容是和代码里边的顺序相反的,而且我之前申请的【‘aaxx’】非分页内存块,在驱动卸载后没有被释放掉(故意没free,驱动卸载不会自动释放),这样就只能重启操作系统了。
这个函数的第一参数确定这块内存的形式:
- 【 PagedPool 】分页池—在需要时能够将页面换出的内存池。
- 【 NonPagedPool 】非分页池—永远不会换出页面,保证驻留在RAM里的内存池。
引用书中的描述,我觉非常好,虽然第一次看的时候没什么感觉😗😗😗
很显然,非分页池是一个“更好”的内存池,因为它不会导致页 面错误。在本书的后面我们会看到一些需要从非分页池分配的例子。 驱动程序要尽可能少地使用非分页池,除非必需。其他任何情况驱动 程序都应该使用分页池。POOL_TYPE这个枚举类型表示内存池的类型。 这个枚举类型包括很多内存池的“类型”,但是只有三种可以被驱动 程序使用:PagedPool、NonPagedPool和NonPagePoolNx(没有执行权限 的非分页池)。
除此之外,在tiechuiDL视频中提到了更多的参数,在头文件【wdm.h】有这样的定义:
【NonPagedPoolCacheAligned】传入这个参数,有一个对齐的概念,就是要求操作系统分配的这个内存是宽度对齐的。宽度的大小和寻址基线有关系,比方说32位系统就是四字节对齐,64位就是8字节对齐。
【NonPagedPoolMustSucceed】传入这个参数,就要求操作系统必须分配成功一个非分页内存。
【NonPagedPoolCacheAlignedMustS】传入这个参数,是上边的超级组合:非分页+字节对齐+必须成功!
1.调试看细节
接下来是双机调试的骚操作,真的炫呀,tiechuiDL的双机调试连接怎么这么快!我就暂时用了WinDbg来看(VS2019调试Win7一直gg😒):
先搞一下入口断点:
bu ListEntry!driverentry
进入入口函数后使用Windbg打断点:
之后使用g命令跳出,就和视频里边一样命中断点调试了...(还是好奇为啥tiechuiDL这么快???
【 bl 】查看已经打过的断点:
【 dv /v 】查看变量状态
【 db 0xfffffa80`1b178000 】查看tempbuffer内存地址的变化
清零之后【RtlZeroMemory(tempbuffer, 0x1000);】
填充之后【RtlFillMemory(tempbuffer, 0x1000, 0xcc);】
释放之后【ExFreePoolWithTag(tempbuffer, 'dcba');】中断一下再看,被别的进程用了...
调试就这样了,学到了咋通过命令看内存,然后介绍了两个运行时函数,用来内存比较:
- RtlCompareMemory
- RtlEqualMemory
RtlEqualMemory其实是宏定义而且用的比较多,主要是看一些区域相不相等。
2. Lookaside
最后还有一个概念是【Lookaside】
Windows内存管理中使用了类似于容器的东西,叫做Lookaside对象,每次程序员申请内存都会从Lookaside里面申请,只有不足的时候,Lookaside才会向内存又一次申请内存空间,这样减少了频繁申请内存而导致的内存碎片。
当Lookaside对象内部有大量没有使用的内存时候,它会自动让windows回收一部分内存,总之,Lookaside很智能。
一般Lookaside用于以下情况:
- 程序员每次申请固定的内存大小
- 申请和回收内存的次数较多,很频繁
其实这个在官网的例子还出现挺频繁的,不过不在这里展开了,下次一定😉
二、链表 LIST_ENTRY
接下来是想通过链表来存储进程信息。
1.定义链表
链表结构的定义:
//
// 链表结构定义
//
typedef struct _MyStruct
{
LIST_ENTRY list; // 加入链表
HANDLE pid; // 进程PID
PEPROCESS peprocesspbj; // 进程对象
BYTE processname[16]; // 进程名
}MyStruct,*PMyStruct;
新建一个LIST_ENTRY变量:
// 链表变量
LIST_ENTRY listhead = { 0 };
在使用的时候初始化这个链表变量 ,初始化的时候就是在填充结构中的值(和对象初始化好像😗):
InitializeListHead(&listhead); // 初始化是填充节点的指针值
DbgPrint("%p %p %p\n", &listhead, listhead.Flink, listhead.Blink);
运行看一下:
是一样的地址。
2.获取进程
创建一个进程通知回调:
//设置进程创建回调
PsSetCreateProcessNotifyRoutine(ProcessNotifyFun,FALSE);
PsSetCreateProcessNotifyRoutine 例程将驱动程序提供的回调例程添加到或从中删除该例程列表,每当创建或删除进程时调用该例程。
这个API在前边的程序中也有使用过,在这里发现了一个查看回调参数的方法,算是一个新收获:去查看具体的结构。
通过这个结构,新创建的回调函数参数就是这样定义:
这样就连成线了✅
为了获取进程名需要声明一个半文档化(也就是不公开但是存在)函数【PsGetProcessImageFileName】,需要开头声明一下才能使用:
/** 向前声明 */
NTKERNELAPI
UCHAR* PsGetProcessImageFileName(__in PEPROCESS Process);
回调函数具体的当前样子是:
//08 链表
VOID ProcessNotifyFun(HANDLE pid, HANDLE pid2, BOOLEAN bcareaf)
{
UNREFERENCED_PARAMETER(pid);
if (bcareaf)
{
DbgPrint("process create,PID is %d", pid2);
//PEPROCESS tempep = PsGetCurrentProcess(); //这是获取自身进程ID
PEPROCESS tempep = NULL;
PsLookupProcessByProcessId(pid2, &tempep);
if (!tempep)
{
return;
}
PUCHAR processname = PsGetProcessImageFileName(tempep);
DbgPrint("process name is %s", processname);
}
return;
}
这里有个小插曲,如果使用PsGetCurrentProcess获取只能是自身的进程就是那个“浏览.exe”,换成PsLookupProcessByProcessId这个函数就可以通过pid进行搜索了。
PsLookupProcessByProcessId 例程接受进程的进程 ID,并返回指向进程的 EPROCESS 结构的引用指针。
3. 存储信息
如果CPU是多核的话,一个核在创建进程,另一个核也在创建进程,那么通过链表保存的时候,就会有冲突,需要通过上锁来进行保护。
上边获取到进程名之后,在回调函数中继续插入到链表中:
//08 链表
VOID ProcessNotifyFun(HANDLE pid, HANDLE pid2, BOOLEAN bcareaf)
{
UNREFERENCED_PARAMETER(pid);
if (bcareaf)
{
DbgPrint("process create,PID is %d", pid2);
//PEPROCESS tempep = PsGetCurrentProcess(); //这是获取自身进程ID
PEPROCESS tempep = NULL;
PsLookupProcessByProcessId(pid2, &tempep);
if (!tempep)
{
return;
}
ObDereferenceObject(tempep); // 解引用????
PUCHAR processname = PsGetProcessImageFileName(tempep);
DbgPrint("process name is %s", processname);
//
//保存进程信息
//
PMyStruct ptempptr = ExAllocatePoolWithTag(NonPagedPool, sizeof(MyStruct), 'qwer');
if (ptempptr)
{
KIRQL oldirql = 0;
//PLIST_ENTRY templist = NULL;
RtlZeroMemory(ptempptr, sizeof(MyStruct));
//
//成员赋值
//
ptempptr->peprocesspbj = tempep;
ptempptr->pid = pid2;
RtlCopyMemory(ptempptr->processname, processname, strlen((const char *)processname));
//通过CONTAINING_RECORD查找链表
//templist = CONTAINING_RECORD(ptempptr, MyStruct, list);
//上锁
KeAcquireSpinLock(&spinlock, &oldirql);
//插入信息
InsertTailList(&listhead, &(ptempptr->list));
//解锁
KeReleaseSpinLock(&spinlock,oldirql);
}
}
return;
}
不太明白为什么要解引用
ObDereferenceObject(tempep); // 解引用????
问一下AI:
ObReferenceObject
是 Windows 内核提供的一个宏,用于增加对象的引用计数。在 Windows 内核编程中,对象是内核中的一种基本数据结构,可能是进程、线程、文件、事件等等。由于内核对象被多个线程和进程共享,所以需要使用引用计数来管理对象的生命周期。当一个线程或进程需要访问某个对象时,会调用
ObReferenceObject
宏来增加对象的引用计数。这样可以确保在对象正在被使用时,其他线程或进程不会将其释放掉。当对象不再被使用时,调用ObDereferenceObject
来减少对象的引用计数,如果引用计数为 0,则表示对象可以被释放掉。总之,
ObReferenceObject
宏的主要作用是增加内核对象的引用计数,避免对象在使用期间被意外删除。
那什么是【引用计数】:
引用计数是一种常见的内存管理技术,它被广泛应用于操作系统、程序库和其他软件系统中。引用计数是一种跟踪对象在程序中被使用次数的方法,当一个对象被创建时,其引用计数初始化为 1。每当有代码引用该对象时,其引用计数加 1;当不再需要该对象时,其引用计数减 1。当引用计数变为 0 时,该对象就可以被释放,以便回收其占用的内存。
引用计数是一种内存管理技术,它的基本原理是跟踪每个对象被引用的次数。当对象的引用计数变为零时,系统就会自动回收该对象所占用的内存空间。
在使用引用计数的语言中,每个对象都有一个引用计数器,用于记录有多少个指针引用了该对象。当有新的指针指向该对象时,该对象的引用计数将会增加1,当指针不再指向该对象时,该对象的引用计数将会减少1。当引用计数器变为0时,表示该对象已经没有被任何指针引用了,此时系统将会回收该对象所占用的内存空间。这种方式可以避免内存泄漏和重复释放内存等问题,同时也可以提高程序运行效率。
ObReferenceObject 宏标记的对象需要再删除引用吗?
是的,使用
ObReferenceObject
宏标记的对象,在使用完成后需要调用ObDereferenceObject
函数进行引用计数的减少。
ObReferenceObject
宏用于增加内核对象的引用计数。当内核对象被打开或者传递给驱动程序处理时,必须先使用ObReferenceObject
宏增加它的引用计数,这样可以确保该对象在使用期间不会被释放掉。
ObDereferenceObject
函数用于减少内核对象的引用计数。当一个内核对象不再被使用时,必须调用此函数将其引用计数减少。当引用计数为0时,内核对象的内存空间才会被释放。需要注意的是,在使用
ObReferenceObject
宏和ObDereferenceObject
函数时,需要遵循一定的规则,例如不要在调用ObDereferenceObject
函数之前修改被引用对象的句柄等。否则可能会导致未定义的行为或者系统崩溃。
原来这里调用的是用解引用,那应该是之前的函数有什么地方增加引用了,我又去MSDN搜了一下,发现是有解引用说明的:
如果对 PsLookupProcessByProcessId 的调用成功, PsLookupProcessByProcessID 会增加 Process 参数中返回对象的引用计数。 因此,当驱动程序使用 Process 参数完成时,驱动程序必须调用 ObDereferenceObject取消引用从 PsLookupProcessByProcessID 例程收到的 Process 参数。
看来是谁家的工具就得找谁家的说明书啊,解决完这个疑问之后贴下代码再看下效果:
//卸载函数
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
······
//记得卸载
PsSetCreateProcessNotifyRoutine(ProcessNotifyFun, TRUE);
//
// 只遍历不删除
//
for (templist = listhead.Flink; templist!=&listhead; templist=templist->Flink)
{
DbgPrint("FOR");
tempptr = CONTAINING_RECORD(templist, MyStruct, list);
DbgPrint(" %d %p %s\n", tempptr->pid, tempptr->peprocesspbj, tempptr->processname);
}
//
// 遍历链表并删除
//
PLIST_ENTRY templist = NULL;
PMyStruct tempptr = NULL;
while (listhead.Blink!=&listhead)
{
DbgPrint("WHILE");
templist = RemoveTailList(&listhead);
tempptr = CONTAINING_RECORD(templist, MyStruct, list);
DbgPrint(" %d %p %s\n", tempptr->pid, tempptr->peprocesspbj, tempptr->processname);
//DbgPrint("进程名 %s \n", tempptr->processname);
ExFreePoolWithTag(tempptr, 'qwer');
}
}
三、小结
视频实现的是尾插法,还有其他的插入链表的方法。然后就是可以尝试和应用层联动,来开启数据存储和数据发送等。