[Research] 해키보이즈 막내들이 말아주는 Windows LPE 버그헌팅 체험기 Part 1 (KR)

Introduction
안녕하세요 벌써 2025년이 끝이 나네요.. 다들 연말 잘 즐기고 계신가요?🎅이번에 진행된 화이트햇 스쿨에서 저희 팀원분들 일부가 ‘Windows Kernel Driver & Named Pipe LPE 버그헌팅’이라는 주제로 프로젝트에 참여하게 되었는데요. 때는 2025년 5월.. 멘토님의 ‘프로젝트 들어와’라는 한마디에 윈도우 취약점을 찾게 된 banda입니다.

오늘은 제가 part 1을 작성하면서, 특히 Kernel Driver & Named Pipe 벡터를 중점으로 버그헌팅 꿀팁을 전달해드리도록 하겠습니다! 추가로 이후 시리즈들에서 팀원분들이 더욱 유익한 내용을 담아주실 예정이니, 꼭 읽어주시기 바랍니다! 본 시리즈에서는 Kernel Driver Named Pipe 두 가지 요소를 각각 독립적인 LPE 공격 표면으로 살펴보면서, 저희가 찾은 취약점들을 소개하려고 합니다.
Chapter 1: Kernel Driver as an Attack Surface
1.1 Kernel Driver 공격 표면 개요
Windows 환경에서 LPE 취약점은 유저모드와 커널모드 사이의 신뢰범위 검증이 부족할 때 많이 발생되는데, 이 점에서 커널 드라이버는 좋은 타겟이 됩니다. WDM 구조의 커널 드라이버는 기본적으로 IRP(IRP_MJ_DEVICE_CONTROL)를 처리하면서 IO_STACK_LOCATION 구조체를 통해 IOCTL 코드와 입력 버퍼와 출력 버퍼의 길이 정보를 커널에 전달받는 형태를 가지고 있습니다. 이때 IOCTL 전달 방식에 따라 IRP 구조체의 각 필드를 통해 사용자 입력 버퍼에 접근하는데요!
특히 WDM에서 METHOD_BUFFERED에서는 I/O Manager가 커널 버퍼를 할당하고 그 포인터를 Irp->AssociatedIrp.SystemBuffer로 제공하면서 입력과 출력 모두 이 버퍼를 통해 오갑니다. 즉, 이런 IOCTL 처리 로직이 사용자 입력을 커널 주소 공간에서 해석하도록 설계되어있다는 점, 그리고 I/O Manager가 IRP를 구성할 때 사용자 입력에 대한 검증을 수행하지 않는 특성으로 인해 취약점이 발생할 수 있습니다. 이 점을 기억하면서 타겟을 수집하고 분석해보는 프로세스를 살펴보겠습니다.
1.2 Kernel Driver 타겟 수집

먼저 제가 설치한 서드파티 드라이버를 빠르게 찾아봐야겠죠? 첫 단계에서는 FolderChangesView를 사용해서 사용자 환경에서 생성되는 .sys 파일을 추적하면서, 생성된 sys 파일 명과 어디에 생성되었는지 Path를 확인합니다.

저희는 실제로 커널에 로드되어 실행 중인 드라이버를 대상으로 분석을 진행해야 하니까, 그 다음에는 DriverView를 사용해 현재 커널에 로드된 드라이버 목록을 확인해줍니다. 로드된 상태라는건 IOCTL, IRP 처리 루틴이 활성화되어있다는 소리겠죠?

그리고 저희는 간단한 자동화 툴을 만들어 FolderChangesView를 통해 .sys 파일을 추출하는 과정을 거쳐서 드라이버가 위치한 디렉터리에서 분석용 디렉터리로 복사하는 과정을 거쳤습니다. 추출된 파일이 분석용 디렉터리에 잘 옮겨졌는지 확인하면서 분석을 시작하면 됩니다.
1.3 Kernel Driver 분석
드라이버 분석은 아래와 같은 부분을 순서로 분석해보면 됩니다.
- 노출된
DeviceName→ 유저 접근 가능 여부 판단 - WinObj로
\Device노드 접근 가능 여부 확인 - IDA/Plugin 활용해 WDM 드라이버 분석 시작
- IDA를 통해 IOCTL Table 식별
DispatchRoutine분석 → 입력 검증 부분/ 구조체/ 필드 확인- WinDBG를 통해 함수 흐름
/Device요청 처리 과정 추적하면서 동적 분석 수행

먼저 DriverEntry에서 Device, Symlink가 노출되는지 확인하는 것이 첫 번째 단계입니다. 예시처럼 Device Name이 \Device\PdFwKrnl, Symlink가 \DosDevices\PdFwKrnl 같은 식으로 생성되었다면, 이 단계를 통해 유저 모드에서 CreateFile/DeviceControl로 드라이버에 요청을 보낼 가능한 진입점을 확보했다는 의미가 됩니다. DriverObject->MajorFunction 테이블을 통해IRP_MJ_DEVICE_CONTROL이 어떤 디스패치 루틴(DispatchDeviceControl)에 연결되는지를 확인합니다.

\Device에 대한 정보를 알아냈다면 이 드라이버가 유저모드에서 실제로 접근 가능한 진입점을 노출하고 있는지, 접근 제어가 누구한테 어떻게 열려있는지 확인해야 합니다. WinObj에서 \Device 아래에 우리가 찾은 Device Object가 실제로 생성되어있는지 확인하고, Symlink도 존재하는지 확인합니다. 그리고 Device와 Symlink의 Security 탭을 열어서 Everyone인 일반 사용자에게 Read Write 권한이 열려있는지 꼭 확인해줍니다! 열려있어야 일반 사용자도 핸들을 열고 IOCTL 요청을 보낼 수 있기 때문이죠.

이후 IDA의 DriverBuddy를 통해 주요 디스패치 루틴을 빠르게 확인하고, WDM 드라이버인지 식별했습니다. DriverBuddy가 여기에서 커널 API 호출 흔적들을 찾아주는데, 예시로 위에서 나온 MmMapIoSpace, MmUnmapIoSpace, MmBuildMdlForNonPagedPool, MmMapLockedPages 같은 호출 지점들이 DispatchDeviceControl 근처에 있다는걸 확인할 수 있습니다. 특히 MmMapIoSpace같은 물리 주소를 커널 가상 주소로 매핑하는 루틴, 즉 물리주소, 길이, 캐시 타입을 공격자가 간접적으로 제어하면 취약점이 될 수 있으니 저의 경우에는 이걸 분석 벡터로 지정하고 코드를 분석해보도록 하겠습니다.

그리고 DispatchDeviceControl 내부의 switch-case를 기반으로 IOCTL 분기 구조를 역추적하는 단계를 진행합니다. 위 WDM 드라이버 같은 경우 DispatchDeviceControl 내부 switch-case에서 특정 IOCTL Cases들이 0x80002000, 0x80002004, 0x8000201C… 형태로 분기를 타는 것을 알 수 있는데요. 여기에서 MmMapIoSpace 호출과 직접 연결되는 IOCTL Case를 확인할 수 있었습니다. 이렇게 케이스들을 행위 기반으로 분류하면서 옵션 값, 길이, 포인터 처리같은 입력 검증이 어떻게 처리되는지 확인해보면 됩니다.


예를들면 이 분석에서 핵심으로 본 지점은 0x8000202C Case입니다. 해당 케이스에서는 IRP 내부에서 입력 버퍼에서 특정 오프셋 값을 dst/src/size 같은 인자로 해석해 추출해서 memmove에 전달하는 흐름이 나타나는데요. WDM 관점에서 보면 DeviceIoControl 경로에서 IOCTL이 METHOD_BUFFERED라면 I/O Manager은 IRP→AssociatedIrp.SystemBuffer에 커널이 할당한 버퍼 주소를 넣고, 그 버퍼 안의 내용을 유저 인풋에서 복사해서 채웁니다.

0x8000202C Case에 어떻게 진입하는지 확인해보면, 입력 버퍼 길이 0x38만 맞으면, 그 버퍼 안의 오프셋(+0x20/+0x28/+0x30)에서 꺼낸 값을 포인터/길이로 신뢰하고 memmove(dst, src, size)에 전달하는 형태를 가지고 있네요. 이런 식으로 read/ write primitive가 성립할 수 있음을 확인합니다.

이후 정적 분석을 통해 알아낸 정보를 통해 WinDBG에서 함수 흐름을 따라가보면 됩니다. 아까 0x8000202C에서 SystemBuffer(rsi)+0x20/0x28/0x30 값을 memmove(dst, src, size)에 넣는다는걸 확인했는데, payload를 작성하고 rsi가 가리키는 METHOD_BUFFERED의 SystemBuffer 주변을 확인해보면… 의도한대로 ‘A’로 덮인 것을 확인할 수 있죠?

취약점을 확인했다면 이제 exploit을 해주면 되는데요 예시로 위의 경우를 한번 확인해보면 IOCTL 입력 구조체에다 dst=output_buf(유저 주소), src=읽고 싶은 커널 주소, size=8을 넣어 DeviceIoControl로 호출하면 커널이 memmove(dst, src, size)를 수행하면서 8바이트 단위 Arbitrary Read가 성립합니다. 윈도우 버그 찾는 사람들에게 익숙한 친구인 NtQuerySystemInformation 핸들 테이블을 통해 EPROCESS 주소를 얻고, 이렇게 얻은 SYSTEM EPROCESS 주소에 TOKENOFFSET을 더해 EPROCESS->Token 값을 읽어온 후, 다시 동일한 방식으로 현재 EPROCESS 주소를 구해 Token 필드 위치를 계산한 뒤, IOCTL에 dst=현재 EPROCESS+TOKENOFFSET, src=준비한 SYSTEM 토큰 포인터, size=8 이렇게 넣어서 Arbitrary Write를 수행해 Privilege escalation을 성공시켰습니다. 이렇듯 세부 페이로드 구성은 나타난 취약점 유형에 따라 구성하면 됩니다.
Chapter 2: Named Pipe as an Attack Surface
2.1 Named Pipe 공격 표면 개요
커널 드라이버를 살펴보았다면 이번에는 Named Pipe를 살펴볼 차례인데요. Named Pipe 통신 방식에 대한 약간의 이해를 전제하고 있기 때문에 관련된 연구글이 👉여기에 있으니 한번 확인해보고 오시는걸 추천드립니다! Named Pipe는 IPC 통신 메커니즘으로 사용이 되지만, 커널 오브젝트로 관리되고 커널 드라이버와 마찬가지로 SYSTEM 권한 프로세스가 유저모드와 통신하기 위해 빈번하게 사용됩니다. 특히 서비스 프로세스가 Named Pipe 입력을 보낸 뒤에 이걸 내부 메세지로 신뢰하고 구조체로 파싱하거나 권한 변경, 레지스트리 동작, 프로세스 실행같은 민감 동작으로 연결되는 경우가 꽤 많아서 검증이 부족하면 LPE로 이어질 수 있습니다. IPC는 IOCTL과 다르게 어떻게 접근해봐야 할지 궁금하시죠? 한번 확인해봅시다!
2.2 Named Pipe 타겟 수집
일반적인 타겟 수집은 먼저 (1) SYSTEM/서비스 프로세스 후보를 고리고, (2) 해당 프로세스가 열고 있는 파이프 핸들을 확보한 다음에, (3) 파이프 ACL을 통해 Everyone 같은 비관리자가 Read Write 가능한지 알아보는 순서로 진행이 됩니다.

먼저 Process Explorer을 통해서 프로세스 후보를 잡으면서 시작하게 되는데요. 제가 잡는 기준은 실행 계정이 SYSTEM 프로세스인지, 서드파티 서비스인지, HANDLE에 Named Pipe가 존재하는지, Integrity Level이 SYSTEM인지 정도를 확인하고 타겟을 좁혀갑니다.

특히 Process Explorer 하단의 Handles 탭에서 NamedPipe가 존재하는지 알 수 있는데요. 이때 Type이 File로 잡히면서 Name이 \Device\NamedPipe\… 로 시작하는 항목이 Named Pipe입니다. 여기서 수집된 \Device\NamedPipe\…경로는 는 유저모드에서 \.\pipe\… 같은 형태로 매핑되는 지점이 됩니다.

하지만 가장 큰 관문이 하나 남아있는데요.. 바로 ACL입니다. 힘들게 Named Pipe가 존재하는걸 확인했는데 이걸 읽고 쓸 수가 없으면… 그 Named Pipe와는 작별인사를 해야 합니다.😭 Administrators만 RW이고 나머지에겐 권한이 없으면 LPE가 성공하는 벡터로 보기 힘들고, Everyone, Authenticated Users, Users 같은 광범위한 그룹에 RW가 열려 있는지 accesschk같은 툴을 이용해서 파이프 ACL을 검사하고 타겟을 좁혀나가면 됩니다.

그렇게 타겟을 선정했다면, 바이너리를 열어서 CreateNamedPipeA/W, ConnectNamedPipe, DisconnectNamedPipe 같은 API들을 빠르게 검색해서 이게 서버 구현이 맞는지 확인하고, Connect → Read 흐름, 파싱 분기, 토큰/접근 제어같은 권한 관련 분기들을 확인해보면서 코드를 분석해보면 타겟 수집 프로세스는 끝이 납니다.
2.3 Named Pipe 분석
분석은 아래와 같은 순서로 진행할 수 있었습니다.
- Process Explorer로 서버(SYSTEM 서비스) 프로세스 후보 선정
- 수집한
\Device\NamedPipe\...가 유저 모드에서\\.\pipe\...로 연결되는 지점인지 확인 - accesschk로 Pipe ACL 점검 → Everyone같은 광범위 그룹 Read/Write 열림 여부 확인
- IDA로
CreateNamedPipe*→ConnectNamedPipe→ReadFile/WriteFile같은 흐름과 요청 처리 함수 디스패치 잡기 - 어떤 요청이 레지스트리/HKLM, 서비스 설정 등 권한이 중요한 동작으로 이어지는지 분기로 좁히기
- ProcMon 기반으로 이벤트 잡으면서 동적분석 수행

타겟을 정했으면 동적 분석은 procmon을 통해서 파이프 I/O가 어떤 취약한 행위로 이어지는지 확인하는 방식으로 시작합니다. 먼저 파이프를 생성하고 수신하는 서버 프로세스와 파이프에 접속하고 송신하는 클라이언트 프로세스를 좁힌 후에, CreateNamedPipe, CreateFile, ReadFile, WriteFile같은 IPC 호출 흐름을 해당 프로세스 범위에서 추적할 준비를 합니다. 주로 Process Name, Operation에 레지스트리 키 관련된 이벤트나 named pipe 핵심 I/O 이벤트를 Filter에 등록하고, 서버 Integrity가 System인지 필터로 고정해두는게 좋습니다. 그렇게 해야 유저 입력이 SYSTEM으로 권한 상승이 가능해질 수 있는지 확인하기에 좋습니다.

마침 제가 분석한 프로그램에서는 설정에 Non Admin도 Access Control로 일부 관리 기능이 허용되는 체크박스가 있네요.. 안티바이러스니까 그럴만도 합니다. 그런데 보통 이렇지는 않는데 비관리자 상태에서 설정 변경/ 보호 비활성화 허용 체크박스를 계속 클릭해봐도 UAC가 전혀 뜨지 않네요?? 뭔가 수상하죠? 바로 해당 서비스를 우선 순위로 의심해서 가봅시다.
PS C:\Users\banda\Downloads\AccessChk> .\accesschk.exe -accepteula -lv "\pipe\K7TSMngrService1"
\\.\Pipe\K7TSMngrService1
DESCRIPTOR FLAGS:
[SE_DACL_PRESENT]
[SE_SACL_PRESENT]
[SE_SACL_AUTO_INHERITED]
[SE_SELF_RELATIVE]
OWNER: NT AUTHORITY\SYSTEM
LABEL: Low Mandatory Level
SYSTEM_MANDATORY_LABEL_NO_WRITE_UP
[0] ACCESS_ALLOWED_ACE_TYPE: NT AUTHORITY\SYSTEM
FILE_ADD_FILE
FILE_CREATE_PIPE_INSTANCE
FILE_APPEND_DATA
FILE_LIST_DIRECTORY
FILE_READ_ATTRIBUTES
FILE_READ_DATA
FILE_READ_EA
FILE_WRITE_ATTRIBUTES
FILE_WRITE_DATA
FILE_WRITE_EA
SYNCHRONIZE
READ_CONTROL
[1] ACCESS_ALLOWED_ACE_TYPE: Everyone
FILE_ADD_FILE
FILE_CREATE_PIPE_INSTANCE
FILE_APPEND_DATA
FILE_LIST_DIRECTORY
FILE_READ_ATTRIBUTES
FILE_READ_DATA
FILE_READ_EA
FILE_WRITE_ATTRIBUTES
FILE_WRITE_DATA
FILE_WRITE_EA
SYNCHRONIZE
READ_CONTROL
ACL을 직접 확인해본 결과, \\.\pipe\K7TSMngrService1 쪽에서 의심과 맞게 DACL이 너무 널널하게 설정되어 있습니다.. Everyone에 file read, file write, file append같은게 허용되었으니, 사실상 누구나 이 파이프에 연결을 요청해 write하고 read하는 통신을 할 수 있다는 소리입니다. 이 점을 확인했으면 이 시점부터는 디스패치나 파싱 루틴을 정적 분석 해보면서 어떤 요청이 권한이 중요한 동작으로 이어지는지 두 눈으로 확인해봅시다.

권한 정보를 확인했으니 정적 분석을 통해 Named Pipe의 서버 코드를 확인해보면 좋겠죠? 생성 시점부터 큰 골격으로 확인해보는게 좋은데, 예를들면 여기에서는 CreateNamedPipeA("\\\\.\\pipe\\K7TSMngrService1", ...)로 Named Pipe를 생성하고, ConnectNamedPipe가 성공하면 요청 처리 핸들러 함수로 디스패치하는 흐름을 확인할 수 있습니다! 이런 패턴을 확인했으면 다음 단계는 ReadFile로 어떤 헤더를 읽는지 같은 프로토콜 검증 부분도 한번 확인해봅시다.

// request header (28 bytes)
typedef struct _K7TS_REQ_HDR {
uint32_t magic; // 'K7TS'
uint32_t ver; // 0x1010
uint32_t hdr_sz; // 28
uint32_t reserved; // 0
uint32_t op; // v21
uint32_t in_len; // nNumberOfBytesToRead
uint32_t out_len; // v23
} K7TS_REQ_HDR;
예를들면 \\.\pipe\K7TSMngrService1로 통신하려면, 요청 버퍼를 어떻게 구성해야지 연결에 성공할 수 있을까요? 코드를 뜯어보면 대상 파이프 \\.\pipe\K7TSMngrService1를 잡으면서 result = 0x4B375453('K7TS'), ver=0x1010, hdr_sz=28, 그리고 요청 타입과 입출력 길이를 1mb로 제한한 뒤에만 이후에 heap 버퍼를 할당해 ReadFile로 요청을 수신하고, 내부 디스패처로 넘겨 응답을 만든 다음에 36바이트 응답헤더 + 응답 바디를 WriteFile로 반환하고 DisconnectNamedPipe, CloseHandle로 세션을 종료하는 프로토콜 검증 루틴을 수행하고 있습니다! 파이프에 연결해 통신하기 위한 중요한 정보를 알았습니다.

검증 루틴을 확인했으니 이제는 아까 설정 부분에서 봤던 것처럼 일반 사용자가 관리 기능을 허용할 수 있게 하는 것처럼 권한이 중요한 동작으로 이어지는 부분을 확인해봅시다. 문자열 참조를 따라가면 AdminNonAdminIsValid, AdminChangesNeedPassword, AdminEnableNeedVerification 이런 문자열들을 통해서 K7TS 구성 요소가 레지스트리 기반 정책 플래그를 직접 참조하고 있다는 점을 알 수 있었습니다. 이 파이프 요청 하나로 SYSTEM 서비스가 HKLM에 Write를 수행하고 있는데, 검증이 충분히 제한되지 않으면 저희같은 일반 사용자가 SYSTEM 레지스트리 Write를 할 수 있는 primitive가 될 수 있겠습니다.

이제 위에서 알아낸 정보를 바탕으로 프로토콜 조건을 만족하는 형태로 요청을 잘 보내보고 Process Monitor 로그를 확인해주었는데요. Named Pipe는 일단 IPC 엔드포인트기 때문에, 레지스트리/HKLM, 서비스 설정, 파일 읽기/쓰기, 프로세스 실행같은 바뀌는 지점을 중심으로 이벤트를 잘 확인을 하는게 좋습니다. 예를들면 지금 ProcMon 로그는 일반 사용자인 제가 입력을 통해 파이프 요청을 준 후에, SYSTEM 권한 K7TSMngr.exe가 HKLM에 쓰기 작업을 수행하는 것을 확인할 수 있고, 특히 관련 키인 AdminNonAdminIsValid, AdminChangesNeedPassword, AdminChangesPasswordHash 같은 필드에 write가 발생하는지를 트리거해보고 보면 관리자 권한 요구, 비관리자 허용같은 보안 경계가 풀릴 수 있다는 점을 다시 한번 확인했습니다.

이제 LPE를 해보면 되는데요. 일단은 일반 사용자로 \\.\pipe\K7TSMngrService1에 연결 요청을 보내서 K7TS 헤더와 매직 형식을 만족하는 요청을 보내 SYSTEM이 레지스트리 설정 기능을 대신 설정하게 만든 뒤에, HKLM IFEO를 써서 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\K7TSHlpr.exe 이 키를 대상으로 디버거로 띄울려고 하는걸 cmd.exe /c C:\temp\payload.bat으로 바꿔치기하면..? LPE가 성공하게 됩니다! K7TSMngrService1 파이프 요청을 처리하는 주체는 K7TSMngr.exe였는데, 이때 이 안에서 내부 동작을 수행하는 K7TSHlpr.exe 보조 프로세스가 권한이 높은 컨텍스트로 실행된다는 취약점을 정적 분석을 통해 확인할 수 있었고, 이걸 이용해서 우리가 원하는 bat 파일을 실행시킬 수 있었던 것이죠.

권한 상승을 통해 Local Group Memberships에서 권한이 Administrators가 된 것을 확인할 수 있습니다. 추가로 이처럼 Named Pipe에서는 주로 ACL, 권한 검사 부재, 메모리 취약점, 경로 조작, 레이스컨디션같은 취약점이 존재하니.. 많은 참고가 되었으면 좋겠습니다!
마치며
호호호…🦌이번 글이 좋은 연말 선물이 되었기를 바랍니다! Kernel Driver, Named Pipe 수집과 분석 프로세스가 여러분에게 도움이 되었으면 좋겠습니다. 프로세스를 설명할 때 여기서는 예전 cve들을 살펴봤지만 검증은 Win11 24H2 환경에서 진행한 점을 참고해주세요. 티엠아이인데 사실 글을 써보다보면서 툴들을 열어보다보니까 드라이버 타겟이 막 눈에 들어와서 계속 딴 길로 샜습니다.. 하핫 다음 시리즈에서는 저희가 찾은 취약점들을 소개해보도록 하겠습니다! 감사합니다.
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.