64位内核开发第九讲,内核中常用的链表等数据结构
内核中常用的数据结构
数据结构
何为数据结构
不管是Ring0还是Ring3数据结构都是必须要知道的.数据结构是一种思想.
以及怎么存储数据. 跟语言无关.平台无关.
如:(链表,数组,栈,队列.图.树...)
ring0下数据结构非彼数据结构. 意思就是数据结构思想都是一样的.
你只需要熟悉ring0下数据结构怎么定义的.以及使用即可.
ring0常见的数据结构:
双向链表
LIST_ENTRY
HASH 表
TREE 树
LookAside
二丶链表
2.1 简介
链表在windows内核开发中是最最最常见的数据结构了。 主要分为单向链表和双向链表。 单向链表的链表节点只有一个链表节点指针。 双向则是两个。 分别是指向前链表节点和后链表节点。
双向链表指向了前后两个节点。所以链表在插入移除上面的操作比单向链表更为方便。
2.2 WDK中的链表结构
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
在内核中链表就是一个结构体定义的数据结构。 而链表的使用就是将其放到 其它结构中。并且通过某种方法来进行链接起来。
2.3 链表的使用
首先我们先定义一个链表,作为Node节点。
然后我们定义一个我们自己的结构
如下:
typedef struct _MY_TEST_STRUCT {
ULONG m_ulDataA;
ULONG m_ulDataB;
ULONG m_ulDataC;
ULONG m_ulDataD;
}MY_TEST_STRUCT,*PMY_TEST_STRUCT;
结构定义好了我们如何使用链表哪? 我们可以在结构中定义一个Node节点。
如下:
typedef struct _MY_TEST_STRUCT {
ULONG m_ulDataA;
ULONG m_ulDataB;
LIST_ENTRY m_listentry;
ULONG m_ulDataC;
ULONG m_ulDataD;
}MY_TEST_STRUCT,*PMY_TEST_STRUCT;
我们定义了一个链表节点 名字为 m_listentry 但是其实我们可以将这个成员定义在任何地方。
我们看下在内存的布局吧。
而如果我们多定义几个结构,并且让他们互相链接起来。那么在内存中表现形式就如下:
我们可以看到 节点A中的 Fink指向节点B中的Fink位置 好好品味我这句话 它并不是指向节点B的首地址, 也就是并不是指向m_ulDataA位置
而Bink则是指向前节点位置的Fink位置。
这里操作系统设计的很巧妙。 通过在任意结构中定义 Node节点 那么这个结构就是一个双向链表的节点。
那么可能有人问了。如果给我一个节点A。 那么我要遍历双向链表的时候要怎么遍历。 因为节点位置不固定,且Flink指向的位置并不是结构体的开头。
其实这个问题很简单。 我们算一下结构体的偏移。
假设 m_listentry节点在任意结构体位置处定义,那么我们只需要计算出 m_listentry结构距离 结构体首地址的偏移即可。
如下:
typedef struct _MY_TEST_STRUCT {
ULONG m_ulDataA; //设在内存中的地址为0位置
ULONG m_ulDataB; //+4
LIST_ENTRY m_listentry; //+8
ULONG m_ulDataC;
ULONG m_ulDataD;
}MY_TEST_STRUCT,*PMY_TEST_STRUCT;
通过上面结构体可以看到 +8位置是m_listentry地址。 而我们通过遍历链表所得出的地址也是+8的位置。 那么我们只需要把的出来的地址-8即可得到结构体首地址
例如下:
PMY_TEST_STRUCT BNode = xxx.m_listentry -8
而如果计算出-8这个偏移。 其实我们可以利用指针原理 来获取偏移量。
如下:
(&(MY_TEST_STRUCT*)0).m_listentry
这里利用了个小技巧,设0地址为结构体首地址。 并且强转为我们自己的结构体。然后再去访问成员变量m_listentry。 但是有人说了0地址直接访问变量肯定是错的。直接会蓝屏。因为0地址根本就不是我们自己的结构。 所以这里还有个设计的小技巧。 我们取的是0的地址。 这样一来我们并没有访问变量了。
所以上面的公式变成了
PMY_TEST_STRUCT BNode = xxx.m_listentry -((&(MY_TEST_STRUCT*)0).m_listentry)
是不是挺恶心的。不过别怕,操作系统给我们定义了一个宏。如下:
#define CONTAINING_RECORD(address, type, field) ((type *)( \
(PCHAR)(address) - \
(ULONG_PTR)(&((type *)0)->field)))
这里的宏就是我们上面所说的意思。
看下如何使用吧。
伪代码:
MY_TEST_STRUCT testA;
MY_TEST_STRUCT testB;
PMY_TEST_STRUCT testB = CONTAINING_RECORD(&testB.m_listentry, MY_TEST_STRUCT, m_listentry);
参数一就是我们双向链表得出的地址。 比如 节点A的双向链表位置是+8位置。 那么参数1就是+8地址。
参数二就是结构体类型
参数三就是结构体成员。
看如下图应该更能明白。
2.4 链表API 之初始化节点
链表的头节点是不携带任何内容的,只是表示链表的头部,对链表的所有操作都是从头部开始的。
如果链表只有一个头节点,那么这个链表就是一个空的链表。 头节点的Flink Blink都是指向自身.
如下:
LIST_ENTRY list = { 0 };
InitializeListHead(&list);
其初始化函数的代码实现如下:
VOID InitializeListHead(_Out_ PLIST_ENTRY listHead)
{
listHead->flink = listHead->Blink = ListHead;
return;
}
2.5 链表操作API 节点的插入
常见的节点插入操作有两种方式,一种是插入到尾部,一种是放到头。 而注意一下头节点是没有body
数据的一个基础的listentry类型。 所以我们所说的插入到头其实是插入到头节点指向的下一个节点的位置
如A -> B-> C 我们插入D 变成了 A->D->B-C
VOID InsertHeadList(_Inout_ PLIST_ENTRY ListHead,_Out_ PLIST_ENTRY Entry) //插入节点到头部
VOID InsertTailList(_Inout_ PLIST_ENTRY ListHead,_Out_ PLIST_ENTRY Entry)//插入节点到尾部
例子:
//初始化头节点
LIST_ENTRY list = { 0 };
InitializeListHead(&list);
//定义自己的结构体
MY_TEST_STRUCT A = { 0 };
MY_TEST_STRUCT B = { 0 };
MY_TEST_STRUCT C = { 0 };
//给头节点插入数据
InsertTailList(&list, &A.m_listentry);
InsertTailList(&list, &B.m_listentry);
InsertHeadList(&list, &C.m_listentry);
注意:
我是在堆栈中使用的API来进行插入的,插入的节点其实是结构体中的LIST_ENTRY成员位置。
因为我是在堆栈中使用的,所以你的链表存储的数据都是基于堆栈的。 所以出了函数就没法使用了。
看一个错误的例子,我把链表定义为了全局。
NTSTATUS use_headinfo()
{
//初始化头节点
LIST_ENTRY list = { 0 };
InitializeListHead(&list);
//定义自己的结构体
MY_TEST_STRUCT A = { 0 };
MY_TEST_STRUCT B = { 0 };
MY_TEST_STRUCT C = { 0 };
//给头节点插入数据
InsertTailList(&g_HeadInfo, &A.m_listentry);
InsertTailList(&g_HeadInfo, &B.m_listentry);
InsertHeadList(&g_HeadInfo, &C.m_listentry);
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pCurobj, PUNICODE_STRING pReg)
{
use_headinfo();
PLIST_ENTRY pNextNode = g_HeadInfo.Flink;
while (pNextNode != &g_HeadInfo)
{
PMY_TEST_STRUCT pcur = CONTAINING_RECORD(pNextNode, MY_TEST_STRUCT, m_listentry);
if (pcur != nullptr)
{
//
pcur->m_ulDataA = 1; //会出错的位置
}
pNextNode = pNextNode->Flink;
}
}
其实这也是C语言以及C++程序员容易反的低级错误。 首先我们使用了use_headinfo 将节点插入,这个没问题。 出问题的是我们在DriverEntry里面
遍历链表,并且取出值来将其修改。 遍历链表也没错,错就错在 我们的链表数据存储的是 栈内存呀。 也就是 A BC三个结构体在出了use_headinfo 就已经
释放了。 你存在链表中的数据就是错误的。此时继续使用肯定会蓝屏。
解决方法我们也知道,就是将A B C 结构体都变成堆内存。 这样出了作用域也不会释放。
2.6 链表操作API 节点的遍历
节点遍历我们接触过了,其实就是 判断 Next的指针是否与原地址相等。 相等了就说明遍历完了。
思路就是建立一个节点指向你想遍历的任意节点的值。 然后取出 next几点。 然后判断是否与原节点一致。 不一致就循环,循环完了之后请注意
我们要更新Next节点的值,也就是让其指向下一个节点位置。 就这样一层一层遍历直到Next节点指向了原节点就退出循环。
PLIST_ENTRY pNext = xxx->Flink;
while(pNext != xxx)
//xxx如果是指针那么直接判断是否 == xxx即可。 如果不是那么就取出他的地址来 所以这里是 xxx 因为xxx使用的是->符合。 如果是.那么就是&xxx
{
//逻辑
auto value = CONTAINING_RECORD(PNext,type,filed);
pNext = pNext->Flink;
}
2.7 链表操作API 节点的删除
删除节点有三种方式,分别是 从头部删除 从尾部删除 以及删除特定节点。
PLIST_ENTRY RemoveHeadList(_Inout_ PLIST_ENTRY ListHead);
PLIST_ENTRY RemoveTailList(_Inout_ PLIST_ENTRY ListHead);
BOOLEAN RemoveEntryList( _In_ PLIST_ENTRY Entry);
前两个函数类似, 参数分别是给定头尾节点进行删除,删除后返回删除的节点,如果没有则返回NULL
删除特定节点的参数则是给定一个节点然后删除,如果删除后链表变成了空链表则是返回true否则就是FALSE
2.8 链表操作API 链表判断是否为空
BOOLEAN IsListEmpty(_In_ const LIST_ENTRY * ListHead);
函数很简单,如果头节点等于自身代表就是空链表。返回TRUE
否则就是FALSE
三丶 平衡树.
使用现成的树. 在 wrk-v1.2\base\ntos\rtl里面有一个文件
AvlTable.c. 这里面就有树.抠出代码来即可使用.
WDK中 有一个树 RTL_AVL_TABLE 这个是WDK中的.
可以自己实现.也可以自己做. 主要是要了解 LIST_ENTRY
四丶 LookAside结构.
我们调用 malloc new 或者 ExAllocatePoolWithTag等分配内存的
时候.都会产生碎片.
而对于我们频繁分配内存.每次都是固定大小的时候.如结构体.
就可以使用这个结构用来分配.
它有两种分配类别. 一种是分页内存.一种是非分页内存.
PAGED_LOOKASIDE_LIST 分页
NPAGED_LOOKASIDE_LIST 非分页
使用方法:
PAGED_LOOKASIDE_LIST MyPagedList;
//第一步要初始化.你指定你要分配的大小
ExInitializePagedLookasideList(&MyPagedList,NULL,NULL,NULL,0,sizeof(MY_DATA),'BINI',0):
注意sizeof(MY_DATA) 这个是你指定的.
//分配内存
MY_DATA *pData = ExAllocatFromPagedLookasideList(&MyPagedList);
//释放内存
ExFreeToPageLookasideList(&MyPagedList,pdata);//注意要还回去.
//卸载的时候要删除
ExDeletPageLookasideList(&MyPagedList);
坚持两字,简单,轻便,但是真正的执行起来确实需要很长很长时间.当你把坚持两字当做你要走的路,那么你总会成功. 想学习,有问题请加群.群号:725864912(收费)群名称: 逆向学习小分队 群里有大量学习资源. 以及定期直播答疑.有一个良好的学习氛围. 涉及到外挂反外挂病毒 司法取证加解密 驱动过保护 VT 等技术,期待你的进入。
详情请点击链接查看置顶博客 https://www.cnblogs.com/iBinary/p/7572603.html
本文来自博客园,作者:iBinary,未经允许禁止转载 转载前可联系本人.对于爬虫人员来说如果发现保留起诉权力.https://www.cnblogs.com/iBinary/p/11026661.html
欢迎大家关注我的微信公众号.不定期的更新文章.更新技术. 关注公众号后请大家养成 不白嫖的习惯.欢迎大家赞赏. 也希望在看完公众号文章之后 不忘 点击 收藏 转发 以及点击在看功能. QQ群: