



Demo
1. Introduction
Windows loads the appropriate kernel driver by interpreting USB Descriptor when a USB device is connected. If the data sent by the device is not properly validated, this can lead to kernel-level vulnerabilities.
Local Privilege Escalation (LPE) via USB is possible in environments where physical access is available.
In Active Directory environments, attackers can steal machine account credentials with SYSTEM integrity on a workstation and leverage them for lateral movement within the domain. Additionally, they can bypass some restrictions imposed by limited user environments in kiosk or POS devices, and similar conditions apply to public PCs and conference room devices.
In this article, we discuss Windows USB driver vulnerabilities and demonstrate how an attacker can achieve SYSTEM integrity using a malformed USB device and a userland program. We share the full process, from vulnerability discovery to root cause analysis and exploit development.
We are security researchers at ENKI WhiteHat and Windows bug hunters, Dongjun Kim and Jongseong Kim. We previously shared our research on Windows vulnerabilities in a prior post [link].
2. Background
2.1 What does ACE mean?
If you are interested in Windows vulnerabilities, you may know that they are disclosed by the Microsoft Security Response Center via Patch Tuesday.
To the best of our knowledge, starting in 2024, the Microsoft Security Response Center has begun disclosing root cause information for vulnerabilities, including CWE classifications.
When reviewing CVE information, you may occasionally come across statements like the following.
According to the CVSS metric, the attack vector is local (AV:L). Why does the CVE title indicate that this is a remote code execution?
The word Remote in the title refers to the location of the attacker. This type of exploit is sometimes referred to as Arbitrary Code Execution (ACE). The attack itself is carried out locally. This means an attacker or victim needs to execute code from the local machine to exploit the vulnerability.
Then, what is Arbitrary Code Execution (ACE)? ACE refers to a vulnerability that can occur when an attacker's code is executed on a victim's local machine. By the way, what is the difference between this and general types of vulnerabilities?
In this case, the vulnerability can be triggered when a specially crafted device is connected to a victim's local machine.
The vulnerability we present is also an ACE vulnerability.

2.2 USB Stack in Windows
How does windows implement USB specification to support commonly used USB devices? Windows has a wide and sophisticated USB stack structure.
First, when we connect a USB device to a Windows PC, the PCI bus driver finds newly connected devices by checking PCI slots. At this time, the PCI bus driver finds the USB host controller and reports it to the Windows PnP manager to create a Physical Device Object (PDO).
The PnP manager recognizes that this device is a USB host controller and loads the appropriate driver. If the newly connected USB device uses the USB 3.0 specification, it creates a Functional Device Object (FDO) and loads usbxhci.sys from C:\Windows\System32\drivers.
After the USB host controller driver is loaded, it creates a root hub within the controller. This root hub plays a bus driver role and starts the USB enumeration stage. In the USB enumeration stage, it allocates a unique address to the USB device and gets device information by receiving descriptors from the USB device. The PnP manager reads this information and determines which additional drivers should be loaded.
For instance, if descriptors related to a keyboard are delivered to the PnP manager, it loads the kbdclass.sys driver for the USB device.
From this, we can see that the USB stack has a very sophisticated structure.

https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/driver-stacks
2.3 USB Enumeration
Wait a moment. What is the USB enumeration stage? The USB enumeration stage is an initialization process that identifies what the device is, allocates a unique address, and loads the appropriate driver to enable communication between the host and the USB device when the device is newly connected to a port, as briefly mentioned earlier.
To briefly explain the steps of USB enumeration,
When a USB device is newly connected, the USB hub detects a voltage change and alerts the host.
The host sends a reset signal to the bus, and the USB device starts communication using address 0.
The host checks the maximum packet size of the USB device by requesting the device descriptor at address 0.
The host allocates a unique address between 1 and 127 to the USB device, and the device starts responding using the assigned address.
The host requests descriptors again via the assigned address and checks the manufacturer ID, power consumption, number of interfaces, and endpoint types.
The PnP manager loads the appropriate drivers and changes the USB device’s state to Configured by sending commands to the device.
This USB enumeration stage is not only executed within one driver but also across multiple drivers.
From a security perspective, the USB enumeration stage is a very important process. If a vulnerability exists in this stage and is successfully exploited by an attacker, the PC can be fully controlled simply by connecting a specially crafted USB device.
2.4 Plug-and-Play
We briefly explain what Plug-and-Play (PnP) is before moving on from the Background part. PnP is a part of the Windows architecture that automatically detects changes in system hardware configuration. PnP is a useful technology that allows new devices to be used immediately without a system reboot by working with the user-mode/kernel PnP manager, I/O manager, bus drivers, function drivers, etc.

https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/pnp-components
3. CVE-2026-32223
3.1. Vulnerability Root Cause
Now that we have some background on USB devices, we will explain the vulnerability we discovered, CVE-2026-32223.
CVE-2026-32223 is a heap-based buffer overflow vulnerability that occurs in the Windows USB print driver (usbprint.sys). This vulnerability is caused by improper validation of device configuration, and it is triggered when usbprint.sys uses a malformed device configuration.
usbprint.sys processes IRP_MJ_DEVICE_CONTROL requests in the USBPRINT_ProcessIOCTL function. The vulnerability occurs in the Make1284IdStringFromUsbStrings function, which is invoked by IOCTL 0x220064. For this function to be executed, v8[1064] must be 0, and v8[1065] must not be 0.
v8 is the DeviceExtension field in the DeviceObject. A Windows device driver can define a DeviceExtension field that can optionally store additional data within the DeviceObject structure. The DeviceExtension is usually created near the DeviceObject creation routine.
Similarly, in usbprint.sys, the DeviceExtension (v8) is initialized in the USBPRINT_StartDevice function, which is the point where the device is actually started by a request from the PnP manager. In this function, the USBPRINT_ConfigureDevice function is called at [1].
In the USBPRINT_ConfigureDevice function, the USBPRINT_GETUSBConfigs function is invoked at [1], and it stores the Configuration Descriptor, which is one of the USB descriptors, in P. If the descriptor is successfully retrieved, it calls the USBPRINT_CheckConfigs function at [2], using P as the second argument.
The USBPRINT_CheckConfigs function calls the USBD_ParseConfigurationDescriptorEx function at [1] and [2] and finds descriptor arrays with matching InterfaceClass and InterfaceProtocol in the configuration descriptor. The results are stored in DeviceExtension[1064] and DeviceExtension[1065], respectively.
This means that we have to properly set the configuration descriptor to invoke IOCTL 0x220064.
Returning to the USBPRINT_ProcessIOCTL function, if we examine the arguments of the Make1284IdStringFromUsbStrings function, the first argument is the DeviceObject, the second argument is the user-provided output buffer, and the third argument is the length of the output buffer.
The vulnerability occurs in the Make1284IdStringFromUsbStrings function. First of all, it retrieves the device’s MFG and MDL by calling the USBPRINT_GetUsbStringDescriptor function at [1] and [2]. Then, it calculates the lengths of MFG and MDL at [3] and [4], respectively, and stores the sum of the two lengths plus 11 in v13.
If v13 is less than 0x20A, it allocates NonPaged Pool memory at [7] with the size of the output buffer. If the pool allocation succeeds, it copies a formatted string into the pool using RtlStringCbPrintfA at [8]. If the string copy succeeds, it copies the contents of the pool to the output buffer at [9].
In this process, if the size of the output buffer is smaller than the allocated pool, a heap-based buffer overflow may occur.
3.2. Triggering the Vulnerability
To trigger the vulnerability, a USB device with a malformed descriptor and a userland program that invokes IOCTL 0x220064 are required.
3.2.1 USB Device Emulation
To construct a malicious USB device, we used the Odroid C4 board and Armbian. The Odroid C4 supports a USB OTG port, allowing the board itself to operate as a USB device. By using Linux Raw Gadget on Armbian, we can freely construct USB descriptors at the byte level.
Raw Gadget is a low-level interface of the USB Gadget subsystem, and it allows user space to directly control USB enumeration responses through the /dev/raw-gadget device. Unlike traditional approaches based on GadgetFS or ConfigFS, it allows direct handling of each control transfer request from the host, enabling flexible construction of abnormal descriptor combinations.
3.2.2 Descriptor Structure
To satisfy the conditions of the previously analyzed USBPRINT_CheckConfigs function, we set bInterfaceClass of the interface descriptor to 0x07 (Printer) and bInterfaceProtocol to 0x04 (IPP over USB). In this case, an interface with bInterfaceProtocol = 0x02 (Bidirectional) must not be included.
With this configuration, DeviceExtension[1065] = 1 and DeviceExtension[1064] = 0 in USBPRINT_CheckConfigs, allowing us to reach the Make1284IdStringFromUsbStrings function.
3.2.3 Set Malformed String Descriptor
The overflow size is determined by the lengths of the MFG and MDL string descriptors. The string descriptors referenced by iManufacturer and iProduct in the device descriptor are directly handled in the Raw Gadget EP0 handler.
The ASCII string is converted to UTF-16LE to construct a USB string descriptor. In the Make1284IdStringFromUsbStrings function, the number of characters in this wide string is counted to compute v13 = len(MFG) + len(MDL) + 11, so increasing the length of the MFG string allows us to control the overflow size.
3.2.4 EP0 Control Transfer Handling
The point of Raw Gadget is that it directly processes every control transfer request from the host in the EP0 loop. When the host requests a string descriptor, it responds with the malformed MFG/MDL string configured above.
3.2.5 Vulnerability Trigger Scenario

4. Exploitation
4.1 제약 조건 분석
익스플로잇을 개발하기에 앞서, 먼저 이 취약점의 제약 조건들을 설명합니다.
풀 타입(Pool Type): [7]의 ExAllocatePool2 호출 시 플래그 값은 0x40(POOL_FLAG_NON_PAGED)입니다. 오버플로우는 NonPagedPoolNx 풀 타입에서 발생합니다.
데이터 제약: [8]의 RtlStringCbPrintfA에 의해 생성된 null 종료 ASCII 문자열이 복사 대상이 됩니다. 페이로드 중간에 NULL 바이트(0x00)를 삽입할 수 없으며, null 종료자는 오직 끝에만 존재합니다. 그러나 오버플로우 데이터가 MDL 문자열 디스크립터의 마지막 부분에 해당하므로, USB 장치는 임의의 데이터를 배치할 수 있습니다. 실제 값들은 익스플로잇 전략에 따라 달라집니다.
오버플로우 크기: 이 취약점에서는 오버플로우 크기를 결정하는 두 가지 변수가 있습니다.
v13 =
MFG_len + MDL_len + 11: 이 값은 USB 문자열 디스크립터에 있는 MFG와 MDL의 길이에 의해 결정됩니다. 각각 최대 253자까지 가질 수 있으므로 v13의 최댓값은253 + 253 + 11 = 517 = 0x205이며,v13 ≤ 0x209조건을 만족해야 합니다.OutputBufferLength: 유저랜드 프로세스가
DeviceIoControl을 호출할 때 결정됩니다.IopXxxControlFile함수 내에서 SystemBuffer는ExAllocatePool2(0x69, max(InputBufferLength, OutputBufferLength), 'IoSB')를 사용해 할당되므로, 이 값이 할당 크기를 결정합니다. IOCTL 0x220064는 입력 버퍼를 사용하지 않기 때문에 크기는 OutputBufferLength와 같습니다.USBPRINT_ProcessIOCTL함수에서는 길이가 0인 경우 에러를 반환하므로 길이가 최소 1 이상이어야 합니다.
실제 복사는 [9]의 memmove(a2 + 2, v16, v13)에 의해 수행됩니다. USB 문자열 디스크립터의 bLength 필드는 uint8 타입이므로 최댓값은 0xFF(255)입니다. WCHAR 데이터 길이는 (bLength - 2) / 2이지만, bLength가 홀수(예: 0xFF)인 경우 남은 1바이트는 0으로 초기화된 버퍼의 다음 바이트와 결합하여 0이 아닌 하나의 WCHAR로 카운트될 수 있습니다. 따라서 문자열당 최대 127개의 WCHAR이 가능하며, v13의 최댓값은 127 + 127 + 11 = 265 = 0x109가 됩니다. 결과적으로 memmove는 최대 0x109 바이트까지 복사합니다. 오버플로우 크기는 (v13 + 2) - OutputBufferLength로 계산할 수 있으며, v13과 OutputBufferLength가 모두 공격자에 의해 제어 가능하므로 오버플로우 크기를 자유롭게 제어할 수 있습니다.
CACHE_ALIGNED 할당: 한 가지 더 고려해야 할 부분이 있습니다. IOCTL 0x220064는 METHOD_BUFFERED를 사용하며, DeviceIoControl 경로에서 I/O 관리자는 CACHE_ALIGNED 플래그를 사용하여 SystemBuffer를 할당합니다.
CACHE_ALIGNED 할당에서는 반환된 포인터가 캐시 라인(0x40) 경계에 정렬됩니다. 할당자는 풀 헤더를 위해 요청 크기를 0x10만큼 올림(round up)하고, 사용자 데이터를 블록 내에서 0x40 정렬된 위치에 놓을 수 있도록 0x40의 여유 공간(slack)을 추가합니다. POOL_HEADER와 정렬된 포인터 사이의 이 공간이 정렬 패딩(meta)입니다. 올림 과정 자체는 정적인 반면, 결과로 나오는 meta는 선택된 블록이 LFH 서브세그먼트 내부 어디에서 시작하는지에 따라 달라집니다. LFH 서브세그먼트는 페이지 정렬되어 있고 각 블록은 고정된 block_size 간격으로 배치되므로, block_size가 0x40의 배수가 아닌 버킷은 블록 시작 위치가 mod 0x40 기준으로 {0x00, 0x10, 0x20, 0x30}을 순환하며, LFH가 다른 프리 슬롯들을 선택함에 따라 meta 역시 동일한 네 가지 값을 순환하게 됩니다. 따라서 정확한 오버플로우 크기는 meta에 종속됩니다.
요약하면, v13과 OutputBufferLength가 모두 공격자에 의해 제어될 수 있으므로 오버플로우 크기를 원하는 범위 내에서 제어할 수 있습니다. 단 한 가지 걸림돌은 block_size가 0x40의 배수가 아닌 버킷들의 경우, meta가 마치 LFH 슬롯 추첨에 의해 랜덤화되는 것처럼 동작한다는 점입니다. 이 문제를 완전히 피할 수 있는 깔끔한 방법은 block_size가 0x40의 배수인 버킷에 할당이 안착하도록 OutputBufferLength를 선택하는 것입니다. 이러한 버킷들에서는 모든 슬롯이 이미 0x40 정렬되어 있어 meta가 컴파일 타임 상수 값으로 고정됩니다. 다음 섹션에서 이 선택 과정을 살펴봅니다.
4.2 풀 펭수이 (Pool Feng Shui)
이 오버플로우 취약점을 정밀하게 익스플로잇하려면 오버플로우가 발생하는 청크가 공격자가 제어하는 오브젝트 옆에 위치해야 하므로, Named Pipe 풀 스프레이를 사용하여 힙 레이아웃을 조작합니다.
4.2.1 오버플로우 크기 선택
섹션 4.1에서 요약했듯이 복사할 데이터의 크기인 v13과 대상 버퍼의 크기인 OutputBufferLength는 공격자가 선택할 수 있습니다. 당사는 다음과 같은 값들을 사용했습니다.
MFG 110, MDL 75 →
v13 = 110 + 75 + 11 = 196 = 0xC4OutputBufferLength = 0xB0
OutputBufferLength에 풀 헤더(0x10)와 CACHE_ALIGNED 오버헤드(0x40)를 더하면 실질적인 할당 크기는 정확히 0x100이 되어 LFH의 0x100 버킷에 들어가게 됩니다. 0x100 mod 0x40 == 0이므로 선택된 서브세그먼트 내의 모든 블록 시작 주소는 이미 0x40 정렬되어 있으며, 따라서 meta는 매 시도마다 0x30으로 고정됩니다. 이로써 오버플로우 크기는 항상 (v13 + 2) - (0x100 - 0x10 - 0x30) = 0xC6 - 0xC0 = 6 바이트가 되어, 인접한 POOL_HEADER를 덮어쓰기에 정확히 충분한 크기가 되며 그 이상을 침범하지 않습니다.
전형적인 Named Pipe 익스플로잇에서 프리미티브는 오버플로우를 통해 인접한 DATA_QUEUE_ENTRY(DQE) 필드들을 직접 조작하는 방식으로 획득됩니다. v13 및 OutputBufferLength를 조정해 오버플로우 크기를 임의로 늘릴 수 있으므로 DQE까지 닿는 것은 가능합니다. 문제는 섹션 4.1에서 언급한 NULL 바이트 제약입니다. 오버플로우 데이터가 RtlStringCbPrintfA에 의해 ASCII 출력으로 생성되므로 중간에 NULL 바이트(0x00)를 삽입할 수 없습니다. 그러나 DQE의 포인터 필드(Flink, Irp)들을 유의미하게 조작하기 위해서는 NULL 바이트가 필수적으로 요구됩니다.
반면, POOL_HEADER는 1바이트 필드들(PreviousSize, PoolIndex, BlockSize, PoolType)로 구성되어 있어 NULL 바이트 없이도 정밀한 제어가 가능합니다. 따라서 당사는 ExFreePool의 동작 방식을 제어하기 위해 POOL_HEADER를 오염시키는 방안을 선택했습니다. 구체적인 수정 방식은 다음 섹션에서 설명합니다.
4.2.2 LFH 버킷 정렬
IOCTL의 SystemBuffer(IoSB)는 다음과 같이 할당됩니다.
Named Pipe에 WriteFile이 호출될 때, npfs는 DQE를 할당합니다.
우리는 다음과 파이프 스프레이를 수행할 수 있습니다. WriteFile을 통해 0xC0 바이트가 쓰여질 때 커널에서는 0xF0 크기의 NpFr 할당이 발생하며(NpAddDataQueueEntry가 0x30 바이트의 DQE 헤더를 앞에 추가하기 때문에), 이는 IoSB와 동일한 LFH 0x100 버킷에 배치됩니다.
4.2.3 CPU 고정을 통한 스프레이 안정화
LFH는 CPU별 친화성 슬롯 구조를 사용합니다. RtlpHpLfhBucketActivate 함수를 분석해보면, LFH 버킷이 활성화될 때 각각의 CPU에 대한 오너(owner) 슬롯이 생성되는 것을 관찰할 수 있습니다.
[1]에서 RtlpHpLfhPerfFlags의 5번째 비트를 검사합니다. 만약 이 비트가 설정되어 있으면 CPU별로 오너들이 생성되며, 그렇지 않다면 단일 오너만 생성됩니다. [2]에서는 각 오너가 초기화됩니다. 오너별로 독립적인 서브세그먼트 체인을 관리하기 때문에, 서로 다른 CPU에서 같은 버킷에 행해진 할당들은 서로 다른 서브세그먼트에 배치됩니다.
이 문제를 해결하기 위해 당사는 SetThreadAffinityMask를 사용해 스프레이 스레드를 단일 CPU에 고정했습니다. 이를 통해 모든 스프레이 할당을 동일한 오너의 서브세그먼트 체인에 집중시킬 수 있으며, 인접하게 배치될 확률을 높일 수 있습니다.
4.2.4 스프레이 레이아웃
정상적으로 스프레이가 완료된 후의 이상적인 풀 레이아웃은 다음과 같습니다. IoSB 블록의 바로 뒤에 오는 블록이 NpFr 파이프이어야 합니다.
오버플로우가 발생하면 IoSB 다음 블록(NpFr)의 POOL_HEADER 중 6바이트가 MDL 문자열 디스크립터의 뒷부분 데이터로 덮여 쓰입니다. 아래는 오버플로우 발생 후 오염된 POOL_HEADER 상태를 나타냅니다.
각 필드의 역할은 다음과 같습니다:
PreviousSize = 0x0C: 이 값은
ExFreePoolWithTag의 캐시 정렬 역방향 이동 단계에서 사용되며, 이로 인해0x0C × 0x10 = 0xC0만큼 역방향 이동하게 됩니다.PoolType = 0x06: 비트 2(CacheAligned)가 설정되어 역방향 이동 단계를 트리거합니다. 이 경우 비트 3(PoolQuota)은 해제되어야 합니다. 이에 관한 자세한 내용은 섹션 4.3.1에서 설명합니다.
이 두 값은 USB 장치로부터 제공된 MDL 문자열 디스크립터의 테일 영역에 의해 제어됩니다.
4.3 Ghost Chunk 고스트 청크
섹션 4.2에서 우리는 오버플로우를 통해 인접한 블록의 POOL_HEADER를 변조했습니다. 이제 이렇게 오염된 POOL_HEADER를 이용해 어떻게 "Ghost Chunk"를 생성하는지 설명합니다. 고스트 청크 기술은 Synacktiv의 "Scoop the Windows 10 pool!" 논문에서 소개된 Aligned Chunk Confusion 공격에 기반합니다.
4.3.1 CacheAligned 역방향 이동 단계
Windows 세그먼트 힙에서 CACHE_ALIGNED 플래그를 사용하여 할당된 청크들은 해제될 때 특별한 처리를 거칩니다. ExFreePoolWithTag 함수를 분석해보면, PoolType의 비트 2(CacheAligned)가 설정되었을 때 PreviousSize를 사용해 본래의 할당 베이스(allocation base)를 계산하는 것을 확인할 수 있습니다.
[1]에서 PoolType의 비트 3(PoolQuota)을 확인합니다. 만약 이 비트가 설정되어 있으면 [2]의 ExpPoolQuotaCookie를 사용하여 ProcessBilled 포인터를 디코딩하고, [3]에서 올바른 EPROCESS인지 확인합니다. 오염된 POOL_HEADER 속의 ProcessBilled 필드에는 올바른 값이 담겨있지 않기 때문에, 비트 3이 설정된 상태라면 블루스크린(BSOD)이 발생하게 됩니다. 따라서 PoolType은 오직 비트 2만 설정된 0x06이어야 하며, 0x0E(비트 2 + 비트 3)는 사용해서는 안 됩니다.
[1]을 패스한 이후, [4]에서 PoolType의 비트 2(CacheAligned)를 확인합니다. 변조된 PoolType(0x06)은 해당 비트가 설정되어 있기 때문에, [5]에서 PreviousSize × 0x10만큼 역방향 이동합니다. 변조된 값 PreviousSize = 0x0C에 따라 뒤쪽으로 0x0C × 0x10 = 0xC0 바이트 이동합니다. 그리고 [6]에서 이동된 새로운 주소의 POOL_HEADER 속 BlockSize를 읽고 해제할 크기를 결정합니다.
이 역방향 이동 후에 도착하게 되는 목표 지점이 익스플로잇의 핵심 키포인트입니다. 오버플로우가 발생할 시점의 블록 레이아웃을 다시 살펴봅시다.
IOCTL 호출이 반환될 때 커널은 IoSB를 해제하여 N+1번 블록 슬롯을 빈 상태로 만듭니다. 이 빈 슬롯에 재스프레이(respray) 파이프를 배치함으로써 파이프 데이터의 첫 번째 바이트(data[0x00])에 가짜 POOL_HEADER를 삽입할 수 있습니다. 이후 N+2번 블록이 해제되는 시점에 역방향 단계는 N+2_hdr (slot + 0x200) - 0xC0 = slot + 0x140 = N+1 block + 0x40으로 떨어지며, 이 주소는 바로 재스프레이 파이프의 가짜 POOL_HEADER 위치와 일치합니다.
4.3.2 동적 룩어사이드 (Dynamic Lookaside)를 통한 고스트 해제
역방향 이동 이후 [6]에서 가짜 POOL_HEADER가 제공한 BlockSize(0x21) 값을 읽어 들여, 해제하려는 대상 크기를 0x21 × 0x10 = 0x210으로 계산하게 됩니다. 이로 인해 0x210 바이트 규모의 "고스트 청크"가 해제 연산에 들어갑니다.
해당 청크가 어느 경로로 해제되는지가 아주 중요합니다. ExFreePoolWithTag 함수 후반부를 살펴보면 Dynamic Lookaside 분기 흐름을 확인할 수 있습니다.
[1]에서 _SEGMENT_HEAP의 UserContext 필드를 읽어 들여 Dynamic Lookaside 구조체 주소를 획득합니다. [2]에서는 해제하려는 대상 크기가 [0x201, 0xF80] 범위에 들어오는가를 확인합니다. 0x210 - 0x201 = 0x0F ≤ 0xD7F에 해당하므로 조건이 만족됩니다. [3]에서 만약 Dynamic Lookaside가 존재하고 잔여 공간 깊이(depth)가 비어있다면 [4]의 SList로 밀어넣어(push) 집어넣게 됩니다.
일단 Lookaside SList에 추가되고 나면, 이후에 발생하는 고정 크기(0x210)의 동일한 크기 할당 요청들에 매칭되어 다시 동일한 주소를 즉각 반환할 것입니다. 만약 이 청크가 대신 VS FreeChunkTree 체계로 프리될 경우 인접한 빈 공간들과 강제로 머지(merge)될 수 있으며, 이로 인해 고스트를 다시 재확보하는 과정이 오작동할 수 있습니다. 그러므로 Lookaside 경로로 유도하는 것이 반드시 필요합니다.
생성하려는 대상 크기의 Dynamic Lookaside가 사전에 활성화되어 있도록 만들려면, 커널의 밸런스 셋 매니저(Balance Set Manager, BSM)가 해당 규격 크기의 버킷에 대해 잦은 할당 및 해제 행위를 미리 감지하는 조건이 확보되어야 합니다. 이 특징은 RtlpDynamicLookasideRebalance 함수 분석 과정에서 확인할 수 있습니다.
[1]에서 각 버킷의 할당/해제 빈도수를 연산하고, [2]에서 이를 빈도 크기 순으로 정렬합니다. [3]에서 오직 누적 빈도수가 최소 25(0x19)를 넘어서는 경우에만 선택하여, [4]의 EnabledBucketBitmap에 업데이트 탑재합니다. BSM은 대략 1초 주기로 이 리밸런싱 루틴을 처리합니다.
그러므로 익스플로잇 시작 단계에서 0x210 바이트 규모의 충분한 사전 할당/해제 사이클을 반복해 미리 해당 타겟 버킷을 트레이닝시켜 두는 과정이 필수적입니다. 당사에서는 vp777의 CVE-2020-17087 익스플로잇에 적용된 EnableLookaside 설계 기법에 맞추어, 2회에 걸쳐 0x1000회만큼 파이프 할당과 해제를 발생시킨 뒤 BSM이 활성화하도록 대기 시간을 두었습니다.
4.3.3 고스트 청크의 재확보 및 영역 중첩 (Overlap)
고스트 청크(0x210)가 Lookaside SList에 등재되어 대기하고 있는 상태이므로, 동일한 크기의 대상을 갖는 Named Pipe 생성을 연이어 트리거하면 직전에 집어넣었던 고스트 영역이 그대로 튀어 복원되어 나옵니다. 이 부분은 ExAllocateHeapPool 내의 SList 팝(pop) 경로 코드에서 증명됩니다.
[1]에서 해당 SList가 비어있지 않은지를 확인하고, [2]에서 즉각 엔트리를 출력합니다. 수립된 가상 파이프에 대고 WritePipe(0x1D0)를 날리는 작업은 결국 NpAddDataQueueEntry → ExAllocatePool2(0x200) → 0x210 규모 블록 풀 할당 확보로 종결되며, [2]에 이르러 앞서 올려놓았던 고스트 공간 주소가 그대로 인도됩니다.
이 단계가 진행되면 복제되어 올라온 고스트 파이프 영역의 NpFr 블록 구조가 이미 존재하고 있던 재스프레이용 파이프 정보의 데이터 구역과 물리적으로 오버랩(중첩)되는 정렬 구도가 달성됩니다.
재스프레이 파이프에 대고 외부 유저랜드에서 PeekNamedPipe 명령을 날리는 동작만으로도 커널은 재스프레이 고유 DQE 타겟 주소 영역(+0x40)을 읽어 올 것입니다. 이 데이터 타겟 정렬 구도가 고스트용 파이프 내부 DQE 세부 필드들과 맞대어 겹쳐있는 상태이기 때문에, 커널이 고스트 파이프 DQE에 기록해둔 Flink 포인터 데이터(실제 커널 영역 주소)를 그대로 원본 통째 유저 모드로 흘려보내 읽을 수 있게 됩니다. 확보해낸 Flink 정보는 고스트 파이프 CCB에 할당된 OutQueue 링크 헤더의 실제 커널 포인터 주소를 나타내며, 이것이 곧 임의 주소 읽기/쓰기 구현의 첫단추가 됩니다.
4.4 임의 주소 읽기 (Arbitrary Read)
이 릭(leak)된 커널 포인터를 주춧돌 삼아, 후속 단계에서 마저 고스트 파이프 DQE를 조작하여 최종 임의의 커널 포인터 영역 주소를 원격 판독(Arbitrary Read)할 수 있는 가설을 통제합니다.
4.4.1 고스트 DQE 필드 전면 덮어쓰기
유지 중이던 재스프레이용 파이프 해제(ReadFile 호출)를 먹이면 대응되던 LFH 슬롯 지점이 바로 일시 반납 상태가 됩니다. 곧바로 동일 위치 크기에 맞춰 신규 파이프(재작성용 덮어쓰기용 파이프)를 그 주소에 다시 끼워맞추어, 타겟 고스트 DQE 구조 내부를 우리가 원하는 구성 형태로 오염 세팅할 수 있습니다:
4.4.2 임의 구성 IRP 탑재를 통한 주소 전 영역 읽기
우리가 가짜로 만든 IRP 구조는 유저 영역 공간에 미리 VirtualAlloc으로 만들어둔 특정 타겟 메모리 범위에 상주시킵니다. 판독하길 원하는 임의의 실제 커널 주소 정보(target addr)를 이 위장용 가짜 IRP의 AssociatedIrp.SystemBuffer 필드(+0x18)에 가리키도록 설정해 둡니다.
고스트 파이프를 대상으로 유저 모드에서 PeekNamedPipe 명령을 주입하게 되면, 커널 시스템 함수 npfs!NpReadDataQueue 측은 세팅된 EntryType 값(1, unbuffered) 분기를 따르며 target IRP 필드의 SystemBuffer 주소가 가리키는 실제 커널 타겟 레지스트리 공간으로부터 그대로 데이터를 복사해 유저랜드 버퍼로 긁어옵니다:
PeekNamedPipe 수행 방식 자체는 타겟 DQE 노드 데이터를 풀 영역에서 해제해 망가뜨리지 않고 단지 내용만 조회하므로, 한 번 준비 작업이 세팅되고 나면 원하는 다른 target 주소가 있을 때마다 위장용 IRP의 SystemBuffer 위치 값만 껍데기 교체하듯 고치면서 제한 없이 연속적인 커널 읽기 프레임을 획득하고 활용하는 것이 가능해집니다:
4.5 커널 데이터 구조 투어 (Structure Walk)
임의 영역 읽기 능력이 수립되었지만 권한 상승 프로세스 로직으로 진입하기 위해서는 부팅 단계에서 고유 할당을 겪는 ntoskrnl.exe 파일의 동작 실제 기본 시작 베이스 및 PsInitialSystemProcess 주소 등을 취득해 보아야 합니다. 현재 손에 들고 있는 단서는 앞 단계 릭으로 확보한 g_queue_addr (고스트 파이프 CCB의 OutQueue 필드 주소) 정보뿐이므로, 이 지점부터 연쇄적으로 트리 구조를 따라 투어(walk)를 시작합니다.
4.5.1 CCB 정보부터 npfs.sys 소속 DRIVER_OBJECT 데이터 수집까지
g_queue_addr 값은 NP_CCB 구조체 안의 OutQueue 필드 오프셋 위치(+0xA8)를 그대로 지목하고 있으므로 역연산을 통해 본체 구조체의 베이스 주소를 알아낼 수 있습니다: CCB_addr = g_queue_addr - 0xA8.
Named Pipe용 CCB 구조체 정보에는 서버에 대한 유효 포인터 정보인 ServerFileObject 정보가 마킹되어 있습니다. 이 FILE_OBJECT 멤버에서 DEVICE_OBJECT를 순차 참조하고, 해당 장치 인스턴스를 소유 및 구동 중인 오리지널 드라이버 계층 정보인 DRIVERS_OBJECT 구조체 링크까지 도달해냅니다. 고스트 파이프는 npfs.sys 정식 드라이버의 관리 하에 놓인 파이프 오브젝트에 속하므로 이 연결을 밟는 행위를 통해 최종적으로 npfs.sys 모듈의 전체 구동 데이터가 매핑되어 있는 DRIVER_OBJECT 고유 주소 영역을 발견해내게 됩니다:
4.5.2 DRIVER_OBJECT로부터 ntoskrnl.exe 본래의 베이스 주소 위치 검출하기
현재 커널 링 구역에 언로드/로드 상태로 안착해 활성화된 모든 드라이버 구조들은 KLDR_DATA_TABLE_ENTRY 구조가 연속해 있는 일련의 양방향 연결 리스트(Linked List)로 유지 및 가구동됩니다. DRIVER_OBJECT 구조체의 내부 필드 중 DriverSection 포인터 정보(+0x28)가 이 해당 운용 드라이버 엔트리 테이블 구조를 향해 있으므로, 취득한 npfs.sys 고유 링크를 시작점으로 하여 InLoadOrderLinks 필드들을 거치며 리스트상의 이전/이후 드라이버들을 조회해가다 보면, 시스템에서 가장 우선 로드 완료 상태로 대기하고 있어야 할 커널 베이스 ntoskrnl.exe의 위치와 실제 덤프 맵핑 Base 주소를 정상 추출해내게 됩니다:
4.6 임의 주소 쓰기 (Arbitrary Write)
우측 단계에서 안정 수립해낸 임의 영역 커널 읽기 프레임을 교두보로 하여 시스템 제어 요소를 장악했으므로, 권한 상승 작업을 완수하기 위한 마지막 관문인 임의 주소 쓰기(Arbitrary Write) 테크닉 조건을 건설합니다.
4.6.1 동작 기작 분석
유저 측 프로그램이 Named Pipe를 타겟으로 ReadFile 조작을 걸어 진행하게 되면 커널은 해당 파이프에 물린 DQE 등과 연관해 생성 보류 중이었던 IRP 정보를 즉시 처리해 완료 판정(complete)시킵니다. 이 시점에 만약 대상 IRP 플래그 상에 BUFFERED_IO 옵션이 표기되어 있었다면, 완료 처리 직후 이어 실행되는 시스템 커널 루틴 IopProcessBufferedIoCompletion 내부에서 memmove(UserBuffer, SystemBuffer, Information) 동작을 강제로 수반시키게 됩니다. 이 흐름을 세팅해 우리가 지정할 커널 시스템 전역 변수나 타겟 포인터 데이터를 덮어쓰도록 유도합니다.
4.6.2 위장용 가짜 IRP용 파라미터 구조
4.6.3 IRP 완료 단계 경로 추적하기
IRP 완료 단계가 시작되면 IopProcessBufferedIoCompletion에서 세부 Flags 구성을 보고 타겟 memmove를 발생시킵니다. 이 대목이 임의 포인터 덮어쓰기의 기본 전제입니다. 복사가 끝나는 직후 곧바로 뒤를 잇는 IopCompleteRequest가 해당 임무가 끝난 IRP 영역 메모리를 다시 풀 상에 반납(free)하려는 작업을 행하는데, 우리가 오염시켜 등재해둔 해당 IRP 본체 정보는 실제 커널 영역에서 만들어진 시스템 풀이 전혀 아니라 유저랜드 측 메모리 맵 상에 임시 배치해둔 유저 메모리 영역 중 하나일 뿐입니다. 만약 이상태로 커널이 해당 주소를 IoFreeIrp 에 전해 풀 공간으로 환수하게 시도한다면 원치 않는 커널 손상 크래시로 연결됩니다. 이를 무력화하여 무탈하기 넘어가기 위해 IRP Flags에 0x8000 비트를 더해 명시하고 AllocationSize 필드를 명시적으로 0으로 구성해 커널이 오동작 우회(Bypass) 처리를 하도록 지정합니다:
4.6.4 IRP 완료 유도 과정 통과하기
위장용 IRP 구성 정보가 유저 가상 공간에 마킹된 채 상주 중이므로, 이 정보 완료 절차 과정 도중 커널 코어 단계에서 조회해 볼 세부 영역 조건들 또한 안전하게 매칭되어야 합니다. 최소한 아래 세 가지 포인트가 시스템 룰에 맞춰 방어되어야 크래시를 안 먹습니다:
ETHREAD 주소 검증: 가짜 IRP 내에 정의된 Tail.Overlay.Thread 필드(+0x98)의 정보는 정당한 ETHREAD 인스턴스 주소 값을 품고 있어야 합니다. IopCompleteRequest 루틴 진행 도중 해당 쓰레드의 운용 중인 IRP 목록 링크 정보 조작을 시도하기 때문입니다. 섹션 4.4에서 확보한 임의 리딩 프리를 가동해 PsInitialSystemProcess 내부로부터 현재 운용 중인 타겟 프로세스 PID를 순차 조회 대조해 맞춘 뒤, 소속된 ThreadListHead 내부 가동 구조 일원 중 대상 실행 thread TID 가 가리키는 ETHREAD 최종 실 주소를 캐치해 이 구간에 입력해 줍니다.
ThreadListEntry 구조체 순환 참조화 (self-link): IopDequeueIrpFromThread 함수가 구동되는 시점에, 쓰레드 고유 가동 IRP 체인으로부터 완결 처리된 대상을 꺼내는 리스트 양방향 노드 제거 연산(RemoveEntryList)이 집행됩니다. 임의 날조한 가짜 IRP 노드이다 보니 실제 이 리스트 노드 선상에 탑재된 적이 없습니다. 따라서 연산 직전 Flink/Blink 체킹 대조 검증을 시도할 때 오차가 뜨면 곧장 커널 검출 결함으로 뻗어버리게 됩니다. 이를 방지하고자 가짜 IRP 내부에 배치된 ThreadListEntry 주소 공간(+0x20) 값에 지정할 자기 자신 지목 주소(&IRP + 0x20)를 강제로 마킹해 둠으로써 검증 공식( Flink->Blink == &entry && Blink->Flink == &entry) 수식이 어리숙하게 뚫리며 정상 스킵되도록 설계합니다:
NpRemoveDataQueueEntry 검사 우회: ReadFile 작동에 따른 NpReadDataQueue의 후속 임무로 NpRemoveDataQueueEntry 함수가 뒤처리 조작을 이어 달립니다. 이 시동 분지 내부에서 엮어두었던 IRP 캔슬러 핸들러인 CancelRoutine (+0x68) 정보를 InterlockedExchange64 코드를 기동해 0 상태로 초기화시키는 작업 및 시큐리티 관련 정리 로직을 먹입니다. 그 뒤에 이 DQE 메모리 블록을 해제하긴 하지만 IRP 영역 자체는 손대지 않고, 완결 조작(IofCompleteRequest) 전담은 최초 콜을 발생시켰던 caller가 도로 쥐어 가져갑니다. 그러므로 변조한 가짜 IRP의 CancelRoutine 필터(+0x68)에도 최소한 0이 아닌 임의의 유효 형태 값을 입력해 두어, 다가올 아토믹 연산 시 에러를 방지해야 합니다.
4.6.5 시스템 권한 장악 (Privilege Escalation)
조작이 본질적 단계로 완료되어, 대상을 타겟하는 임의의 포인터 오염 덮어쓰기 기능(Arbitrary Write)을 안정성 높게 확보했습니다. 획득한 이 지배력으로 실질적인 시스템 타겟인 로컬 권한 탈취를 수행합니다. 이를 위한 구현 구조로 당사는 DevCore 블로그 기술 문서 등지에 설명된 바 있는 고유 방식인 SeDebugPrivilege 마크 변조를 활용하는 방법론을 추정 채택했습니다.
이것은 커널 전역 제어 주소 영역에 배치된 nt!SeDebugPrivilege 의 값을 임의 수정해서 시스템상 소유 등급 조건을 위반 획득하게 조작하는 절차입니다.
어플리케이션이 임의의 대상을 뒤흔들고자 OpenProcess(PROCESS_ALL_ACCESS) API를 트리거하면, 커널 시스템 제어 함수인 PsOpenProcess가 가동하며 조회 주체가 된 해당 호출자 소유의 인증 토큰에 SeDebugPrivilege 권한 활성 상태가 정의되어 있는지 판독 검증을 통과해야 합니다. 이 대조 작업을 처리하기 위해 참조할 기준 LUID 상수 데이터 세트가 실제로 로드된 커널 전역 변수 nt!SeDebugPrivilege 영역 내부 정보 값들입니다:
[1] 단계에서 전역 타겟인 nt!SeDebugPrivilege (LUID 규격 정보인 8바이트 구성) 데이터를 읽고, [2]에서 SepPrivilegeCheck 함수를 운용해 상기 추출한 타겟 LUID 정보가 현재 API를 건 사용자 세션 권한 토큰 리스트 범주 안에 마킹 정합하는지 대조합니다. 만약 대조 성공에 준하는 매칭이 확인되어 [3] 문턱을 통과하면 최종 목적이었던 프로세스 제어 권한을 획득합니다.
원래 보편적으로 보급 통용 중인 디버크 특권의 디폴트 LUID 상수 규격은 {LowPart=0x14, HighPart=0x0}에 입각합니다. 하지만 통상적인 보통 유저 인스턴스인 Medium 등급 세션의 실행 토큰 내부에는 이 디버깅 특권 정보 리스트가 처음부터 마킹 탑재되어 있지 않아, 검증 루틴 진행 후 FALSE를 응답하게 됩니다.
여기서 기발한 착상을 얹어, 커널 전역에 보관 중이던 기준 타겟 LUID 상수의 LowPart 부분만 살짝 도려내어 보통 유저 등급의 프로세스 토큰들도 기본 장착 상태로 활성화되어 태어나게 마련인 아주 일반적인 하위 권한 LUID 규격 번호로 유도 변조하는 시나리오를 설정합니다. 대표적인 사례인 SeChangeNotifyPrivilege (식별 번호 LUID=0x17) 특권의 경우는 윈도우 타겟 내 모든 기본 실행 세션 상에 탑재 완료 상태로 제공됩니다. 이 점을 파고들어, 커널 전역에 적재되어 검증 대조 시 쓰일 nt!SeDebugPrivilege 의 8바이트 구조체 중 하위 단 1바이트만 임의 개조하여(원값인 0x14 정보를 0x17로 살짝 교체해 둠) 꼬아 놓는다면, 검증 루틴인 [2]를 돌 때 커널은 사용자가 제공한 세션 속에 있는 SeChangeNotifyPrivilege를 기준 값 패스로 인정 처리해 승인을 떨어뜨리게 되고, [3] 단계를 경유해 최종 윈도우 SYSTEM 소유 등급으로 가동 중이었을 핵심 보안 권한 노드(예: winlogon.exe 등)들까지 통째로 완벽히 PROCESS_ALL_ACCESS 범위 제어 모드로 장악하는 것이 가능해집니다.
작업이 이 단계까지 클리어 완료되면 OpenProcess(winlogon.exe, PROCESS_ALL_ACCESS)에 대한 완벽한 권한 진입을 승인받게 되며, 후속타로 CreateRemoteThread를 해당 시스템 백엔드 본체 서비스 속에 적재 및 운용하게 함으로써 비로소 최고 등급 윈도우 쉘(SYSTEM-level cmd.exe)을 획득하게 됩니다.
4.7 최종 요약 정리
지금까지 살펴본 모든 단편적 과정들을 기점으로, 실제 USB 장치 투입 단계 이후부터 SYSTEM 최종 특권 탑재 프로세스 구동까지 이어지는 전체 체인 진행 순번을 최종 시놉시스로 수집 정리합니다.
1. USB 모조 모듈 링킹 및 전용 디바이스 모니터 로딩
특별 제작된 타겟 구성 규격의 USB 프린터 인스턴스를 기기로 부착 연동 작동시킵니다. PnP 관리 매니저가 기기를 알아보고 대응 드라이버인 usbprint.sys를 호출하며, 이어서 로드 완료된 descriptor 상의 옵션 InterfaceProtocol = 0x04 규격이 내부 확장 구조체 요소인 DeviceExtension[1065] = 1 및 DeviceExtension[1064] = 0 조건을 성사시킵니다.
2. 동적 룩어사이드 트레이닝 사전 추진
커널 내부 힙 구조에서 목표로 할 규션 크기인 0x210 바이트 가상 파이프 생성/해제 행위에 무리수를 투입해 약 0x1000여 번가량의 소모성 노드를 2차례 연달아 발생 연출한 다음, 백그라운드 운용 주체인 밸런스 셋 매니저가 힙 튠업 결정을 낼 수 있게 텀을 둡니다. 이를 통과해 나면 커널 링 영역에 0x210 등급 버킷에 룩어사이드 노선 활성 맵이 고안 확보되어, 매끈하게 SList 가동 하에 재배치 순환 주기를 맞이할 사전 상태가 수립됩니다.
3. LFH 레이아웃 정지 스프레이
커널 풀 내에 Named Pipe 구조들을 엄청나게 뿌려서 0x100 LFH 슬롯들을 채워나갑니다. 배포해 배치하는 개별 스프레이 파이프 데이터 내부 영역 맨 앞단(data[0x00])들에 추후 활용 가능한 형태 규약의 가짜 가짜 POOL_HEADER (BlockSize = 0x21, PoolType = 0x0A)를 억지로 적재해 대기시켜 둡니다. 작업 시 사용되는 쓰레드들의 연산 CPU 결합 선상에 강제 조정을 가함으로써 전부다 한 종류의 단일 LFH 타겟 소스 오너 풀 서브 세그먼트에 우겨 들어가 자궁을 이뤄 모여 앉도록 정위치해 둡니다.
4. 도미노 타겟 홀 생성 및 오버플로우 사격
스프레이 완료 지대 중 끝 지점에 위치한 대상 파이프 멤버 몇 개를 선별적으로 해제하여 배치 선상 중간에 인위적인 공간(Hole)을 만든 후, 곧바로 파고들 타겟 IOCTL 0x220064를 즉각 가동합니다. Make1284IdStringFromUsbStrings 가 일차 IoSB를 이 준비해 둔 공백 터전 속에 끼워 안착시킵니다. 시스템 meta 값이 0x30로 성립하던 순간에 맞물려 타겟 6바이트 오버플로우가 뻗어나가며, 기 배치 대기 중이었던 옆 칸의 NpFr 구조체 헤더 구역(POOL_HEADER) 내부값 구조를 가로질러 PreviousSize = 0x0D 및 PoolType = 0x06로 조형 오염시킵니다. 작업 IOCTL 콜 완료와 동시에 메인 커널은 임무가 끝난 IoSB를 즉각 소멸 해제시킵니다.
5. 신속한 재스프레이 (Respray) 침투
방금 전 소멸 처리로 비게 된 그 IoSB 고유 슬롯 자리를 빼앗기 위해, 즉각 기회를 노려 신규 가상 파이프(재스프레이 파이프)를 동일 세션 슬롯 상에 쏘아 넣습니다. 계획된 fake POOL_HEADER 데이터 영역(BlockSize = 0x21 지정 상태)이 침투 성공 결과 재스프레이용 파이프의 최전방 헤드인 첫 단에 안정적으로 안착하며, 향후 터져 나올 역방향 슬라이딩 단계의 완벽한 랜딩 베이스 기지로 연출 구축됩니다.
6. 메인 스프레이 풀 정리 및 고스트 공간의 발생
장막을 구성하던 메인 스프레이 파이프 소유 풀 영역들을 전부 정리 정돈해 소멸 환수시킵니다. 이 시점에 헤더 구조 내부 오염 조작이 기 성공 상태였던 타겟 NpFr 풀 코어가 해제 명령을 만나는 시동 분지에서 ExFreePoolWithTag 함수는 기재 조건에 맞춰 CacheAligned 역방향 이탈 루틴을 격발하고, 정확히 오인 계산된 랜딩 포인트를 따라 후진하여 사전에 설계해 두었던 재스프레이 파이프 내부 위장용 fake POOL_HEADER 정보(BlockSize = 0x21) 지점을 취득해냅니다. 꼬인 BlockSize 연산 공식에 오인 속박된 커널은 0x210 규격의 부피를 가진 거대 고스트 공간을 가구동 상대로 판단한 채, 활성 준비 상태였던 Dynamic Lookaside SList 상에 집어넣어 보관 상태로 등재해 버립니다.
7. 고스트 블록의 환원 확보 및 중첩 데이터 누설
0x1D0 데이터 할당 크기를 요구하는 Named Pipe 구조 생성을 속개 요청하면, 커널 힙 시스템은 부서 이탈해 보관 중이던 룩어사이드 노드를 끄집어내며 0x210 규모의 빈 고스트 주소 본지를 그대로 응답해 넘겨줍니다. 이 과정을 거치며 인입 성공한 고스트 파이프 DQE 구조는 여전한 상태로 살아남아 머물고 있었던 재스프레이 파이프 데이터 영역 내부 구성 전반과 동일 메모리 위상 상에서 서로 강하게 오버랩 겹쳐 맞물립니다. 완곡한 뒤틀림 정렬 구도가 잡히자마자, 사용자는 유저랜드 구동 PeekNamedPipe를 연출 가동해 겹친 재스프레이 쪽에서 기어나온 포인터 정보들을 긁어 내며, 안전하게 위장 가짜 고스트 DQE 필드에 등재 보관 중이었을 기밀 커널 정보 Flink 포인터 누출 값을 확인 확보하게 됩니다.
8. 고유 임의 영역 리딩 장치(Arbitrary Read)의 구현
유지관리 중이던 재스프레이 파이프의 구동 완료 반납 기믹을 작동시켜 슬롯 여유 처리를 내린 뒤, 마저 신종 복제용 파이프(재작성용 파이프)를 그 주소에 다시 안착시켜 겹친 타겟 고스트 DQE 구조 제어 상태를 완전히 가져옵니다. 제어 레벨 내부 지시 영역 EntryType = 1 입력 후, 이어서 타겟 Irp 지점이 임의 조형 가공한 유저 가상공간 fake IRP 구조 영역을 명시하도록 설계 배치합니다. 준비 상태 완료 후부터는, 고스트 파이프 PeekNamedPipe 연산 조율 한번 당 가 지정한 fake IRP SystemBuffer 타겟 주소가 내포한 영역에 입각한 원격 영토 읽기가 수사 형태로 안정 운용되기 시작합니다.
9. 코어 커널 모듈(ntoskrnl.exe) 운용 베이스 역추적
확보한 Flink 시작 주소의 좌표계를 축으로 사슬형 데이터 구조 투핑을 행합니다. 연결된 장치 드라이브 실 노드 DRIVER_OBJECT의 DriverSection 파생 포인터를 조회 획득하고, 여기서 이어 달리는 KLDR_DATA_TABLE_ENTRY 데이터 체인 InLoadOrderLinks 리스트 상에 마킹 등재되어 오가던 모듈들을 필터 검색하면서 핵심 코어 드라이버인 ntoskrnl.exe 의 실행 로드 물리 디렉토리 주소 DllBase 지점을 마침내 안정 확보해 냅니다.
10. 표적 임의 주소 쓰기(Arbitrary Write) 조율 및 타겟 토큰 변조 돌입
더 큰 능력을 투척하기 위해 타겟 고스트 DQE 제어 영역 구조물에 더 안전하게 날조한 Fake IRP 세트를 기입합니다. 해당 타겟 IRP Flags 지점에 BUFFERED_IO | INPUT_OPERATION | 0x8000 구성을 고정하고, 판독 타겟 소스 주소를 SystemBuffer 위치에 탑재하고 타겟팅 쓸 위치 목적 주소를 UserBuffer 포인터 정보로 지정합니다. 사용자가 이 고스트 파이프 타겟으로 ReadFile 조작을 인가하는 순간, 코어 처리부 IopProcessBufferedIoCompletion가 내부 복사 기전 memmove(UserBuffer, SystemBuffer, size)를 호출하고 도망침에 따라 대망의 타겟 대상 임의 영역 커널 덮어쓰기가 성취 완료됩니다. 이 전능한 수단을 가동해, 로드되어 구동 중인 커널 검증 식별자인 nt!SeDebugPrivilege 의 8바이트 구성 변수 값 중 하위 단 1바이트 크기만Medium 레벨 프로세스 세션도 항상 가지고 태어나는 권한 인자 즉 SeChangeNotifyPrivilege 식별 키인 0x17 문양으로 바꾸어 기입합니다.
11. 최상위 등급 SYSTEM 쉘의 호출 권장 성공
검증 체크 변수의 수정을 완료하고 나면, OpenProcess(winlogon.exe, PROCESS_ALL_ACCESS)에 검사 필터 조건은 하위 권한 토큰을 가지고 가더라도 아무 저항 없이 논스톱 패스로 승인 도장을 밀어주며, 시스템 핸들을 온전히 확보하게 됩니다. 인계된 해당 통제권을 바탕으로 내부 타겟 쓰레드 삽입 API 인 CreateRemoteThread를 날려 WinExec("cmd.exe") 실행 페이로드를 인젝션 탑재함에 따라, 부하 없는 최고 수순 윈도우 SYSTEM 등급 cmd 실행 콘솔 화면을 완전 획득하게 됩니다.
5. Patch
This vulnerability was patched by adding a bounds check before the copy operation. The code now verifies that the output buffer is large enough before writing data, instead of copying data without validating the buffer size.
6. Result
We identified a heap-based buffer overflow in usbprint.sys and demonstrated a full exploitation chain from a crafted USB device to SYSTEM privilege escalation.
By combining controlled USB descriptors, IOCTL interaction, and advanced heap manipulation techniques, we achieved reliable exploitation despite modern mitigations.
Our results highlight that even well-established subsystems like USB enumeration can expose critical attack surfaces when input validation is insufficient.
The full source code for the USB device emulator and exploit is available on our GitHub repository.

Popular Articles








