
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 とそれに続くバイトが上書きされます。
以下は、ヒープオーバーライトを引き起こすバグの逆アセンブリコードであり、これを利用して偽の trunk を構築できます:
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 などのおなじみの関数を使用します。ユーザーモードヒープとは異なり、デスクトップヒープはフロントエンドアロケーターを使用しないため、Low Fragmentation Heap や Lookaside list などはありません。また、Windows 8 以降までヒープエンコーディングもありません。以下は、win7_sp1_x86 上の trunk の構造です:
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 の場合は使用中ではありません。
次の図は、これらのフィールドとチャンクブロック間の関係を示しています。2 番目の緑の下線 WORD (0x000f) は、現在のチャンクサイズが 0x78 バイトであることを表します。2 番目の黒の下線 WORD (0x0003) は、前のチャンクサイズが 0x18 バイトであることを表します。赤い下線 WORD (0x0001) は、現在のチャンクが使用中であることを表します。ここで、チャンクサイズにはヘッダーサイズが含まれます。ヘッダーは上記の HEAP_ENTRY 構造体として定義されます。

ヒープ破損にとって最も重要な特徴は、ヒープアロケーターが常に最近解放されたチャンクを取得することです。これは、任意のサイズと特定の位置でチャンクを実際に割り当てることができることを意味します。
バグを利用して、デスクトップヒープ内の一部のバイトを上書きできるため、偽のチャンクを取得します。それが通常のチャンクを置き換え、置き換えられたチャンクを解放すると、偽のチャンクがフリーチャンクリストの先頭にプッシュされます。その後、偽のチャンクが再利用され、その中に任意のバイトを書き込むことができます。書き込み可能な領域は複数の通常のチャンクと重なりますが、カーネルランド全体をカバーすることはできません。したがって、重複領域内に別の R/W プリミティブを構築する必要があります。これには、任意のアドレスを書き込むために 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、MenuITEM のチャンクヘッダーが損傷します。プロセスを正常に終了するために、一部のデータを保存し、ステップ 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 によってエクスポートされています。次のように定義されます: