
CVE-2015-0057(win32k.sys use-after-free 취약점)에 대한 상세한 기술 분석 및 익스플로잇 구현으로, XP부터 8.1까지의 32비트 및 64비트 Windows 시스템을 다룹니다.
저자: 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);