Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일

windbg와 Win32 API로 알아보는 Windows Heap 정보 분석

지난 글에서,

windbg - CoTaskMemFree/FreeCoTaskMem에서 발생한 덤프 분석 사례 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12062

엎어진 김에 쉬어간다고 ^^ 기왕 이렇게 되었으니 힙 관련 정보를 얻기 위한 방법을 이참에 짚고 넘어가 보겠습니다. 우선, HeapWalk Win32 API를 이용해 프로그램 스스로 HeapCreate 및 HeapAlloc으로 할당받은 메모리를 알아낼 수 있습니다.

HeapWalk function
; https://docs.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapwalk

Enumerating a Heap
; https://docs.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://docs.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와는 다르게 - 예를 들어 000001614C0F0750이 아닌 000001614c0f0740으로 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.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 11/28/2019 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2019-11-28 10시34분
최신 윈도우즈 익스플로잇 개발 04. 힙 (Heap)
; https://hackability.kr/entry/%EC%9D%B5%EC%8A%A4%ED%94%8C%EB%A1%9C%EC%9E%87-%EA%B0%9C%EB%B0%9C-04-%ED%9E%99-Heap

보안 취약점(19.5.29) Heap Types
; https://sudeky.tistory.com/39

Heap 메모리 생성한지 알아볼때 (WinDbg) Who called HeapAlloc
; https://gosera.tistory.com/68

정성태
2020-02-07 11시08분
정성태

1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12094정성태12/26/2019492.NET Framework: 873. C# - 코드를 통해 PDB 심벌 파일 다운로드 방법
12093정성태12/26/2019657.NET Framework: 872. C# - 로딩된 Native DLL의 export 함수 목록 출력파일 다운로드1
12092정성태12/25/2019489디버깅 기술: 148. cdb.exe를 이용해 (ntdll.dll 등에 정의된) 커널 구조체 출력하는 방법
12091정성태12/25/2019670디버깅 기술: 147. pdb 파일을 다운로드하기 위한 symchk.exe 실행에 필요한 최소 파일
12090정성태12/24/2019483.NET Framework: 871. .NET AnyCPU로 빌드된 PE 헤더의 로딩 전/후 차이점
12089정성태12/23/2019371디버깅 기술: 146. gflags와 _CrtIsMemoryBlock을 이용한 Heap 메모리 손상 여부 체크
12088정성태12/23/2019314Linux: 28. Linux - 윈도우의 "Run as different user" 기능을 shell에서 실행하는 방법
12087정성태12/21/2019467디버깅 기술: 145. windbg/sos - Dictionary의 entries 배열 내용을 모두 덤프하는 방법 (do_hashtable.py)
12086정성태12/20/2019529디버깅 기술: 144. windbg - Marshal.FreeHGlobal에서 발생한 덤프 분석 사례
12085정성태12/20/2019435오류 유형: 586. iisreset - The data is invalid. (2147942413, 8007000d) 오류 발생 - 두 번째 이야기 [1]
12084정성태12/21/2019494디버깅 기술: 143. windbg/sos - Hashtable의 buckets 배열 내용을 모두 덤프하는 방법 (do_hashtable.py)
12083정성태12/17/2019690Linux: 27. linux - lldb를 이용한 .NET Core 응용 프로그램의 메모리 덤프 분석 방법 [1]
12082정성태12/17/2019480오류 유형: 585. lsof: WARNING: can't stat() fuse.gvfsd-fuse file system
12081정성태12/16/2019523개발 환경 구성: 465. 로컬 PC에서 개발 중인 ASP.NET Core 웹 응용 프로그램을 다른 PC에서도 접근하는 방법
12080정성태12/16/2019764.NET Framework: 870. C# - 프로세스의 모든 핸들을 열람
12079정성태12/13/2019507오류 유형: 584. 원격 데스크탑(rdp) 환경에서 다중 또는 고용량 파일 복사 시 "Unspecified error" 오류 발생
12078정성태12/13/2019858Linux: 26. .NET Core 응용 프로그램을 위한 메모리 덤프 방법 [1]
12077정성태12/13/2019603Linux: 25. 자주 실행할 명령어 또는 초기 환경을 "~/.bashrc" 파일에 등록
12076정성태12/17/2019719디버깅 기술: 142. Linux - lldb 환경에서 sos 확장 명령어를 이용한 닷넷 프로세스 디버깅 - 배포 방법에 따른 차이
12075정성태12/18/2019781디버깅 기술: 141. Linux - lldb 환경에서 sos 확장 명령어를 이용한 닷넷 프로세스 디버깅
12074정성태12/11/2019523디버깅 기술: 140. windbg/Visual Studio - 값이 변경된 경우를 위한 정지점(BP) 설정(Data Breakpoint)
12073정성태12/10/2019880Linux: 24. Linux/C# - 실행 파일이 아닌 스크립트 형식의 명령어를 Process.Start로 실행하는 방법
12072정성태12/9/2019377오류 유형: 583. iisreset 수행 시 "No such interface supported" 오류
12071정성태12/9/2019543오류 유형: 582. 리눅스 디스크 공간 부족 및 safemode 부팅 방법
12070정성태12/9/2019909오류 유형: 581. resize2fs: Bad magic number in super-block while trying to open /dev/.../root
12069정성태12/19/2019754디버깅 기술: 139. windbg - x64 덤프 분석 시 메서드의 인자 또는 로컬 변수의 값을 확인하는 방법
1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...