Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2016-3308 — CVE-2016-3308로 win32k 데스크톱 힙 손상 | Kitploit
도구/GitHubGitHub/jackhuyh/cve-2016-3308
Memory ForensicsVulnerability AnalysisExploitationBinary Exploitation
GitHubjackhuyh/cve-2016-3308

CVE-2016-3308

CVE-2016-3308로 win32k 데스크톱 힙 손상

저장소 보기
1211110년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

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

버그를 유발하려면 다음 단계를 수행해야 합니다:

  1. 메뉴를 생성합니다.
  2. 메뉴의 첫 번째 ITEM으로 ID 0x123인 하위 메뉴를 생성합니다. 이때 MENUITEMINFO.hbmpItem에 HBMMENU_SYSTEM을 설정해야 합니다.
  3. ID가 0x1001부터 0x1007까지인 다른 ITEM 7개를 메뉴에 추가합니다.
  4. ID가 1인 ITEM을 하위 메뉴에 추가합니다.
  5. 16개의 슬롯을 가진 tagMENU.rgItems를 얻기 위해 하위 메뉴에 다른 ITEM 8개를 추가합니다. 하지만 크래시만 필요한 경우 이 단계는 트리거에 필요하지 않습니다.
  6. ID 0x123인 9번째 ITEM을 메뉴에 추가하여, 잘못된 매개변수로 의도한 RtlMoveMemory()가 호출되도록 합니다.

데스크톱 힙

데스크톱 힙은 모든 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뿐입니다.

다음 그림은 힙 변경 절차를 보여줍니다:

위에서 보여준 것처럼 데스크톱 힙을 단계적으로 손상시키고 다음 단계로 익스플로잇을 구현합니다:

  1. WndText 크기가 1인 Window를 생성하고 FILL_HOLE_COUNT 횟수만큼 반복하여 기존 구멍을 채웁니다.
  2. 청크 데이터를 조작하기 위해 WND_0 ~ WND_5를 생성합니다.
  3. 메뉴를 생성하고, ID 0x123인 하위 메뉴를 메뉴의 첫 번째 ITEM으로 생성합니다. 그런 다음 ID 0x1001~0x1007인 다른 ITEM 7개를 메뉴에 추가합니다.
  4. ID 1인 ITEM과 ID 0x2001~0x2007인 다른 ITEM 7개를 하위 메뉴에 추가합니다.
  5. 16개의 슬롯을 가진 tagMENU.rgItems를 얻기 위해 ID 0x2008인 9번째 ITEM을 하위 메뉴에 추가합니다.
  6. WND_5 텍스트를 0x360바이트로 설정하여 하위 메뉴 ITEM 8개의 원래 구멍을 채웁니다.
  7. WND_1 텍스트를 0x70바이트로 설정하여 가짜 청크 헤더를 준비합니다.
  8. WND_2 텍스트를 0x70바이트로 설정하여 가짜 청크를 위한 자리 표시자를 만듭니다.
  9. WND_0 텍스트를 0x6c0바이트로 설정하여 향후 메뉴 ITEM 16개를 위한 자리 표시자를 만듭니다.
  10. 자동 생성된 tagPROPLIST와 함께 Primitive-WND를 생성합니다.
  11. 자동 생성된 tagPROPLIST와 함께 Corrupt-WND를 생성합니다.
  12. 8단계의 청크 뒤에 이어질 다음 가짜 청크를 위해 WND_3 텍스트를 0x10바이트로 설정합니다.
  13. 종료 단계에서 복원할 수 있도록 힙 레이아웃을 저장합니다.
  14. WND_0 텍스트를 0x700바이트로 재설정하여 0x6c0 크기의 구멍을 해제합니다.
  15. 9번째 ITEM을 메뉴에 추가하면 14단계에서 해제된 청크를 재사용하며 버그가 유발됩니다.
  16. WND_2 텍스트를 0x80바이트로 재설정하면 가짜 청크가 해제 목록의 맨 위로 밀려납니다.
  17. Corrupt-WND 텍스트를 0x8e0바이트로 설정하면 가짜 청크가 재사용됩니다.
  18. Primitive-WND 텍스트를 설정하여 쓰기 프리미티브를 실행하고 nt!HalDispatchTable[1]을 덮어씁니다.
  19. NtQueryIntervalProfile을 호출하여 셸코드를 트리거합니다.
  20. 저장된 힙 레이아웃을 복원하고 종료합니다.

위 목록에서 힙 펭슈이 구축을 위한 핵심 단계는 다음과 같습니다:

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;

따라서 다음 함수를 사용하여 핸들에서 커널 주소를 얻을 수 있습니다:

도구 다운로드