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
Privilege EscalationVulnerability AnalysisExploitationBinary Exploitation
GitHubh1bana/cve-2023-21768

CVE-2023-21768

CVE-2023-21768에 대한 개념 증명 익스플로잇, Windows 보조 함수 드라이버(AFD.sys)의 임의 커널 쓰기 취약점으로 I/O 링을 통해 로컬 권한 상승을 가능하게 합니다.

저장소 보기
33년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2023-21768

Windows Ancillary Function Driver for WinSock

CVE-2023-21768에 대한 Microsoft Security Response Center(MSRC)가 공개한 상세 설명에 따르면, 취약점은 Ancillary Function Driver (AFD) (시스템 파일 이름 afd.sys)에 존재합니다. AFD 모듈은 WinSock API의 커널 진입점입니다. 이 분석에서는 이를 사용하여 Windows 11에서 권한 상승을 악용할 것입니다.

Patch Diff and Root Cause Analysis

afd.sys의 두 버전을 Winbindex에서 다운로드합니다. 하나는 패치 이전의 최신 버전이고, 다른 하나는 패치 이후 버전입니다. 그런 다음 Bindiff를 사용하여 두 버전을 비교합니다. bindiff

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

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

  • 패치 전 afd.sys 버전 10.0.22621.608 code1

  • 패치 후 afd.sys 버전 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
);

reverse and debug

각 드라이버에 대해 커널에 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라는 두 개의 디스패치 테이블이 있습니다. dispatchtable1 dispatchtable2

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

AfdImmediateCallDispatch의 시작 주소와 AfdNotifySock이 저장된 주소 사이의 거리로부터 인덱스가 73이고 컨트롤 코드가 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를 통해 사용자 모드에서 직접 전달됩니다. 그런 다음 네 번째 매개변수에 해당하는 AfdNotifySock에 전달되고, 세 번째 매개변수에 해당하는 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에서 보류 중인 I/O 카운터를 1 증가시키고 시간 초과 전에 NtRemoveIoCompletion 함수를 종료할 수 있습니다. 여러 번 시도한 결과 기록되는 값은 항상 0x1임을 확인했습니다.

exploit - IORING을 이용한 LPE

커널 모드 주소에 0x1 값을 쓸 수 있게 됨으로써, I/O ring(Microsoft가 새로 도입한 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의 인덱스를 버퍼로 지정함으로써 원하는 모든 주소에 대한 읽기 및 쓰기를 생성할 수 있습니다:

  • 읽기 작업 + 커널 주소: 커널은 선택한 파일에서 지정된 커널 주소로 "읽기"를 수행하여 임의 쓰기를 유발합니다.
  • 쓰기 작업 + 커널 주소: 커널은 지정된 주소의 데이터를 선택한 파일로 "쓰기"를 수행하여 임의 읽기를 유발합니다.

자세한 내용은 위 링크에 있는 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

도구 다운로드