
기사 번역, CVE-2015-0057 취약점의 32비트 및 64비트 시스템에서의 악용. win32k!xxxEnableWndSBArrows의 use-after-free (CVE 2015-0057) 버그를 32비트 및 64비트에서 악용 (NCC의 Aaron Adams)
저자: Aaron Adams
번역: 55-AA
번역 주: 이 문서는 부분적으로 의역되었습니다. 의문 사항이 있으면 원문을 참조하십시오.
용어:
올해 초, 저는 win32k.sys(CVE-2015-0057)의 흥미로운 취약점을 접했고, 32비트 및 64비트 시스템에서 안정적인 악용을 구현했습니다. 그 적용 범위는 XP부터 Windows 8.1까지입니다(일부 예외 있음). 이 문서에서는 두 플랫폼에서 어떻게 악용을 완료했는지 자세히 설명하고, 끝부분에 몇 가지 다른 내용도 포함되어 있습니다. 또한 SMEP가 활성화된 Windows 8.1에서 낮은 무결성 권한으로 악용하는 방법도 설명합니다.
이 문서는 길지만, 이 취약점 악용의 복잡성을 숨기지 않고 가능한 한 많은 세부 정보를 제공하려고 노력했습니다. 물론 일부 세부 사항은 생략했습니다. 이러한 세부 정보가 여러분에게 도움이 되기를 바랍니다.
2015년 2월 10일, 마이크로소프트는 MS15-010의 관련 세부 사항을 발표했습니다. 이 버그는 enSilo의 Udi Yavo가 처음 발견했습니다. Udi는 breaking malware blog에서 훌륭한 분석을 제공했습니다: "one bit rule-bypassing windows 10 protections using single bit". 이 버그를 깊이 이해하려면 이 글을 꼭 읽어보길 권장합니다. 비록 이 문서에서 가능한 한 많은 세부 사항을 제공할 것이지만, 취약점 트리거 시 극복해야 할 몇 가지 장애물에 관한 내용입니다. 이 취약점의 악용은 매우 흥미로우며, 많은 세부 사항은 Udi의 블로그에서 비롯되었습니다. 다음은 그의 발언입니다:
적절한 공개: 이 블로그는 기술적이지만, 기술 전문가가 취약점 악용을 재현하지 못하도록 코드와 완전한 세부 사항은 공개하지 않습니다.
이 취약점을 악용한 추가 보상으로 포켓몬 진화인 '기술 정령'을 얻었습니다. Udi가 이 버그를 발견하고 블로그에서 관련 상황과 악용 세부 사항을 제공해 준 점에 대해 감사를 표하고 싶습니다. 이 정보는 매우 유용했습니다.
이전에 저는 win32k.sys의 취약점을 악용해 본 적이 없었고, 사용자 모드 콜백 및 많은 관련 API에도 익숙하지 않았습니다. 따라서 Skywing, Tarjei Mandt, Alex Ionescu, j00ru 등 유명한 보안 연구자들이 인터넷에 제공한 훌륭한 리소스에 감사드립니다. 이들은 많은 기술 정보를 공개했으며, 모두 칭찬받아 마땅합니다. 제가 많이 참고한 것은 Tarjei Mandt의 문서 Win32k.sys exploitation paper입니다.
이 취약점 악용을 작성하는 동안, 훌륭한 리버스 엔지니어가 CVE-2015-1701의 안정적인 악용을 구현했습니다. 사용자 모드 콜백에 관한 예제 코드는 매우 유용했습니다. 해당 저자에게 감사드립니다.
참고로, 제 분석은 Windows 7에서 수행되었습니다. 이 버전은 win32k.sys의 모든 구조에 해당하는 심볼이 있는 유일한 버전인 것 같습니다. 이러한 심볼은 대부분 다른 버전의 Win32k.sys 구조에도 사용할 수 있습니다. 어떤 이유에서인지 마이크로소프트는 Windows 8에서 이러한 심볼을 제거했습니다.
마지막으로, 제 악용 방법은 상당히 복잡합니다. 더 쉬운 방법이 존재할 가능성이 높지만, 저는 발견하지 못했습니다. 다른 방법을 사용한 사람의 이야기를 듣고 싶습니다. 어쨌든, 이 모든 것이 win32k.sys 취약점 연구에 도움이 되기를 바랍니다.
아래에서 win32k!xxxEnableWndSBArrows의 디스어셈블리 코드에서 이 버그를 살펴보겠습니다. 매우 정교한 버그입니다:
패치되지 않은 경우:
.text:FFFFF97FFF1B157D mov r8d, r13d
.text:FFFFF97FFF1B1580 mov rdx, r14
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar ; usermode callback 트리거
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
[...]
.text:FFFFF97FFF1B1519 mov eax, [rbx] ; tagSBINFO 포인터를 검사 없이 참조
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh
.text:FFFFF97FFF1B1520 xor eax, esi
위 코드에서 Win32K!xxxdrawscrollbar는 적절한 조건에서 사용자 공간으로 콜백할 수 있습니다. 사용자 공간 코드에서 tagSBINFO 포인터가 공격자에 의해 해제될 수 있으며, 위 코드로 다시 돌아오면 0xFFFFF97FFF1B1519의 코드는 유효하지 않은 포인터를 참조합니다.
패치된 경우:
.text:FFFFF97FFF1D69C3 xor r8d, r8d
.text:FFFFF97FFF1D69C6 mov rdx, rbp
.text:FFFFF97FFF1D69C9 call xxxDrawScrollBar ; usermode callback 트리거
.text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h] ; tagSBINFO 포인터가 올바른지 확인
---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; 올바르면 원래 흐름 계속
| .text:FFFFF97FFF1D69D7 mov rcx, rbp
| .text:FFFFF97FFF1D69DA call _ReleaseDC
| .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958 ; 함수 종료로 점프
|->.text:FFFFF97FFF1D69E4 mov eax, [rbx] ; 안전하게 올바른 tagSBINFO 포인터 사용
.text:FFFFF97FFF1D69E6 xor eax, r14d
위 패치 버전에서는 tagSBINFO 포인터를 사용하기 전에 널 검사를 수행하는 것을 볼 수 있습니다. 관련 구조 정보는 나중에 제공됩니다.
이 악용을 구현할 때 여러 번의 손상(corruption)을 수행했습니다. 그 중 한 번의 손상에서 이 취약점을 트리거했습니다.
이 버그의 기술적 원인은 데스크톱 힙의 UAF(use-after-free)입니다. 처음에는 win32k.sys의 사용자 모드 콜백 메커니즘에 익숙하지 않고 어떻게 작동하는지 몰라서 혼란스러웠습니다. 따라서 이것이 잠금 경쟁 조건으로 인한 UAF라고 생각했습니다. 사실 해당 구조의 잠금은 올바르게 사용되었고, 그 흐름은 예상대로였습니다. 간단히 말해, 실제 문제의 원인은 다음과 같습니다:
이것이 전부입니다. 사용자 모드 콜백을 고려하지 않으면 이 단계는 매우 명확합니다.
그러나 어떻게 손상을 수행하고 왜 수행해야 할까요? Udi의 블로그에서 언급했듯이, 당신은 어떤 위치에서 2비트를 설정하거나 지울 수 있으며, 이 위치는 시스템 코드에서 tagSBINFO 구조의 WSBflags 필드로 간주됩니다. 이것은 일반적인 UAF 악용 방식은 아니지만, 어떻게 조작하는지에 대한 힌트가 기사에 나와 있습니다. 다음 장에서 설명하겠습니다. 먼저 이러한 비트를 어떻게 제어하는지 이해합시다.
tagSBINFO 구조(32비트와 64비트 동일):
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
이 UAF 취약점은 win32k!xxxEnableWndSBArrows() 함수에 있습니다. 이 함수는 하나 또는 두 개(수평 또는 수직)의 스크롤바 컨트롤의 화살표를 활성화 또는 비활성화합니다. 스크롤바 컨트롤은 스크롤바를 조작하기 위한 특수 창입니다. CreateWindow() 함수에 내장된 "SCROLLBAR" 창 클래스를 인자로 사용하여 생성할 수 있습니다.
win32k!xxxEnableWndSBArrows()의 함수 프로토타입:
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);
매개변수 WSBflags의 의미는 WinUser.h에 정의된 것과 동일하며, 어떤 스크롤바가 조작될 것인지를 나타냅니다:
#define SB_HORZ 0
#define SB_VERT 1
#define SB_CTL 2
#define SB_BOTH 3
매개변수 wArrows는 화살표의 상태가 사용 가능한지 또는 사용 불가능한지를 나타냅니다. 설정되면 화살표가 사용 불가능함을 의미하고, 그렇지 않으면 사용 가능함을 의미합니다. wArrows의 최하위 2비트는 수평 스크롤바를, 다음 2비트는 수직 스크롤바를 나타내며, 나머지 비트는 이 악용 목적과 관련이 없습니다.
다음 코드는 win32k!xxxEnableWndSBArrows() 함수에서 가져온 것이며, SB_HORZ 또는 SB_BOTH가 설정되면 수평 화살표 관련 비트를 설정하거나 해제합니다:

이 버그는 수평 스크롤바와 수직 스크롤바 플래그를 설정할 때 존재합니다. 수평 스크롤바를 새로 고친 후, 해당 스크롤바에 해당하는 창이 데스크톱에 표시되면 win32k!xxxEnableWndSBArrows() 함수는 win32k!xxxDrawScrollBar()를 호출합니다. 앞서 언급했듯이 이것은 잠재적인 사용자 모드 콜백을 트리거할 수 있습니다.
사용자 모드 콜백을 논의하기 전에, win32k!xxxDrawScrollBar() 호출 후에 발생하는 일을 계속 논의합시다. 이것은 사실 수평 스크롤바와 동일한 논리를 가지며, 약간의 비트 차이만 있습니다. 수직 스크롤바를 비활성화하기로 선택하고 UAF를 트리거한다고 가정하면, 이것은 tagSBINFO 힙 블록의 어떤 위치에 2비트를 쓰게 됩니다. 따라서 원래 값이 0x2이면 이제 0xe가 됩니다. 아래 그림과 같습니다.

이 비트의 변경은 최종 코드 실행으로 이어지기에 충분합니다. 비트를 지움으로써 악용을 완료하는 방법은 깊이 연구하지 않았지만 가능합니다.
위에서 설명한 핵심은 수평 및 수직 스크롤바를 동시에 조작하려면 두 요소를 모두 가진 스크롤바 컨트롤을 생성해야 한다는 것입니다. 이는 CreateWindow()를 호출하고 WS_HSCROLL 및 WS_VSCROLL 플래그를 설정하여 수행됩니다. 코드는 다음과 같습니다:
g_hSBCtl = CreateWindowEx(
0, // 확장 스타일 없음
"SCROLLBAR", // 클래스
NULL, // 이름
SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // 수직 + 수평
10, // x
10, // y
100, // 너비
100, // 높이
g_hSpray[UAFWND], // 모달리스 부모 창
(HMENU)NULL,
NULL, // 창 소유자
NULL // 추가 매개변수
);
다음 코드를 통해 표시되는지 확인할 수 있습니다(일반적으로 기본값이지만 여기서는 명시적으로 호출):
result = ShowWindow(g_hSBCtl, SW_SHOW);
스크롤바는 기본적으로 활성화되어 있습니다. 취약점 코드를 트리거하려고 할 때 스크롤바를 비활성화로 설정하여 필요한 비트를 손상시킬 수 있습니다:
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);
위에서 버그의 세부 사항과 관련 코드를 트리거하는 방법을 설명했지만, 여전히 가장 중요한 단계인 win32k!xxxDrawScrollBar()에서 시작된 사용자 모드 콜백을 가로채서 win32k!xxxEnableWndSBArrows()가 계속 실행되기 전에 힙의 내용을 변경하는 것을 간과했습니다. 실제로 버그를 트리거해야 하지만, Win32k.sys 관련 내용과 API에 대해 전혀 모르는 상태(제가 시작했을 때처럼)라면 이것은 자신에게 하나의 모험이 됩니다.
이전 기사에는 좋은 콜 스택 다이어그램이 있으며, 이 과정을 깊이 보여줍니다. win32k!xxxDrawScrollBar()를 통해 트리거한 후 ClientLoadLibrary()가 호출되고, KeUserModeCallback()을 통해 디스패치됩니다. 우리는 KeUserModeCallback()의 호출 상황을 정확히 파악하여 자체 프로세스에서 HOOK해야 합니다.
사용자 모드 콜백에 대한 좋은 자료를 찾았습니다. win32k와 관련된 부분은 매우 유용합니다:
일반적으로 각 프로세스에는 사용자 모드 콜백 함수의 포인터 테이블이 있으며, PEB->KernelCallBackTable이 이 테이블을 가리킵니다. 커널이 사용자 모드 함수를 호출하려고 할 때, 함수 인덱스를 KeUserModeCallBack()에 전달합니다. 위의 예에서 인덱스는 사용자 모드의 __ClientLoadLibrary() 함수를 가리킵니다.
KeUserModeCallBack()은 PEB->KernelCallBackTable에서 인덱스에 따라 해당 함수를 찾아 실행하고, 최종적으로 사용자 모드에서 KiUserModeCallbackDispatch()를 호출합니다.
지정된 진입점을 HOOK하려면 PEB->KernelCallBackTable에서 __ClientLoadLibrary()의 인덱스를 찾아 자체 함수로 교체해야 합니다. 이 인덱스는 운영 체제 버전 및 하드웨어 플랫폼에 따라 다릅니다.
PEB->KernelCallBackTable을 보려면 WinDbg를 사용하여 이 테이블의 주소를 찾을 수 있습니다. 32비트와 64비트 플랫폼을 비교해도 큰 차이는 없습니다.
kd> dt !_PEB @$peb
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0 ''
+0x003 BitField : 0x8 ''
+0x003 ImageUsesLargePages : 0y0
[...]
+0x02c KernelCallbackTable : 0x76daf620 Void
kd> dds 0x76daf620
76daf620 76d96443 user32!__fnCOPYDATA
76daf624 76ddf0e4 user32!__fnCOPYGLOBALDATA
76daf628 76da736b user32!__fnDWORD
76daf62c 76d9d603 user32!__fnNCDESTROY
76daf630 76dc50f9 user32!__fnDWORDOPTINLPMSG
76daf634 76ddf1be user32!__fnINOUTDRAG
76daf638 76dc6cd0 user32!__fnGETTEXTLENGTHS
76daf63c 76ddf412 user32!__fnINCNTOUTSTRING
76daf640 76d9ce49 user32!__fnINCNTOUTSTRINGNULL
[...]
76daf724 76da3962 user32!__ClientLoadLibrary
kd> ?? (0x76daf724-0x76daf620)/4 int 0n65
위의 예에서 __ClientLoadLibrary의 인덱스는 65임을 알 수 있으며, 이것이 우리가 HOOK할 곳입니다. HOOK 후에 __ClientLoadLibrary가 win32k 관련 코드에 의해 여러 번 호출된다는 것을 발견했습니다! 먼저 해야 할 일은 관심 있는 호출이 발생하기 전에 HOOK 코드에 알리는 방법입니다. 따라서 HOOK 코드에서는 전역 변수를 사용하여 플래그를 설정하고, 이 플래그가 설정된 경우에만 관련 작업을 수행합니다.
이제 두 가지 장애물이 있습니다:
따라서 HOOK 함수는 다음과 같습니다:
void ClientLoadLibraryHook(void * p)
{
CHAR Buf[PGSZ];
memset(Buf, 0, sizeof(Buf));
if (g_PwnFlag)
{
dprintf("[+] __ClientLoadLibrary hook called\n");
if (++g_HookCount == 2)
{
g_PwnFlag = 0; // 한 번만 실행..
ReplaceScrollBarChunk(NULL);
}
}
fpClientLoadLibrary(&Buf); // 원래 함수 호출
}
현재 win32k!xxxDrawScrollBar() 함수에서 호출이 온 것임을 확인하면 버그를 트리거하려고 시도할 수 있습니다. 이제 트리거만 고려하면 DestroyWindow(g_hSBCtl)을 호출하면 됩니다. 이렇게 하면 창의 tagSBINFO 구조가 해제됩니다. 창 구조 자체는 참조 카운트가 여전히 원래 호출에 의해 사용 중이므로 즉시 해제되지 않지만, tagSBINFO에는 그러한 참조 카운트 메커니즘이 없으므로 즉시 해제됩니다.
이 시점에서 우리는 버그를 트리거했습니다. tagSBINFO를 포함하는 힙 블록을 다시 할당하지 않았지만, 비활성화를 나타내는 2비트를 이미 해제된 힙에 쓸 수 있습니다. 다음 단계는 이 해제된 힙 블록을 우리가 원하는 것으로 교체하여 비트 몇 개를 설정하는 것보다 더 흥미로운 일을 할 수 있도록 하는 것입니다. 이를 위해 데스크톱 힙에 대한 배경 지식이 필요합니다.
win32k.sys는 데스크톱 힙을 사용하여 주어진 데스크톱과 관련된 GUI 개체를 저장합니다. 여기에는 창 개체 및 속성 목록, 창 텍스트, 스크롤바와 같은 관련 구조가 포함됩니다. Tarjei의 문서에서 이 내용을 언급했지만, 특히 데스크톱 힙은 실제로 사용자 모드 백엔드 할당기의 단순화된 버전이며 RtlAllocateHeap() 및 RtlHeapFree()를 사용하여 작동합니다. 데스크톱 힙은 _HEAP 구조에 의해 유지 관리되며, 프론트엔드 할당기가 없으므로 낮은 조각 힙(LFH)이나 사이드뷰 리스트 같은 것은 없습니다.
데스크톱이 생성될 때마다 해당 데스크톱에 대한 데스크톱 힙이 제공됩니다. 즉, 새로운 데스크톱을 할당하여 "깨끗한" 데스크톱 힙을 얻을 수 있으며, 이 힙에서 작업이 더 예측 가능해집니다. 그러나 낮은 무결성 권한의 프로세스에게는 의미가 없습니다. 이러한 프로세스는 새 데스크톱을 만들 수 없기 때문입니다.
이제 주요 문제는 할당 과정을 추적하는 것입니다(메타데이터 등에 대한 자세한 내용은 나중에 다룹니다).
데스크톱 힙 할당 및 해제를 추적하기 위해 WinDbg 스크립트를 사용했습니다:
64비트 힙 모니터링
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", @rcx, @edx, @r8; .echo ; gc";
ba e 1 nt!RtlAllocateHeap "r @$t2 = @r8; r @$t3 = @rcx; gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @rax; gc";
32비트 힙 모니터링
ba e 1 nt!RtlAllocateHeap "r @$t2 = poi(@esp+c); r @$t3 = poi(@esp+4); gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @eax; gc";
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", poi(@esp+4), poi(@esp+8), poi(@esp+c); .echo ; gc"
이러한 디버깅 스크립트 외에도 데스크톱 힙은 사용자 모드 백엔드 할당기의 단순화된 형태이므로 WinDbg에 내장된 !heap 명령을 사용할 수도 있습니다.
이 버그를 악용하려면 최근에 해제된 tagSBINFO 힙 블록을 교체해야 하며, 이러한 전형적인 버그를 사용하여 인접한 데이터를 손상시키는 방법도 알고 있습니다. 이를 위해 손상시키려는 구조 근처에 힙 블록을 미리 할당해야 합니다. 힙 블록이 할당될 위치를 예측하려면 전체 힙 레이아웃을 제어해야 합니다(또는 가능한 한 많이). 이를 위해 가능한 많은 힙 블록을 할당하여 이미 해제된 블록을 채우면 새로 할당된 블록이 연속적으로 배치됩니다. 구멍이 필요할 때는 할당된 블록을 해제하여 예측 가능한 위치에 구멍을 만들 수 있습니다.
이 부분은 할당에 영향을 미치는 몇 가지 요인에 대한 간단한 이해입니다. 위의 WinDbg 스크립트가 도움이 됩니다. Tarjei는 win32k에 대한 PPT에서 데스크톱 힙에 할당되는 주요 개체를 언급했으며, 제가 본 것과 완전히 일치합니다. 이러한 개체는:
데스크톱 힙은 상당히 흥미롭습니다. 대부분의 할당은 창 개체에 직접 연결되며 tagWND 구조를 통해 관리됩니다. 즉, 임의의 크기(소위 작은 블록 작은 구멍)의 힙 블록을 할당하려면 먼저 관련 창을 할당해야 합니다. 창 구조가 힙 할당의 인터페이스라고 생각할 수 있습니다. 또 다른 흥미로운 점은 창 작업을 통해 할당된 많은 힙 블록은 창 자체가 삭제되지 않는 한 즉시 해제될 수 없다는 것입니다. 이는 분명히 힙에 영향을 미칩니다. 마지막으로, 위의 예에서처럼 창을 통해 크기 N의 힙 블록을 할당한다고 가정해 보겠습니다. 크기 N의 힙 블록을 많이 할당해야 합니까? 할당된 창 구조는 크기에 관계없이 연결 리스트에 저장되지 않는다는 것을 확인할 수 있습니다. 따라서 각 창은 크기 N의 힙 할당 하나를 제어할 수 있습니다. 즉, 크기 N의 힙 블록을 많이 할당해야 하는 경우 먼저 많은 수의 창을 만든 다음 창을 사용하여 힙 블록 할당을 지원해야 합니다.
데스크톱 힙에 할당되는 또 다른 중요한 데이터 유형이 세 가지 있으며, 창 개체를 통해 간접적으로 사용하여 힙의 데이터를 제어할 수 있습니다. 이러한 데이터 유형을 사용하여 악용을 구현하고 힙 풍수(heap feng shui)를 구성합니다. 세 가지 데이터 유형은:
그림 2는 이러한 데이터 유형 간의 관계를 보여줍니다:

힙을 초기화하기 위해 많은 수의 tagWND 구조(창 개체 생성)를 만들었습니다. 이렇게 하면 많은 큰 구멍을 메울 수 있고, 다른 필요한 힙 블록을 할당할 수 있는 인터페이스도 제공됩니다. Win8 및 Win8.1에서는 새 창을 할당하면 tagPROPLIST 구조가 자동으로 할당됩니다(앞서 언급한 WinDbg 스크립트로 관찰할 수 있음). Win7 및 이전 버전에서는 새 tagPROPLIST를 직접 할당하여 작은 구멍을 메웁니다.
여기서 우리가 분사한 모든 창 개체에는 창 텍스트 문자열이 없습니다. 그러나 필요한 경우 이를 사용하여 임의 크기의 힙 블록을 할당하거나 해제할 수 있습니다. 일단 생성되면 창이 삭제되지 않는 한 기존 속성 목록(property list)을 제거할 수 없지만, 새 속성을 수용하기 위해 이 목록의 재할당을 제어할 수 있습니다. 이 메커니즘을 사용하여 이전 위치에 구멍을 만들 수 있습니다. 필요한 것은 이전 목록에 없던 새 속성(atomey로 구분)을 설정하는 것뿐입니다.
흥미롭게도 데스크톱 힙은 사용자 공간에 매핑되지만 읽기 전용입니다. 즉, 구축한 풍수 레이아웃을 검증하고 제대로 작동하는지 확인할 수 있습니다. 먼저 데스크톱 힙이 사용자 모드의 어느 위치에 매핑되는지 확인해야 합니다. Tarjei의 win32k 논문에서 이 내용이 언급되었습니다. TEB에는 공개되지 않은 구조 Win32ClientInfo가 있으며, 대략적인 정의는 다음과 같습니다:
typedef struct _CLIENTINFO {
ULONG_PTR CI_flags;
ULONG_PTR cSpins;
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
DWORD dwTIFlags;
PDESKTOPINFO pDeskInfo;
ULONG_PTR ulClientDelta; // 불완전. reactos 참조
} CLIENTINFO, *PCLIENTINFO;
여기서 PDESKTOPINFO 구조 정의는 다음과 같습니다:
typedef struct _DESKTOPINFO {
PVOID pvDesktopBase;
PVOID pvDesktopLimit; // 불완전. reactos 참조
} DESKTOPINFO, *PDESKTOPINFO;
첫 번째 필드 pvDesktopBase는 데스크톱 힙의 커널 모드 주소를 가리킵니다. Win32ClientInfo의 ulClientDelta 필드는 커널 모드 주소와 사용자 모드 주소의 차이이며, 이 정보를 통해 원하는 것을 얻을 수 있습니다.
그러나 힙 구조를 직접 분석하고 싶지 않고, HWND 값과 같은 user32 핸들을 사용자 모드 매핑 주소로 변환할 수 있기를 바랍니다. 이렇게 하면 다른 힙 할당과 연관되어 있는지 확인할 수 있습니다. 이 핸들을 찾으려면 gShared라는 구조를 찾아야 합니다. 일반적으로 uer32.dll에 있으며, Win7 이상에서는 내보내지므로 쉽게 찾을 수 있습니다.
대부분의 시스템에서 이 구조는 다음과 같이 정의됩니다:
kd> dt !tagSHAREDINFO
win32k!tagSHAREDINFO
+0x000 psi : Ptr32 tagSERVERINFO
+0x004 aheList : Ptr32 _HANDLEENTRY
+0x008 HeEntrySize : Uint4B
+0x00c pDispInfo : Ptr32 tagDISPLAYINFO
+0x010 ulSharedDelta : Uint4B
+0x014 awmControl : [31] _WNDMSG
+0x10c DefWindowMsgs : _WNDMSG
+0x114 DefWindowSpecMsgs : _WNDMSG
위 구조에서 aheList는 _HANDLEENTRY 배열을 가리키며, 각 _HANDLEENTRY는 커널 모드 주소를 가리키는 핸들을 포함합니다. "커널 모드 주소와 사용자 모드 주소의 차이"를 통해 사용 가능한 사용자 모드 주소를 얻을 수 있습니다. 불행히도 Win7 이전 버전에서는 gSharedInfo가 내보내지지 않으므로 사용할 수 없습니다. Tarjei의 문서에 따르면, 공개되지 않은 함수 CsrClientConnectToServer를 사용하여 gSharedInfo의 복사본을 얻을 수 있지만, 실행 가능한 예제를 찾지 못했습니다. 성가신 점은 이 함수가 구현 시 필요한 구조의 길이가 시스템마다 다르기 때문에 ReactOS에서 본 내용을 완전히 신뢰할 수 없다는 것입니다.
매핑된 위치를 계산하면 창 개체가 데스크톱 힙의 어디에 있는지 알려주는 함수를 구성할 수 있습니다. 그런 다음 해당 속성 목록이나 텍스트 문자열의 힙 블록이 어디에 할당되었는지 알고 싶으면 사용자 모드 구조를 분석하기만 하면 됩니다.
이제 마침내 취약점 악용에 가까워졌습니다. 힙 블록을 제어하는 방법, 힙 블록의 위치가 올바른지 확인하는 방법, 그리고 버그를 트리거할 수 있으므로 이제 선택한 tagPROPLIST 속성 목록으로 해제된 tagSBINFO 힙 블록을 교체할 수 있습니다. tagPROPLIST는 단지 큰 목록의 헤더이므로 목록의 크기를 스크롤바 힙 블록의 크기와 일치시킬 수 있습니다. tagPROPLIST의 뒤쪽 부분은 기본적으로 tagPROP 구조의 배열, 즉 속성 목록입니다. 따라서 배열과 목록이라는 용어를 구분하지 않겠습니다. tagPROPLIST 구조는 64비트 시스템에서 다음과 같습니다:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
앞서 언급했듯이, 창 개체에는 관련 속성 목록이 있습니다. 이 목록은 SetProp() 함수를 통해 생성됩니다. atomey를 일치시켜 존재하는 속성을 찾고, 속성이 없으면 속성 목록에 새 속성 항목을 만듭니다. 속성 목록이 전혀 없으면 속성 목록을 만들어 tagWND 구조에 연결합니다.
이미 버그가 있는 tagWND를 분사하고 관련 tagPROPLIST 항목을 생성했다면 최종 레이아웃은 그림 3과 같습니다:

이 설정이 완료되면 악용할 스크롤바 컨트롤을 할당합니다. 그러면 그림 4와 같은 결과가 발생합니다:

그런 다음 스크롤바를 조작하여 사용자 모드 콜백의 HOOK를 트리거하고, HOOK 함수에서 창을 파괴하여 tagSBINFO 구조를 해제합니다. 그러면 그림 5의 상황이 발생합니다:

64비트에서 tagSBINFO 구조는 0x28바이트이고, tagPROPLIST 배열 항목은 0x18바이트이며, 그 중 0x10바이트는 기본 tagPROP입니다. 따라서 두 개의 배열 항목이 있는 속성 목록은 0x28바이트(0x8 + 0x10 + 0x10)이며, 이는 완벽하게 맞습니다. 이미 메모리를 분사하여 구멍을 채울 준비가 되어 있다고 가정합니다. 속성 목록이 있는 창이 필요하며, 이 창이 tagSBINFO 구조를 해제한 직후(앞 그림 참조) 새 속성 목록 항목을 추가합니다. 이 과정은 먼저 이전 0x18바이트의 tagPROPLIST 힙 블록을 해제합니다. 힙이 이미 분사되어 있으므로 주변에 빈 힙 블록이 없으며, 힙 병합도 발생하지 않아 새로 할당된 0x28바이트를 수용할 충분한 공간이 없습니다. 따라서 방금 해제된 tagSBINFO 위치가 사용됩니다(크기가 정확히 0x28바이트임). 이 상황은 그림 6과 같습니다:

HOOK된 콜백 함수에서 반환되면 UAF가 트리거되고, 몇 비트가 tagPROPLIST의 cEntries 필드에 쓰입니다. 원래 cEntries 값은 0x2였으며, 이는 두 개의 속성 목록 항목을 만들었음을 나타냅니다. 오버플로 후 0xe가 되며, 3번째와 4번째 비트(1부터 시작)가 1로 설정됩니다.
이 시점에서 새 힙 오버플로가 완료되었고, 속성 목록의 항목 수가 0xc보다 큰 값으로 증가했습니다. 다음으로 인접한 힙 블록을 오버플로할 것입니다. 이를 2단계 손상(Phase 2 Corruption)이라고 합니다.
Udi의 블로그에서는 여기까지 설명했습니다. 이전에 이것은 "전형적인 힙 오버플로"라고 불렸지만, 제 경험에 따르면 이 지점에서 임의 주소 읽기/쓰기 또는 코드 실행을 구현하는 것은 매우 어렵습니다. 64비트에서 tagPROPLIST 구조를 다시 살펴보겠습니다:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
1단계 손상을 통해 손상된 tagPROPLIST 배열을 얻었고, 배열 항목 tagPROP를 늘릴 수 있습니다. tagPROPLIST에는 두 개의 필드가 있습니다:
새 항목이 목록에 삽입될 때, 적절한 iFirstFree 인덱스 값을 찾을 때까지 각 항목을 스캔하는 함수가 호출됩니다. 찾을 수 없으면 iFirstFree 값이 cEntries보다 큰지 확인합니다. atomey에 해당하는 항목이 목록에 없으면 iFirstFree != cEntries인지 확인합니다. 같지 않으면 iFirstFree 인덱스에 항목을 삽입하고, 같으면 삽입된 속성 목록을 수용할 수 있는 새 항목을 할당하고 기존 항목을 복사한 후 새 항목을 삽입합니다.
atomKey 필드는 LPCTSTR lpString에 해당합니다. MSDN의 SetProp() 문서에서 설명한 대로 호출자는 문자열 포인터 또는 16비트 atome 값을 전달할 수 있습니다. 문자열 포인터를 전달하면 속성 목록에 저장되기 전에 자동으로 atome 값으로 변환됩니다. SetProp() 함수에 임의의 atome 값을 전달할 수 있으므로 이 두 바이트를 제어할 수 있는 능력이 생기지만 몇 가지 제약 조건이 있습니다. 즉, 손상 데이터가 있는 atomeKey가 중복되면 안 됩니다. 그렇지 않으면 새 속성 항목을 설정할 때 동일한 atome 값을 가진 기존 항목을 덮어씁니다. 또한 fs 필드는 제어할 수 없으며, 값이 0이면 atomeKey 값 < 0xBFFF를 의미하므로 정수 atome 값에 해당합니다. fs 필드 값이 2이면 atomeKey 값 >= 0xC000을 의미합니다.
또 다른 주의할 점은 tagPROP가 0xc바이트뿐이라는 것입니다. 64비트 시스템에서 이 구조는 0x10바이트로 정렬되므로 tagPROP 항목을 삽입할 때 추가로 4바이트가 손상되지 않습니다. 마지막 중요한 점은 tagPROPLIST 힙 블록의 시작 8바이트 데이터가 항목의 크기를 정의한다는 것입니다. 즉, 새로 삽입된 각 tagPROP 항목은 항상 8바이트 정렬 위치에 기록됩니다.
64비트 시스템에서 각 삽입된 tagPROP에 대해 상황은 다음과 같습니다:
* Offset 0x0: 8바이트 임의 제어 가능 데이터(hData)
* Offset 0x8: 2바이트 대부분 제어 가능 데이터 (atomKey)
* Offset 0xa: 2바이트 제어 불가능 데이터 (fs)
* Offset 0xc: 4바이트 수정 불가능 데이터 (padding)
이 상황은 2비트보다는 훨씬 낫지만 여전히 완벽하지 않습니다. 시작 8바이트가 완전히 제어 가능한 hData 필드에서 오는 것을 사용하여 무언가를 덮어쓸 수 없다면 매우 제한적입니다. 인접한 구조의 더 깊은 필드를 써야 하는 경우 일부 값의 제어 불가능한 손상을 피할 수 없습니다. 데스크톱 힙에서 다양한 객체를 찾는 데 시간을 보냈으며, 앞서 언급한 손상 제한을 고려할 때 임의 주소 읽기/쓰기를 구현하기 위해 이 제한을 우회할 수 있는 유일한 방법은 tagWND의 strName 필드를 손상시키는 것이라고 생각했습니다. 이 필드는 _LARGE_UNICODE_STRING 구조입니다:
kd> dt !_LARGE_UNICODE_STRING
win32k!_LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr64 Uint2B
이 구조의 Buffer 필드를 손상시킬 수 있다면, 창 텍스트를 조작하여 주어진 주소에서 MaximumLength 바이트를 읽거나 쓸 수 있습니다. 이것이 제가 하려는 것입니다. 이전 장에서 이 구조가 데스크톱 힙에 임의의 크기와 값을 가진 힙 블록을 생성하는 방법에 대해 언급했을 수 있습니다. 따라서 동일한 상황이 여기에 적용될 수 있습니다.
이제 tagPROPLIST 항목을 사용하여 데이터를 손상시키는 방법, 제어할 수 있는 부분, 그리고 가장 중요한 32비트와 64비트에서 차이가 있는 제한 사항이 무엇인지 알게 되었습니다. 64비트에서 수행한 작업은 32비트에서 작동하지 않습니다. 곧 2단계 손상(tagPROP 구조를 통한 데이터 쓰기)에서 완전히 제어 가능한 데이터를 쓸 수 있는 다른 손상 "작업 원시"로 전환할 것입니다. 이것이 제가 3단계 손상(Phase 3 Corruption)이라고 부르는 것입니다.
목표 계획은 인접한 tagWND의 strName 필드를 손상시키는 것입니다. 이것이 LARGE_UNICODE_STRING 구조임을 이미 알고 있지만, tagWND 구조의 더 많은 세부 사항을 살펴보겠습니다. 다음과 같습니다:kd> dt !tagWND win32k!tagWND +0x000 head : THRDESKHEAD +0x028 state : Uint4B +0x028 bHasMeun : Pos 0, 1 Bit +0x028 bHasVerticalScrollbar : Pos 1, 1 Bit +0x028 bHasHorizontalScrollbar : Pos 2, 1 Bit [SNIPPED FLAGS] +0x028 bDestroyed : Pos 31, 1 Bit +0x02c state2 : Uint4B [SNIPPED FLAGS] +0x02c bWMCreateMsgProcessed : Pos 31, 1 Bit +0x030 ExStyle : Uint4B +0x030 bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit +0x030 bUnused1 : Pos 1, 1 Bit +0x030 bWS_EX_NOPARENTNOTIFY : Pos 2, 1 Bit [SNIPPED FLAGS] +0x030 bUIStateFocusRectHidden : Pos 31, 1 Bit +0x034 style : Uint4B +0x034 bReserved1 : Pos 0, 16 Bits [SNIPPED FLAGS] +0x034 bWS_POPUP : Pos 31, 1 Bit +0x038 hModule : Ptr64 Void +0x040 hMod16 : Uint2B +0x042 fnid : Uint2B +0x048 spwndNext : Ptr64 tagWND +0x050 spwndPrev : Ptr64 tagWND +0x058 spwndParent : Ptr64 tagWND +0x060 spwndChild : Ptr64 tagWND +0x068 spwndOwner : Ptr64 tagWND +0x070 rcWindow : tagRECT +0x080 rcClient : tagRECT +0x090 lpfnWndProc : Ptr64 int64 +0x098 pcls : Ptr64 tagCLS +0x0a0 hrgnUpdate : Ptr64 HRGN +0x0a8 ppropList : Ptr64 tagPROPLIST +0x0b0 pSBInfo : Ptr64 tagSBINFO +0x0b8 spmenuSys : Ptr64 tagMENU +0x0c0 spmenu : Ptr64 tagMENU +0x0c8 hrgnClip : Ptr64 HRGN__ +0x0d0 hrgnNewFrame : Ptr64 HRGN__ +0x0d8 strName : LARGE_UNICODE_STRING +0x0e8 cbwndExtra : Int4B +0x0f0 spwndLastActive : Ptr64 tagWND +0x0f8 hImc : Ptr64 HIMC_ +0x100 dwUserData : Uint8B +0x108 pActCtx : Ptr64 _ACTIVATION_CONTEXT +0x110 pTransform : Ptr64 _D3DMATRIX +0x118 spwndClipboardListenerNext : Ptr64 tagWND +0x120 ExStyle2 : Uint4B +0x120 bClipboardListener : Pos 0, 1 Bit [SNIPPED FLAGS] +0x120 bChildNoActivate : Pos 11, 1 Bit
위는 64비트 구조이며, 우리가 덮어쓰려는 _LARGE_UNICODE_STRING 구조의 오프셋이 0xd8임을 볼 수 있습니다. 또한 이 구조의 시작 부분에 중요한 필드가 있다는 것을 알 수 있습니다. 처음에는 이것을 마음껏 조작하고 싶었지만, _THRDESKHEAD 에는 많은 포인터가 있어 주의해야 하며, 불행히도 우리가 쓸 위치를 제어할 수 없습니다. 제한 이유는 앞서 논의했습니다.
_THRDESKHEAD 구조 정의:
kd> dt !_THRDESKHEAD
win32k!_THRDESKHEAD
+0x000 h : Ptr64 Void
+0x008 cLockObj : Uint4B
+0x010 pti : Ptr64 tagTHREADINFO
+0x018 rpdesk : Ptr64 tagDESKTOP
+0x020 pSelf : Ptr64 UChar
_THRDESKHEAD의 문제는 우리를 혼란스럽게 할 뿐만 아니라 정렬 제약 조건을 다시 살펴보게 합니다. 새로운 tagPROP 목록 항목이 어떤 오프셋에 있든, 우리의 쓰기 작업은 _LARGE_UNICODE_STRING 의 시작 부분을 직접 덮어씁니다:
win32k!_LARGE_UNICODE_STRING
+0x000 Length <-- hData (완전히 제어 가능) 가 여기를 덮어씀
+0x004 MaximumLength <-- 그리고 여기
+0x004 bAnsi <-- 그리고 여기
+0x008 Buffer <-- atomKey 및 fs (부분적으로 제어 가능) 가 여기를 덮어씀
분명히 우리는 임의 주소의 메모리에 접근할 수 있도록 Buffer 포인터를 덮어쓰려고 합니다. 그러나 이 구조의 다른 필드를 안전하게 공격할 수 있더라도 필요한 포인터를 제어할 수는 없습니다.
임의의 데이터를 손상시킬 수 없습니다. 이 문제의 해결은 더 이상 tagPROPLIST 손상 문제가 아니라 완전히 다른 손상 메커니즘으로 바뀝니다.
Windows XP 이후 버전에서 사용자 모드 백엔드 할당자(예: 커널 데스크탑 힙)의 힙 블록 헤더(즉, _HEAP_ENTRY)는 힙에 저장되며 블록의 실제 내용 앞에 위치합니다. 데스크탑 힙 자체는 _HEAP 구조를 통해 관리되며, 이는 힙 블록을 이용할 때 일부 자유를 제공합니다.
_HEAP_ENTRY 구조 정의:
kd> dt !_HEAP_ENTRY
ntdll!_HEAP_ENTRY
+0x000 PreviousBlockPrivateData : Ptr64 Void
+0x008 Size : Uint2B
+0x00a Flags : UChar
+0x00b SmallTagIndex : UChar
+0x00c PreviousSize : Uint2B
+0x00e SegmentOffset : UChar
+0x00f UnusedBytes : UChar
힙 블록 헤더는 총 0x10 바이트이며, 처음 8바이트는 PreviousBlockPrivateData입니다. 요청된 크기가 일반적인 0x10(8바이트 미만은 8바이트 정렬)을 초과할 때 이전 실제 블록 데이터를 수용하는 데 사용됩니다. 이는 Leviathan 블로그 글에서 간략히 설명되었으며, 초기 사용자 모드 힙에 대한 글에도 설명되어 있습니다. Size 및 PreviousSize는 현재 블록 크기와 이전 블록 크기를 나타내며, 단위는 0x10 바이트입니다. Flags는 블록이 비어 있는지 등을 나타냅니다. _HEAP에서 _HEAP_ENTRY의 보안 모드가 활성화된 경우 SmallTagIndex는 블록 데이터의 XOR 체크섬을 포함합니다.
정렬 제한이 우리에게 불리하지만, 실제로 존재합니다. tagPROPLIST를 호출하면 항상 최소 0x18 바이트이며, tagPROP 하나당 0x10 바이트가 추가됩니다. 두 개의 목록 항목이 있는 tagPROPLIST의 경우 0x28 바이트는 0x20 바이트 힙 블록에 배치되며, PreviousBlockPrivateData로 표시되는 추가 바이트는 인접한 힙 블록을 사용합니다. 즉, 세 번째 목록 항목을 추가할 때 인접한 힙 블록이 손상되며, 제어 가능한 8바이트 hData가 _HEAP_ENTRY의 상단을 덮어씁니다.
우리가 하려는 것은 이를 이용하여 Buffer 상단 위치에 임의의 데이터를 쓸 수 있는 방법을 찾는 것입니다. 먼저, 우리가 손상시킨 tagPROPLIST 힙 블록에 가까운 힙 레이아웃을 변경합니다. 제어 과정에서 창과 관련된 텍스트 문자열을 포함하는 작은 힙 블록이 있으며, 이를 "덮어쓰기 힙 블록"이라고 부릅니다. 이 "덮어쓰기 힙 블록" 옆에 tagWND를 배치하여 이 tagWND를 손상시킵니다. 아래 그림 7은 이 과정을 보여줍니다. 이전에 스프레이한 힙 블록은 생략하여 공간을 절약했으므로, 이제 이들은 암시된 것으로 간주해야 합니다.

다음으로, 세 번째 tagPROP를 tagPROPLIST 목록에 삽입하면, 이는 _HEAP_ENTRY의 뒤쪽 8바이트와 "덮어쓰기 힙 블록"의 앞쪽 8바이트를 덮어씁니다. 이렇게 하면 "덮어쓰기 힙 블록"의 _HEAP_ENTRY를 수정하여 실제 크기보다 더 크게 만들고, 인접한 tagWND 구조를 충분히 포함할 수 있습니다.
이제 방금 손상된 "덮어쓰기 힙 블록"을 해제하여 힙 관리자가 이를 해당 블록 크기(각 자유 목록은 고정 크기에 해당)보다 큰 자유 목록에 배치하도록 합니다. 그런 다음 창 텍스트(완전히 제어 가능)를 수정하여 이 블록을 재사용합니다. 그러나 해결해야 할 작은 문제가 있습니다. "덮어쓰기 힙 블록"이 해제될 때, 힙 관리자는 손상된 Size 필드에 따라 이전 인접 블록을 찾으려고 합니다. 힙 관리자는 이 인접 블록이 비어 있는지 확인하여 병합하려고 합니다. 어쨌든 우리는 이를 제어하고 사용 중 플래그를 설정하려고 합니다. 힙 레이아웃을 약간 수정하면 이를 달성할 수 있습니다. 이 시점에서, 우리는 "사용 중" 플래그가 설정된 가짜 힙 블록을 배치하고, PreviousSize 값은 손상된 Size 필드 값으로 설정합니다. 이는 다른 창의 창 텍스트를 할당하여 간단히 수행할 수 있습니다. 새로운 힙 레이아웃은 다음과 같습니다(그림 8):

이제 관련 창의 텍스트 문자열을 업데이트하여 길이를 원래 0x10 바이트보다 크게 만들어 손상된 "덮어쓰기 힙 블록"을 해제할 수 있습니다. 그러면 손상된 "덮어쓰기 힙 블록"이 먼저 해제되고 자유 목록에 배치되지만, 그 크기는 손상된 대로 실제보다 더 큽니다. 이 크기는 실제 필요에 따라 조정할 수 있습니다. 그러면 우리의 문자열 데이터가 이 "덮어쓰기 힙 블록"에 기록되고, 이 "덮어쓰기 힙 블록"을 사용하여 인접한 tagWND를 임의의 데이터로 손상시킵니다. 다음과 같습니다(그림 9):

이것이 3단계 손상입니다. 이제 원하는 데이터로 strName.Buffer 포인터를 덮어쓸 수 있습니다. 그러나 tagWND의 다른 데이터를 손상시키는 것은 여전히 약간 번거롭지만, 데스크탑 힙이 사용자 공간에 매핑되어 있기 때문에 문제가 되지 않습니다! 따라서 모든 것을 손상시키기 전에 tagWND의 모든 내용을 읽고, strName 구조의 내용을 원하는 대로 수정한 다음 창 텍스트를 수정하여 모든 데이터를 보냅니다.
strName을 통해 임의 읽기/쓰기 "프리미티브"를 얻을 수 있을 뿐만 아니라, strName을 반복적으로 수정할 수도 있습니다. 이는 창 텍스트 수정 메커니즘에서 허용됩니다. 쓰는 문자열 길이가 MaximumLength 값을 초과하지 않는 한 동일한 힙 블록을 계속 사용할 수 있습니다. 따라서 strName의 주소를 수정하여 어떤 값이든 읽고 싶을 때마다 새 문자열을 추가하고 데이터를 "덮어쓰기 힙 블록"에 업데이트합니다. 이 재사용은 그림 10에 나와 있습니다. 각 손상을 자세히 보여주기 위해 다시 조감도를 확대했습니다.

이는 결국 (원래 tagPROPLIST 목록 항목 외에) 두 개의 추가 항목만 손상시키면 된다는 것을 의미합니다:
이제 메모리의 특정 위치에서 몇 바이트를 읽으려면, 손상된 strName 항목이 있는 InternalGetWindowText() 함수를 통해 창 텍스트를 쿼리합니다. Length 필드가 선언한 바이트 수를 읽을 수 있습니다. 마찬가지로 메모리의 임의 위치에 쓰려면, NtUserDefSetText() 함수를 사용하여 손상된 창 텍스트를 업데이트하되, MaximumLength 필드가 선언한 값(우리가 설정할 수 있음) 이하로 써야 합니다. 그러면 기존 버퍼가 재사용되며 우리가 원하는 메모리 주소를 가리키게 됩니다.
Windows Vista부터 사용자 모드 백엔드 할당자는 힙 인코딩(heap encoding)을 사용하지만, 데스크탑 힙은 Windows 8까지 이를 활성화하지 않았습니다. 따라서 Windows 8 이후 시스템에서 "덮어쓰기 힙 블록"을 덮어쓸 때 장애가 발생합니다. 그러나 사실 힙을 수용하는 _HEAP 구조에는 이 쿠키가 포함되어 있으며, 이를 사용하여 전체 힙 헤더를 인코딩합니다. 따라서 사용자 공간에 매핑된 데스크탑 힙에서 이 쿠키를 읽은 다음 할당자 코드를 역공학하여 얻은 방법으로 "덮어쓰기 힙 블록"의 헤더를 인코딩할 수 있으며, 할당자는 이를 수용할 수 있습니다.
먼저 주목할 점은, 32비트 시스템의 tagPROP 구조는 8바이트이며, 64비트 시스템의 0xc 바이트가 아닙니다. 그리고 우리가 제어하는 hData 필드는 64비트 시스템의 8바이트가 아닌 4바이트에 불과합니다. 또한 64비트 시스템에는 8바이트의 패딩이 있었지만, 32비트 시스템에는 없어서 전체 구조가 정확히 8바이트입니다. 이는 데이터를 부분적으로만 제어할 수 있다면 인접한 힙 블록 헤더를 완전히 손상시킬 수 없음을 의미합니다. 일부 Windows 버전에서는 가장 중요한 필드를 제어할 수 있으므로 가능하지만, Windows 8 및 8.1에서는 힙 헤더가 인코딩되어 fs 필드를 통해 힙 헤더의 일부를 안전하지 않게 덮어쓰게 됩니다. 32비트 시스템의 _HEAP_ENTRY 헤더는 비슷해 보이지만 PreviousBlockPrivateData 필드가 없습니다.
여전히 tagWND의 모든 부분을 손상시킬 수는 없습니다. 잘린 포인터를 피할 수 없기 때문입니다. 그리고 이 조건을 만족하는 객체를 아직 찾지 못했습니다. _LARGE_UNICODE_STRING이 64비트 시스템에서 잘 작동하므로, 32비트 시스템에서도 이를 사용하려고 합니다.
제 생각은, tagPROPLIST 구조의 iFirstFree 필드(속성 목록에서 처음으로 해제된 속성 항목의 인덱스)를 인덱스 값을 증가시켜 손상시킬 수 있다면, 더 멀리 있는 힙 위치를 가리키도록 할 수 있다는 것입니다. 예를 들어, tagWND.strName의 상단을 가리키도록 할 수 있습니다. 그림 11은 이 아이디어를 보여줍니다:

과정을 명확히 하기 위해, 이제 UAF에 사용되는 "속성 목록 A"와 다른 "속성 목록 B"라는 두 개의 tagPROPLIST 구조를 사용합니다. "속성 목록 A"에 삽입된 tagPROP의 어떤 부분이 "속성 목록 B"의 iFirstFree 필드를 덮어쓸지 정확히 알아야 합니다. 또한 한 번에 8바이트만 쓸 수 있다는 점을 명심해야 합니다. 따라서 "속성 목록 A"에 최소한 하나의 추가 tagPROP를 삽입하여 첫 번째 손상이 인접 힙 헤더를, 두 번째 손상이 "속성 목록 B"의 tagPROPLIST 필드를 타격하도록 해야 합니다. 이는 운영 체제와 힙 블록 크기에 따라 달라질 수 있으며, 내 exploit에서는 다양한 힙 레이아웃에 적응해야 합니다. 그림 12는 우리가 어떻게 손상시키는지 보여줍니다. 그림에서 첫 번째 tagPROPLIST는 개별 필드로 분할되지 않았으므로 tagPROP[0]은 암시적입니다. 그러나 두 번째 tagPROPLIST에서는 내부 멤버를 분할하여 손상 과정을 보여줍니다. 이것이 tagPROP[0]이 표시되는 이유입니다:

먼저, 각 tagPROP에 대해 8바이트를 쓰면 iFirstFree에 대한 덮어쓰기를 부분적으로만 제어할 수 있으며(atomKey 및 fs 필드에서 오기 때문에), 이것이 우리가 가장 관심을 두는 부분입니다. atomKey 값을 통해 최소한 두 개의 중요한 바이트를 완전히 제어할 수 있으며, 이 값이 충분히 작으면 fs 필드는 0이 됩니다. 따라서 hData 값을 사용하여 cEntries를 합리적인 값으로 덮어쓰고, atomKey를 사용하여 iFirstFree가 tagWND를 가리키도록 합니다. 우리가 덮어쓰려는 strName.Buffer 포인터가 tagWND에 있습니다. Length 및 MaximumLength 값을 직접 덮어쓸 수 없다면, 대상 창에 미리 문자열을 할당하여 해당 길이가 이미 설정된 값이 되도록 할 수 있습니다.
32비트 tagWND 구조를 살펴보고 무엇을 얻을 수 있는지 봅시다. 이번에는 -b 매개변수를 사용하여 strName에서 Buffer의 오프셋을 쉽게 계산할 수 있습니다.
kd> dt -b !tagWND
win32k!tagWND
+0x000 head : _THRDESKHEAD
+0x000 h : Ptr32
+0x004 cLockObj : Uint4B
+0x008 pti : Ptr32
+0x00c rpdesk : Ptr32
+0x010 pSelf : Ptr32
+0x014 state : Uint4B
+0x014 bHasMeun : Pos 0, 1 Bit
[SNIPPED FLAGS]
+0x014 bDestroyed : Pos 31, 1 Bit
+0x018 state2 : Uint4B
[SNIPPED FLAGS]
+0x018 bWMCreateMsgProcessed : Pos 31, 1 Bit
+0x01c ExStyle : Uint4B
+0x01c bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit
[SNIPPED FLAGS]
+0x01c bUIStateFocusRectHidden : Pos 31, 1 Bit
+0x020 style : Uint4B
+0x020 bReserved1 : Pos 0, 16 Bits
[SNIPPED FLAGS]
+0x020 bWS_POPUP : Pos 31, 1 Bit
+0x024 hModule : Ptr32
+0x028 hMod16 : Uint2B
+0x02a fnid : Uint2B
+0x02c spwndNext : Ptr32
+0x030 spwndPrev : Ptr32
+0x034 spwndParent : Ptr32
+0x038 spwndChild : Ptr32
+0x03c spwndOwner : Ptr32
+0x040 rcWindow : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x050 rcClient : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x060 lpfnWndProc : Ptr32
+0x064 pcls : Ptr32
+0x068 hrgnUpdate : Ptr32
+0x06c ppropList : Ptr32
+0x070 pSBInfo : Ptr32
+0x074 spmenuSys : Ptr32
+0x078 spmenu : Ptr32
+0x07c hrgnClip : Ptr32
+0x080 hrgnNewFrame : Ptr32
+0x084 strName : _LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr32
+0x090 cbwndExtra : Int4B
+0x094 spwndLastActive : Ptr32
+0x098 hImc : Ptr32
+0x09c dwUserData : Uint4B
+0x0a0 pActCtx : Ptr32
+0x0a4 pTransform : Ptr32
+0x0a8 spwndClipboardListenerNext : Ptr32
+0x0ac ExStyle2 : Uint4B
+0x0ac bClipboardListener : Pos 0, 1 Bit
[SNIPPED FLAGS]
+0x0ac bChildNoActivate : Pos 11, 1 Bit
strName의 오프셋은 0x84, Buffer의 오프셋은 0x8c입니다. tagPROP 목록 항목의 인덱스가 있고 8바이트를 쓸 수 있다는 것을 알고 있습니다. 따라서 iFirstFree가 MaximumLength를 가리키는 창의 0x88 오프셋을 인덱싱하는지 쉽게 알 수 있습니다. Buffer의 두 바이트만 제어할 수 있기 때문에 쓰기 작업이 불가능하며, 우리의 목표는 이를 임의 읽기/쓰기 "프리미티브"로 사용하는 것이므로 이 결과는 받아들일 수 없습니다. 다음 인덱스를 0x90을 가리키도록 쓰면 cbwndExtra를 덮어쓰게 되며, 이는 우리가 원하는 바가 아닙니다.
이전에 힙 풍수를 수행할 때 제어할 수 있었던 것을回顾하고 tagWND에서 제어할 수 있는 흥미로운 오프셋이 있는지 살펴보겠습니다. tagWND의 오프셋 0x70에는 pSBInfo 필드가 있습니다. 이 오프셋은 8로 나눌 수 있으므로, 가짜 tagPROP의 hData 부분 데이터로 이 포인터를 덮어쓸 수 있습니다.
pSBInfo를 동일한 tagWND 구조 내의 strName을 직접 가리키도록 덮어쓸 수 있을까요? 아마도 스크롤바 API를 사용하여 strName을 손상시켜 목적을 달성할 수 있습니다.
pSBInfo는 tagSBINFO 구조를 가리키며, 이는 초기 UAF 과정에서 언급되었습니다.
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
WSBflags는 우리에게 더 많은 제어를 제공하지 않지만, 스크롤바가 활성화되면 1로 설정되고 비활성화되면 0으로 설정된다는 것을 알고 있습니다. 이 플래그 필드는 임의의 값으로 설정할 수 없습니다. 관련 기능 함수를 역공학한 결과, 스크롤바 상태를 변경하지 않으면 이 플래그 필드는 변하지 않습니다. tagSBDATA 구조의 값이 더 흥미로워 보입니다. SetScrollInfo() 문서를 읽으면 이러한 값의 의미를 잘 이해할 수 있습니다. SCROLLINFO 구조를 통해 SetScrollInfo()에 매개변수를 설정할 수 있는 것 같습니다. 손상시키려는 창 근처에 스크롤바 컨트롤이 있으면 pSBInfo 포인터를 직접 조작할 수 있습니다(연결된 창 컨트롤에 특수 창 메시지를 보냅니다). 분명히 posMin 및 posMax 값을 조건 없이 제어할 수 있습니다. page 및 pos 필드는 약간 번거롭습니다. 특정 범위로 제한되므로 현재는 이를 피하려고 합니다. SCROLLINFO 구조에 SIF_RANGE 플래그를 설정하여 최소값과 최대값을 설정하려는 위치를 선언합니다.
우리는 임의의 데이터로 Buffer를 덮어쓰려고 하므로, posMin이 이를 덮어쓰도록 하려면 pSBInfo를 strName.MaximumLength를 가리키도록 덮어쓸 수 있습니다. 스크롤바를 활성화하거나 비활성화하지 않는 한 WSBflags 필드는 변경되지 않으므로 strName.MaximumLength의 무결성이 보장됩니다. 즉, posMin(SCROLLINFO의 nMin을 통해)을 어떻게 설정하든 Buffer가 덮어쓰여지고, posMax는 cbwndExtra에 기록됩니다. 이것은 큰 문제가 아닙니다. 64비트 시스템에서는 이 값을 미리 읽고 나중에 복원할 수 있습니다. 오버플로의 일반적인 아이디어는 그림 13에 나와 있습니다:

이제 그림 14를 사용하여 32비트 시스템에서의 공격 과정을 설명합니다. UAF와 관련된 주소를 손상시키기 전에 먼저 관련 힙 블록과 힙 레이아웃을 살펴보겠습니다. 이제 더 많은 세부 사항을 알았으므로 다음에 할 일이 명확해집니다.

다음으로, "속성 목록 A"에 두 개의 속성 항목을 삽입합니다. 이는 이전 UAF 손상 덕분에 "속성 목록 A" 근처의 데이터를 손상시키고, 동시에 "속성 목록 A"의 iFirstFree가 pSBInfo를 가리키도록 합니다. 이로 인해 근처의 pSBInfo 값도 손상되지만, 손상 전에 미리 읽어서 손상 후 복원할 수 있습니다.

"속성 목록 B"에 새로운 tagPROP를 삽입합니다. 이 tagPROP의 atom 식별자는 목록에 있는 기존 것과 다르므로, 다음 사용 가능한 인덱스 항목에 삽입됩니다. 이로 인해 pSBInfo가 손상되어 동일한 tagWND의 strName.MaximumLength를 가리키게 됩니다.

마지막으로 스크롤바를 새로고침하여 strName.Buffer 필드를 손상시킵니다(그림 17):

64비트의 경우와 달리 strName의 길이 값을 손상시킬 수 없다는 점을 알아야 합니다. 적절한 길이의 창 텍스트 문자열을 미리 할당하여 해당 값이 이미 사용 중이도록 할 수 있습니다. 이후에 커널 주소에서 데이터를 읽거나 쓰려면 SetScrollInfo()를 호출하여 대상 창의 Buffer 값을 업데이트한 다음 창 텍스트 API를 사용하면 됩니다.
이제 32비트 시스템에서 재사용 가능한 임의 주소 읽기/쓰기 "프리미티브"를 얻었습니다!
지금부터는 임의 읽기/쓰기 "프리미티브"가 있다고 가정합니다. 따라서 어떤 값을 누출/읽거나 덮어쓴다고 할 때, 이는 이전 손상 단계에서 설정한 "프리미티브"를 실행하는 것을 의미합니다. 이 "프리미티브"는 두 플랫폼에서 거의 동일합니다. 남은 것은 함수 포인터를 덮어쓰고 이를 어딘가의 Shellcode 페이로드가 가리키도록 하는 것뿐입니다. 일반적인 방법은 nt!HalDispatchTable의 두 번째 항목을 덮어쓰는 것입니다. 이 항목은 HalQuerySystemInformation() 함수에 해당합니다. 그런 다음 사용자 모드에서 NtQueryInternalProfile() 함수를 호출하여 트리거합니다.
커널 모듈의 로드 베이스를 알아야 nt!HalDispatchTable의 커널 주소를 계산할 수 있습니다. 이를 위해 사용자 모드에서 NtQuerySystemInformation()을 호출하여 모듈 정보를 얻을 수 있습니다. 이 정보에는 모듈 베이스가 포함됩니다.
// 열거 값 11은 SystemModuleInformation을 나타냅니다. 문서화되지 않았습니다...
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);
그런 다음 사용자 모드에서 ntoskrnl.exe를 로드하여 nt!HalDispatchTable의 오프셋을 찾고, 커널 공간의 주소를 얻습니다. 그런 다음 "읽기 프리미티브"를 사용하여 HaliQuerySystemInformation()의 커널 주소(내보내지지 않은 함수)를 읽어서 수정한 다음, "쓰기 프리미티브"를 사용하여 해당 함수 포인터를 shellcode의 주소(커널 주소 공간 또는 사용자 주소 공간일 수 있음, 자세한 내용은 후술)로 손상시킵니다. 읽고 쓰는 바이트 수는 32비트 및 64비트 시스템에서 동일합니다.
Windows 8 및 8.1은 SMEP에 대한 지원을 도입했으며, 일부 보안 제품은 Windows 7에서도 이를 활성화할 수 있으므로 항상 존재한다고 가정합니다. SMEP는 커널 권한으로 사용자 공간에서 코드를 실행하는 것을 차단하므로, nt!HalDispatchTable의 항목을 사용자 공간 주소를 가리키도록 수정하는 것이 불가능해집니다. 따라서 SMEP를 비활성화하기 위해 cr4 레지스터를 수정하는 코드가 있는 커널 공간의 제어 가능한 위치를 가리키도록 한 다음 사용자 주소 공간으로 점프해야 합니다. MWR의 글에서는 페이지 테이블 항목을 직접 매핑하여 임의의 가상 주소에 대해 커널 모드 유효 주소를 얻는 흥미로운 64비트 트릭을 소개합니다. 그런 다음 "쓰기 프리미티브"를 사용하여 페이지 테이블 항목을 직접 수정하고 마스크 비트를 변경합니다. 이 트릭을 32비트 시스템으로 포팅했지만, PAE가 활성화된 시스템과 그렇지 않은 시스템 사이에 약간의 차이가 있습니다.
이를 달성하는 명백한 방법은 사용자 모드 주소를 커널 공간에 매핑한 다음 "쓰기 프리미티브"를 사용하여 페이지 테이블 항목이 사용자 권한 대신 시스템 권한을 가지도록 하는 것입니다. 이것이 첫 번째 단계입니다. Windows 8에서 구현할 때 흥미로운 문제가 발생했습니다. Windows 8 이후의 데스크톱 관리자(dwm.exe)는 정기적으로 데스크톱의 창을 스캔하고 해당 이름을 쿼리합니다. 정확한 이유는 조사하지 않았습니다. 이 작업은 창에 메시지를 보내지 않지만, 해당 창 처리 함수가 있으며 GetInternalWindowText()를 호출합니다. 따라서 문제는 창 구조의 strName 필드를 사용하여 shellcode를 포함하는 메모리의 페이지 테이블 항목을 수정하려고 할 때 발생합니다. 이 메모리는 우리 프로세스 공간의 페이지 테이블에 속합니다. dwm.exe가 커널에서 창 이름을 가져올 때 수정된 페이지 테이블 항목으로 인해 커널이 strName.Buffer가 NULL인지 확인하고, 해당 주소를 간접 참조하게 되며, 주소가 유효하지 않으면 시스템이 충돌합니다.
dwm.exe의 쿼리를 충족시키기 위해 페이로드로 커널 주소를 사용했습니다. 이렇게 하면 현재 프로세스가 무엇을 로드하든 해당 주소와 관련된 페이지 테이블 항목이 항상 유효합니다. 데스크탑 힙에 이를 배치하기로 선택했습니다. 앞서 언급한 방법으로 커널 주소를 계산할 수 있기 때문입니다. 여전히 페이지 테이블 항목을 직접 매핑하는 방법을 사용하지만, 이 시점에서 페이지 테이블은 높은 권한으로 표시되었지만 실행 가능으로 표시되지는 않습니다. 따라서 해야 할 일은 실행 비트를 설정하는 것뿐입니다.
단계는 다음과 같습니다:
Windows 8.1에는 또 다른 문제가 있습니다. NtQuerySystemInformation()은 낮은 무결성 SID 값을 확인하므로, 중간 무결성 이상만 커널 베이스를 얻을 수 있습니다. 이는 잘 알려진 sidt 트릭을 사용하여 쉽게 구현할 수 있습니다. IDT의 주소를 사용자 모드에 저장하고(권한 검사 불필요), 그런 다음 "읽기 프리미티브"를 사용하여 필요한 IDT 인덱스를 읽습니다. 대부분 커널 주소 공간을 가리키므로 인터럽트 핸들러의 커널 주소를 유출할 수 있으며, 그런 다음 커널 모듈의 해당 PE 파일에서 오프셋을 찾습니다.
커널 로드 베이스를 얻은 후 nt!HalDispatchTable의 주소를 계산할 수 있습니다.
일반적인 방법은 ntoskrnl.exe 파일을 로드하고 해당 심볼의 오프셋을 해석한 후 유출된 커널 로드 베이스를 더하는 것입니다. 그러나 강화된 샌드박스에서는 파일 시스템 자체에 제한이 있어 C:\windows\system32\ntoskrnl.exe를 읽을 수 없기 때문에 작동하지 않습니다. 이 제한을 우회하기 위해 "유출 프리미티브"를 사용하여 메모리 내 커널 PE 이미지에서 필요한 심볼 주소를 해석합니다.
이것이 모든 자료입니다. 읽어 주셔서 감사합니다. 이 문서에서 제시된 기술을 사용하여 XP, Vista, 7, 8, 8.1 및 Server 2012의 32비트 및 64비트 시스템에서 안정적인 exploit을 모두 구현할 수 있었습니다. Windows 2003 및 2008은 기본적으로 사용자 모드 콜백을 후킹할 수 없으므로 공격할 수 없습니다. 필요한 조건이 충족되지 않으면 이 두 시스템을 공격할 수 없습니다. exploit 과정은 상당히 복잡하고 극복해야 할 많은 장애물이 있지만, 이는 많은 즐거움과 배울 점을 제공했습니다. 이 문서에서 사용된 많은 실행 가능한 방법과 연구 결과는 일부 연구자들의 기사에서 이미 언급되었습니다. 제가 아는 한, win32k.sys가 악용되는 것을 방지할 수 있는 완화 조치는 Google Chrome 샌드박스가 사용하는 것뿐입니다. 이는 win32k의 런타임 커널 시스템 호출을 효과적으로 차단합니다. 개선 사항이나 피드백이 있으면 환영하며, 제가 제시한 일부 기술에 부족한 점이 있으면 알려주십시오. 이 문서를 업데이트하겠습니다. Twitter @fidgetingbits 또는 이메일 [email protected]로 연락할 수 있습니다.