[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];
}
다음 압축 해제된 데이터를 이용한 로직이 존재합니다.
먼저 ExceptionDirectory
와 ntoskrnl
이미지 베이스의 차를 구하여 오프셋을 구합니다.
그리고 해당 오프셋을 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
지금까지 위의 내용들을 정리하면 다음과 같습니다.
LdrResFindResource
를 통해ntoskrnl
이미지 내FUNCTIONEXTENTLIST
라는 리소스를 찾는다.RtlImageDirectoryEntryToData
를 이용하여ExceptionDirectory
를 가져오며, 존재하는 경우 리소스의 압축을 해제한다.-
해당 리소스(압축됨)의 구조는 아래와 같다.
struct PG_COMPRESSED_DATA{ ULONG Header; ULONG DecompressSize; BYTE CompressedData[1]; }
-
RtlDecompressBufferEx
루틴을 통해 압축을 해제한다.struct PG_UNCOMPRESS_DATA{ ULONG IntegrityCheckLength; ULONG InitIntegrityCheckData; BYTE UnknownData[sizeof(UncompressedData-8)]; }
-
ExceptionDirectory
를 이용하여 비교할 값을 생성한다.ULONG CompareValue = ExceptionDirectory - ntoskrnl; for(int index = 0; index<ExceptionDirectorySize; index+=4) { CompareValue += *(ULONG*)((DWORD64)ExceptionDirectory + index); }
-
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);
-
검증을 통과한 경우 위에서 설정된 지역 변수들을 전역변수에 설정한다.
/* 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
섹션 주소 입니다.
이후 아래와 같이 해당 섹션의 VirtualAddress
와 VirtualSize
를 변수에 복사하고 디버깅 유무 확인 후 다음 로직이 진행됩니다.
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
에 값이 존재하며, InvalidFunctionEntry
는 nullptr
입니다. 때문에 별 문제 없이 주석과 같이 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+=8
과 RoutineIndex
의 값을 +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
에 존재하는지 확인하고, 없는 경우 이를 추가하는 로직입니다. 이를 통해 CriticalInformation
내 RoutineCount
는 0x10
이 됩니다.
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
풀에는 어떠한 의도든 Head
와 Tail
에 랜덤한 사이즈의 랜덤한 값이 복사된 다는 것을 확인하였습니다. Head
와 Tail
의 랜덤한 값으로 채워진 다음은, 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
는 다음과 같은 연산에 의한 사이즈 입니다.
CriticalInformaiton.NestingSize
CriticalRoutine
15(0xf)개에 대한 루틴 크기와HaliHaltSystem
포인터 크기의 합CriticalInformation.CriticalRoutinePteList
개수를 의미하는CurrentArrayCount
* 0x10
CriticalInformationSize
NestingSize
+RoutineCount
* 0x10
FixPgContextSize
CriticalInformationSize
+ 0xAA8
SizeResult
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
의 호출 로직을 살펴보면 아래와 같은 참조를 확인할 수 있습니다.
InitBootProcessor
:KeInitSystem(0)
Phase1InitializationDiscard
:KeInitSystem(1)
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
를 어떻게 찾을 수 있을 것인가에 대해 분석을 진행하도록 하겠습니다.