Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2023-21768 — CVE-2023-21768の概念実証エクスプロイト。Windowsの補助機能ドライバー(AFD.sys)における任意カーネル書き込みの脆弱性で、I/Oリングを介したローカル権限昇格を可能にします。 | Kitploit
ツール/GitHubGitHub/h1bana/cve-2023-21768
特権昇格脆弱性分析エクスプロイトバイナリエクスプロイト
GitHubh1bana/cve-2023-21768

CVE-2023-21768

CVE-2023-21768の概念実証エクスプロイト。Windowsの補助機能ドライバー(AFD.sys)における任意カーネル書き込みの脆弱性で、I/Oリングを介したローカル権限昇格を可能にします。

リポジトリを見る
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 で特権昇格を悪用するためにこれを使用します。

パッチ差分と根本原因分析

Winbindex から afd.sys の 2 つのバージョンをダウンロードします。1 つはパッチ適用前の直近のバージョン、もう 1 つはパッチ適用後のバージョンです。次に Bindiff を使用してこれら 2 つのバージョンを比較します。 bindiff

2 つのバージョンを概観すると、AfdNotifyRemoveIoCompletion という 1 つの関数のみに違いがあることがわかります。この関数の 2 つのバージョン間の違いを詳しく見てみましょう。 bindiff

2 つのバージョン間の違いはそれほど多くありません。パッチ適用後バージョンでは、パラメータを設定して ProbeForWrite 関数を呼び出すためのアセンブリ命令が追加されています。Microsoft の ドキュメント によると、この関数は、アドレスが実際にユーザーモードに属しているか、書き込み権限があるか、そして正しくアラインされているかをチェックするために使用されます。このコードをより詳しく分析します:

  • パッチ適用前 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 は、カーネルとユーザーモード間の通信を処理するドライバーのディスパッチ関数を格納する配列です。DeviceIoControl の呼び出しに対応するディスパッチ関数は 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 には、AfdIrpCallDispatch と AfdImmediateCallDispatch という 2 つのディスパッチテーブルがあります。 dispatchtable1 dispatchtable2

AfdDispatchDeviceIoControl が IoControlCode から subscript を計算し、AfdIoctlTable から subscript に対応する値を取得して 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

最初に述べたように、この脆弱性は、検証されていないポインタを構造体を通じて渡せる場合に発生します。この構造体は、DeviceIoControl の lpInBuffer を介してユーザーモードから直接渡されます。その後、4 番目のパラメータとして AfdNotifySock に渡され、3 番目のパラメータとして AfdNotifyRemoveIoCompletion に渡されます。

para1 para2 para3

struct の構成が不明なため、IDA に struct を自動生成させました。次に、この struct にデータを渡し、必要なチェックをバイパスして脆弱なコードに到達する方法を見つける必要があります。まず AfdNotifySock 関数から始めます:

check1

まず、struct のサイズは 0x30 バイトである必要があります。

check2

0 以外にする必要がある値:

check3

もう 1 つ、デバッグ中に、以前の UserBuffer チェックの箇所で fail にジャンプすることがわかったため、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 に渡します。 このチェックをバイパスすると、プログラムのフローはループに入ります。このループ内には fail フローに移行する箇所がないため、dword20 の値を 0x1 に設定してループを抜けました。

check5

ループを抜けると、プログラムは AfdNotifyRemoveIoCompletion を呼び出します。引き続き AfdNotifyRemoveIoCompletion 関数を分析します:

check6

まず、プログラムは struct の別のフィールドをチェックし、0 以外でなければなりません。次に、その値は 0x20 で乗算され、struct の別のフィールドとともに ProbeForWrite 関数のパラメータとして使用されます。ここでは、書き込み権限を持つユーザーモードメモリ領域のアドレスと dwLen = 1 を指定するだけで十分です。バグをトリガーする前の最後のチェックは、IoRemoveCompletion 関数の戻り値が STATUS_SUCCESS であることです。調査したところ、NtRemoveIoCompletion 関数は呼び出されると IoRemoveCompletion 関数を呼び出すことがわかりました。この ドキュメント によると、NtRemoveIoCompletion は「waiting call」として機能し、指定された Io Completion Object 内に 1 つ以上の完了レコードがあると終了します。レコードは 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 リング(Microsoft が発表した新しい I/O メカニズム)を活用することにより、任意のアドレスの読み取り/書き込みを完全に行うことができます。Yarden Shafir 氏はこの方法について非常に詳細な分析記事を書いており、ここ から読むことができます。アプリケーションが実行できる操作の 1 つは、将来の I/O 操作のためにすべてのバッファを割り当ててから、それらを I/O リングに登録することです。事前登録されたバッファは、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 リング API を使用してカーネルメモリを読み書きできます。ただし、NtQuerySystemInformation 関数を使用するには Medium IL 権限が必要です。Low IL から LPE するには、カーネルアドレスをリークする何らかの方法が必要です。

IoRing->RegBuffers がユーザー制御の fakeBuffer を指すようになると、通常の I/O リング操作を使用して、fake 内のインデックスをバッファとして指定することにより、任意のアドレスへの読み取りと書き込みを実行できます:

  • 読み取り操作 + カーネルアドレス: カーネルは、選択したファイルから指定されたカーネルアドレスに「読み取り」を行い、任意の書き込みにつながります。
  • 書き込み操作 + カーネルアドレス: カーネルは、指定されたアドレスのデータを選択したファイルに「書き込み」、任意の読み取りにつながります。

詳細については、上記のリンクにある Yarden Shafir 氏の分析記事を読むことができます。

問題

上記の PoC コードで IO Ring オブジェクトを作成して書き込もうとしたところ、DeviceIOControl 呼び出し後に Windows がクラッシュした /_ \ ため、Nt 関数を直接呼び出す方法 を使用しました (˘・_・˘)

影響範囲

  • Windows 11 21H1/22H2 で OS ビルド 22000.1455/22621.1105 より前
  • Windows Server 2022 で OS ビルド 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

ツールをダウンロード