Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2023-21768 — CVE-2023-21768 的概念验证漏洞利用程序,这是一个 Windows 辅助功能驱动程序(AFD.sys)任意内核写入漏洞,可通过 I/O ring 实现本地权限提升。 | Kitploit
工具/GitHubGitHub/h1bana/cve-2023-21768
权限提升漏洞分析漏洞利用二进制利用
GitHubh1bana/cve-2023-21768

CVE-2023-21768

CVE-2023-21768 的概念验证漏洞利用程序,这是一个 Windows 辅助功能驱动程序(AFD.sys)任意内核写入漏洞,可通过 I/O ring 实现本地权限提升。

查看仓库
33年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2023-21768

用于 WinSock 的 Windows 辅助功能驱动程序

根据 Microsoft Security Response Center (MSRC) 发布的 CVE-2023-21768 详细描述,该漏洞存在于 Ancillary Function Driver (AFD) 中,其在系统中的文件名为 afd.sys。AFD 模块是 WinSock API 的内核入口点。在本文分析中,我将利用它在 Windows 11 上进行本地权限提升(LPE)。

补丁差异与根因分析

从 Winbindex 下载两个版本的 afd.sys,一个是最接近修复前的版本,另一个是修复后的版本。然后使用 Bindiff 对这两个版本进行比较。 bindiff

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

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

  • 修复前 afd.sys version 10.0.22621.608 code1

-修复后 afd.sys version 10.0.22621.1105 code2

两者都会检查 r15_1 的值,如果为 0,则将 var_304 的值写入 struct_1 中某个字段所指定的指针。如果不为 0,则会调用 ProbeForWrite 以确保指针指向有效地址。在修复前版本中,后续直接将 var_304 的值写入该指针,缺少了这一检查。从该补丁可以推断,我们可以在可控 arg3_1->field_18 值的情况下调用这段代码。如果能在 field_18 中设置一个内核地址值,就可以将 var_304 写入内核内存地址。

=> 漏洞类型:任意内核 Write-Where

现在需要找到触发该漏洞的方法。AfdNotifyRemoveIoCompletion 函数在 AfdNotifySock 函数中被直接调用。 crossRef

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

该地址紧邻 AfdIrpCallDispatch 之前。 cross3

为了触发该漏洞,我将使用 IOCTL_AFD_NOTIFY_SOCK 调用 DeviceIoControl,这样 AfdNotifySock 就会被调用。

root@kitploit:~
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 对象,其定义如下:

root@kitploit:~
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] 中。

root@kitploit:~
#define IRP_MJ_DEVICE_CONTROL           0x0e

从 afd.sys 的 DriverEntry 函数中,我们可以看到驱动程序创建了设备对象 "\Device\Afd": code3

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

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

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

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

root@kitploit:~
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;
}

成功触发!

bp1

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

para1 para2 para3

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

check1

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

check2

以下值必须非 0:

check3

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

debug1 debug2

下一个需要绕过的检查:

check4

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

绕过这个检查后,程序流程进入一个循环;在该循环中没有会跳转到失败流程的地方,因此我只需将 dword20 处的值设为 0x1,即可退出循环。

check5

退出循环后,程序将调用 AfdNotifyRemoveIoCompletion。继续分析 AfdNotifyRemoveIoCompletion 函数:

check6

首先,程序检查 struct 的另一个字段,该字段必须非 0。然后将其乘以 0x20,并作为参数与 struct 的另一个字段一起调用 ProbeForWrite。这里只需要使用一个属于用户模式内存区域、具有写权限的地址,且 dwLen = 1 即可。在能够触发漏洞之前的最后一项检查是:调用 IoRemoveCompletion 函数的返回值必须为 STATUS_SUCCESS。经过一番搜索,我了解到 NtRemoveIoCompletion 被调用后会调用 IoRemoveCompletion 函数。根据这个文档,NtRemoveIoCompletion 是一个“等待调用”,当指定的 Io Completion Object 中至少有一个记录完成时才会结束。记录在 I/O 过程完成时被添加。

root@kitploit:~
NtRemoveIoCompletion(
  IN HANDLE               IoCompletionHandle,
  OUT PULONG              CompletionKey,
  OUT PULONG              CompletionValue,
  OUT PIO_STATUS_BLOCK    IoStatusBlock,
  IN PLARGE_INTEGER       Timeout OPTIONAL );

此外,还有一个可选的 Timeout 参数,当达到超时值时函数会结束。然而,仅将 timeout 设为 0 并不足以让函数返回,而是会返回超时错误码。我们可以使用 NtSetIoCompletion 函数将 IoCompletionObjectType 中待处理 IO 的计数器加 1,从而在超时前结束 NtRemoveIoCompletion 函数。经过多次尝试,我发现写入的值始终为 0x1。

漏洞利用 - 使用 IORING 进行 LPE

由于能够向内核模式地址写入值 0x1,我们可以利用此漏洞,借助 I/O ring(微软推出的一种新 I/O 机制)获得完整的任意地址读/写能力。Yarden Shafir 撰写了一篇关于此方法的非常详细的分析文章,你可以阅读此处。应用程序可以执行的操作之一是为其未来的 I/O 操作分配所有缓冲区,然后将它们注册到 I/O ring。预注册的缓冲区通过 I/O 对象进行引用:

root@kitploit:~
typedef struct _IORING_OBJECT
{
    USHORT Type;
    USHORT Size;
    NT_IORING_INFO UserInfo;
    PVOID Section;
    PNT_IORING_SUBMISSION_QUEUE SubmissionQueue;
    PMDL CompletionQueueMdl;
    PNT_IORING_COMPLETION_QUEUE CompletionQueue;
    ULONG64 ViewSize;
    ULONG InSubmit;
    ULONG64 CompletionLock;
    ULONG64 SubmitCount;
    ULONG64 CompletionCount;
    ULONG64 CompletionWaitUntil;
    KEVENT CompletionEvent;
    UCHAR SignalCompletionEvent;
    PKEVENT CompletionUserEvent;
    ULONG RegBuffersCount;
    PVOID RegBuffers;
    ULONG RegFilesCount;
    PVOID* RegFiles;
} IORING_OBJECT, *PIORING_OBJECT;

如果某个安全漏洞(例如本文提到的漏洞)允许你更新/修改 RegBuffersCount 和 RegBuffers 字段,就可以使用标准 I/O ring API 来读写内核内存。然而,使用 NtQuerySystemInformation 函数需要 Medium IL 权限。若要从 Low IL 进行 LPE,则需要某种方式泄露内核地址。

当 IoRing->RegBuffers 指向由用户控制的 fakeBuffer 后,我们可以使用常规的 I/O ring 操作,通过指定 fake 中的一个 index 作为 buffer,从而在我们想要的任何地址实现读写:

  • 读操作 + 内核地址:内核将“读”取我们选择的文件到指定的内核地址,从而实现任意写。
  • 写操作 + 内核地址:内核将指定地址中的数据“写”入我们选择的文件,从而实现任意读。

如需进一步了解,可以阅读上方链接中 Yarden Shafir 的分析文章。

问题

尝试创建 IO Ring object 并使用上述 POC 代码进行写入后,Windows 在调用 DeviceIOControl 后崩溃了 /_ \ 因此我改用直接调用 Nt 函数的方法 (˘・_・˘)

影响范围

  • Windows 11 21H1/22H2,早于 OS Build 22000.1455/22621.1105
  • Windows Server 2022,早于 OS Build 20348.1487

补丁

  • 该补丁添加了调用 ProbeForWrite 的代码段
  • 补丁版本:
    • Windows 11 21H1: KB5022287 (OS Build 22000.1455)
    • Windows 11 22H2: KB5022303 (OS Build 22621.1105)
    • Windows Server 2022: KB5022291 (OS Build 20348.1487)

POC

下载工具