분석 환경
• Windows10 1909 x64
설명
CVE-2021-1732 취약점은
win32kfull!xxxCreateWindowEx의 xxxClientAllocWindowClassExtraBytes 콜백으로 인해tagWND.WndExtra 및 해당 플래그의 설정이 동기화되지 않아 커널 내의 임의의 메모리 값에 읽기 및 쓰기가 가능하여 LPE가 가능하다.
xxxCreateWindowEx가 WndExtra 영역이 있는 창을 만들때 win32kfull!xxxClientAllocWindowClassExtraBytes를 호출하여 콜백을 트리거할 것이고, 콜백은 WndExtra 영역을 할당하기 위해 사용자모드로 돌아간다. 이때 후킹된 콜백함수에서 NtUserConsoleControl()에 인자로 현재 창의 핸들을 넣어주면 tagWndExtra가 오프셋으로 변경되고 오프셋임을 나타내는 플래그가 설정이된다. 그 후 NtCallbackReturn에 임의의 값을 반환해 준다.
이때 콜백이 종료되고 커널 모드로 돌아가면 반환 값이 tagWND.WndExtra를 덮어쓰지만 해당 플래그는 지워지지 않는다. 그 후 확인되지 않은 오프셋 값은 힙 메모리 주소 지정을 위해 커널 코드에서 직접 사용되어 경계를 벗어난 액세스를 유발한다.
기본 개념
• Win32k
Graphical (GUI) component of the Microsoft Windows Subsystem
• tagWND
- tagWND is a kernel structure which represents a WINDOW object in kernel memory
- Windows에서 CreateWindowExW() API를 통해 창을 생성하게 되면, 커널 내부에서 tagWND 구조체로 객체가 생성된다.
– Win10 부터 Windbg에서 확인이 불가능하다.
– Win10에서 win32k의 tagWND 구조체가 많이 바뀌었지만, 여전히 Win7에서의 tagWND 구조체를 많이 참고하고 있다.
– tagWND의 주소를 leak하는 방법 중 가장 잘 알려진 방법은 HMValidateHandle()을 이용하는 것이다.
• HMValidateHandle() API
– This function allows to map the tagWND object in the user mode memory space
• Desktop heap
- 윈도우 데스크탑의 창, 메뉴, 아이콘 등의 개체를 특수한 메모리 힙을 사용하여 관리
• NtCallbackReturn() API
– 쉽게 말해 불려진 콜백 함수에서 return을 해줄때 사용을 한다.
– 내부적으로 여러개의 callback함수가 불려질 것이고, 이를 그냥 return으로 값을 보내면 운영체제?에서 콜백함수가 동작이 끝났는지 모른다. 그래서 NtCallbackReturn()을 통해 return해주면 어떤 함수가 끝났는지 알 수 있다.
상세 분석
CreateWindowEx를 사용하여 창을 만들때 cbWndExtra 필드를 통해 사용자가 원하는 만큼의 메모리 공간을 tagWND 개체 바로 뒤에 할당할 수 있다.
즉, cbWndExtra가 0이 아닌 경우 win32kfull!xxxCreateWindowEx는 내부적으로 커널 콜백 메커니즘을 통해 user32!__xxxClientAllocWindowClassExtraBytes를 호출한다.
이전에 cbWndExtra에 32을 넣어 주었고, 내부적으로 0x20만큼 힙을 할당해 주는 것을 볼 수 있다.
위 사진은 참고
여기까지 일반적으로 윈도우 창을 만들때의 흐름이다.
우리는 xxxClientAllocWindowClassExtraBytes를 후킹해서 NtUserConsoleControl()을 실행시켜야 한다.
xxxClientAllocWindowClassExtraBytes는 콜백 함수로 KernelCallbackTable에 저장이 되어있다. 그래서 위 사진과 같이 PEB -> KernelCallbackTable -> xxxClientAllocWindowClassExtraBytes를 구한다.
우리는 xxxClientAllocWindowClassExtraBytes를 후킹해서 NtUserConsoleControl()을 실행시켜야 한다.
xxxClientAllocWindowClassExtraBytes는 콜백 함수로 KernelCallbackTable에 저장이 되어있다. 그래서 위 사진과 같이 PEB -> KernelCallbackTable -> xxxClientAllocWindowClassExtraBytes를 구한다.
먼저, 정상적인 윈도우 창을 10개 정도를 만들어준다.
이때
우리에게 실질적으로 필요한건 윈도우 창들 중 BaseAddress가 가장 작은 것부터 2개이다.
그래서 위 과정을 통해 최소 BaseAddress를 구해준다.
이렇게 메모리에서 가장 낮은 주소를 찾았고, 이 윈도우 창을 tagWND0, tageWND1이라고 하겠다.
추가로 이 두개의 창은 서로 가까운 메모리에 생성이 될 것이다.
이렇게 윈도우 창 10개를 만드는 과정 중간에 창의 Handle을 가지고 HMValidateHandle()을 가지고 tagWND를 구해서 배열에 저장을 한다.
이를 통해 우리는 Handle에 1:1 매칭되는 tagWND를 가지고 있다.
[중요]
추가로 tagWND는 커널 영역에 있는 구조체 인데 HMValidateHandle()을 하면 커널 주소가 반환이 되느냐? 정답은 아니다. 유저영역의 tagWND의 주소가 반환이 된다.
즉, 유저영역의 주소에 offset을 더해 특정 멤버의 값을 들고 오는 거나 커널 영역의 주소에서 offset으로 멤버의 값을 들고 오나 같은 결과이다.
위에서 윈도우 창 10개를 만들기 전에 ClientAllocWindowClassExtraBytes을 후킹 했었다.
이 말은 10개의 창을 만들면서 후킹 함수가 계속해서 호출 되었을 것이다.
다음 코드를 살펴보기 전에 창을 만들면서 실행되었을 후킹 함수를 살펴보자.
GuessHwnd()를 통해 CreateWindowEx를 호출 했을 때 반환되는 Handle을 구해서
NtUserConsoleContorl()로 전달한다. 그러면 2가지 일이 발생한다.
1. 해당 창은 콘솔 창로 인식하기 위해 tagWND+0xE8(ExtraFlag) 플래그가 0x800로 바뀐다.
2. tagWND.ExtraBytes은 주소로 인식하는 것이 아닌 offset으로 인식한다.
(원래 tagWND.ExtraBytes에는 DesktopAlloc으로 할당한 주소가 들어간다.)
이후 NtCallbackReturn을 해줌으로써 사용자가 원하는 offset을 tagWND.ExtraBytes에 넣어 줄 수 있다.
-> 요약하자면, 콜백이 완료되고 커널 모드로 돌아가면 반환 값이 오프셋 멤버를 덮어 씌어지고 해당 플래그는 지워지지 않습니다.
Windbg로 확인을 해보면 [rax+0x128]에 사용자가 원하는 rax값을 넣어 준다.
이렇게 return되는 offset을 이용하면 tagWND0에서 tagWND1까지 침범하는 OOB가 발생할 수 있다.
현재까지의 과정을 살펴보면 아래 그림과 같다.
계속해서 다음 코드를 살펴보기 전에 알아두어야할 개념이 있다.
윈도우 창을 생성해주면 tagWND영역도 있고, 윈도우 창 전체를 포함?하고 있는 영역이 있다.
파란색 처럼 말이다. 그래서 전체 tagWND의 base가 존재하고, 각 tagWND의 Base가 존재한다.
이제 코드를 보자.
arrayEntryDesktop은 tagWND의 주소가 들어 있다. tagWND+0x8은 전체 tagWND영역의 base로부터의 offset이다. 즉, 위 그림에서 파란색 영역으로부터의 offset이다.
위에서 "유저영역의 주소에서 offset 한 값과 커널영역에서 offset한 값이 같다."라고 한번 언급했었다.
위 사진은 Windbg에서 offset_from_base를 구하는 과정인데, 유저영역 주소에서 offset한 결과와 커널 영역에서 offset한 결과가 동일한 것을 확인할 수 있다.
처음에 10개의 윈도우 창을 생성해서 각종 정보를 구한 다음 3-10번째 창을 파괴시킨다.
실질적으로 우리는 0번과 1번 창을 사용하는데, 왜 3-10번까지 만들었는지 잘 모르겠다.
이 취약점은 결국 tagWND0으로 offset을 이용해서 tagWND1에 OOB가 가능해 진다는 것이다.
그래서 tagWND0를 Offset으로 바꾸어 주는 작업이다.
이제 우리는 WND_Malicious를 만들어서 WND0를 조작해 줄 것이다.
후킹 함수에서 if문 조건을 보면 WND_Malicious일 때 작업을 해주는 것을 알 수 있다.
NtCallbackReturn()을 통해 return되는 값을 WND0의 offset으로 해 줌으로써
WND_Malicious.ExtraBytes에 WND0의 offset이 들어갈 것이고, 즉 WND_Malicious를 통해 WND0을 컨트롤 할 수 있게 된다.
Malicious Window를 만든 후 SetWindowLongW()를 사용해 WND_Malicious 핸들을 가지고 0x128에 값을 넣어주는 것을 볼 수 있다.
WND_Malicious는 후킹함수를 거쳐서 WND0를 가리키고 있는 것을 잊지마라.
즉, SetWindowLong()에 WND_Malicious 핸들을 넣고 0x128 떨어진 곳에 값을 넣어준다는 것은 WND0+0x128에 값을 써주는 것으로 해석할 수 있다.
이 작업은 커널 메모리의 tagWND 구조 및 ExtraBytes를 넘어 WND1 내의 필드에 쓸 수 있게 하기 위해서이다.
이제 커널의 주소를 leak하기 위해서 하는 작업들이다.
윈도우 창을 묘사하는 커널 데이터 구조인 "spmenu"를 이용하여 커널 주소를 leak할 것이다.
먼저, spmenu에 대해 살펴보자.
The function NtUserSetWindowLongPtr replaces the target window’s spmenu field with the function's argument without any checks when using GWLP_ID and the target window's style is WS_CHILD.
위 설명과 같이 WS_CHILD와 GWLP_ID를 통해 spmenu 필드에 담긴 커널 주소를 leak할 수 있다.
참고로 SetWindowLongW()의 반환 값은 해당 위치의 이전 데이터를 반환한다. (=커널 주소)
이 과정을 통해 담긴 커널 주소를 얻을 수 있다.
디버거로 확인해 보면
SetWindowLongPtrA()으로 tagWDN+0x98떨어진 곳에 fake addr로 바뀌어진 것을 볼 수 있다.
하지만! leak되는 주소는 커널이고, 이전의 값이 커널 주소여야하는게 정상이다...
이전에 값을 살펴보면 0x48f40이다.. 커널 주소여야하는데,,,
위 사진은 win32kfull!xxxsetwindowlong()함수의 일부분이다.
Index가 -12일때 if문으로 style 조건을 확인한 후 동작을 보자.
0x98만큼 떨어진 곳에 fake_addr이 들어가는 것은 이미 확인 하였다.(바로 위 그림)
v14 = tagWND_base+0x15에서 커널 주소를 저장해 두고 이를 return한다.
즉 return 14;를 하기 때문에 tag_base+0x15의 값을 leak할 수 있다.
이후 leak addr로 EPROCCESS까지 구하는 과정이다.
System 토큰을 구해서 write해주면 LPE가 가능하다.
참고자료
• https://www.mcafee.com/blogs/enterprise/mcafee-enterprise-atr/technical-analysis-of-cve-2021-1732/
• https://developpaper.com/cve-2021-1732-lpe-vulnerability-analysis/
• https://www.real-sec.com/2022/01/technical-analysis-of-cve-2021-1732/
• https://getdigitaltech.com/technical-evaluation-of-cve-2021-1732-mcafee-blogs/
• https://venzux.com/technical-evaluation-of-cve-2021-1732-mcafee-weblog/
• https://www.blackhat.com/docs/eu-16/materials/eu-16-Liang-Attacking-Windows-By-Windows.pdf
• https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2021/CVE-2021-1732.html
'ETC' 카테고리의 다른 글
Code execution with de-serialization (leads to UAC bypass) (0) | 2022.05.09 |
---|---|
[CVE-2016-5195] Dirty COW 분석 (0) | 2022.05.02 |
LLVM 난독화 (feat. 기법 3가지) (0) | 2022.01.26 |
COM(Component Object Model)의 개념 잡기 (0) | 2021.11.21 |
[COM 객체] 가상 멤버함수 후킹 ! (0) | 2021.11.14 |