
cve-2019-1458용 POC
12월에 Kaspersky는 [야생에서 사용된 0day 익스플로잇][1]에 관한 블로그 포스트를 게시했습니다. 그들의 분석에는 익스플로잇이 어떻게 동작하는지 설명되어 있었지만 POC가 포함되어 있지 않았기 때문에 제 관심을 끌었습니다.
그래서 저는 Kaspersky의 블로그 포스트와 패치 분석을 기반으로 이 취약점에 대한 POC를 작성해 보기로 결정했습니다.
이 포스트는 그 과정을 다룹니다.
가장 먼저 이 취약점에 대해 가능한 한 많은 정보를 수집했습니다. 언급된 블로그 포스트를 읽으면서 다음 정보를 추출했습니다:
NtUserMessageCall API에 대한 호출이 두 번 필요합니다win32k!DrawSwitchWndHilite에 대한 언급이 있었습니다그 외에도 앞서 나열한 일부 내용을 보여주는 디컴파일된 코드의 멋진 스크린샷이 있습니다.
정확히는 전환 창 생성, toggle_alt_key라는 함수 호출, 그리고 여러 번의 NtUserMessageCall 호출을 보여줍니다.
[이미지 출처][1]
유용한 정보가 많지만, 이 취약점이 정확히 어떻게 동작하고 어떻게 트리거하는지는 여전히 설명되지 않습니다.
영향을 받은 모듈은 [win32k.sys][2]였습니다. 이 모듈의 패치 버전과 패치 전 버전을 모두 다운로드했습니다.
Win7 x64의 경우 다음과 같습니다:
이들은 [Microsoft Update Catalog][3]에서 다운로드할 수 있습니다
두 버전을 비교한 bindiff 결과는 다음과 같습니다

DebugHook 기능과 관련된 함수들을 제외하고 나면 실제로 남는 것은 약간 변경된 함수 InitFunctionTables()뿐입니다.

확실히 가장 큰 패치는 아닙니다.
이것만으로 이 취약점의 근본 원인을 즉시 식별하는 데는 도움이 되지 않습니다. 하지만 *(gpsi+0x14E), *(gpsi+0x154), *(gpsi+0x180) 변수에 대한 초기 값이 추가되었다는 점은 주목할 만합니다. 따라서 이는 초기화되지 않은 변수와 관련된 버그일 수 있습니다.
이 섹션에서는 이 취약점을 트리거하는 POC를 단계적으로 구축하면서 동시에 취약점의 정체를 파악해 나가는 과정을 설명하겠습니다.
패치 디핑은 처음에 그다지 유용한 정보를 주지 않았기 때문에 개발 초기 단계에서는 주로 Kaspersky의 블로그 포스트에 의존했습니다.
좋은 테스트 환경을 위해 마지막 취약 버전의 win32k가 실행되는 Win7 SP1 x64 VM을 준비했습니다. 그 위에 Windbg를 연결하여 커널 디버깅을 수행하고, 동시에 심볼 서버 경로도 설정했습니다.
블로그 포스트에서 언급된 win32k!DrawSwitchWndHilite를 살펴보면서 조사를 시작했습니다. 이 함수는 xxxMoveSwitchWndHilite와 xxxPaintSwitchWindow 두 곳에서 호출되는데, 후자는 원래 보고서에서 언급된 주변의 GetKeyState/GetAsyncKeyState 호출 때문에 즉시 관심을 끌었습니다. 게다가 그 호출들은 ALT 키가 눌렸는지 확인하고 있습니다.

xxxPaintSwitchWindow에서 DrawSwitchWndHilite 호출
호출 교차 참조(xxxWrapSwitchWndProc->xxxSwitchWndProc->xxxPaintSwitchWindow->DrawSwitchWndHilite)를 더 따라가면서, 그 체인의 첫 번째 요소가 패치에서 수정된 InitFunctionTables에서 참조된다는 것을 발견했습니다.
다음으로 디컴파일된 코드의 스크린샷에서 NtUserMessageCall을 살펴보았습니다.
이 함수의 선언은 다음과 같습니다```cpp
NtUserMessageCall(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType, BOOLEAN bAnsi)
Exploit은 `msg = 0x14`와 `dwType = 0xE0`로 그것을 호출하고 있습니다. 그것이 무엇을 하는지 살펴보겠습니다.```cpp
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
printf("[*] Registering window\n");
ATOM wndAtom = RegisterClassEx(&wcx);
if (wndAtom == INVALID_ATOM) {
printf("[-] Failed registering SploitWnd window class\n");
exit(-1);
}
printf("[*] Creating instance of this window\n");
HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
if (sploitWnd == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create SploitWnd window\n");
exit(-1);
}
NtUserMessageCall(sploitWnd, WM_ERASEBKGND, 0, 0, 0, 0xE0, 1);
여기서 간단한 창 클래스를 등록하고 해당 클래스의 창을 생성했습니다. 그런 다음 익스플로잇과 동일한 매개변수로 NtUserMessageCall을 호출했습니다. 내부에서 어떤 일이 발생하는지 확인하기 위해 중단점 kd> ba e 1 win32k!NtUserMessageCall을 설정하고 코드를 실행했습니다.
이 함수에 대한 호출이 꽤 많았기 때문에 올바른 호출을 찾아야 했지만, 그리 어렵지 않았습니다. 콜스택이 매우 짧은 호출이 바로 그것이었습니다.

NtUserMessageCall
코드를 단계별로 살펴보면 gapfnMessageCall 배열의 함수를 호출하며, 인덱스는 msg 값을 기준으로 계산되고 0이므로 NtUserfnDWORD를 호출하게 됩니다.

NtUserfnDWORD
다음 호출은 dwType 값을 사용하여 이루어지며, 이제 gpsi 오프셋은 0x40이고, 호출은 xxxWrapSwitchWndProc로 이어집니다 (이 함수는 DrawSwitchWndHilite 호출 체인을 확인할 때 이미 나타났습니다).
xxxWrapSwitchWndProc는 단순히 xxxSwitchWndProc를 호출합니다.

xxxSwitchWndProc
그리고 이것이 끝입니다. 코드는 여기서 실패하며, msg 값(0x14)에 따라 도달하려는 xxxPaintSwitchWindow로 더 이상 진행되지 않습니다. 왜 그런지 확인해 봅시다.
이 단계에서 코드가 실패하는 이유는 이전 이미지에서 강조된 것처럼 창의 fnid가 0x2A0(FNID_SWITCH)와 같지 않고 보내는 메시지가 1이 아니기 때문입니다. 따라서 xxxDefWindowProc로 빠지게 됩니다. 이 상황을 피하려면 fnid를 FNID_SWITCH로 설정한 상태로 xxxSwitchWndProc를 호출해야 합니다. 그래야 switch 문으로 바로 이동하고 나중에 xxxPaintSwitchWindow에 도달할 수 있습니다.
올바른 fnid를 설정하는 방법은 무엇일까요? 실제로 같은 함수가 첫 번째 if 블록에서 이 작업을 수행하므로, fnid를 설정하는 명령에 도달하려면 그 안의 모든 검사를 실패시키기만 하면 됩니다.
다음은 세 개의 if 검사를 모두 실패시키기 위해 충족해야 하는 조건입니다.
fnid == 0 및 cbwndExtra + 0x128 >= *(gpsi + 0x154)0입니다.
패치되지 않은 win32k!에서 *(gpsi+0x154)는 0입니다. 하지만 패치된 버전처럼 0x130으로 설정된 경우에도 cbwndExtra를 8 이상으로 설정하면 첫 번째 검사를 여전히 우회할 수 있습니다.msg == 1NtUserMessageCall 호출에서 설정할 수 있습니다. msg를 1로 설정하면 제어 흐름은 NtUserfnDWORD 대신 NtUserfnINLPCREATESTRUCT를 거치지만 여전히 xxxSwitchWndProc에서 끝납니다.extraData == 0cbwndExtra를 사용하여 창 클래스를 등록할 때 설정할 수 있습니다. ExtraData는 tagWND 구조 바로 뒤에 추가됩니다 (저는 디컴파일된 코드를 좀 더 보기 좋게 만들기 위해 이 필드를 IDA의 tagWND 구조에 sizeof(tagWND) 오프셋의 QWORD로 추가했습니다). ExtraData 값은 SetWindowLongPtr 호출로 설정할 수 있습니다.이 모든 조건이 충족되면 창의 fnid가 FNID_SWITCH로 설정됩니다.
이제 원하는 fnid를 설정하기 위해 msg를 1로 하여 NtUserMessageCall을 한 번 호출하고, xxxPaintSwitchWindow에 도달하기 위해 두 번째로 호출해야 합니다.```cpp
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
wcx.cbWndExtra = 8; //to pass check in xxxSwitchWndProc
printf("[*] Registering window\n"); ATOM wndAtom = RegisterClassEx(&wcx); if (wndAtom == INVALID_ATOM) { printf("[-] Failed registering SploitWnd window class\n"); exit(-1); }
printf("[*] Creating instance of this window\n"); HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL); if (sploitWnd == INVALID_HANDLE_VALUE) { printf("[-] Failed to create SploitWnd window\n"); exit(-1); }
printf("[] Calling NtUserMessageCall to set fnid = 0x2A0 on window\n"); NtUserMessageCall(sploitWnd, WM_CREATE/ = 1*/, 0, 0, 0, 0x0, 1);
printf("[] Calling NtUserMessageCall second time"); NtUserMessageCall(sploitWnd, WM_ERASEBKGND/ = 0x14*/, 0, 0, 0, 0x0, 1);
창 클래스에 `extraData`를 추가하고 두 번째 `NtUserMessageCall` 호출을 추가했습니다. 이제 제어 흐름이 `xxxPaintSwitchWindow`에 도달할 수 있습니다.
(참고: `dwType`은 `0xE0`일 필요는 없습니다. 어차피 `NtUserfnDWORD`에서 `0x1F`와 AND 연산을 하기 때문에 `0`도 그냥 동작합니다.)

*`xxxPaintSwitchWindow`*
자세히 살펴보니 창 객체에서 가져온 `extraWndData` 값(25번째 줄)이 쓰기 대상 포인터로 사용되고 있음을 확인했습니다(46-52번째 줄)! 내가 제어하는 값으로 `extraWndData`를 설정하는 코드에 도달할 수 있다면 임의의 메모리를 손상시킬 수 있습니다!
그 코드에 도달하려면 먼저 빨간색으로 표시된 몇 가지 검사를 더 통과해야 합니다