얼마전에 SSL 2기도 종료했고, 스틸리언 기술블로그를 구경하다가 phpmyadmin에서 취약점을 찾아 CVE ID를 획득한 것을 봤다. 그래서 스틸리언 연구원분들이 발견하신 다른 취약점도 보고 싶어서 검색하다가 윈도우에서 취약점을 찾은 것을 확인했다. 이와 관련한 원데이 분석글(https://versprite.com/blog/security-research/silently-patched-information-leak/)도 있어서 이를 번역하며 어떤 취약점인지 공부해보려고 한다.
서론
2019년 11월에 VS-Labs에서 patch diffing을 진행했는데 이때 win32kfull.sys의 NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수에서 민감 정보 노출 취약점이 발견되었다. 해당 취약점은 스틸리언의 장선웅 연구원이 발견했고 Microsoft에서 CVE-2019-1436으로 패치되었다.
패치 분석
Windows N-Day 분석을 위한 첫 번째 단계는 patch analysis이다. Patch analysis는 다양한 도구를 통해 할 수 있지만 대부분 patch diffing을 이용한다. Patch diffing은 patch된 내용이 포함되지 않는 파일과 패치가 완료된 새로운 파일을 비교분석하는 것이다. 이를 통해 분석자는 빠르고 정확하게 패치된 구문을 찾을 수 있다. 일반적으로 사용하는 patch diffing 도구는 Diaphora이다. Diaphora는 두 개의 파일을 그래프 등과 같이 다양한 시각으로 표현해주고, IDA Pro와 같은 디스어셈블러와도 함께 사용될 수 있는 파이썬 스크립트다.
VS-Lab 연구원들은 2019년 11월 패치를 분석하기 위해 Diaphora 도구를 이용해서 10.0.18362.449 버전과 10.0.18362.476 버전의 win32kfull.sys 파일을 patch diffing 했다.아래 그래프는 NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수를 patch diffing한 결과다. 이전 버전의 함수는 왼쪽, 최신 버전의 함수는 오른쪽에 위치해있다.
이 그래프를 통해 Microsoft는 NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수는 늘 0x0을 리턴하게 패치된 것을 확인할 수 있다. Github에서 해당 함수의 Prototype을 확인할 수 있다.
__kernel_entry W32KAPI VOID
NtGdiEnsureDpiDepDefaultGuiFontForPlateau(
_In_ int iDpi);
NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수는 VOID를 반환하기 때문에 반환하는 값은 0x0이고 아무런 정보도 리턴하지 않아야 한다. GreEnsureDpiDepDefaultGuiFontForPlateau 함수를 이해하며 공격자는 어떤 정보를 얻을 수 있고, 악용할 수 있는지 분석해보자.
GreEnsureDpiDepDefaultGuiFontForPlateau 분석
GreEnsureDpiDepDefaultGuiFontForPlateau 함수에서는 먼저 win32kbase!gdmLogPixels라는 값을 복구하기 위해 win32kbase!DrvGetLogPixels 함수를 호출한다. 테스트를 통해 gdmLogPixels 값에는 60이 저장되는 것을 확인했다. 그리고 win32kbase!gdmLogPixels와 공격자가 NtGdiEnsureDpiDepDefaultGuiFontForPlateau의 인자로 넘긴 값과 비교한다.
만약 NtGdiEnsureDpiDepDefaultGuiFontForPlateau의 인자로 넘긴 값의 하위 32비트가 win32kbase!gdmLogPixels와 일치하면, GreEnsureDpiDepDefaultGuiFontForPlateau 함수는 종료되고 leak은 발생하지 않는다. 일치하지 않는다면, 다음 구문이 실행된다.
다음 구문에서는 공격자가 인자로 넘긴 NtGdiEnsureDpiDepDefaultGuiFontForPlateau의 하위 32비트 값이 60인지 검사한다. 만약 60이라면, GreEnsureDpiDepDefaultGuiFontForPlateau는 종료되고, NtGdiEnsureDpiDepDefaultGuiFontForPlateau으로 다시 실행된다. 60이 아니라면, 코드는 계속 실행된다. 아래 이미지를 통해 설명한 2개의 검사를 확인할 수 있다.
- ebx에는 NtGdiEnsureDpiDepDefaultGuiFontForPlateau의 첫번째 인자로 넘긴 Buffer 포인터 저장
- DrvGetLogPixels의 반환값인 eax는 gdmLogPixels이다. (테스트 할 때는 0x60으로 설정)
- ebx와 eax 비교
- RCX = user mode memory 포인터, first argument
- ebx와 0x60 비교
검사가 끝나고, 계속 실행되면 공격자가 NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수의 첫 번째 인자로 넘긴 하위 32비트 값은 0x2AAAAAAB와 곱셈연산을 하고 EDX:EAX에 저장된다. 그리고 EDX를 4로 나누고 그 결과가 0x80000000 이상이 아닌지 확인하기 위해 검사를 진행한다.
만약 0x80000000 이상이라면 ECX를 -1로 설정하고, 아니라면 0으로 설정한다. 그리고 ECX는 곱셈 결과의 상위 32비트를 4로 나눈 값을 포함하는 EDX를 더한다. EDX는 [rdx+rdx*2]한 값을 저장한다. 최종적으로 EDX에 8을 곱하고 공격자가 NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수의 인자로 넘긴 하위 32비트 값과 같은지 비교한다. 다음은 해당 구문이다.
- imul ebx
- EDX:EAX = EAX * EBX
- sar edx, 2
- edx를 4로 나눈다.
- mov ecx, edx
- ECX를 EDX로 설정하며 위의 곱셈 결과의 상위 32비트를 4로 나눈 값으로 overwrite한다.
- shr ecx, 0x1F ; add edx, ecx
- ECX가 0x80000000 이상이면 연산 결과 -1, 0x80000000 미만이면 0이 설정된다.
- 연산 결과를 EDX에 더한다.
- lea edx, [rdx+rdx*2]
- shl edx, 3
- EDX = EDX * 8
- cmp ebx, edx ; jnz ...
- EBX와 EDX를 비교한다.
- EBX는 공격자가 인자로 넘긴 하위 32비트 값이다.
- 연산 과정 정리
EDX = [공격자가 넘긴 인자의 하위 32비트] * 0x2AAAAAAB
EDX = [위 연산결과의 상위 32비트] / 4
EDX = EDX + (EDX * 2)
EDX = EDX * 8
다음 코드는 공격자가 NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수의 인자로 전달한 값이 0x1E0보다 작거나 같은지 확인한다. 그리고 인자값에 0x2AAAAAAB를 곱하고 4로 나누는 이전과 동일한 작업을 수행한다. 결과를 확인하여 signed 숫자인지 아닌지 검사한다. signed가 아니라면 실행을 계속한다.
위의 모든 코드실행이 끝나고, win32kbase!gahDpiDepDefaultGuiFonts 포인터는 RAX에 저장된다. RAX는 역참조되어 RCX에 저장된다. 공격자가 NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수의 인자로 넘긴 값을 0x2AAAAAAB 곱하고 4로 나눈 값이 저장된 RDI는 RCX의 Buffer index로 사용된다. 만약, 해당 index의 QWORD 값이 0이 아니라면 loc_1C028DAEC로 점프한다.
loc_1C028DAEC를 살펴보면, RAX을 수정하고 영향을 끼치는 코드는 없는 것을 확인할 수 있다. 이는 여전히 RAX에는 win32kbase!gahDpiDepDefaultGuiFonts 포인터가 저장되어 있고, GreEnsureDpiDepDefaultGuiFontForPleateau의 리턴값이 NtGdiEnsureDpiDepDefaultGuiFontForPlateau의 리턴값으로 반환되어 공격자의 사용자 코드로 반환될 수 있다는 것이다.
이를 통해 마이크로소프트에서 NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수를 항상 0을 리턴하게 패치한 이유를 설명할 수 있다. 이 패치를 통해 NtGdiEnsureDpiDepDefaultGuiFontForPlateau 함수로부터 커널 포인터 주소와 같은 민감한 데이터의 leak을 방지할 수 있다.
공격자는 해당 취약점으로 win32kbase!gahDpiDepDefaultGuiFonts 포인터 주소를 얻을 수 있고, 이를 통해 KASLR을 우회하고 win32kbase.sys의 base address를 확인할 수 있다. 이를 위해 공격자는 win32kbase!gahDpiDepDefaultGuiFonts가 win32kbase.sys의 시작 주소로부터 상대주소가 어떻게 되는 알고 있어야 한다.
Offset을 구했다면, win32kbase!gahDpiDepDefaultGuiFonts 포인터 값에서 해당 값을 빼서 win32kbase.sys의 base address를 구할 수 있다. 하지만, 업데이트나 패치 주기로 인해 win32kbase.sys와 win32kbase!gahDpiDepDefaultGuiFonts가 바뀔 수 있어서 target user's system에 맞게 공격을 진행해야한다.
공격자는 해당 정보를 얻기 위해 두 가지 방법을 사용할 수 있다. 첫 번째 접근 방식은 IDA Pro나 Relyze와 같은 디스어셈블러를 사용해서 win32kbase.sys를 리버싱해서 offset을 확인하는 것이다. 다른 방식은 시스템에서 win32kbase.sys 파일의 메모리를 읽어서 동적으로 파싱하는 프로그램을 작성하여 시도할 수도 있다.
Final Exploitation Notes
안정적인 익스플로잇을 위해 해당 취약점을 2번 이용해야 한다. 그 이유는 GreEnsureDpiDepDefaultGuiFontForPlateau 함수 내용이 포함된 아래 코드 때문이다.
익스플로잇이 처음 실행될 때, cmp 명령에서 qword ptr [rcx+rdi*8] 값이 0으로 셋팅되어 점프를 건너뛰는 경우 많이 발생했다. (점프를 뛰어야지 성공적인 익스플로잇 가능) 이를 우회하기 위해 다음의 아래의 코드를 자세히 살펴보자.
첫 번째 블록에서는 qword ptr [rcx+rdi*8] 값이 여전히 0인지 검사하기 전에, win32kbase!gahDpiDepDefaultGuiFonts 값을 가져온다. 만약, 0이라면 [RDX+RDI*8]이 win32kbase!gahDpiDepDefaultGuiFonts으로 설정된다. [RCX+RDI*8] 검사하기 이전에 [RDX+RDI*8]이 설정되기 때문에 0이 아니게 되므로 검사를 성공적으로 통과할 수 있다.
남은 것은 공격자가 [RCX+RDI*8] 값이 0이 안되게 하는 input을 입력하여 취약점을 트리거하는 것과 user mode code로부터 win32kbase!gahDpiDepDefaultGuiFonts 포인터 값을 얻을 수 있도록 올바른 경로를 사용하는 것이다.
'Security' 카테고리의 다른 글
hackingzone X-MAS CTB 후기 (0) | 2022.01.08 |
---|---|
3D Accelerated Exploitation 분석 (0) | 2022.01.03 |
[Black hat USA 2020] Exploiting Kernel Races through Taming Thread Interleaving 리뷰 (0) | 2021.11.27 |
[pwnable.xyz] WriteUp (0) | 2021.11.25 |
ALLES! CTF 2021 writeup (0) | 2021.09.10 |