Windows PG Initialize Analysis

[0x00] Concept

기존에 여러 문서들을 확인하며 KPP(Kernel Patch Protection) 의 초기화와 주요 내용들을 살펴봤다면, 실제로 초기화되는 내용을 디버깅하며 분석해봤습니다.

몇 가지 새로 알게된 사실과 간단한 로직들에 대한 내용들이 존재합니다.

해당 포스팅 다음 순서로는 패치가드가 트리거되는 부분부터 역분석을 진행합니다.

테스트가 진행된 버전은 Windows 10, 2004(19041) 입니다.

[0x01] FUNCTIONEXTENTLIST

처음 KiInitializePatchGuardContext 함수를 Hex-ray 를 통해 확인하면 간단한 안티 디버깅 로직들을 지나 특정 전역변수를 확인합니다.

INITDATA 섹션에 존재하는 해당 전역변수들은 $$2b, $$20, $$2c 와 같이 알 수 없는 이름으로 되어 있습니다. 아래는 분석하며 네이밍한 의사코드들 입니다. 하나씩 차례대로 살펴보겠습니다.

...
if(!g_Ntoskrnl)
 {
    NtoskrnlBase = 0x140000000ui64;
    UncompressedBuffer = 0i64;
    if ( LdrResFindResource(
           0x140000000i64,
           0xA,
           L"FUNCTIONEXTENTLIST",
           0,
           &ResourceBuffer,
           &ReturnLength,
           0i64,
           0i64,
           0) >= 0
      && (ReturnLength - 8) <= 0xFFFFFFF7 )
    {
      ResourceLength = ReturnLength;
      if ( RtlImageNtHeader(0x140000000ui64) )
      {
        LOBYTE(bMappedAsImage) = 1;
        ExceptionDirectory = RtlImageDirectoryEntryToData(
                               0x40000000,
                               bMappedAsImage,
                               IMAGE_DIRECTORY_ENTRY_EXCEPTION,
                               &ExceptionDirectorySize);// Get Exception Directory
        if ( ExceptionDirectory )
        {
          WorkSpaceBuffer = 0i64;
          Status = 0xC0000001;
          if ( *ResourceBuffer != 'EXTC' )      // Validate check resource header
          {
            if ( *ResourceBuffer != 'EXTL' )
              goto LABEL_33;
            goto LABEL_86;
          }
          if ( RtlGetCompressionWorkSpaceSize(4i64, BufferWorkSpaceSize, FragmentWorkSpaceSize) >= 0 )// COMPRESSION_FORMAT_XPRESS_HUFF
          {
            UncompressedBufferSize = *(ResourceBuffer + 4);
            if ( UncompressedBufferSize < 8 )
            {
              BufferWorkSpaceSize[1] = 0xD0002D61;
              KeBugCheckEx(__ROR4__(0xD0000013, 0x5C), 0xEui64, ResourceBuffer, 0x140000000ui64, ResourceLength);
            }
            TimeStampCounter = __rdtsc();
            v25 = (__ROR8__(TimeStampCounter, 3) ^ TimeStampCounter) * 0x7010008004002001ui64;
						
// random tag 생략            

            WorkSpaceBuffer = ExAllocatePoolWithTag(0x200, BufferWorkSpaceSize[0], v29);
            if ( !WorkSpaceBuffer )
            {
LABEL_61:
              UncompressedBuffer = 0i64;
              goto LABEL_33;
            }
            v35 = __rdtsc();
            RandValue = (__ROR8__(v35, 3) ^ v35) * 0x7010008004002001ui64;
            v4106 = *(&RandValue + 1);

// random tag 생략

            UncompressedBuffer = ExAllocatePoolWithTag(0x200, UncompressedBufferSize, v40);
            if ( !UncompressedBuffer )
              goto LABEL_116;
            Status_1 = RtlDecompressBufferEx(
                         4,                     // COMPRESSION_FORMAT_XPRESS_HUFF
                         UncompressedBuffer,
                         UncompressedBufferSize,
                         ResourceBuffer + 8,
                         ResourceLength - 8,
                         &ResourceLength,
                         WorkSpaceBuffer);
            if ( Status_1 < 0 )
            {
              v3883 = 0xD0002D61;
              KeBugCheckEx(__ROR4__(0xD0000013, 0x5C), 0xFui64, ResourceBuffer, 0x140000000ui64, Status_1);
            }
LABEL_86:
            if ( UncompressedBuffer )
            {
LABEL_110:
              Index = 0;
              for ( AddExceptionValue = ExceptionDirectory - 0x40000000;
                    Index < ExceptionDirectorySize;
                    AddExceptionValue += *(ExceptionDirectory + 4 * (NextIndex >> 2)) )// ExceptionDirectory 의 값을 4바이트씩 읽고, OffsetValue 와 더한다. 현재 기준으로 0xcc000 이 ExceptionDirectory의 오프셋이고 해당 값과 ExceptionDirectory에 존재하는 값과 더
              {
                NextIndex = Index;
                Index += 4;
              }                                 // 해당 루프가 끝나면 ExceptionDirectory 사이즈를 4바이트씩 돌아 더한 값인 0x26250b8c 가 나온다.
              InitializeCompareData = *(UncompressedBuffer + 4);// 해당 값이 압축이 풀린 리소스 데이터와 같은지 확인한다.
              if ( AddExceptionValue != InitializeCompareData )
              {
                v3894 = 0xD0002D61;
                KeBugCheckEx(
                  __ROR4__(0xD0000013, 0x5C),
                  0x10ui64,
                  UncompressedBuffer,
                  0x140000000ui64,
                  InitializeCompareData ^ AddExceptionValue);
              }
              InitializeCompareLength = *UncompressedBuffer;
              if ( InitializeCompareLength >= 0x1FFFFFFF || 8 * InitializeCompareLength > ResourceLength )// stack 으로, OUT 된 UncompressedBufferSize 임 
              {
                v3895 = 0xD0002D61;
                KeBugCheckEx(
                  __ROR4__(0xD0000013, 0x5C),
                  0x11ui64,
                  UncompressedBuffer,
                  0x140000000ui64,
                  InitializeCompareLength);
              }
              UncompressedBufferSize_1 = ResourceLength;
              Status = 0;
              goto LABEL_116;
            }
            v49 = __rdtsc();
            v50 = (__ROR8__(v49, 3) ^ v49) * 0x7010008004002001ui64;

// random tag 생략            

            PoolWithTag = ExAllocatePoolWithTag(0x200, ResourceLength - 4i64, v54);
            UncompressedBuffer = PoolWithTag;
            if ( PoolWithTag )
            {
              memmove(PoolWithTag, (ResourceBuffer + 4), ResourceLength - 4i64);
              goto LABEL_110;
            }
LABEL_116:
            if ( WorkSpaceBuffer )
              ExFreePoolWithTag(WorkSpaceBuffer, InitializeCompareLength);// 불필요한 데이터 free?
            if ( !UncompressedBuffer || Status >= 0 )
              goto LABEL_33;
            ExFreePoolWithTag(UncompressedBuffer, InitializeCompareLength);
            goto LABEL_61;
          }
        }
      }
    }
LABEL_33:
    UncompressedBufferPtr = UncompressedBuffer;
    NtoskrnlBasePtr = &NtoskrnlBase;
    index = 0x18;
    GlobalVariablePtr = &g_Ntoskrnl;            // 첫 세팅은 해당 전역변수임 
    Count = 3i64;
    do
    {
      index -= 8;
      *GlobalVariablePtr++ = *NtoskrnlBasePtr++;
      --Count;
    }
    while ( Count );
    for ( ; index; --index )
    {
      v22 = *NtoskrnlBasePtr;
      NtoskrnlBasePtr = (NtoskrnlBasePtr + 1);
      *GlobalVariablePtr = v22;
      GlobalVariablePtr = (GlobalVariablePtr + 1);
    }
    goto COMPLETE_GLOBAL_VAR;
  }
  v66 = 0x18;
  v67 = &g_Ntoskrnl;
  p_NtoskrnlBase = &NtoskrnlBase;
  v69 = 3i64;
  do
  {
    v66 -= 8;
    *p_NtoskrnlBase++ = *v67++;
    --v69;
  }
  while ( v69 );
  for ( ; v66; --v66 )
  {
    v70 = *v67;
    v67 = (v67 + 1);
    *p_NtoskrnlBase = v70;
    p_NtoskrnlBase = (p_NtoskrnlBase + 1);
  }
COMPLETE_GLOBAL_VAR:

ResourceData

먼저 LdrResFindResource 함수를 이용합니다. ntoskrnl 이미지 내 리소스를 찾는 함수로 원형은 알 수 없으나 함수 호출 결과 내용으로 보았을 때 다음과 같이 확인할 수 있었습니다.

NTSTATUS LdrResFindResource(
	IN  PVOID ImageBase,
	IN  ULONG ResourceType,
	IN  wchar_t* ResourceName,
	IN  ULONG Unknown,
	OUT PVOID ResourceBuffer,
    OUT PULONG ResourceSize,
    OPTIONAL ULONG64 Unknown,
	OPTIONAL ULONG64 Unknown,
	OPTIONAL ULONG Unknown,
);

리소스 타입의 경우 FindResource 호출 시 사용되는 타입과 동일하게 보였습니다. 0xA이므로 RT_RCDATA 로 추정됩니다.

이 때 사용되는 리소스 이름은 FUNCTIONEXTENTLIST 입니다. 해당 리소스 내용을 어떻게 사용하는지는 더 분석이 필요합니다.

다음으로 RtlImageDirectoryEntryToData 루틴을 이용하여 ExceptionDirectory 를 가져옵니다.

ExceptionDirectory = RtlImageDirectoryEntryToData(
                               0x40000000,
                               bMappedAsImage,
                               IMAGE_DIRECTORY_ENTRY_EXCEPTION,
                               &ExceptionDirectorySize);

ExceptionDirectory 가 존재하는 경우 아래와 같이 리소스의 고유 시그니처 값을 검증합니다. 즉 첫 4바이트의 값이 ‘EXTC(CTXE)’ 여야 하는 것으로 보입니다.

if ( *ResourceBuffer != 'EXTC' )      // Validate check resource header
{
    if ( *ResourceBuffer != 'EXTL' )
        goto LABEL_33;
    goto LABEL_86;
}

해당 헤더가 올바른 경우 아래의 로직으로 진행합니다. RtlGetCompressionWorkSpaceSize 루틴을 이용하여 WorkSpace의 올바른 크기를 구합니다.

MSDN 에 따르면 RtlCompressBuffer, RtlDecompressFragment 루틴을 사용하는데 올바른 크기를 구하기 위해 사용한다고 되어 있습니다.

원형은 아래와 같습니다.

NT_RTL_COMPRESS_API NTSTATUS RtlGetCompressionWorkSpaceSize(
  [in]  USHORT CompressionFormatAndEngine,
  [out] PULONG CompressBufferWorkSpaceSize,
  [out] PULONG CompressFragmentWorkSpaceSize
);

위의 내용으로 미루어보았을 때, 첫 번째 인자인 압축 포맷과 압축 엔진에 따른 올바른 크기를 반환해주는 기능으로 추정됩니다.

if ( RtlGetCompressionWorkSpaceSize(4i64, BufferWorkSpaceSize, FragmentWorkSpaceSize) >= 0 )// COMPRESSION_FORMAT_XPRESS_HUFF
{
    UncompressedBufferSize = *(ResourceBuffer + 4);
    if ( UncompressedBufferSize < 8 )
    {
        BufferWorkSpaceSize[1] = 0xD0002D61;
        KeBugCheckEx(__ROR4__(0xD0000013, 0x5C), 0xEui64, ResourceBuffer, 0x140000000ui64, ResourceLength);
    }
...

위의 로직을 확인하면 첫 번째 파라미터로 4 가 전달되며 이는 COMPRESSION_FORMAT_XPRESS_HUFF 입니다. 해당 루틴이 성공하면 ResourceBuffer(FUNCTIONEXTENTLIST) 의 4바이트 뒤에 값을 가져와 특정 변수에 담습니다. 해당 변수는 UncompressedBufferSize 로 명명하였으며 이는 좀 더 뒤에 설명됩니다.

RtlGetCompressionWorkSpaceSize 루틴으로 반환된 BufferWorkSpaceSize를 이용하여 랜덤한 태그의 풀을 할당하고 아래와 같은 또 다른 풀을 할당하게 됩니다.

UncompressedBuffer = ExAllocatePoolWithTag(0x200, UncompressedBufferSize, v40);
            if ( !UncompressedBuffer )
              goto LABEL_116;
            Status_1 = RtlDecompressBufferEx(
                         4,                     // COMPRESSION_FORMAT_XPRESS_HUFF
                         UncompressedBuffer,
                         UncompressedBufferSize,
                         ResourceBuffer + 8,
                         ResourceLength - 8,
                         &ResourceLength,
                         WorkSpaceBuffer);

위에서 *(ResourceBuffer+4)UncompressedBufferSize로 명명한 이유가 바로 위의 로직 때문입니다.

RtlDecompressBufferEx 루틴의 원형은 아래와 같으며 위에서 설명한 WorkSpace 풀을 할당한 이유도 설명됩니다.

NT_RTL_COMPRESS_API NTSTATUS RtlDecompressBufferEx(
  [in]  USHORT CompressionFormat,
  [out] PUCHAR UncompressedBuffer,
  [in]  ULONG  UncompressedBufferSize,
  [in]  PUCHAR CompressedBuffer,
  [in]  ULONG  CompressedBufferSize,
  [out] PULONG FinalUncompressedSize,
  [in]  PVOID  WorkSpace
);

마찬가지로 COMPRESSION_FORMAT_XPRESS_HUFF 포맷입니다. 3번째 파라미터로 전달되는게 압축 해제된 사이즈이므로 위에서 *(ResourceBuffer+4) 위치에 존재하는 값이 압축 해제된 크기를 의미한다는 것을 알 수 있었습니다.

4번째 파라미터인 CompressedBuffer 의 경우 압축을 풀기 위한 대상으로 즉, 압축 된 데이터 버퍼를 의미합니다. 이 때 전달되는 값은 ResourceBuffer 이며 4바이트의 헤더와 4바이트의 압축 해제 크기를 제외하기 때문에 ResourceBuffer + 8 로 전달합니다.

ResourceLength 의 경우도 마찬가지로 실제로 압축된 데이터의 크기를 전달하므로 헤더와 압축 해제 크기를 제외하여 -8 이 되는 것으로 확인됩니다.

위의 내용으로 보았을 때 FUNCTIONEXTENTLIST 리소스의 내용은 아래와 같이 예상할 수 있습니다.

struct PG_COMPRESSED_DATA{
    ULONG Header;
    ULONG DecompressSize;
    BYTE  CompressedData[1];
}

다음 압축 해제된 데이터를 이용한 로직이 존재합니다.

먼저 ExceptionDirectoryntoskrnl 이미지 베이스의 차를 구하여 오프셋을 구합니다.

그리고 해당 오프셋을 IV 값으로 두고, *(ULONG*)((DWORD64)ExceptionDirectory + index * 4) 의 형식으로 값을 더합니다.

if ( UncompressedBuffer )
            {
LABEL_110:
              Index = 0;
              for ( AddExceptionValue = ExceptionDirectory - 0x40000000;
                    Index < ExceptionDirectorySize;
                    AddExceptionValue += *(ExceptionDirectory + 4 * (NextIndex >> 2)) )
              {
                NextIndex = Index;
                Index += 4;
              }                                 
          ...

해당 값이 모두 더해지면 무결성에 대한 검증으로 보이는 체크를 시작합니다.

              ...
              InitializeCompareData = *(UncompressedBuffer + 4);// 해당 값이 압축이 풀린 리소스 데이터와 같은지 확인한다.
              if ( AddExceptionValue != InitializeCompareData )
              {
                v3894 = 0xD0002D61;
                KeBugCheckEx(
                  __ROR4__(0xD0000013, 0x5C),
                  0x10ui64,
                  UncompressedBuffer,
                  0x140000000ui64,
                  InitializeCompareData ^ AddExceptionValue);
              }
              InitializeCompareLength = *UncompressedBuffer;
              if ( InitializeCompareLength >= 0x1FFFFFFF || 8 * InitializeCompareLength > UncompressedBufferSize) 
              {
                v3895 = 0xD0002D61;
                KeBugCheckEx(
                  __ROR4__(0xD0000013, 0x5C),
                  0x11ui64,
                  UncompressedBuffer,
                  0x140000000ui64,
                  InitializeCompareLength);
              }
              UncompressedBufferSize_1 = ResourceLength;
              Status = 0;
              goto LABEL_116;
            }

UncompressedBuffer 에서 4바이트 떨어진 위치에 존재하는 4바이트 값과 위의 연산을 통해 구해진 값이 일치하는지 확인합니다. UncompressedBuffer 시작 위치에서부터 4바이트 값에 대한 크기 검증을 진행합니다.

이를 토대로 ntoskrnl 이 초기화 될 때 리소스 영역의 압축 해제 된 내용과 초기 ExceptionDirectory 에 있는 값들을 이용하여 무결성 체크를 한다고 생각했습니다. 때문에 아래와 같이 정의하였습니다.

struct PG_UNCOMPRESS_DATA{
    ULONG IntegrityCheckLength;
    ULONG InitIntegrityCheckData;
    BYTE UnknownData[sizeof(UncompressedData-8)];
}

이제 위의 로직을 지나 무결성이 확인되면, 압축 해제에 사용된 WorkSpaceBuffer 를 해제합니다. Status 값은 0이므로 UncompressedBuffer는 해제되지 않습니다.

LABEL_116:
            if ( WorkSpaceBuffer )
              ExFreePoolWithTag(WorkSpaceBuffer, InitializeCompareLength);// 불필요한 데이터 free?
            if ( !UncompressedBuffer || Status >= 0 )
              goto LABEL_33;
            ExFreePoolWithTag(UncompressedBuffer, InitializeCompareLength);
            goto LABEL_61;

다음은 각각의 전역변수들을 설정합니다. 위에서 언급한 g_ntoskrnl 의 경우에도 바로 여기서 설정됩니다.

LABEL_33:
    UncompressedBufferPtr = UncompressedBuffer;
    NtoskrnlBasePtr = &NtoskrnlBase;
    index = 0x18;
    GlobalVariablePtr = &g_Ntoskrnl;             
    Count = 3i64;
    do
    {
      index -= 8;
      *GlobalVariablePtr++ = *NtoskrnlBasePtr++;
      --Count;
    }
    while ( Count );
    for ( ; index; --index )
    {
      v22 = *NtoskrnlBasePtr;
      NtoskrnlBasePtr = (NtoskrnlBasePtr + 1);
      *GlobalVariablePtr = v22;
      GlobalVariablePtr = (GlobalVariablePtr + 1);
    }
    goto COMPLETE_GLOBAL_VAR;

해당 루틴을 통해 아래와 같이 전역변수들이 사용되는 것을 확인할 수 있습니다.(물론 이미 패치가드가 초기화 된 이후에 해당 값들은 알 수 없는 값들로 채워집니다.)

Conclusion

지금까지 위의 내용들을 정리하면 다음과 같습니다.

  1. LdrResFindResource 를 통해 ntoskrnl 이미지 내 FUNCTIONEXTENTLIST 라는 리소스를 찾는다.
  2. RtlImageDirectoryEntryToData 를 이용하여 ExceptionDirectory 를 가져오며, 존재하는 경우 리소스의 압축을 해제한다.
  3. 해당 리소스(압축됨)의 구조는 아래와 같다.

     struct PG_COMPRESSED_DATA{
         ULONG Header;
         ULONG DecompressSize;
         BYTE  CompressedData[1];
     }
    
  4. RtlDecompressBufferEx 루틴을 통해 압축을 해제한다.

     struct PG_UNCOMPRESS_DATA{
         ULONG IntegrityCheckLength;
         ULONG InitIntegrityCheckData;
         BYTE UnknownData[sizeof(UncompressedData-8)];
     }
    
  5. ExceptionDirectory 를 이용하여 비교할 값을 생성한다.

     ULONG CompareValue = ExceptionDirectory - ntoskrnl;
        
     for(int index = 0; index<ExceptionDirectorySize; index+=4)
     {
         CompareValue += *(ULONG*)((DWORD64)ExceptionDirectory + index);  
     }
    
  6. 5에서 획득한 값을 통해 무결성을 검증하고 사이즈를 검증한다.

     if(UncompressedBuffer.InitIntegrityCheckData== CompareValue)
     {
         if(
         UncompressedBuffer.IntegrityCheckLength < 0x1FFFFFFF 
         && UncompressedBuffer.IntegrityCheckLength < UncompressedBufferSize)
         { pass }
         else { goto CRITICAL_INITIALIZATION_FAILURE; } 
     }
     else { goto CRITICAL_INITIALIZATION_FAILURE; }
        
     CRITICAL_INITIALIZATION_FAILURE:
         KeBugCheck(CRITICAL_INITIALIZATION_FAILURE);
    
  7. 검증을 통과한 경우 위에서 설정된 지역 변수들을 전역변수에 설정한다.

     /* Global Variable */
     PVOID g_ntoskrnl = nullptr;                                 // 0x00
     0x08 PG_UNCOMPRESSED_DATA* g_UncompressedData = nullptr;    // 0x08
     0x10 PULONG g_UncompressedSize = 0;                         // 0x10
     /* Global Variable */
        
     /* Local Variable */
     PVOID ntoskrnlbase;                // 0x00
     PG_UNCOMPRESSED_DATA* UncompData;  // 0x08
     PULONG UncompSize;                 // 0x10
     /* Local Variable */
        
     PVOID g_Pointer = &g_ntoskrnl;
     PVOID l_Pointer = &ntoskrnl
     for(int index = 0; index < 3; index ++)
     {
         g_Pointer[i] = l_Pointer[i];
            
     }
    

여기서 흥미로운 점은 INITDATA 의 해당 전역변수 주위에 있는 대부분의 전역변수들은 패치가드 초기화와 관련된 루틴들에서 참조된다는 점 입니다.

[0x02] INITKDBG, KiErrataNPresent

INITKDBG

다음 로직은 INITKDBG 섹션과 연관되어 있으며, KiErrata 로 시작하는 함수와 관련된 로직입니다. 먼저 첫 번째 로직입니다.

LABEL_125:
  if ( !RtlPcToFileHeader(FsRtlUninitializeSmallMcb, BaseOfImage) )
    return 0;
  NtHeader = RtlImageNtHeader(BaseOfImage);
  if ( !NtHeader )
    return 0;
  SectionHeader = RtlSectionTableFromVirtualAddress(NtHeader, BaseOfImage, FsRtlUninitializeSmallMcb - BaseOfImage));
  if ( !SectionHeader )
    return 0;

FsRtlUninitializeSmallMcb 라는 루틴이 존재합니다. 해당 루틴의 용도에 대해서는 패치가드의 주요 로직 중 하나라는 요약 내용만 확인하였습니다.

먼저 RtlPcToFileHeader 루틴을 이용합니다.

NTSYSAPI PVOID RtlPcToFileHeader(
  [in]  PVOID PcValue,
  [out] PVOID *BaseOfImage
);

PcValue 는 예상컨데 Program Counter 로 예상됩니다. 해당 값을 포함하는 모듈을 검색하는 기능을 한다고 MSDN 에 설명되어 있습니다. 즉, FsRtlUninitializeSmallMcb 루틴을 주소를 전달하여 해당 루틴이 존재하는 모듈을 반환하는 것으로 이해할 수 있습니다. 당연히 ntoskrnl 의 이미지 베이스가 반환되게 됩니다.

그 이후 RtlImageNtHeader 루틴을 이용하여 ntoskrnl_IMAGE_NT_HEADERS 구조의 포인터를 가져와 RtlSectionTableFromVirtualAddress 루틴이 호출됩니다.

이름에서 알 수 있듯이 가상 주소를 이용하여 현재 이미지에서 해당 주소가 어떤 섹션에 존재하는가를 확인할 수 있는 루틴입니다.

PIMAGE_SECTION_HEADER RtlSectionTableFromVirtualAddress(
    IN PIMAGE_NT_HEADERS NtHeaders,
    IN PVOID             ImageBase,
    IN ULONG             Address
);

여기서 주의해야할 점은 Address 파라미터는 이미지 베이스로부터의 오프셋(RVA) 값을 의미합니다. 때문에 FsRtlUninitializeSmallMcb 주소에서 ntoskrnl 베이스 주소의 차이 값을 파라미터로 전달하는 것을 확인할 수 있습니다.

리턴되는 값은 INITKDBG 섹션 주소 입니다.

이후 아래와 같이 해당 섹션의 VirtualAddressVirtualSize 를 변수에 복사하고 디버깅 유무 확인 후 다음 로직이 진행됩니다.

INITKDBG_VirtualAddress_1 = BaseOfImage + *(SectionHeader + 0xC);
v79 = *(SectionHeader + 8);
FsRtlUninitializeSmallMcb_Offset = FsRtlUninitializeSmallMcb - INITKDBG_VirtualAddress_1;
INITKDBG_VirtualAddress = INITKDBG_VirtualAddress_1;
LODWORD(INITKDBG_Size) = v79;

_disable();
if ( !KdDebuggerNotPresent )
{
    while ( 1 );
}
_enable();

다음은 알 수 없는 루틴 2개와 RtlLookupFunctionEntryEx 루틴의 오프셋(INITKDBG 섹션의 베이스로부터 상대 주소)을 구하여 변수에 저장합니다. 해당 변수들은 뒤에서 참조되는 것으로 보입니다.

...
  sub_140A1CD10_Offset = &sub_FFFFF80321A1CD10 - INITKDBG_VirtualAddress_1;    // Unknown Routine 1
  v80 = RtlLookupFunctionEntryEx - INITKDBG_VirtualAddress_1;
  RtlLookupFunctionEntryEx_Offset = v80;
  sub_140A1D830_Offset = &sub_FFFFF80321A1D830 - INITKDBG_VirtualAddress_1;    // Unknown Routine 2
  if ( v80 > &unk_7FFFFFFF )
  {
    v3895 = &unk_D0002D61;
    KeBugCheckEx(
      __ROR4__(&unk_D0002D61 ^ 0x2D72, 0x5C),
      9,
      v80,
      v79,
      FsRtlUninitializeSmallMcb - INITKDBG_VirtualAddress_1,
      v3843);
  }
...

위의 로직에서 첫 번째 루틴인 sub_FFFFF80321A1CD10 의 경우 흥미로운 내용이 발견되어 아래쪽에 별도로 정리하였습니다.

KiErrataNPresent, Hypervisor Detection?

위와 이어지는 코드는 아래와 같습니다.

...
  v81 = 0;
  if ( MmStrongCodeGuaranteesEnforced() && (sub_FFFFF80321A450D4)() )
    v81 = 1;
  if ( Arg_ScheduleMethod == 7 )
    v81 = 1;

  v3859 = v81;
  if ( Arg_ScheduleMethod == 3
    && (v82 = __rdtsc(),
        v83 = (__ROR8__(v82, 3) ^ v82) * 0x7010008004002001ui64,
        BaseOfImage[1] = *(&v83 + 1),
        (v83 ^ *(&v83 + 1)) % 0xA < 5) )
  {
    LODWORD(v3863) = 3;
  }
  else
  {
    v84 = __rdtsc();
    v85 = (__ROR8__(v84, 3) ^ v84) * 0x7010008004002001ui64;
    BaseOfImage[2] = *(&v85 + 1);
    if ( (v85 ^ *(&v85 + 1)) % 0xA >= 2 )
    {
      LODWORD(v3863) = 0;
    }
    else
    {
      v86 = __rdtsc();
      v87 = (__ROR8__(v86, 3) ^ v86) * 0x7010008004002001ui64;
      BaseOfImage[3] = *(&v87 + 1);
      v3863 = true & (DWORD2(v87) ^ v87);
    }
  }
...

v81 변수는 BOOLEAN 자료형으로 추정되며, 총 2개의 조건에 따라 true 값이 설정되는 것으로 예상됩니다.

첫 번째 조건은 MmStrongCodeGuaranteesEnforced 루틴의 결과와 sub_FFFFF80321A450D4 의 결과가 모두 참이어야 하며, 두 번째 조건은 ScheduleMethod 가 7인 경우 입니다.

MmStrongCodeGuaranteesEnforced

해당 루틴은 매우 간단합니다. MiFlags 라는 전역변수를 이용하여 해당 변수의 2바이트 위치에 있는 1바이트를 가져와 AND 연산의 결과를 반환합니다.

__int64 MmStrongCodeGuaranteesEnforced()
{
  return BYTE2(MiFlags) & 1;  // char MiFlags[4]; MiFlags[2] & 1;
}

MiFlags 의 플래그 값에 대해서는 알려진 바가 없습니다.

sub_FFFFF80321A450D4

KiErrataNPresent 라는 루틴을 포함하고 있습니다. 해당 루틴은 구글링을 해보면 Hypervisor 와 관련된 내용이 있단것을 어렴풋이 알 수 있습니다.

__int64 sub_FFFFF80321A450D4()
{
  v0 = __readcr8();
  v5 = v0;
  __writecr8(0xFui64);
  v1 = __readcr0();
  __writecr0(v1 & 0xFFFFFFFFFFFEFFFFui64);
  v2 = *(KiErrata671Present + 2);
  *(KiErrata671Present + 2) = 0xC3;
  v3 = KiErrata671Present();
  if ( *(KiErrata671Present + 2) != v2 )
    *(KiErrata671Present + 2) = v2;
  __writecr0(v1);
  __writecr8(v5);
  return v3;
}

위의 코드 내용에선 __try, __except 문이 빠져있습니다. 실제 코드는 아래와 같습니다.

INIT:0000000140A450F5
INIT:0000000140A450F5                         loc_140A450F5:                          ; DATA XREF: .rdata:00000001400B46A8↑o
INIT:0000000140A450F5                         ;   __try { // __except at loc_140A4515B
INIT:0000000140A450F5 48 8B C3                                mov     rax, rbx
INIT:0000000140A450F8 48 0F BA F0 10                          btr     rax, 10h
INIT:0000000140A450FD 0F 22 C0                                mov     cr0, rax
INIT:0000000140A450FD                         ;   } // starts at 140A450F5
INIT:0000000140A45100
INIT:0000000140A45100                         loc_140A45100:                          ; DATA XREF: .rdata:00000001400B46A8↑o
INIT:0000000140A45100 48 8D 3D 79 90 FD FF                    lea     rdi, KiErrata671Present
INIT:0000000140A45107 40 8A 77 02                             mov     sil, [rdi+2]
INIT:0000000140A4510B 40 88 74 24 58                          mov     [rsp+48h+arg_8], sil
INIT:0000000140A45110
INIT:0000000140A45110                         loc_140A45110:                          ; DATA XREF: .rdata:00000001400B46B8↑o
INIT:0000000140A45110                         ;   __try { // __except at loc_140A45121
INIT:0000000140A45110 C6 47 02 C3                             mov     byte ptr [rdi+2], 0C3h
INIT:0000000140A45114 E8 67 90 FD FF                          call    KiErrata671Present
INIT:0000000140A45119 8B C8                                   mov     ecx, eax
INIT:0000000140A4511B 89 44 24 20                             mov     [rsp+48h+var_28], eax
INIT:0000000140A4511F EB 1A                                   jmp     short loc_140A4513B
INIT:0000000140A4511F                         ;   } // starts at 140A45110
INIT:0000000140A45121                         ; ---------------------------------------------------------------------------
INIT:0000000140A45121
INIT:0000000140A45121                         loc_140A45121:                          ; DATA XREF: .rdata:00000001400B46B8↑o
INIT:0000000140A45121                         ;   __except(1) // owned by 140A45110
INIT:0000000140A45121 B9 01 00 00 00                          mov     ecx, 1
INIT:0000000140A45126 89 4C 24 20                             mov     [rsp+48h+var_28], ecx
INIT:0000000140A4512A 48 8D 3D 4F 90 FD FF                    lea     rdi, KiErrata671Present
INIT:0000000140A45131 48 8B 5C 24 60                          mov     rbx, [rsp+48h+arg_10]
INIT:0000000140A45136 40 8A 74 24 58                          mov     sil, [rsp+48h+arg_8]
INIT:0000000140A4513B
INIT:0000000140A4513B                         loc_140A4513B:                          ; CODE XREF: sub_140A450D4+4B↑j
INIT:0000000140A4513B 40 38 77 02                             cmp     [rdi+2], sil
INIT:0000000140A4513F 74 04                                   jz      short loc_140A45145
INIT:0000000140A45141 40 88 77 02                             mov     [rdi+2], sil
INIT:0000000140A45145
INIT:0000000140A45145                         loc_140A45145:                          ; CODE XREF: sub_140A450D4+6B↑j
INIT:0000000140A45145                                                                 ; DATA XREF: .rdata:00000001400B46C8↑o
INIT:0000000140A45145                         ;   __try { // __except at loc_140A4514A
INIT:0000000140A45145 0F 22 C3                                mov     cr0, rbx
INIT:0000000140A45148 EB 04                                   jmp     short loc_140A4514E
INIT:0000000140A45148                         ;   } // starts at 140A45145
INIT:0000000140A4514A                         ; ---------------------------------------------------------------------------
INIT:0000000140A4514A
INIT:0000000140A4514A                         loc_140A4514A:                          ; DATA XREF: .rdata:00000001400B46C8↑o
INIT:0000000140A4514A                         ;   __except(1) // owned by 140A45145
INIT:0000000140A4514A 8B 4C 24 20                             mov     ecx, [rsp+48h+var_28]
INIT:0000000140A4514E
INIT:0000000140A4514E                         loc_140A4514E:                          ; CODE XREF: sub_140A450D4+74↑j
INIT:0000000140A4514E 0F B6 44 24 50                          movzx   eax, [rsp+48h+arg_0]
INIT:0000000140A45153 44 0F 22 C0                             mov     cr8, rax
INIT:0000000140A45157 8B C1                                   mov     eax, ecx
INIT:0000000140A45159 EB 0E                                   jmp     short loc_140A45169
INIT:0000000140A4515B                         ; ---------------------------------------------------------------------------
INIT:0000000140A4515B
INIT:0000000140A4515B                         loc_140A4515B:                          ; DATA XREF: .rdata:00000001400B46A8↑o
INIT:0000000140A4515B                         ;   __except(1) // owned by 140A450F5
INIT:0000000140A4515B 0F B6 44 24 50                          movzx   eax, [rsp+48h+arg_0]
INIT:0000000140A45160 44 0F 22 C0                             mov     cr8, rax
INIT:0000000140A45164 B8 01 00 00 00                          mov     eax, 1
INIT:0000000140A45169
INIT:0000000140A45169                         loc_140A45169:                          ; CODE XREF: sub_140A450D4+85↑j
INIT:0000000140A45169 48 83 C4 30                             add     rsp, 30h
INIT:0000000140A4516D 5F                                      pop     rdi
INIT:0000000140A4516E 5E                                      pop     rsi
INIT:0000000140A4516F 5B                                      pop     rbx
INIT:0000000140A45170 C3                                      retn
INIT:0000000140A45170                         ; } // starts at 140A450D4
INIT:0000000140A45170                         sub_140A450D4   endp

복잡해보이지만 원리는 간단합니다. WP(WriteProtection) 을 제거하고, 이에 실패하면 true를 반환합니다.

성공 후에, KiErrata671Present 의 2바이트 위치의 값을 C3 로 변경하여 0을 반환하게 합니다. (해당 루틴은 단순히 xor eax, eax, inc eax, retn 으로 이루어져 있으며 1을 반환하도록 되어있습니다.)

중요한 예외처리문만 다시 재구성한 의사코드 입니다.

bool sub_FFFFF80321A450D4()
{
  KIRQL OldIrql;
  KeRaiseIrql(HIGH_LEVEL, &OldIrql);
  Cr0 = __readcr0();
  __try{ __writecr0(Cr0 & 0xFFFFFFFFFFFEFFFFui64); } // remove WriteProtection
  __except(EXCEPTION_EXECUTE_HANDLER) { result = 1;  goto DONE;}
  
  OriginalValue = *(KiErrata671Present + 2);
  *(KiErrata671Present + 2) = 0xC3; // xor eax, eax | ret

  result = KiErrata671Present(); // will return zero

  if ( *(KiErrata671Present + 2) != OriginalValue )
    *(KiErrata671Present + 2) = OriginalValue ;
  __writecr0(Cr0);

DONE:
  KeLowerIrql(OldIrql);
  return result;
}

해당 내용을 찾아보았을 때, 하이퍼바이저와 관련된 내용이란 주장들이 매우 많았습니다. 어떤 이는 위의 함수가 EPT Hooking 과 같은 내용을 탐지하기 위함이지 않을까라는 의문을 제기하기도 했습니다. 이유는 위와 같이 코드를 변조하여도 EPT Hook 에 의해 페이지가 분리될 것이고, 코드가 변하지 않고 1을 반환할 것이다 라는 예상이었습니다.

하지만 패치가드의 로직 상, 이를 탐지하기 위함이기 보단 하이퍼바이저 기반 시스템에서 OS가 로드되었을 때 이를 확인하기 위한 로직으로 예상됩니다.

그렇다면 MiFlags 내 특정 비트(22 bit, 23번째 비트)가 이를 의미하는 플래그라고 추정해도 될 것으로 보입니다.

처음으로 돌아가 v81 변수의 명을 저는 bHypervisorBase 라고 지었습니다.

[0x03] Critical Routine Protection

해당 챕터를 이야기하기 전에, 문득 깨달은게 있습니다. 패치가드의 주요 로직에는 아래와 같이 IF(Interrupt Flag) 를 제어하고, 디버그 모드 판정 로직이 항상 존재합니다

_disable();
if ( !KdDebuggerNotPresent )
{
  while ( 1 );
}
_enable();

위와 같은 로직을 찾아 아래로 조금 내려보면 다음과 같은 로직을 확인할 수 있습니다. 위와 연장되는 내용으로 Hypervisor 기반에서 로드된 경우 ScheduleMethod 를 확인하는 과정입니다.

_disable();
if ( !KdDebuggerNotPresent )
{
  while ( 1 );
}
_enable();
ScheduleMethod = Arg_ScheduleMethod;
if ( bHypervisorBase )
{
  LODWORD(v3864) = 0;
  ScheduleMethod = Arg_ScheduleMethod;
  if ( Arg_ScheduleMethod != 7 )
  {
    v83 = 0;
    if ( (Arg_ScheduleMethod - 3) > 1 )
      v83 = Arg_ScheduleMethod;
    ScheduleMethod = v83;
  }
}

위의 로직을 지나면 조금은 거대한 로직이 기다리고 있습니다. 결론부터 말하면, HaliHaltSystem 이란 처음 보는 루틴을 발견하였습니다.

해당 루틴과 관련된 내용을 검색해보니,

PatchGuard 에서 허용하지 않는 동작이 발생하여 트리거되는 경우, 이를 실제 실행하게 되는 루틴으로 매우 중요한 루틴이다.

라는 내용들을 확인하였습니다.

우선 해당 로직을 조각으로 나누어 살펴보겠습니다.

...
CriticalRoutines[0] = pHaliHaltSystem;    // HaliHaltSystem 의 주소를 CriticalRoutines[0] 에 복사함
v4341 = 0i64;
Index = 0;
v4342 = 0i64;
v90 = 0;
v4340 = 0i64;
LODWORD(v3873) = 0;
v3875 = 0;
do
{
  CriticalRoutineAddress = CriticalRoutines[Index];
  v92 = RtlLookupFunctionTable(CriticalRoutineAddress, &ImageBase, Size);
  v4372 = v92;
...

위의 내용은 커다란 루프 실행에 앞서 초기화를 하는 부분과 루프의 초기 실행 부분입니다. 그럼 먼저 CriticalRoutines 배열은 어떻게 구성되어 있는가를 먼저 확인해보겠습니다.

현재는 명령이 실행되기 전이기 때문에 0번째 인덱스의 값은 비어있습니다. 예상대로라면 해당 위치에는 HaliHaltySystem 이 복사될 것 이며, 다음과 같은 구조로 되어 있습니다.(이미 루틴의 이름만으로 크리티컬한 것이 느껴집니다.)

PVOID CriticalRoutines[15] = {
    HaliHaltSystem,
    KeBugCheckEx,
    KeBugCheck2,
    KiBugCheckDebugBreak,
    KiDebugTrapOrFault,
    DbgBreakPointWithStatus,
    RtlCaptureContext,
    KeQueryCurrentStackInformation,
    KiSaveProcessorControlState,
    memmove,
    IoSaveBugCheckProgress,
    KeIsEmptyAffinityEx,
    VfNotifyVerifierOfEvent,
    _guard_check_icall,
    KeGuardDispatchICall
};

그럼 해당 루틴들을 보호하기 위해 초기화 과정에서 어떤 일이 생기는지 확인해보도록 하겠습니다.

RtlLookupFunctionTable & RtlLookupFunctionEntry

루프 내에서 처음으로 호출되는 루틴으로 문서화되어 있지 않은 루틴이기 때문에 정리를 하였습니다.

CriticalRoutineAddress = CriticalRoutines[Index];
v92 = RtlLookupFunctionTable(CriticalRoutineAddress, &ImageBase, Size);

해당 루틴의 원형은 아래와 같습니다. ControlPC 에 해당하는 활성 함수 테이블(Active Function Table) 을 검색합니다.

PRUNTIME_FUNCTION RtlLookupFunctionTable(
    IN  PVOID  ControlPC,
    OUT PVOID* ImageBase,
    OUT PULONG SizeOfTable
);

MS에서 말하는 Active Function Table 의 의미를 명확하게 이해하진 못했지만, 해당 루틴이 반환하는 RUNTIME_FUNCTION 구조가 함수 테이블 엔트리의 내용입니다. 이러한 엔트리들은 ExceptionDirectory 에 저장되어 있으며, 이를 이용하여 예외처리 핸들러, 종료처리 핸들러 등에 이용할 수 있습니다.

즉, 본인이 정리한 해당 루틴을 설명하면 아래와 같습니다.

ControlPC 가 속한 모듈의 ExceptionDirectory 가 반환되며, ImageBase 에는 ControlPC 가 속한 모듈의 이미지 베이스, SizeOfTable 에는 ExceptionDirectory 의 사이즈가 반환된다.

각 루틴들은 ntoskrnl 내 위치하고 있으므로 반환되는 값은 15개의 루틴 모두 동일합니다.

다음은 RtlLookupFunctionEntry 입니다.

if ( ntoskrnl_base == ImageBase )
    {
      UncompressedBuffer_1 = Temp_UncompressBuffer;
      v4377 = Temp_UncompressBuffer;
      v4378 = 0i64;
      v4379 = UncompressedSize;
      v4380 = 0;
      Temp_FunctionEntry = RtlLookupFunctionEntry(CriticalRoutineAddress, &ImageBase, 0i64);
      FunctionEntry = Temp_FunctionEntry;
      if ( Temp_FunctionEntry )
      {
         ...

압축이 해제된 리소스를 이용한 몇 가지 연산이 기다리고 있습니다. 그 전에 RtlLookupFunctionEntry 에 대해 확인합니다.

NTSYSAPI PRUNTIME_FUNCTION RtlLookupFunctionEntry(
  [in]  DWORD64               ControlPc,
  [out] PDWORD64              ImageBase,
  [out] PUNWIND_HISTORY_TABLE HistoryTable
);

이름과 같이 Function Table Entry 포인터를 반환합니다. CFF Explorer 와 같은 PE Viewer 를 통해 확인하면 아래와 같이 확인할 수 있는데, 여기에 해당하는 엔트리를 반환해주는 것 입니다.

추후에 포스팅 할 예정이지만 간단히 RUNTIME_FUNCTION 에 대해 알아보면 아래와 같은 구조로 이루어져 있으며, UNWIND_INFO 정보를 토대로 SCOPE_TABLE 정보를 확인하여 예외처리 핸들러에 대한 내용 등을 확인할 수 있습니다.

typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
    DWORD BeginAddress; // Function start address
    DWORD EndAddress;   // Function end address
    union {
        DWORD UnwindInfoAddress;
        DWORD UnwindData; // UNWIND_INFO
    } DUMMYUNIONNAME;
} _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;

해당 로직을 디버깅해보면 다음과 같은 내용을 확인할 수 있습니다.

FunctionEntry->BeginAddress = 0x4C3360
FunctionEntry->EndAddress   = 0x4C33FC
FunctionEntry->UnwindData   = 0xB5248

// Version2 의 경우 정보가 존재하지 않으며, UNWIND_CODE 구조가 아닌 것으로 보임(UnwindData->Version = 2, Flags = 02, SizeOfProlog = 0xA, CountOfCodes = 4)

각 정보는 올바르게 되어 있으며, 뒤의 로직에서 나오지만 이를 이용하여 함수의 크기를 구하는 코드도 존재합니다.

Integrity Check for Critical Routines

위의 내용과 이어 바로 분석을 진행합니다. 복잡해보이지만 몇 가지만 주의하면 됩니다.

/* Non-Fixed */
if ( Temp_FunctionEntry )
      {
        FunctionEntryOffset = Temp_FunctionEntry - ImageBase;
        index = 0;
        CmpValue = *UncompressedBuffer_1 - 1;    // UncompressedBuffer->IntegrityCheckLength - 1;
        if ( CmpValue >= 0 )
        {
          do // loop start
          {
            UncompDataIndex = (CmpValue + index) >> 1; // UncompDataIndex = (CmpValue + index) / 2;
            SpecificValue = &UncompressedBuffer_1[2 * UncompDataIndex + 2]; // ULONG* SpecificValue = &UncompressedBuffer[(UncompDataIndex + 1) * 8]
            if ( FunctionEntryOffset - *SpecificValue >= 0 )
            {
              if ( FunctionEntryOffset - *SpecificValue <= 0 )
                break;
              index = UncompDataIndex + 1;
            }
            else
            {
              if ( !UncompDataIndex)
                goto LABEL_163;
              CmpValue = UncompDataIndex - 1;
            }
          }
          while ( CmpValue >= index );
          v100 = v4378;
          if ( CmpValue >= index )
            v100 = &UncompressedBuffer_1[2 * UncompDataIndex + 2];
          v4378 = v100;
        }
/* Non-Fixed */

/* Fix */
if(FunctionEntry)
{
    FunctionEntry_Offset = FunctionEntry - ImageBase;
    Index = 0; // Weight?
    CmpValue = UncompressedBuffer->IntegrityCheckLength - 1;
    if(CmpValue >= 0)
    {
        do
        {
            UncompDataIndex = (CmpValue + Index) / 2;
            ULONG* SpecificValue = &UncompressedBuffer[(UncompDataIndex+1) * 8];
            ULONG DiffValue = FunctionEntry_Offset - *SpecificValue;
            if(DiffValue >= 0)
            {
                if(DiffValue == 0) break;
                Index = UncompDataIndex + 1;
            }
            else
            {
                if(UncompDataIndex == 0) goto LABEL_163;
                CmpValue = UncompDataIndex - 1;
            }
        }while(CmpValue >= Index);
        TempValue = InvalidFunctionEntry;
        if ( CmpValue >= index )
            TempValue = &UncompressedBuffer[(UncompDataIndex+1) * 8];
        InvalidFunctionEntry = TempValue;
    }
}
/* Fix */

한국에서는 영어 독해 공부를 할 때 첫 문장과 끝 문장만 보면 문장의 흐름이 이해된다는 이야기가 있습니다. 결론적으로 해당 루프는 v4378, v100 에 어떤 값이 복사되고 이용되는지가 중요합니다.

해당 연산에 대해 곰곰히 로직을 보며 고민을 많이 했습니다. 정확한 계산의 원리는 파악하지 못했지만, 다음과 같은 가설을 세워보았습니다.

루프가 진행되며 CriticalRoutines 배열에 존재하는 루틴들의 FunctionEntry_Offset 을 구하고 이를 UncompressedData 의 어떤 인덱스에 존재하는지 확인한다. 그리고 찾지 못하고 루프를 빠져나오게 된 경우, 현재 무결성에 문제가 있다고 판단한다.

그 이유는 루프를 빠져나오는 조건 때문인데, 해당 조건은 두 가지 입니다. FunctionEntry의 오프셋 값과 연산을 통해 얻은 UncompressedData 가 같은 경우와 결국 찾지 못하고 Index 값보다 크거나 같아질 때 입니다. 때문에 InvalidFunctionEntry 로 명명한 이유는 함수 테이블 엔트리의 값이 조작되거나 했을 때 탐지하기 위함이라고 판단했습니다.

Save PTE?

위의 로직에서부터 이어지는 내용입니다.

...
    FunctionEntry = FunctionEntryList;
    ImageBasePtr = ImageBase;
    v4362 = CriticalRoutineAddress;
    Size[0] /= 0xCu;                            // Size(Count) = ExceptionDirectorySize / sizeof(RUNTIME_FUNCTION)
    v4367 = FunctionEntryList;
    v3859 = ImageBase;
    EndFunctionTable = FunctionTable_1 + 0xCi64 * Size[0];
LABEL_167:
    while ( FunctionTable_1 && FunctionTable_1 != EndFunctionTable )
    {
      if ( UncompressedBufferPtr )
      {
        if ( !TempFunctionEntry )
          break;
        if ( InvalidFunctionEntry )
        {
          v103 = *(InvalidFunctionEntry + 4);
          if ( (v103 & __1) != 0 )
          {
            *FunctionEntry = ImageBase + (v103 & 0xFFFFFFFE);
            InvalidFunctionEntry = 0i64;
          }
          else
          {
            v104 = (v4371 + v103);
            v105 = v104;
            v106 = v104 + 4;
            if ( v106 < v105 || v106 > v4370 )
            {
              Size[0xB] = &unk_D0002D61;
              KeBugCheckEx(__ROR4__(&unk_D0002D61 ^ 0x2D72, 0x5C), 0x12, UncompressedBufferPtr, ImageBase, v105, v3842);
            }
            v107 = *(UncompressedBufferPtr + v105);
            v4371 += 4;
            InvalidFunctionEntry &= -((__1 & v107) != 0);
            if ( (__1 & v107) != 0 )
              v107 &= ~1u;
            *FunctionEntry = ImageBase + v107;
          }
        }
        else
        {
          *FunctionEntry = TempFunctionEntry;  // this 
          FunctionEntry_ = 0i64;
        }
      }
      else
      {
        while ( 1 )
        {
          v108 = FunctionTable_1;
          v109 = RtlpConvertFunctionEntry(FunctionTable_1, ImageBase, FunctionEntry);
          v110 = RtlpSameFunction(v109, ImageBase, v4362);
          FunctionTable_1 = FunctionTable + 0xC;
          FunctionTable += 0xCi64;
          if ( v110 )
            break;
          if ( FunctionTable_1 == EndFunctionTable )
            goto LABEL_182;
        }
        *v4367 = v108;
      }
      BeginAddress = *FunctionEntryList[0];
      FunctionSize = *(FunctionEntryList[0] + 4) - BeginAddress;
      HIDWORD(UnknownStruct) += FunctionSize;
      v3860 = v90 + 1;
      PageBasedAddress = (ImageBasePtr + BeginAddress) & 0xFFFFFFFFFFFFF000ui64;
      EndAddress = ImageBasePtr + BeginAddress + FunctionSize;

복잡해보이지만 위에서의 내용과 동일하다면 UncompressedBuffer 에 값이 존재하며, InvalidFunctionEntrynullptr 입니다. 때문에 별 문제 없이 주석과 같이 FunctionEntry 의 주소를 저장하게 됩니다.

중요 내용은 위의 의사코드에서 가장 아래쪽에 존재합니다.

BeginAddress = *FunctionEntryList[0];
FunctionSize = *(FunctionEntryList[0] + 4) - BeginAddress;
HIDWORD(UnknownStruct) += FunctionSize;
v3860 = v90 + 1;
PageBasedAddress = (ImageBasePtr + BeginAddress) & 0xFFFFFFFFFFFFF000ui64;
EndAddress = ImageBasePtr + BeginAddress + FunctionSize;

FunctionEntry 에 대한 파싱이 이루어집니다. 위에서 잠깐 언급했지만 RUNTIME_FUNCTION 구조를 이용하여 함수의 크기를 구할 수 있습니다. 그리고 해당 FunctionEntry 에 해당하는 루틴(CriticalRoutines)의 하위 3바이트를 제거하여 0x1000 으로 정렬합니다. 때문에 해당 루틴이 속한 페이지를 찾기 위함이라 판단하여 PageBasedAddress 로 명명하였습니다.

마찬가지로 루틴 사이즈를 이용하여 루틴의 마지막 주소를 저장합니다. 지금부터는 꽤 많은 루프를 만나게 됩니다. 루프의 깊이에 대해 유의하세요. 현재 최상위 루프는 LABEL_167 으로 표기되어 있으며 현재 챕터 첫 의사코드 내에 존재합니다.

먼저 위의 내용을 지나면 바로 하위 루프로 진입하게 됩니다.

...
      do
      {
        PtrAddressPtr = PteAddressList;
        LoopCount = 4i64;
        PtrAddress = MmPteBase + ((PageBasedAddress >> 9) & 0x7FFFFFFFF8i64);
        do
        {
          *PtrAddressPtr++ = PtrAddress;
          PtrAddress = MmPteBase + ((PtrAddress >> 9) & 0x7FFFFFFFF8i64);
          LoopCount--;
        }
        while ( LoopCount );

반복문에 진입하게 되면 PTE 주소를 구하는 연산의 루프가 존재합니다. PteAddressPtr 에는 총 4개의 값이 저장됩니다. PteAddressPtr[0] 의 값이 올바른 PTE 주소로 예상되며, 첫 PTE 주소를 포함하여 총 4번의 동일한 연산을 거치며 PteAddressPtr 을 완성시킵니다. 다음 내용입니다.

...
        bFound = false;
        IndexCount = 3;
        do
        {
          TempValue = IndexCount;
          if ( bFound )
          {
            PteAddressList[IndexCount] = 0i64;
          }
          else
          {
            if ( !IndexCount )
              break;
            if ( *PteAddressList[IndexCount] < 0 )    // <= Important, PteAddressList[IndexCount] 의 1바이트 값만 가져와 8번째 비트(bit 7)이 1인지 확인함
              bFound = true;
          }
          --IndexCount;
        }
        while ( TempValue );
...

위에서 저장된 PteAddressList 에서 특정 조건을 확인합니다. 해당 조건은 위의 주석과 같은 내용입니다. 만약 PTE 라면 PAT 비트의 활성 유무를 확인하는 것이며, 해당 PTE 를 찾는 경우, 하위 인덱스의 값을 모두 0으로 제거합니다.

예를 들어 PteAddressList[3] 에 해당 비트가 활성화 되어 있다면 0,1,2 인덱스의 값은 제거됩니다.

...
        do
        {
          LoopCount = (LoopCount- 1);
          v123 = PteAddressList[LoopCount];
          if ( !v123 )
            break;
          v124 = 0;
          if ( HIDWORD(v4332) )
          {
            do
            {
              if ( v123 == *(v4332 + 8i64 * v124) )
                break;
              ++v124;
            }
            while ( v124 < HIDWORD(v4332) );
            v121 = v3866;
          }
          if ( v124 == HIDWORD(v4332) )
          {
            if ( HIDWORD(v4332) == v121 )
            {
              v121 = 2 * v121 + 0x40;
              v3866 = v121;
              v125 = __rdtsc();
              v126 = (__ROR8__(v125, 3) ^ v125) * 0x7010008004002001ui64;
              FunctionEntryList[1] = *(&v126 + 1);
              v127 = (v126 ^ *(&v126 + 1)) % 0xB;

              ... // Tag pass

              v136 = ExAllocatePoolWithTag(NonPagedPoolNx, 8i64 * v121, v130);
              if ( !v136 )
                return 0;
              v137 = v4332;
              if ( v4332 )
              {
                v138 = v136;
                v139 = 8 * HIDWORD(v4332);
                if ( (8 * HIDWORD(v4332)) >= 8 )
                {
                  v140 = v139 >> 3;
                  do
                  {
                    v139 -= 8;
                    *v138 = *v137++;
                    v138 += 8;
                    --v140;
                  }
                  while ( v140 );
                }
                if ( v139 )
                {
                  v141 = v138 - v137;
                  do
                  {
                    *(v137 + v141) = *v137;
                    v137 = (v137 + 1);
                    --v139;
                  }
                  while ( v139 );
                }
                ExFreePoolWithTag(v4332);
              }
              *&v4332 = v136;
              DWORD2(v4332) = v121;
            }
            v142 = HIDWORD(v4332);
            ++HIDWORD(v4332);
            HIDWORD(UnknownStruct) += 0x10;
            *(v4332 + 8 * v142) = v123;
          }
        }
        while ( LoopCount );

꽤 복잡해보이지만 위에서 만든 PteAddressList 를 저장할 풀을 할당하고 특정 구조체에 몇 가지 값을 설정합니다. 해당 루틴을 분석 내용에 맞게 재구성 해보았습니다.

...
        do
        {
          LoopCount_1 = (LoopCount_1 - 1);
          PteAddress = PteAddressList[LoopCount_1];
          if ( !PteAddress )
            break;
          PteIndex = 0;
          if ( CriticalInformation.CurrentArrayCount )
          {
            do                                  // Check dup
            {
              if ( PteAddress == *(CriticalInformation.CriticalRoutinePteList + PteIndex) )
                break;
              ++PteIndex;
            }
            while ( PteIndex < CriticalInformation.CurrentArrayCount );
            LimitCount = v3873;
          }
          if ( PteIndex == CriticalInformation.CurrentArrayCount )
          {
            if ( CriticalInformation.CurrentArrayCount == LimitCount )
            {
              LimitCount = 2 * LimitCount + 0x40;
              v3873 = LimitCount;
              v125 = __rdtsc();
              v126 = (__ROR8__(v125, 3) ^ v125) * 0x7010008004002001ui64;
              
              //... tagging

              AllocPool = ExAllocatePoolWithTag(0x200i64, 8i64 * LimitCount, v130);
              if ( !AllocPool )
                return 0;
              CriticalRoutinePteList = CriticalInformation.CriticalRoutinePteList;
              if ( CriticalInformation.CriticalRoutinePteList )
              {
                v138 = AllocPool;
                v139 = 8 * CriticalInformation.CurrentArrayCount;
                if ( (8 * CriticalInformation.CurrentArrayCount) >= 8 )
                {
                  v140 = v139 >> 3;
                  do
                  {
                    v139 -= 8;
                    *v138 = *CriticalRoutinePteList++;
                    v138 += 8;
                    --v140;
                  }
                  while ( v140 );
                }
                if ( v139 )
                {
                  v141 = v138 - CriticalRoutinePteList;
                  do
                  {
                    *(CriticalRoutinePteList + v141) = *CriticalRoutinePteList;
                    CriticalRoutinePteList = (CriticalRoutinePteList + 1);
                    --v139;
                  }
                  while ( v139 );
                }
                ExFreePoolWithTag();
              }
              CriticalInformation.CriticalRoutinePteList = AllocPool;
              CriticalInformation.LimitArrayCount = LimitCount;
            }
            CurrentIndex = CriticalInformation.CurrentArrayCount++;
            CriticalInformation.NestingSize += 0x10;
            *(CriticalInformation.CriticalRoutinePteList + CurrentIndex) = PteAddress;
          }
        }
        while ( LoopCount_1 );
        PageBasedAddress += 0x1000i64;
      }
      while ( PageBasedAddress < EndAddress );

위의 내용에 사용된 구조체는 아래와 같이 유추하였습니다.

struct PG_CRITICAL_INFORMATION{
    PVOID* PteAddressList;
    ULONG  LimitArrayCount;
    ULONG  CurrentArrayCount;
    ULONG  RoutineCount;
    ULONG  NestingSize;
};

해당 루프를 빠져나오면 PageBasedAddress += 0x1000 연산의 결과가 CriticalRoutine 의 끝 주소보다 작은 경우 반복하게 됩니다. 다만 현재 루틴들의 내용으로 보았을 때 0x1000 사이즈만큼의 루틴은 없는 것으로 보입니다.

...
      v90 = RoutineIndex;
      FunctionEntry = v4373;
      TempFunctionEntry = FunctionEntry_;       // init
      FunctionTable_1 = FunctionTable;
      ImageBasePtr = v3866;
      CriticalInformation.RoutineCount = RoutineIndex;
    }
LABEL_182:
    TargetRoutineCount = __1 + v3871;
    LODWORD(v3871) = TargetRoutineCount;
  }
  while ( TargetRoutineCount < 0xF );

이 루프를 빠져나오면 위와 같이 각 변수들을 초기화를 진행합니다. 그리고 해당 챕터의 첫 루프의 시작인 LABEL_167 로 돌아가게 됩니다. TempFunctionEntry 는 위에서 0으로 초기화 된 값 FunctionEntry_ 의 값을 복사하므로, LABEL_167 의 루프 또한 빠져나오게 됩니다.

RtlLookupFunctionTable 을 처음 사용한 내용을 기억합니까? 기억이 나지 않는다면 스크롤을 올려 확인해야 합니다. 이 루프는 CriticalRoutine 의 개수만큼 반복됩니다.

[0x04] Allocate PgContext

지금까지 준비한 내용의 마지막 내용입니다. 너무 길고 먼 여정입니다.

위의 내용에서 루프가 끝난 지점부터 확인합니다. 우선 분석 내용의 단락입니다.

...
  CriticalInformation.NestingSize += 8;
  RoutineIndex = v90 + 1;
  v111 = &HaliHaltSystemPtr & 0xFFFFFFFFFFFFF000ui64;
  while ( 1 )
  {
    PteAddressPtr = new_PteAddressList;
    new_PteAddress = MmPteBase + ((v111 >> 9) & 0x7FFFFFFFF8i64);
    Count = 4i64;
    do
    {
      *PteAddressPtr++ = new_PteAddress;
      new_PteAddress = MmPteBase + ((new_PteAddress >> 9) & 0x7FFFFFFFF8i64);
      Count -= __1;
    }
    while ( Count );
    v146 = 0;
    v147 = 3;
    do
    {
      v148 = v147;
      if ( v146 )
      {
        new_PteAddressList[v147] = 0i64;
      }
      else
      {
        if ( !v147 )
          break;
        if ( *new_PteAddressList[v147] < 0 )
          v146 = __1;
      }
      --v147;
    }
    while ( v148 );
    v149 = RoutineIndex;
    LODWORD(LoopCount_2) = 4;
    do
    {
      LoopCount_2 = (LoopCount_2 - 1);
      v151 = new_PteAddressList[LoopCount_2];
      if ( !v151 )
        break;
      v152 = 0;
      if ( CriticalInformation.CurrentArrayCount )
      {
        do
        {
          if ( v151 == *(CriticalInformation.CriticalRoutinePteList + v152) )
            break;
          ++v152;
        }
        while ( v152 < CriticalInformation.CurrentArrayCount );
        v149 = RoutineIndex;
      }
      if ( v152 == CriticalInformation.CurrentArrayCount )
      {
        if ( CriticalInformation.CurrentArrayCount == v3873 )
        {
          v153 = 2 * v3873 + 0x40;
          v3873 = v153;
          v154 = __rdtsc();
          v155 = (__ROR8__(v154, 3) ^ v154) * 0x7010008004002001ui64;

          // tagging

          v165 = ExAllocatePoolWithTag(0x200i64, 8i64 * v153, v159);
          if ( !v165 )
            return 0;
          v166 = CriticalInformation.CriticalRoutinePteList;
          if ( CriticalInformation.CriticalRoutinePteList )
          {
            v167 = v165;
            v168 = 8 * CriticalInformation.CurrentArrayCount;
            if ( (8 * CriticalInformation.CurrentArrayCount) >= 8 )
            {
              v169 = v168 >> 3;
              do
              {
                v168 -= 8;
                *v167 = *v166++;
                v167 += 8;
                --v169;
              }
              while ( v169 );
            }
            if ( v168 )
            {
              v170 = v167 - v166;
              do
              {
                *(v166 + v170) = *v166;
                v166 = (v166 + 1);
                --v168;
              }
              while ( v168 );
            }
            ExFreePoolWithTag();
          }
          CriticalInformation.LimitArrayCount = v3873;
          CriticalInformation.CriticalRoutinePteList = v165;
        }
        v171 = CriticalInformation.CurrentArrayCount++;
        CriticalInformation.NestingSize += 0x10;
        *(CriticalInformation.CriticalRoutinePteList + v171) = v151;
      }
    }
    while ( LoopCount_2 );
    v111 += 0x1000i64;
    if ( v111 >= &off_FFFFF80067A005E0 )
      break;
    __1 = 1i64;
  }

또한 현재 15개의 CriticalRoutine 에 대한 연산이 모두 끝난 상황에서의 CriticalInformation 데이터는 다음과 같습니다.

CriticalRoutinePteList = AllocPool
LimitArrayCount = 0x40
CurrentArrayCount = 0x6
RoutineCount = 0xF
NestingSize = 0x19A1 (15 루틴의 크기들과 PteList  할당  추가되는 0x10 만큼의 사이즈들이 모두 누적된 )

다음 코드는 이전에 PTE 연산이 동일하게 적용되어 있습니다. 다만 루프를 진행하기 전에 NestingSize+=8RoutineIndex 의 값을 +1만큼 늘려주는 것을 확인할 수 있습니다.

해당 챕터의 메인 루프 안에는 크게 3개의 루프가 존재합니다. 이전에는 보호되는 15개의 CriticalRoutine 에 대해 PTE 연산을 진행했다면, 이번엔 HaliHaltSystem 의 주소가 저장되어 있는 HalPrivateDispatchTable 내에 있는 *HaliHaltSystem 에 대한 연산을 진행합니다.

CriticalInformation.NestingSize += 8;
RoutineIndex = v90 + 1;
new_PageBasedAddress = &HaliHaltSystemPtr & 0xFFFFFFFFFFFFF000ui64;
while(1)    // main loop
...

위의 메인 루프 내 2개의 하위 루프입니다. 마찬가지로 PTE 연산을 이용해 4개의 배열을 만들고(new_PteAddressList), 그 중 bit 7의 값이 1인 인덱스를 찾고 하위 인덱스의 값들을 제거 합니다.

...
    PteAddressPtr = new_PteAddressList;
    new_PteAddress = MmPteBase + ((new_PageBasedAddress >> 9) & 0x7FFFFFFFF8i64);
    Count = 4i64;
    do
    {
      *PteAddressPtr++ = new_PteAddress;
      new_PteAddress = MmPteBase + ((new_PteAddress >> 9) & 0x7FFFFFFFF8i64);
      Count --;
    }
    while ( Count );

    bFound_1 = 0;
    IndexCount_1 = 3;
    do
    {
      TempValue_1 = IndexCount_1;
      if ( bFound_1 )
      {
        new_PteAddressList[IndexCount_1] = 0i64;
      }
      else
      {
        if ( !IndexCount_1 )
          break;
        if ( *new_PteAddressList[IndexCount_1] < 0 )
          bFound_1 = __1;
      }
      --IndexCount_1;
    }
    while ( TempValue_1 );
...

다음 루프의 코드는 아래와 같습니다.

...
    v149 = RoutineIndex;
    LODWORD(LoopCount_2) = 4;
    do
    {
      LoopCount_2 = (LoopCount_2 - 1);
      v151 = new_PteAddressList[LoopCount_2];
      if ( !v151 )
        break;
      v152 = 0;
      if ( CriticalInformation.CurrentArrayCount )
      {
        do
        {
          if ( v151 == *(CriticalInformation.CriticalRoutinePteList + v152) )
            break;
          ++v152;
        }
        while ( v152 < CriticalInformation.CurrentArrayCount );
        v149 = RoutineIndex;
      }
      if ( v152 == CriticalInformation.CurrentArrayCount )
      {
        if ( CriticalInformation.CurrentArrayCount == v3873 )
        {
          v153 = 2 * v3873 + 0x40;
          v3873 = v153;
          v154 = __rdtsc();
          v155 = (__ROR8__(v154, 3) ^ v154) * 0x7010008004002001ui64;
          
          // tagging

          v165 = ExAllocatePoolWithTag(0x200i64, 8i64 * v153, v159);
          if ( !v165 )
            return 0;
          v166 = CriticalInformation.CriticalRoutinePteList;
          if ( CriticalInformation.CriticalRoutinePteList )
          {
            v167 = v165;
            v168 = 8 * CriticalInformation.CurrentArrayCount;
            if ( (8 * CriticalInformation.CurrentArrayCount) >= 8 )
            {
              v169 = v168 >> 3;
              do
              {
                v168 -= 8;
                *v167 = *v166++;
                v167 += 8;
                --v169;
              }
              while ( v169 );
            }
            if ( v168 )
            {
              v170 = v167 - v166;
              do
              {
                *(v166 + v170) = *v166;
                v166 = (v166 + 1);
                --v168;
              }
              while ( v168 );
            }
            ExFreePoolWithTag();
          }
          CriticalInformation.LimitArrayCount = v3873;
          CriticalInformation.CriticalRoutinePteList = v165;
        }
        v171 = CriticalInformation.CurrentArrayCount++;    // set new data
        CriticalInformation.NestingSize += 0x10;
        *(CriticalInformation.CriticalRoutinePteList + v171) = v151;
      }
    }
    while ( LoopCount_2 );

    new_PageBasedAddress += 0x1000i64;
    if ( new_PageBasedAddress >= &off_FFFFF80067A005E0 )
      break;
    __1 = 1i64;
  }
  CriticalInformation.RoutineCount = RoutineCount;
  CriticalInformationSize = CriticalInformation.NestingSize + 0x10 * RoutineCount;

HaliHaltSystem 포인터에 대한 PTE 리스트 중 현재 CriticalInformation.CriticalRoutinePteList 에 존재하는지 확인하고, 없는 경우 이를 추가하는 로직입니다. 이를 통해 CriticalInformationRoutineCount0x10 이 됩니다.

NestingSize 는 루틴의 전체 크기와 PTE 연산을 통해 누적된 사이즈이 값이었습니다. CriticalInformationSize 에는 이러한 누적 값과 현재 루틴의 개수 * 0x10 을 더한 값이 복사됩니다.

위에 로직과 이어지는 다음 내용입니다.

  _disable();
  if ( !KdDebuggerNotPresent )
  {
    while ( 1 )
      ;
  }
  _enable();
  v173 = bHypervisorBase;
  Size = 0;
  FixPgContextSize = 0xAA0;
  if ( !bHypervisorBase )
    Size = VirtualSize_INITKDBG;
  if ( !bHypervisorBase )
    FixPgContextSize = CriticalInformationSize + 0xAA8;
  RoutineIndex = Size;
  LimitCount_1 = FixPgContextSize;
  v176 = Size+ FixPgContextSize;
  LODWORD(v3871) = v176;
  v177 = __rdtsc();
  v178 = __ROR8__(v177, 3);
  v3937 = ((v178 ^ v177) * 0x7010008004002001ui64) >> 0x40;
  v179 = v176 + &unk_80000 + (((0x4002001 * (v178 ^ v177)) ^ v3937) & 0x7FF);
  v180 = __rdtsc();
  v181 = (__ROR8__(v180, 3) ^ v180) * 0x7010008004002001ui64;
  
  // Tagging

  v191 = __rdtsc();
  v192 = __ROR8__(v191, 3);
  v3940 = ((v192 ^ v191) * 0x7010008004002001ui64) >> 0x40;
  v193 = (v3940 ^ (0x4002001 * (v192 ^ v191))) & 0x7FF;
  v194 = __rdtsc();
  v195 = (__ROR8__(v194, 3) ^ v194) * 0x7010008004002001ui64;
  v3941 = *(&v195 + 1);
  RandomSeed = (*(&v195 + 1) ^ v195) % (v193 + 1);
  TempPgContext = ExAllocatePoolWithTag(0i64, v193 + v179, v185);

드디어 PatchGuardContext 와 직접적 연관이 있는 로직이 나타났습니다. 여기서 FixPgContextSize 라고 이름을 붙힌 이유는, 실제로 풀은 해당 사이즈만큼 할당되지 않습니다. PgContext 사이즈를 포함한 다른 데이터도 포함되기 때문입니다.

여기서 현재 기준으로 PgContext 사이즈가 0xAA0 인 점은 여러 문서들을 통해 확인할 수 있었습니다.

예를 들어, KiMarkBugCheckRegions 루틴의 최하단을 확인하면 아래와 같습니다.

.text:00000001403C57B6 48 8B 0D 0B 5C 93 00                    mov     rcx, cs:Src
.text:00000001403C57BD 48 85 C9                                test    rcx, rcx
.text:00000001403C57C0 74 0A                                   jz      short loc_1403C57CC
.text:00000001403C57C2 BA A0 0A 00 00                          mov     edx, 0AA0h                             ; <<= PgContext Size
.text:00000001403C57C7 E8 3C FF FE FF                          call    IoAddTriageDumpDataBlock

위에서 할당된 랜덤한 사이즈의 풀이 바로 PgContext 입니다. 다음은 어떤 루프를 만나게 됩니다.

Head&Tail Setup

...
  if ( !TempPgContext )
    return 0;
  RemainValue = RandomSeed;
  UnknownDataPtr = TempPgContext;
  if ( RandomSeed >= 8 )
  {
    RandomLoopCount = RandomSeed >> 3;
    do
    {
      RemainValue -= 8;
      v201 = __rdtsc();
      v202 = (__ROR8__(v201, 3) ^ v201) * 0x7010008004002001ui64;
      v3942 = *(&v202 + 1);
      *UnknownDataPtr++ = v202 ^ *(&v202 + 1);
      --RandomLoopCount;
    }
    while ( RandomLoopCount );
  }

위에서 풀 할당을 하기 전에, rdtsc 를 이용해 랜덤한 값을 미리 확보해두었습니다.(RandomSeed) 이 값을 8로 나누어 루프 카운트를 결정합니다. 그리고 이 값을 어떤 연산을 거쳐 PgContext 에 순서대로 값을 복사합니다.

저는 이 부분에 대해 할당된 풀을 쉽게 찾지 못하도록 하기 위한 일종의 더미 값이 아닐까 조심스레 유추해보았습니다. 만약 RandomLoopCount 가 1인 경우, 랜덤한 값 8바이트를 PgContext 에 복사되고 실제 데이터가 저장되는 부분은 PgContext+8 위치부터 시작한다고 생각했습니다.

RandomSeed 가 8로 나누었을 때 나머지가 남게되는 경우는 아래의 로직이 실행됩니다.

...
  if ( RemainValue )
  {
    v203 = __rdtsc();
    v204 = (__ROR8__(v203, 3) ^ v203) * 0x7010008004002001ui64;
    v3943 = *(&v204 + 1);
    RandomValue = v204 ^ *(&v204 + 1);
    do
    {
      *UnknownDataPtr = RandomValue; // copy 1byte
      UnknownDataPtr = (UnknownDataPtr + 1);
      RandomValue >>= 8;
      --RemainValue;
    }
    while ( RemainValue );
  }

새롭게 생성한 랜덤한 값을 나머지 값을 카운트로 사용하여 1바이트씩 복사합니다. 결국 RandomSeed 의 값만큼 의도를 알 수 없는 랜덤한 값이 PgContext 에 복사되게 됩니다.

다음은 PgContext 풀에 끝 위치에 위와 동일한 방식으로 랜덤한 값을 복사합니다.

...
  v206 = RandomSeed;
  RandomSeed_2 = v193 - RandomSeed;
  UnknownDataPtr_2 = (TempPgContext + RandomSeed + v179);
  if ( RandomSeed_2 >= 8 )
  {
    RandomLoopCount_2 = RandomSeed_2 >> 3;
    do
    {
      RandomSeed_2 -= 8;
      v210 = __rdtsc();
      v211 = (__ROR8__(v210, 3) ^ v210) * 0x7010008004002001ui64;
      v3944 = *(&v211 + 1);
      *UnknownDataPtr_2++ = v211 ^ *(&v211 + 1);
      --RandomLoopCount_2;
    }
    while ( RandomLoopCount_2 );
  }
  if ( RandomSeed_2 )
  {
    v212 = __rdtsc();
    v213 = (__ROR8__(v212, 3) ^ v212) * 0x7010008004002001ui64;
    v3945 = *(&v213 + 1);
    v214 = v213 ^ *(&v213 + 1);
    do
    {
      *UnknownDataPtr_2 = v214;
      UnknownDataPtr_2 = (UnknownDataPtr_2 + 1);
      v214 >>= 8;
      --RandomSeed_2;
    }
    while ( RandomSeed_2 );
  }

PgContext 풀에는 어떠한 의도든 HeadTail 에 랜덤한 사이즈의 랜덤한 값이 복사된 다는 것을 확인하였습니다. HeadTail 의 랜덤한 값으로 채워진 다음은, memset 을 이용하여 실제 데이터가 복사될 메모리를 초기화해주는 로직입니다.

...
  PgContext = (RandomSeed + TempPgContext);
  PgContextPoolStart = TempPgContext;
  PgContextDataStart = RandomHeadValue + TempPgContext;
  if ( !(RandomHeadValue + TempPgContext) )
    return 0;
  v216 = 0x80000 + SizeResult;
  backup_size = v216;
  memset(PgContext, 0i64, v216);
...

SizeResult 는 다음과 같은 연산에 의한 사이즈 입니다.

  1. CriticalInformaiton.NestingSize
    1. CriticalRoutine 15(0xf)개에 대한 루틴 크기와 HaliHaltSystem 포인터 크기의 합
    2. CriticalInformation.CriticalRoutinePteList 개수를 의미하는 CurrentArrayCount * 0x10
  2. CriticalInformationSize
    1. NestingSize + RoutineCount * 0x10
  3. FixPgContextSize
    1. CriticalInformationSize + 0xAA8
  4. SizeResult
    1. FixPgContextSize + VirtualSize_INITKDBG

SizeResult 값에 0x80000 을 더하여 memset 을 호출하게 됩니다.

이 때 당연히 PgContext 는 랜덤한 헤더 사이즈를 지난 실제 데이터가 복사 될 시작 위치입니다. 해당 내용들을 토대로 간단히 그림으로 표현해보았습니다.

아직 초기 분석 내용이라 틀린 부분도 분명히 존재할 것 입니다. 관련하여 연구, 분석을 진행하는 사람이 있다면 언제든 연락해주세요

[0x05] Bonus

sub_140A1CD10

두 개의 루틴에 대해 잠깐 확인해보면 두 개의 루틴 모두 PatchGuardContext 를 파라미터로 사용한다는 것을 눈치챌 수 있습니다. 그 중 첫 번째 sub_FFFFF80321A1CD10 루틴의 경우 참조를 찾아보면 호기심이 생겨나게 됩니다.

위의 참조를 역으로 확인해보았습니다. 먼저 KeFreeInitializationCode 루틴의 경우 KeInitiSystem 루틴에서만 호출되는 루틴이었습니다. 저는 주로 커널 내 주요 루틴을 찾을 때 ReactOS 프로젝트를 자주 확인합니다. 다만 KeInitSystem 루틴의 경우 분명 하나의 파라미터가 존재하는 것으로 확인됩니다. (적어도 IDA를 통해서는…)

위의 KeFreeInitializationCode 루틴의 경우 아래와 같은 루틴의 영향으로 호출됩니다.

if ( !a1 )
  {
    ...
    return 1;
  }
  if ( a1 != 1 )
  {
    if ( a1 == 2 )
    {
      TraceLoggingRegisterEx_EtwRegister_EtwSetInformation(&dword_140C0EBA8, 0i64, 0i64);
    }
    else if ( a1 == 3 )
    {
      KiInitializeReservedCpuSets();
    }
    else                              // <= this
    {
      ...
      if ( (HvlpFlags & 0x100000) != 0 )
      {
        Pool = ExAllocatePoolWithTag(0x200, 0x4A0ui64, 0x4850654Bu);
        KiEpfHashTable = Pool;
        ...
      }
      KeFreeInitializationCode();    // <= this
    }
    return 1;
  }

KeInitSystem 의 호출 로직을 살펴보면 아래와 같은 참조를 확인할 수 있습니다.

  1. InitBootProcessor : KeInitSystem(0)
  2. Phase1InitializationDiscard : KeInitSystem(1)
  3. Phase1InitializationIoReady : KeInitSystem(4)

즉 3번의 케이스의 경우 주석으로 표시해둔 else 문이 동작하게 됩니다. 이 때 HvlpFlags 를 확인하게 되며 해당 내용으로만 확인했을 때 이는 하이퍼바이저와 관련이 있다고 짐작할 수 있었습니다.

다음은 RtlpComputeEpilogueOffset 루틴입니다. 해당 루틴은 매우 짧은 코드로 보여집니다.

__int64 __fastcall RtlpComputeEpilogueOffset(__int64 a1, _QWORD *a2, signed __int64 a3)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v3 = a2;
  for ( i = 0; i < 0x19; ++i )
    *a2++ ^= a3;
  *v3 ^= a3;
  v6 = *(v3 + 0xC4);
  if ( a3 && v6 )
  {
    v7 = &a2[v6 - 1];
    while ( 1 )
    {
      *v7 ^= a3;
      v7 += 0xFFFFFFFF;
      v9 = __ROR8__(a3, v6);
      _bittestandcomplement64(&v9, v9 & 0x3F);
      v6 = (v6 - 1);
      if ( !v6 )
        break;
      a3 = v9;
    }
  }
  if ( (*(v3 + 0x994) & 0x100000) != 0 )
    KeExitRetpoline(v6, a2);
  else
    _mm_lfence();
  INITKDBG_sub_140A1CD10(a1, v3);
  return (*(v3 + 0x110))(v3 + 0x798, 1i64);
}

다만 여기서 해당 루틴의 참조를 찾으면 아래와 같은 참조 목록을 확인할 수 있습니다.

해당 루틴이 호출되는 모든 함수가 패치가드와 연관이 있었습니다. KiInitializePatchGuardContext 에서 첫 번째 파라미터로 전달되는 DPC 루틴들이 존재합니다. 물론 모든 루틴이 존재하진 않지만 참조되는 것만으로도 해당 루틴은 조사해볼 가치가 있다고 생각됩니다.

[0x06] Conclusion

패치가드에 대한 프로젝트는 계속해서 진행됩니다. 현재까지 약 10회 이상의 패치가드 초기화 루틴을 디버깅하였습니다.

무엇보다 매우 큰 함수이기 때문에 HexRay 디컴파일에서 가장 많은 시간이 소요된 것 같습니다.

이제 PgContext 풀의 할당, 크기, 더미 값 등 여러가지 비밀을 확인하였으므로, 다음 포스팅에서는 PgContext 에 값이 복사되는 것과 할당된 PgContext 를 어떻게 찾을 수 있을 것인가에 대해 분석을 진행하도록 하겠습니다.