windbg와 Win32 API로 알아보는 Windows Heap 정보 분석
지난 글에서,
windbg - CoTaskMemFree/FreeCoTaskMem에서 발생한 덤프 분석 사례 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12062
엎어진 김에 쉬어간다고 ^^ 기왕 이렇게 되었으니 힙 관련 정보를 얻기 위한 방법을 이참에 짚고 넘어가 보겠습니다. 우선, HeapWalk Win32 API를 이용해 프로그램 스스로 HeapCreate 및 HeapAlloc으로 할당받은 메모리를 알아낼 수 있습니다.
HeapWalk function
; https://learn.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapwalk
Enumerating a Heap
; https://learn.microsoft.com/en-us/windows/win32/memory/enumerating-a-heap
위의 함수를 이용해
지난 글에 다룬 예제 코드에 살을 붙여 보면,
#include <iostream>
#include <stdio.h>
#include <wchar.h>
#include <combaseapi.h>
#include <tchar.h>
void ShowHeapInfo(HANDLE hHandle)
{
PROCESS_HEAP_ENTRY entry;
entry.lpData = nullptr;
while (HeapWalk(hHandle, &entry) != false)
{
if ((entry.wFlags & PROCESS_HEAP_ENTRY_BUSY) != 0) {
_tprintf(TEXT("Allocated block"));
if ((entry.wFlags & PROCESS_HEAP_ENTRY_MOVEABLE) != 0) {
_tprintf(TEXT(", movable with HANDLE %#p"), entry.Block.hMem);
}
if ((entry.wFlags & PROCESS_HEAP_ENTRY_DDESHARE) != 0) {
_tprintf(TEXT(", DDESHARE"));
}
}
else if ((entry.wFlags & PROCESS_HEAP_REGION) != 0) {
_tprintf(TEXT("Region\n %d bytes committed\n") \
TEXT(" %d bytes uncommitted\n First block address: %#p\n") \
TEXT(" Last block address: %#p\n"),
entry.Region.dwCommittedSize,
entry.Region.dwUnCommittedSize,
entry.Region.lpFirstBlock,
entry.Region.lpLastBlock);
}
else if ((entry.wFlags & PROCESS_HEAP_UNCOMMITTED_RANGE) != 0) {
_tprintf(TEXT("Uncommitted range\n"));
}
else {
_tprintf(TEXT("Block\n"));
}
_tprintf(TEXT(" Data portion begins at: %#p\n Size: %d bytes\n") \
TEXT(" Overhead: %d bytes\n Region index: %d\n\n"),
entry.lpData,
entry.cbData,
entry.cbOverhead,
entry.iRegionIndex);
}
}
int main()
{
int size = 20;
HANDLE hHandle = HeapCreate(0, 0, 8192);
printf("Handle == 0x%I64x\n", hHandle);
LPVOID pVoid1 = HeapAlloc(hHandle, 0, size);
printf("pVoid1 == 0x%I64x\n", pVoid1);
memset(pVoid1, 0xff, size);
LPVOID pVoid2 = HeapAlloc(hHandle, 0, size + 1);
printf("pVoid2 == 0x%I64x\n", pVoid2);
memset(pVoid2, 0xee, size + 1);
LPVOID pVoid3 = HeapAlloc(hHandle, 0, size + 2);
printf("pVoid3 == 0x%I64x\n", pVoid3);
memset(pVoid3, 0xcc, size + 2);
LPVOID pVoid4 = HeapAlloc(hHandle, 0, 1);
printf("pVoid4 == 0x%I64x\n", pVoid4);
memset(pVoid4, 0xaa, 1);
ShowHeapInfo(hHandle);
printf("Wait...\n");
getchar();
HeapFree(hHandle, 0, pVoid4);
HeapFree(hHandle, 0, pVoid3);
HeapFree(hHandle, 0, pVoid2);
HeapFree(hHandle, 0, pVoid1);
HeapDestroy(hHandle);
printf("Exited!");
return 0;
}
실행 후 HeapAlloc으로 할당받은 메모리의 주소를 printf로 출력한 내용이,
Handle == 0x1614c0f0000
pVoid1 == 0x1614c0f0750
pVoid2 == 0x1614c0f0770
pVoid3 == 0x1614c0f0790
pVoid4 == 0x1614c0f07b0
그대로 HeapWalk에서도 나오고 심지어 HeapAlloc 호출 시 요청한 Size까지 구할 수 있습니다.
Region
8192 bytes committed
0 bytes uncommitted
First block address: 000001614C0F0750
Last block address: 000001614C0F2000
Data portion begins at: 000001614C0F0000
Size: 1856 bytes
Overhead: 0 bytes
Region index: 0
Allocated block Data portion begins at: 000001614C0F0750
Size: 20 bytes
Overhead: 12 bytes
Region index: 0
Allocated block Data portion begins at: 000001614C0F0770
Size: 21 bytes
Overhead: 11 bytes
Region index: 0
Allocated block Data portion begins at: 000001614C0F0790
Size: 22 bytes
Overhead: 10 bytes
Region index: 0
Allocated block Data portion begins at: 000001614C0F07B0
Size: 1 bytes
Overhead: 31 bytes
Region index: 0
Block
Data portion begins at: 000001614C0F07E0
Size: 6112 bytes
Overhead: 32 bytes
Region index: 0
Uncommitted range
Data portion begins at: 000001614C0F2000
Size: 0 bytes
Overhead: 0 bytes
Region index: 0
만약
CoTaskMemAlloc으로 할당한 메모리 내역을 보고 싶다면 Default Heap handle을 구한 다음 그것을 전달하면 됩니다.
HANDLE hDefaultHeap = GetProcessHeap();
printf("hDefaultHeap == 0x%I64x\n", hDefaultHeap); // hDefaultHeap == 0x1614bf20000
ShowHeapInfo(hDefaultHeap);
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
코드를 통해 알아봤으니, 이제 메모리 덤프를 다루기 위해 windbg에서 heap 정보를 알아내는 것도 보겠습니다.
Common WinDbg Commands (Thematically Grouped) - 20) Memory: Heap
; http://windbg.info/doc/1-common-cmds.html#20_memory_heap
!heap
; https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/-heap
우선, 프로그램에서 생성한 Heap 목록을 "-s" 옵션으로 구할 수 있습니다.
0:000> !heap -s
Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
LFH Key : 0x0363460674ab5132
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-------------------------------------------------------------------------------------
000001614bf20000 00000002 1220 104 1020 2 6 1 0 0 LFH
000001614bcf0000 00008000 64 4 64 2 1 1 0 0
000001614c0f0000 00001000 8 8 8 6 1 1 0 0
-------------------------------------------------------------------------------------
보는 바와 같이 000001614bf20000 == GetProcessHeap이고, 000001614c0f0000 항목이 코드에서 HeapCreate로 생성한 것입니다. (중간의 000001614bcf0000 항목은 아마도 CRT에서 생성했을 것입니다.) 또한, "-s" 옵션 말고 "-m" 옵션으로도 유사한 정보를 볼 수 있습니다.
0:000> !heap -m
Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
HEAPEXT: Unable to get address of ntdll!RtlpHeapInvalidBadAddress.
Index Address Name Debugging options enabled
1: 1614bf20000
Segment at 000001614bf20000 to 000001614c01f000 (00018000 bytes committed)
2: 1614bcf0000
Segment at 000001614bcf0000 to 000001614bd00000 (00001000 bytes committed)
3: 1614c0f0000
Segment at 000001614c0f0000 to 000001614c0f2000 (00002000 bytes committed)
그리고 HeapWalk처럼 개별 힙 내부에 할당된 메모리 조각을 열람하려면 "-stat -h" 옵션을 적용하거나,
0:000> !heap -stat -h 000001614c0f0000
heap @ 000001614c0f0000
group-by: TOTSIZE max-display: 20
size #blocks total ( %) (percent of total busy bytes)
16 1 - 16 (34.38)
15 1 - 15 (32.81)
14 1 - 14 (31.25)
1 1 - 1 (1.56)
"-stat" 옵션을 빼면 더욱 자세한 정보를 얻을 수 있습니다.
0:000> !heap -h 000001614c0f0000
Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
Index Address Name Debugging options enabled
3: 1614c0f0000
Segment at 000001614c0f0000 to 000001614c0f2000 (00002000 bytes committed)
Flags: 00001000
ForceFlags: 00000000
Granularity: 16 bytes
Segment Reserve: 00100000
Segment Commit: 00002000
DeCommit Block Thres: 00000100
DeCommit Total Thres: 00001000
Total Free Size: 00000180
Max. Allocation Size: 00007ffffffdefff
Lock Variable at: 000001614c0f02c0
Next TagIndex: 0000
Maximum TagIndex: 0000
Tag Entries: 00000000
PsuedoTag Entries: 00000000
Virtual Alloc List: 1614c0f0110
Uncommitted ranges: 1614c0f00f0
FreeList[ 00 ] at 000001614c0f0150: 000001614c0f07d0 . 000001614c0f07d0 (1 block )
Heap entries for Segment00 in Heap 000001614c0f0000
address: psize . size flags state (requested size)
000001614c0f0000: 00000 . 00740 [101] - busy (73f)
000001614c0f0740: 00740 . 00020 [101] - busy (14)
000001614c0f0760: 00020 . 00020 [101] - busy (15)
000001614c0f0780: 00020 . 00020 [101] - busy (16)
000001614c0f07a0: 00020 . 00020 [101] - busy (1)
000001614c0f07c0: 00020 . 01800 [100]
000001614c0f1fc0: 01800 . 00040 [111] - busy (3d)
000001614c0f2000: 00000000 - uncommitted bytes.
위의 출력 결과에서 "Heap entries"를 보면 HeapWalk와는 다르게 - 예를 들어 000001614C0F0
750이 아닌 000001614c0f0
740으로 0x10 바이트 먼저 나오는 것은 헤더 영역을 가리킨 것이기 때문입니다.
참고로, 할당 받은 메모리 중 아무 주소나 알 수 있으면 그것이 속한 Heap Handle과 HeapAlloc 블록을 알아낼 수 있습니다.
0:000> !heap -p -a 000001614c0f0754
address 000001614c0f0754 found in
_HEAP @ 1614c0f0000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
000001614c0f0740 0002 0000 [00] 000001614c0f0750 00014 - (busy)
0:000> !heap -p -a 000001614c0f0777
address 000001614c0f0777 found in
_HEAP @ 1614c0f0000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
000001614c0f0760 0002 0000 [00] 000001614c0f0770 00015 - (busy)
0:000> !heap -p -a 000001614c0f0797
address 000001614c0f0797 found in
_HEAP @ 1614c0f0000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
000001614c0f0780 0002 0000 [00] 000001614c0f0790 00016 - (busy)
0:000> !heap -p -a 000001614c0f07B7
address 000001614c0f07b7 found in
_HEAP @ 1614c0f0000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
000001614c0f07a0 0002 0000 [00] 000001614c0f07b0 00001 - (busy)
자, 그럼 조금 더 ^^ 내려가 "!heap" 명령의 도움을 받지 않고 직접 바닥부터 알아보겠습니다. 우선, 프로세스가 가진 Heap 목록을 PEB로부터 알아낼 수 있습니다.
0:000> dt _PEB @$peb NumberOfHeaps, ProcessHeaps
ntdll!_PEB
+0x0e8 NumberOfHeaps : 3
+0x0f0 ProcessHeaps : 0x00007ffa`0c543c40 -> 0x00000161`4bf20000 Void
NumberOfHeaps를 통해 3개의 힙이 생성되어 있다는 점과, ProcessHeaps 항목을 통해 0x00007ffa`0c543c40 포인터를 덤프해 보면,
0:000> dq /c1 0x00007ffa`0c543c40 L3
00007ffa`0c543c40 00000161`4bf20000
00007ffa`0c543c48 00000161`4bcf0000
00007ffa`0c543c50 00000161`4c0f0000
차례대로 3개의 포인터가 그대로 HeapCreate로 반환받은 Heap Handle 값임을 알 수 있습니다. 자... 이렇게 구한 Heap Handle을 ntdll의 _HEAP 구조체로 덤프해 보면,
0:000> dt ntdll!_HEAP 000001614c0f0000
+0x000 Segment : _HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x010 SegmentSignature : 0xffeeffee
+0x014 SegmentFlags : 0
+0x018 SegmentListEntry : _LIST_ENTRY [ 0x00000161`4c0f0120 - 0x00000161`4c0f0120 ]
+0x028 Heap : 0x00000161`4c0f0000 _HEAP
+0x030 BaseAddress : 0x00000161`4c0f0000 Void
+0x038 NumberOfPages : 2
+0x040 FirstEntry : 0x00000161`4c0f0740 _HEAP_ENTRY
+0x048 LastValidEntry : 0x00000161`4c0f2000 _HEAP_ENTRY
+0x050 NumberOfUnCommittedPages : 0
+0x054 NumberOfUnCommittedRanges : 1
+0x058 SegmentAllocatorBackTraceIndex : 0
+0x05a Reserved : 0
+0x060 UCRSegmentList : _LIST_ENTRY [ 0x00000161`4c0f1fe0 - 0x00000161`4c0f1fe0 ]
+0x070 Flags : 0x1000
+0x074 ForceFlags : 0
+0x078 CompatibilityFlags : 0
+0x07c EncodeFlagMask : 0x100000
+0x080 Encoding : _HEAP_ENTRY
+0x090 Interceptor : 0
+0x094 VirtualMemoryThreshold : 0xff00
+0x098 Signature : 0xeeffeeff
+0x0a0 SegmentReserve : 0x100000
+0x0a8 SegmentCommit : 0x2000
+0x0b0 DeCommitFreeBlockThreshold : 0x100
+0x0b8 DeCommitTotalFreeThreshold : 0x1000
+0x0c0 TotalFreeSize : 0x180
+0x0c8 MaximumAllocationSize : 0x00007fff`fffdefff
+0x0d0 ProcessHeapsListIndex : 3
+0x0d2 HeaderValidateLength : 0x2c0
+0x0d8 HeaderValidateCopy : (null)
+0x0e0 NextAvailableTagIndex : 0
+0x0e2 MaximumTagIndex : 0
+0x0e8 TagEntries : (null)
+0x0f0 UCRList : _LIST_ENTRY [ 0x00000161`4c0f00f0 - 0x00000161`4c0f00f0 ]
+0x100 AlignRound : 0x1f
+0x108 AlignMask : 0xffffffff`fffffff0
+0x110 VirtualAllocdBlocks : _LIST_ENTRY [ 0x00000161`4c0f0110 - 0x00000161`4c0f0110 ]
+0x120 SegmentList : _LIST_ENTRY [ 0x00000161`4c0f0018 - 0x00000161`4c0f0018 ]
+0x130 AllocatorBackTraceIndex : 0
+0x134 NonDedicatedListLength : 0
+0x138 BlocksIndex : 0x00000161`4c0f02e8 Void
+0x140 UCRIndex : (null)
+0x148 PseudoTagEntries : (null)
+0x150 FreeLists : _LIST_ENTRY [ 0x00000161`4c0f07d0 - 0x00000161`4c0f07d0 ]
+0x160 LockVariable : 0x00000161`4c0f02c0 _HEAP_LOCK
+0x168 CommitRoutine : 0x26faddb8`a6556aa4 long +26faddb8a6556aa4
+0x170 StackTraceInitVar : _RTL_RUN_ONCE
+0x178 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x198 FrontEndHeap : (null)
+0x1a0 FrontHeapLockCount : 0
+0x1a2 FrontEndHeapType : 0 ''
+0x1a3 RequestedFrontEndHeapType : 0 ''
+0x1a8 FrontEndHeapUsageData : (null)
+0x1b0 FrontEndHeapMaximumIndex : 0
+0x1b2 FrontEndHeapStatusBitmap : [129] ""
+0x238 Counters : _HEAP_COUNTERS
+0x2b0 TuningParameters : _HEAP_TUNING_PARAMETERS
"
windbg/Visual Studio - HeapFree x64의 동작 분석" 글에서 다룬 SegmentSignature와 해당 Heap Handle에 속한 (HeapAlloc으로 할당받은) 블록의 첫 번째 위치를 "FirstEntry" 값으로 알 수 있습니다. 그리고 이 값은 다음과 같이 _HEAP_ENTRY로 덤프할 수 있습니다.
0:000> dt ntdll!_HEAP_ENTRY 0x00000161`4c0f0740
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 PreviousBlockPrivateData : (null)
+0x008 Size : 0x6e73
+0x00a Flags : 0x35 '5'
+0x00b SmallTagIndex : 0xec ''
+0x008 SubSegmentCode : 0xec356e73
+0x00c PreviousSize : 0x476e
+0x00e SegmentOffset : 0 ''
+0x00e LFHFlags : 0 ''
+0x00f UnusedBytes : 0xc ''
+0x008 CompactHeader : 0x0c00476e`ec356e73
+0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
+0x000 Reserved : (null)
+0x008 FunctionIndex : 0x6e73
+0x00a ContextValue : 0xec35
+0x008 InterceptorValue : 0xec356e73
+0x00c UnusedBytesLength : 0x476e
+0x00e EntryOffset : 0 ''
+0x00f ExtendedBlockSignature : 0xc ''
+0x000 ReservedForAlignment : (null)
+0x008 Code1 : 0xec356e73
+0x00c Code2 : 0x476e
+0x00e Code3 : 0 ''
+0x00f Code4 : 0xc ''
+0x00c Code234 : 0xc00476e
+0x008 AgregateCode : 0x0c00476e`ec356e73
그런데 문제는 "NextEntry"같은 식의 속성이 없습니다. 이전의 결과를 통해 이미 알고 있던 0x00000161`4c0f0760 항목을 역시 덤프해 봐도,
0:000> dt ntdll!_HEAP_ENTRY 0x00000161`4c0f0760
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 PreviousBlockPrivateData : 0x00000000`ffffffff Void
+0x008 Size : 0x6e73
+0x00a Flags : 0x35 '5'
+0x00b SmallTagIndex : 0xec ''
+0x008 SubSegmentCode : 0xec356e73
+0x00c PreviousSize : 0x4718
+0x00e SegmentOffset : 0 ''
+0x00e LFHFlags : 0 ''
+0x00f UnusedBytes : 0xb ''
+0x008 CompactHeader : 0x0b004718`ec356e73
+0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
+0x000 Reserved : 0x00000000`ffffffff Void
+0x008 FunctionIndex : 0x6e73
+0x00a ContextValue : 0xec35
+0x008 InterceptorValue : 0xec356e73
+0x00c UnusedBytesLength : 0x4718
+0x00e EntryOffset : 0 ''
+0x00f ExtendedBlockSignature : 0xb ''
+0x000 ReservedForAlignment : 0x00000000`ffffffff Void
+0x008 Code1 : 0xec356e73
+0x00c Code2 : 0x4718
+0x00e Code3 : 0 ''
+0x00f Code4 : 0xb ''
+0x00c Code234 : 0xb004718
+0x008 AgregateCode : 0x0b004718`ec356e73
PrevEntry 또는 NextEntry와 같은 요소는 찾을 수가 없습니다. 물론 "!heap" 명령을 사용하면 다음과 같이 쉽게 구할 수 있습니다. ^^
0:000> !heap -i 000001614c0f0000
Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
Heap context set to the heap 0x000001614c0f0000
0:000> !heap -i 0x00000161`4c0f0780
Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
Detailed information for block entry 000001614c0f0780
Assumed heap : 0x000001614c0f0000 (Use !heap -i NewHeapHandle to change)
Header content : 0xEC356E73 0x0A004718 (decoded : 0x03010002 0x0A000002)
Owning segment : 0x000001614c0f0000 (offset 0)
Block flags : 0x1 (busy )
Total block size : 0x2 units (0x20 bytes)
Requested size : 0x16 bytes (unused 0xa bytes)
Previous block size: 0x2 units (0x20 bytes)
Block CRC : OK - 0x3
Previous block : 0x000001614c0f0760
Next block : 0x000001614c0f07a0
아니... 도대체 어떻게 구한 것일까요? ^^ 다행히 검색해 보면, 이에 관해 설명한 글을 볼 수 있습니다.
Windows Heap Chunk Header Parsing and Size Calculation
; https://stackoverflow.com/questions/28483473/windows-heap-chunk-header-parsing-and-size-calculation
그러니까, 우선 해당 block이 속한 _HEAP의 "Encoding" 속성 값을 먼저 구해야 합니다.
0:000> dt ntdll!_HEAP 000001614c0f0000 encoding
+0x080 Encoding : _HEAP_ENTRY
0:000> dq 000001614c0f0000 + 0x80 L2
00000161`4c0f0080 00000000`00000000 0000471a`ef346e71
그럼 위와 같이 (64비트의 경우) 8바이트 씩 2개의 값을 구한 다음, 이것을 HeapAlloc으로 할당받은 영역의 헤더 값과 XOR 연산을 합니다.
0:000> dq 0x00000161`4c0f0740 L2
00000161`4c0f0740 00000000`00000000 0c00476e`ec356e73
0:000> ? 00000000`00000000 ^ 00000000`00000000
Evaluate expression: 0 = 00000000`00000000
0:000> ? 0000471a`ef346e71 ^ 0c00476e`ec356e73
Evaluate expression: 864691626721738754 = 0c000074`03010002
(참고로, 32비트의 경우 4바이트 씩 2개의 값을 동일하게 XOR 연산하면 됩니다.)
어차피 8바이트 중 앞의 것은 0이므로 크게 중요하지 않고, 뒤의 8바이트가 의미를 가지는데 이것을 그대로 _HEAP_ENTRY 구조체에 memcpy를 해야 합니다. 하지만 windbg 사용 중에 코딩하는 것은 귀찮으므로(게다가 _HEAP_ENTRY의 구조도 OS마다, 패치마다 달라질 수 있으므로), 이것을 쉽게 해결하기 위해 현재 사용 중이지 않을 법한 메모리를 찾아낸 다음,
0:000> dq 000001614c0f07c0
00000161`4c0f07c0 00000000`00000000 00004718`6e346ff1
00000161`4c0f07d0 00000161`4c0f0150 00000161`4c0f0150
00000161`4c0f07e0 00000000`00000000 00000000`00000000
00000161`4c0f07f0 00000000`00000000 00000000`00000000
00000161`4c0f0800 00000000`00000000 00000000`00000000
00000161`4c0f0810 00000000`00000000 00000000`00000000
00000161`4c0f0820 00000000`00000000 00000000`00000000
00000161`4c0f0830 00000000`00000000 00000000`00000000
0:000> dq 000001614c0f07e0
00000161`4c0f07e0 00000000`00000000 00000000`00000000
00000161`4c0f07f0 00000000`00000000 00000000`00000000
00000161`4c0f0800 00000000`00000000 00000000`00000000
00000161`4c0f0810 00000000`00000000 00000000`00000000
00000161`4c0f0820 00000000`00000000 00000000`00000000
(위의 출력에서는 000001614c0f07e0 주소가 적당한 듯하니) 그 주소에 XOR 연산 결과로 나온 값을 그대로 써주면,
0:000> eq 000001614c0f07e0
00000161`4c0f07e0 00000000`00000000 0
0
00000161`4c0f07e8 00000000`00000000 0c000074`03010002
0c000074`03010002
00000161`4c0f07f0 00000000`00000000
0:000> dq 000001614c0f07e0
00000161`4c0f07e0 00000000`00000000 0c000074`03010002
00000161`4c0f07f0 00000000`00000000 00000000`00000000
00000161`4c0f0800 00000000`00000000 00000000`00000000
00000161`4c0f0810 00000000`00000000 00000000`00000000
00000161`4c0f0820 00000000`00000000 00000000`00000000
결국 _HEAP_ENTRY에 해당하는 메모리로 덤프할 수 있게 됩니다.
0:000> dt _HEAP_ENTRY 000001614c0f07e0
ntdll!_HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 PreviousBlockPrivateData : (null)
+0x008 Size : 2
+0x00a Flags : 0x1 ''
+0x00b SmallTagIndex : 0x3 ''
+0x008 SubSegmentCode : 0x3010002
+0x00c PreviousSize : 0x74
+0x00e SegmentOffset : 0 ''
+0x00e LFHFlags : 0 ''
+0x00f UnusedBytes : 0xc ''
+0x008 CompactHeader : 0x0c000074`03010002
+0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
+0x000 Reserved : (null)
+0x008 FunctionIndex : 2
+0x00a ContextValue : 0x301
+0x008 InterceptorValue : 0x3010002
+0x00c UnusedBytesLength : 0x74
+0x00e EntryOffset : 0 ''
+0x00f ExtendedBlockSignature : 0xc ''
+0x000 ReservedForAlignment : (null)
+0x008 Code1 : 0x3010002
+0x00c Code2 : 0x74
+0x00e Code3 : 0 ''
+0x00f Code4 : 0xc ''
+0x00c Code234 : 0xc000074
+0x008 AgregateCode : 0x0c000074`03010002
이렇게 디코딩된 결과를 보면, 이번 0x00000161`4c0f0740 HeapAlloc 블록은 크기가 2단위이고, 이전의 블록 크기는 0x74 단위였다는 것입니다. 그리고 실제 바이트 크기로 구하려면 _HEAP_ENTRY의 크기를 구한 후,
0:000> ?? sizeof(_HEAP_ENTRY)
unsigned int64 0x10
이 값을 각각의 단위에 곱해서 2 * 0x10 = 0x20(32바이트), 0x74 * 0x10 = 0x740(1,856 바이트)를 구할 수 있습니다. 따라서, 0x00000161`4c0f0740 블록의 다음번 블록 위치는 0x00000161`4c0f0740 + 0x20 = 0x00000161`4c0f0760이 되고 이전 블록의 위치는 0x00000161`4c0f0740 - 0x740 = 0x1614c0f0000이 됩니다. 정확하군요. ^^
아래의 글을 보면,
Windows Heap Note
; http://wg135.github.io/blog/2018/05/31/windows-heap-note/
XOR 했던 값(0c000074`03010002)에서 하위 4바이트 중,
03010002
다시 하위 2바이트(0x0002)가 블록의 크기라고 하며, 상위 2바이트 중 0x03은 SmallTagIndex, 0x01은 "Heap busy"를 나타낸다고 합니다.
0x0002 means this heap is 2 blocks; 0x01 means this heap is busy; 0x03 means SmallTagIndex; Notice: 32 bits system one block is 8 bytes, while 64 bits system one block is 16 bytes.
Heap에 관한 좀 더 자세한 정보는 첨부 파일(
us-16-Yason-Windows-10-Segment-Heap-Internals.zip)로도 제공하는 아래의 PDF 문서를 보시면 도움이 되실 것입니다.
us-16-Yason-Windows-10-Segment-Heap-Internals.zip
; https://www.blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segment-Heap-Internals-wp.pdf
마지막으로,
gflags를 이용해 hpa 옵션이 적용된 프로그램의 메모리 덤프를 보면, _HEAP/_HEAP_ENTRY보다 좀 더 자세한 정보를 담고 있는 _DPH_HEAP_ROOT, _DPH_HEAP_BLOCK을 구할 수 있다고 합니다.
b) Page heap enabled (gflags.exe /i MyApp.exe +hpa)
CreateHeap -> creates a _DPH_HEAP_ROOT (+ _HEAP + 2x _HEAP_ENTRY)**
AllocHeap -> creates a _DPH_HEAP_BLOCK
** With page heap enabled there will still be a _HEAP with two constant _HEAP_ENTRY's for every CreateHeap call.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]