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

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

2 つのバージョン間の違いはそれほど多くありません。パッチ適用後バージョンでは、パラメータを設定して ProbeForWrite 関数を呼び出すためのアセンブリ命令が追加されています。Microsoft の ドキュメント によると、この関数は、アドレスが実際にユーザーモードに属しているか、書き込み権限があるか、そして正しくアラインされているかをチェックするために使用されます。このコードをより詳しく分析します:
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 は、カーネルとユーザーモード間の通信を処理するドライバーのディスパッチ関数を格納する配列です。DeviceIoControl の呼び出しに対応するディスパッチ関数は MajorFunction[IRP_MJ_DEVICE_CONTROL] に保存されます。
#define IRP_MJ_DEVICE_CONTROL 0x0e
afd.sys の DriverEntry 関数から、ドライバーがデバイスオブジェクト "\Device\Afd" を作成したことがわかります:

MajorFunction[IRP_MJ_DEVICE_CONTROL] に AfdDispatchDeviceControl が代入されているため、カーネルと通信するために DeviceIoControl を呼び出すと、この関数が呼び出されます。

AFD には、AfdIrpCallDispatch と AfdImmediateCallDispatch という 2 つのディスパッチテーブルがあります。

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

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

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

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

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

もう 1 つ、デバッグ中に、以前の UserBuffer チェックの箇所で fail にジャンプすることがわかったため、DeviceIoControl を呼び出すときにこの値を NULL に設定しました。上記の値を設定した後、check2 を通過できました。

次にバイパスする必要があるチェック:
