[CVE-2020-1054] Win32k LPE 취약점 분석
목차
0x00. Introduction
CVE-2020-1054는 Windows 커널 그래픽 컴포넌트인 Win32k.sys에서 발생하는 권한 상승(Elevation of Privilege) 취약점이다. Check Point Research의 Netanel Ben-Simon, Yoav Alon과 Qihoo 360 Vulcan Team의 bee13oy에 의해 보고되었다.
이 포스트는 0xeb-bp의 분석 보고서를 바탕으로 작성하였다.
0x01. Root Cause Analysis
Crash Analysis
이 취약점은 win32k!vStrWrite01 함수 내부에서 PAGE_FAULT_IN_NONPAGED_AREA 크래시를 발생시키며 확인되었다. 크래시가 발생한 코드는 다음과 같다.
int
크래시 발생 시 bugcheck 결과는 다음과 같다.
PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced. This cannot be protected by try-except.
Typically the address is just plain bad or it is pointing at freed memory.
Arguments:
Arg1: fffff904c7000240, memory referenced.
Arg2: 0000000000000000, value 0 = read operation, 1 = write operation.
Arg3: fffff960000a5482, If non-zero, the instruction address which referenced
the bad memory address.
Arg4: 0000000000000005, (reserved)
Some register values may be zeroed or incorrect.
rax=fffff900c7000000 rbx=0000000000000000 rcx=fffff904c7000240
rdx=fffff90169dd8f80 rsi=0000000000000000 rdi=0000000000000000
rip=fffff960000a5482 rsp=fffff880028f3be0 rbp=0000000000000000
r8=00000000000008f0 r9=fffff96000000000 r10=fffff880028f3c40
r11=000000000000000b r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei ng nz na po cy
win32k!vStrWrite01+0x36a:
fffff960`000d5482 418b36 mov esi,dword ptr [r14] ds:00000000`00000000=????????
여기서 r14 레지스터가 가리키는 메모리 주소(0xfffff904`c7000240)가 유효하지 않아 크래시가 발생한다. r14는 쓰기 작업의 타겟 주소(Destination)로 사용되는데, 이 주소가 어떻게 계산되는지, 컨트롤 가능한지 살펴보자.
OOB Control
r14는 다음과 같은 lea r14, [rcx + rax*4] 명령어로 계산된다. 다시 rcx와 rax를 추적하기 위해 pseudo code로 나타내면 다음과 같다.
var_64h = 0x7fffffff;
var_6ch = 0x80000000;
while ( r11d )
{
--r11d;
if ( ebp >= var_6ch && ebp < var_6ch )
{
// oob read/write in here
}
++ebp;
ecx += eax;
}
결국 타겟 주소(oob_target)는 다음과 같은 1차 방정식 꼴이 된다.
oob_target = initial_value + loop_iterations * eax
함수 호출 시 인자값을 바꾸는 등 분석을 통해 각 변수는 다음과 같이 제어된다는 사실을 알 수 있다. 하지만 아무리 값을 바꿔도 oob_target의 하위 32비트는 0xc7000240으로 고정되고 상위 32비트만 제어가 가능했다고 한다.
- loop_iterations (반복 횟수) :
DrawIconEx의 5번째 인자(arg5)에 의해 결정loop_iterations = ((1 - arg5) & 0xffffffff) // 0x30
- eax (증가량) :
CreateCompatibleBitmap의 1번째 인자(arg1)에 의해 결정lDelta = arg1 // 8- 메모리 상에서 다음 라인으로 이동하는 오프셋 역할
이제 이 where to write가 결정되었으니 what to write, 즉 어떤 값을 쓸 수 있는지, 그 값을 얼마나 제어할 수 있는지를 확인해야 한다. vStrWrite01을 자세히 확인해보면 다음 두 부분에서 write 명령이 수행된다.
// write 1
win32k!vStrWrite01+0x417
mov dword ptr [r14],esi
// write 2
win32k!vStrWrite01+0x461
mov dword ptr [r14],esi
esi 값은 조건에 따라 어떤 값과 OR이나 AND 연산을 하게 된다. Exploit 과정에서 주로 값을 증가시키거나 특정 비트를 켜기 위해 OR 연산을 사용하므로 조건을 파악하는 것이 중요한데, DrawIconEx의 8번째 인자인 diFlags가 눈에 띄었다고 한다. 공식 문서를 확인해보니 다음과 같은 사실을 알 수 있었다.
DrawIconEx의 8번째 인자(diFlags)가DI_IMAGE (0x6)이면 AND 연산DrawIconEx의 8번째 인자(diFlags)가DI_MASK (0x1)이면 OR 연산
분석 결과, 이 OOB write 취약점은 매우 독특한 제약 사항을 가지고 있다.
- 하위 32비트 고정 :
oob_target주소의 상위 32비트는arg5를 통해 제어할 수 있지만, 하위 32비트는 항상0xc7000240과 같은 특정 값으로 고정된다. 즉, 공격자가 원하는 임의의 주소에 값을 쓸 수 있는 것이 아니라, 하위 주소가 맞는 특정 위치에만 쓸 수 있다. - 제한적인 쓰기 (Bitwise Operation) : 원하는 값을 덮어쓰는(Overwrite) 것이 아니라, 기존 값에 AND 또는 OR 연산을 수행한다.
0x02. Exploit
위에서 언급한 제약 사항(하위 주소 고정, Bitwise OR 연산) 때문에 일반적인 방법으로는 exploit이 어렵다. 하지만 1-byte NULL overflow(House of Einherjar)만으로도 exploit이 가능하다는 것을 상기하면 힘이 난다고 한다. 이를 극복하기 위해 Windows 커널의 비트맵 객체인 SURFOBJ 구조체를 이용한다.
typedef struct SURFOBJ64; // sizeof = 0x50
이 중 exploit에 중요한 필드는 다음과 같다.
pvScan01: 실제 비트맵 데이터를 가리키는 포인터sizlBitmap: 비트맵의 가로(width)와 세로(height), 두 개의 dword로 구성
Step 1. Heap Grooming
먼저 기준이 될 Base SURFOBJ를 할당하여 0xfffff900`c7000000 주소에 위치시킨다. 이후 CreateCompatibleBitmap을 반복 호출하여 커널 힙(Paged Pool)에 SURPOBJ 구조체와 비트맵 데이터를 다수 할당한다.
힙 레이아웃을 조정하여 그 중 하나가(Target A) 정확히 0xfffff901`c7000000 주소에 할당되도록 힙을 그루밍하는 것이 목표이다.

Step 2. Target Selection & OOB Write
우리가 노리는 것은 Target B의 sizlBitmap 필드이다. 이 필드는 비트맵의 가로/세로 크기를 정의한다. 이 값이 커지게 되면 커널은 Target B 비트맵의 크기가 매우 크다고 착각하게 된다.
이 과정에서 Target B의 sizlBitmap 필드를 oob_target과 정확히 일치시키는 것이 까다롭다. 단 한번의 쓰기로 정확히 sizlBitmap을 조작하지 않으면 BSOD가 발할 확률이 높다. 따라서 DrawIconEx와 CreateCompatibleBitmap의 인자를 잘 조합해야 하는데, 이 조합을 찾아내기 위해 다음과 같이 brute force 스크립트를 작성했다고 한다.
# start with size at 0x50000
= // 0x8
# lDelta is always byte alligned so ignore if not
= * 0x4 + 0x240
= + *

Step 3. Relative Read/Write
이제 Target B 비트맵은 자신의 할당된 메모리 영역을 넘어서 접근할 수 있게 되었다. Target B 바로 뒤에는 또 다른 비트맵 구조체 Target C가 위치해 있을 것이다.
SetBitmapBits(Target B, ...) 함수를 호출하면 Target B의 데이터 영역을 넘어 Target C의 pvScan0 필드를 덮어쓸 수 있다.

Step 4. Arbitrary Read/Write
이제 Target C 비트맵을 이용해 완전한 AAR/AAW가 가능하다.
- Where 설정 : Target B(Manager)를 이용하여 Target C(Worker)의
pvScan0을 원하는 주소로 변경한다. - What 실행 : Target C(Worker)를 이용해
GetBitmapBits,SetBitmapBits를 호출하면 커널 주소의 값을 읽거나 쓸 수 있다.
이것이 바로 Arbitrary Read/Write Primitive의 완성이다.
Step 5. Token Stealing
AAR/AAW가 확보되었으니 나머지는 일반적인 기법을 사용한다.
- 현재 프로세스(
EPROCESS)를 찾는다. - System 프로세스(PID 4)의
EPROCESS를 찾는다. - System 프로세스의
Token값을 읽어와서 현재 프로세스의Token값을 덮어쓴다. - 이제 현재 프로세스는 System 권한을 갖게 된다.
0x03. Conclusion
이 취약점은 DrawIconEx라는 그래픽 함수 내부의 복잡한 주소 계산 로직과 인자 검증 부재가 결합되어 발생한 취약점이다. 제한된 OOB write(고정된 하위 주소, Bitwise 연산)를 SURFOBJ 구조체 조작을 통해 강력한 AAR/AAW로 전환하는 창의적인 exploit 기법을 보여준다.
Windows 7 환경에서는 보안 업데이트(KB) 설치 여부에 따라 구조체 오프셋이 달라질 수 있으므로, exploit 작성 시 이를 동적으로 탐지하거나 버전에 맞춰 수정해야 한다.