
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;
따라서 다음 함수를 사용하여 핸들에서 커널 주소를 얻을 수 있습니다: