
CVE-2026-29923에 대한 개념 증명 익스플로잇, pstrip64.sys의 BYOVD 권한 상승. IOCTL을 통한 물리 메모리 읽기/쓰기를 시연하여 SYSTEM 토큰을 탈취하고 상승된 셸을 생성합니다.
면책 조항: 이 코드는 교육 및 방어 연구 목적으로만 제공됩니다. 커널 익스플로잇에 대한 이해를 높이고 방어자가 유사한 취약점으로부터 보호할 수 있도록 작성되었습니다. 이 프로젝트의 무단, 불법 또는 악의적 사용은 엄격히 금지됩니다.
해시: ab01485bb7c8bc1a9c86096eeea6d31d8fad557bf4d44072b46373d2203faa6e
드라이버 이름: pstrip64.sys
CVE: CVE-2026-29923
"취약한 드라이버 가져오기(Bring Your Own Vulnerable Driver, BYOVD)" 공격은 공격자가 운영 체제가 여전히 공식적으로 신뢰하는 레거시 드라이버를 사용하여 최신 Windows 보호 기능을 우회하는 오래되었지만 매우 효과적인 방법입니다. 드라이버가 로드되면 공격자는 그 결함을 무기화하여 표준 권한이 없는 프로세스와 완전한 시스템 수준 제어 사이의 격차를 해소합니다.
이번 주 초, pstrip64.sys 드라이버에서 CVE-2026-29923으로 추적되는 새로운 취약점이 공개되었습니다. 이 블로그 게시물은 익스플로잇의 전체 라이프사이클을 분석합니다: 초기 취약점 연구와 개념 증명(Proof of Concept, PoC) 개발부터 방어자가 환경을 보호하기 위한 실행 가능한 완화 전략까지.
pstrip64.sys 드라이버는 EnTech Taiwan PowerStrip(버전 3.90.736까지)과 연결된 레거시 커널 모드 구성 요소입니다. 합법적인 목적은 고급 그래픽 카드 디스플레이 조정을 가능하게 하는 것이지만, 깊은 시스템 권한 때문에 공격자에게 매우 매력적인 대상이 됩니다.
취약점이 처음 공개되었을 때, 저는 DriverEntry 함수를 분석하는 것으로 시작했습니다. 이 함수는 커널 드라이버의 주요 초기화 루틴으로, \Device\PSTRIP64 디바이스 객체를 생성하고 \DosDevices\PSTRIP64 심볼릭 링크를 통해 사용자 모드 애플리케이션에 노출합니다. 더 중요한 것은 드라이버의 디스패치 테이블을 구성한다는 점입니다. 제 눈길을 가장 먼저 끈 항목은 인덱스 14(IRP_MJ_DEVICE_CONTROL)였으며, 이는 사용자가 제공한 모든 IOCTL 요청을 sub_11340 핸들러 함수로 직접 라우팅합니다. 이것이 우리의 주요 관심 영역입니다.
sub_11340 함수는 주요 IOCTL 디스패처 역할을 하며 사용자 모드의 요청을 해석합니다.
노출된 모든 IOCTL 중에서 0x80002008이 의심할 여지 없이 가장 흥미롭습니다. 기본 케이스는 사소한 I/O 포트 상호 작용을 처리하지만, 0x80002008은 sub_11000으로의 게이트웨이 역할을 합니다. SystemBuffer를 이 함수에 직접 전달함으로써 말입니다.
이 sub_11000 루틴이 결정적인 증거입니다. 먼저, HalTranslateBusAddress를 사용하여 사용자가 제공한 주소를 유효한 시스템 물리적 주소로 변환합니다. 그 다음, \Device\PhysicalMemory를 열고 ZwMapViewOfSection을 사용하여 매핑합니다. 대상 프로세스 핸들을 (HANDLE)0xFFFFFFFFFFFFFFFFLL (이는 ZwCurrentProcess()를 나타냄)로 하드코딩함으로써 드라이버는 이 물리적 메모리를 호출 프로세스의 가상 주소 공간에 직접 매핑합니다. 결정적으로, 드라이버는 그런 다음 이 새로 매핑된 가상 주소를 다시 SystemBuffer에 기록하여 사용자에게 반환함으로써 애플리케이션에 물리적 메모리를 읽고 쓸 수 있는 직접적인 포인터를 공식적으로 제공합니다.
취약점을 완전히 이해하고 물리적 읽기/쓰기 프리미티브가 확립되었으므로, 이제 모든 퍼즐 조각이 준비되었습니다. 개념 증명을 작성할 시간입니다.
참고: 이 PoC는 Windows 10 22H2 환경에서 특별히 개발 및 테스트되었습니다. 익스플로잇이 원시 물리적 메모리 조작에 의존하기 때문에 커널 구조 오프셋과 물리적 메모리 경계는 현재 내 설정에 맞게 하드코딩되어 있습니다. 자신의 머신에서 테스트하려면 Windows 커널 오프셋을 업데이트하고 물리적 주소 스캔 범위를 특정 OS 빌드 및 RAM 구성에 맞게 조정해야 합니다.
익스플로잇의 첫 번째 단계는 드라이버와의 통신을 설정하는 것입니다. 이를 위해 드라이버의 심볼릭 링크(\\.\PSTRIP64)에서 CreateFileA를 호출했습니다. 유효한 핸들을 얻은 후에는 앞서 분석한 0x80002008 IOCTL을 깔끔하게 남용할 방법이 필요했습니다. MapPhysicalMemory()라는 래퍼 함수를 만들었습니다. 이 함수는 대상 물리적 주소와 읽으려는 메모리 청크의 길이로 사용자 정의 PSTRIP_MAP_REQUEST 구조체를 채웁니다.
그런 다음 DeviceIoControl을 통해 이 구조체를 드라이버에 직접 보냅니다. 성공하면 드라이버는 해당 물리적 메모리를 사용자 모드 애플리케이션에 직접 매핑하고 OutputResult 필드에 가상 기본 주소를 반환합니다. 이제 반환된 주소를 표준 C++ 포인터로 캐스팅하여 시스템의 물리적 RAM에 대한 원시적이고 권한 없는 액세스 권한을 얻을 수 있습니다.
물리적 읽기/쓰기 프리미티브가 완전히 작동하게 되면, 제 목표는 프로세스 권한이 포함된 커널 데이터 구조를 찾는 것이었습니다. Windows에서 실행 중인 모든 프로세스는 EPROCESS 구조체로 표현됩니다.
Windows는 특정 4바이트 식별자인 풀 태그(Pool Tag)를 사용하여 커널 풀에 EPROCESS 구조체를 할당합니다. 프로세스의 경우 이 태그는 문자열 Proc(16진수로 0x636F7250에 해당)입니다. 시스템의 물리적 RAM을 스캔하여 이 정확한 문자열을 검색할 수 있습니다.
익스플로잇은 0x10000000에서 0x140000000까지의 물리적 메모리 공간을 2MB 청크(STEP_SIZE = 0x200000)로 반복하며 매핑합니다. 각 매핑된 청크를 원시 바이트 배열로 캐스팅하고 16바이트 청크(sizeof(_POOL_HEADER)) 단위로 스캔합니다.
그러나 물리적 메모리에서 Proc 태그를 찾는 것만으로는 충분하지 않습니다. 메모리는 지저분합니다. 그 태그는 종료된 프로세스의 잔재일 수도 있고, 단순히 16진수 값과 일치하는 임의의 데이터일 수도 있습니다. 모든 Proc 태그가 유효한 EPROCESS 구조체라고 맹목적으로 가정하고 메모리를 수정하기 시작하면 즉시 BSOD가 발생할 것입니다.
안정성을 보장하기 위해 휴리스틱을 사용하여 구조체의 유효성을 검증해야 했습니다. 먼저, EPROCESS 구조체의 시작점(풀 태그에서 약간 떨어진 위치)을 계산합니다. 그런 다음 실행 중인 프로세스에 대한 몇 가지 알려진 상수를 확인합니다:
0x2(Normal Priority)인지 확인합니다.0x0인지 확인합니다.이러한 휴리스틱이 모두 통과하면 유효하고 활성화된 프로세스를 보고 있다고 확신할 수 있습니다. 그런 다음 고유 프로세스 ID(PID)를 읽습니다. PID가 내 익스플로잇 프로세스와 일치하면 토큰 포인터의 물리적 주소를 저장합니다. PID가 4(Windows System 프로세스)인 경우, 높은 권한을 가진 토큰의 실제 값을 추출하여 저장합니다.
마지막으로, 프로세스 토큰 포인터의 저장된 물리적 주소를 가장 가까운 4KB 경계에 정렬하고 MapPhysicalMemory()를 한 번 더 사용하여 해당 특정 페이지만 매핑합니다.
다음으로 정확한 오프셋으로 이동하여 내 토큰을 System 토큰 값으로 덮어씁니다. 그러면 Windows 커널이 내 익스플로잇 프로세스를 NT AUTHORITY\SYSTEM으로 즉시 처리합니다.
시스템 안정성을 보장하기 위해 페이지를 매핑 해제한 후, 간단히 CreateProcessA를 호출하여 cmd.exe를 실행합니다. 현재 프로세스가 상승되었기 때문에 새 명령 프롬프트는 이러한 최상위 권한을 상속받아 공격이 성공적으로 완료됩니다!
참고: 초기 디버깅 단계에서 발견한 중요한 세부 사항은 드라이버가 매핑된 포인터를 처리하는 방식입니다. SystemBuffer->LowPart = (unsigned int)BaseAddress;를 실행함으로써 드라이버는 64비트 가상 기본 주소를 반환하기 전에 32비트 값으로 캐스팅합니다. 이 잘림은 주소의 상위 비트를 잃어버리며, 64비트 익스플로잇에서 이를 역참조하려고 할 때 즉각적인 액세스 위반이 발생했습니다. 이 문제를 깔끔하게 우회하기 위해 사용자 모드 PoC를 32비트 애플리케이션으로 컴파일하여 반환된 포인터가 완벽하게 유효하도록 했습니다.
참고: 초기 테스트 중에 흥미로운 에지 케이스를 발견했습니다. PoC가 메모리에서 익스플로잇 프로세스를 찾는 데는 성공했지만, System 프로세스(PID 4)를 찾지 못했습니다.
그 이유를 이해하기 위해 물리적 메모리를 직접 검사해야 했습니다. 커널 디버거(WinDbg)를 연결하고 명령어를 사용하여 System 프로세스의 가상 주소와 디렉터리 베이스를 검색했습니다. 그런 다음 !vtop을 사용하여 해당 가상 주소를 RAM의 정확한 물리적 주소로 변환했습니다.
PoC에 연결된 사용자 모드 디버거로 전환했습니다. 메모리 스캔 루프에 조건부 중단점을 설정하여 MapPhysicalMemory() 함수가 System 프로세스의 물리적 주소가 포함된 2MB 청크를 가져오는 순간 실행을 일시 중지하도록 지시했습니다.
중단점이 적중되면 매핑된 메모리의 원시 바이트를 수동으로 검사하기 시작했습니다. 여기서 Windows 커널 풀 할당에 대한 중요한 세부 사항을 발견했습니다.
Windows가 프로세스에 메모리를 할당할 때, _POOL_HEADER(우리의 Proc 태그 포함)로 시작하고, 이어서 _OBJECT_HEADER, 그리고 마지막으로 EPROCESS 구조체 자체로 구성됩니다. 표준 사용자 모드 애플리케이션의 경우 이러한 헤더에는 추가 추적 데이터가 포함되어 있으므로 실제 EPROCESS 구조체는 풀 태그로부터 0x80 바이트 이후에 시작됩니다.
그러나 System 프로세스의 메모리를 검사한 결과 다른 레이아웃이 나타났습니다. System 프로세스에는 이러한 표준 추적 헤더 중 일부가 없습니다. Proc 태그에서 EPROCESS 구조체의 시작까지의 오프셋은 0x40 바이트에 불과했습니다!
수정은 간단했습니다. PoC를 업데이트하여 Proc 태그를 만날 때마다 가능한 오프셋(0x40 및 0x80) 배열을 반복하여 두 풀 헤더 크기를 모두 처리하도록 했습니다.
사이버 보안은 공격자와 방어자 사이의 끝없는 숨바꼭질 게임입니다. 공격자가 취약한 드라이버를 끊임없이 찾는 동안, 현대 보안 제품과 블루 팀은 이 정확한 작업을 탐지하고 차단하는 몇 가지 강력한 방법을 가지고 있습니다.
BYOVD(Bring Your Own Vulnerable Driver) 공격을 막는 가장 효과적인 방법은 드라이버가 로드되는 것을 처음부터 방지하는 것입니다.
드라이버가 이미 로드된 경우에도 보안 제품은 토큰 조작 단계에서 익스플로잇을 탐지할 수 있습니다.
NT AUTHORITY\SYSTEM으로 상승시키는 것은 심각한 위험 신호입니다.cmd.exe)를 생성하는 경우, 특히 상위 프로세스가 SYSTEM으로 실행될 이유가 없을 때 이를 탐지하는 규칙을 만들 수 있습니다.