
CVE-2023-21768에 대한 개념 증명 익스플로잇, Windows 보조 함수 드라이버(AFD.sys)의 임의 커널 쓰기 취약점으로 I/O 링을 통해 로컬 권한 상승을 가능하게 합니다.
CVE-2023-21768에 대한 Microsoft Security Response Center(MSRC)가 공개한 상세 설명에 따르면, 취약점은 Ancillary Function Driver (AFD) (시스템 파일 이름 afd.sys)에 존재합니다. AFD 모듈은 WinSock API의 커널 진입점입니다. 이 분석에서는 이를 사용하여 Windows 11에서 권한 상승을 악용할 것입니다.
afd.sys의 두 버전을 Winbindex에서 다운로드합니다. 하나는 패치 이전의 최신 버전이고, 다른 하나는 패치 이후 버전입니다. 그런 다음 Bindiff를 사용하여 두 버전을 비교합니다.

두 버전을 전체적으로 비교하면, 오직 하나의 함수 AfdNotifyRemoveIoCompletion에서만 차이가 있음을 알 수 있습니다. 이 함수의 두 버전 간 차이를 자세히 살펴봅니다.

두 버전 간에 큰 차이는 없습니다. 패치 후 버전에서는 매개변수를 설정하고 ProbeForWrite 함수를 호출하는 어셈블리 명령어가 추가되었습니다. Microsoft 문서에 따르면 이 함수는 주소가 실제로 사용자 모드에 속하는지, 쓰기 권한이 있는지, 올바르게 정렬되었는지 확인하는 데 사용됩니다. 이 코드를 더 자세히 분석합니다:
패치 전 afd.sys 버전 10.0.22621.608

패치 후 afd.sys 버전 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라는 두 개의 디스패치 테이블이 있습니다.

AfdDispatchDeviceIoControl이 IoControlCode를 통해 첨자를 계산하고 AfdIoctlTable에서 첨자에 해당하는 값을 가져와 IoControlCode로 확인하는 것을 쉽게 알 수 있습니다.

AfdImmediateCallDispatch의 시작 주소와 AfdNotifySock이 저장된 주소 사이의 거리로부터 인덱스가 73이고 컨트롤 코드가 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를 통해 사용자 모드에서 직접 전달됩니다. 그런 다음 네 번째 매개변수에 해당하는 AfdNotifySock에 전달되고, 세 번째 매개변수에 해당하는 AfdNotifyRemoveIoCompletion에 전달됩니다.

struct의 구성을 아직 모르기 때문에 IDA가 struct를 자동으로 생성하도록 했습니다. 이제 struct에 데이터를 전달하고 필요한 검사를 우회하여 취약한 코드에 도달하는 방법을 찾아야 합니다. AfdNotifySock 함수부터 시작합니다:

첫째, struct의 크기는 0x30바이트여야 합니다.

0이 아니어야 하는 값들:

또 한 가지는 디버그할 때 이전 UserBuffer 검사에서 실패로 점프하는 것을 확인했기 때문에 DeviceIoControl을 호출할 때 이 값을 NULL로 설정했습니다. 위의 값들을 설정한 후 check2를 통과했습니다.

우회해야 할 다음 검사:
