본 포스팅은 지난번에 발견했던 SMB 취약점에 대해 분석한 글입니다. SMB 서비스가 실행되고 있을때 어떠한 인증 없이도 원격으로 Microsoft Windows 를 Crash 낼 수 있는 취약점입니다. 이에 대한 김치콘 2017 발표자료와 POC 코드는 아래에 있습니다.
# 개요
SMB는 파일과 디렉터리 및 주변 장치들을 공유하는 데 사용되는 메시지 형식으로 윈도우 OS에 기본적으로 탑재되어 있다. 해당 취약점은 어떠한 인증 없이 원격으로 서버에 패킷을 보내 서버를 강제 종료시킬 수 있다. Windows SMB Denial of Service Vulnerability 라는 이름으로 정보 보안 취약점 표준 코드(Common Vulnerabilities and Exposures) 번호인 CVE-2017-11781이 부여되었다.
# 환경
취약한 버전은 다음과 같다.
# 정적 분석
정적 분석은 IDA를 사용해 진행했다. SMB는 파일을 공유하기 위해 다양한 Command와 Data type을 사용한다.
- https://msdn.microsoft.com/en-us/library/ee441466.aspx 해당 취약점은 SMB 프로토콜에서 사용하는 Data type 중 하나인 GEAList의 처리 과정에서 발생한다. Command 중 하나인 Trans-action2에서 다루는 동적 데이터이다.
SMB 드라이버 중 하나인 srv.sys에서 문제가 발생한다. SMB의 Command 중 하나인 FindFirst2는 서버와 클라이언트 사이에서 파일 정보를 리스트로 공유하기 위해 첫 번째 파일을 찾는 역할을 담당한다. 이 Command는 srv.sys 내부의 SrvFind2Loop 함수에서 수행된다. 파일 정보들을 공유하기 위해서 SMB_GEA_LIST라는 이름의 데이터 타입을 사용한다. 서버 측에서 GeaList를 Windwos에서 사용하는 NT 타입으로 변환하는 과정에서 멤버 변수인 SizeOfListInBytes의 타입을 잘못 변환한다. SizeOfListInBytes 변수가 ULONG으로 크기가 DWORD인데 반해 이를 WORD로 강제 캐스팅되는 부분이 존재한다. 즉, 4바이트인 변수를 2바이트로 잘라서 계산하기 때문에 문제가 된다.
잘못된 캐스팅을 한 상태에서 SrvOs2GeaListToNt를 호출한다. 이 때 SrvOs2GeaListToNt 함수의 첫 번째 인자는 GeaList의 포인터이다. SrvOs2GeaListToNt 함수를 호출하기 전 GeaList의 SizeOfListInBytes의 사이즈를 검사하는 루틴이 있다.

GeaList의 크기가 검증하는 부분이 존재하기 때문에 정상적인 흐름이라면 비정상적인 크기를 보냈을 때 프로그램이 예외처리와 함께 잘 종료되어야 한다. “*(WORD *) v49>v48” 코드에서 SizeOfListInBytes가 너무 클 경우 함수를 종료하게 된다. 그러나 문제는 이 루틴이 이미 word로 잘못 캐스팅된 후이기 때문에 무의미한 검사가 된다것에 있다. SizeOfListInBytes를 0x10007로 조작해서 보낼 경우 0x0007만 잘라서 검사하기 때문에 SrvOs2GeaListToNt에 진입이 가능하다. SrvOs2GeaListToNt 내부에서는 SrvOs2GeaListSizeToNt를 호출한다. 해당 함수는 Nt format으로 변환하기 위해 Nonpaged pool의 size를 계산하는 함수이다.
SrvOs2GeaListSizeToNt는 반복문을 돌면서 NT format으로 변환하기 위해 Nonpaged pool을 할당받을 크기를 계산한다. 먼저 GeaList의 시작 부분 포인터에 SizeOfListInBytes를 더하여 GeaList의 끝을 가리키는 포인터를 만든다. 그리고 순환 포인터를 하나 두고 끝을 가리키는 포인터보다 작거나 같을 때까지 gea를 순환한다.
NT format 헤더의 크기(12)와 GEA의 value값의 크기를 더하면서 다음 GEA로 포인터를 계속 옮긴다. 그러나 SizeOfListInBytes가 비정상적으로 조절되어 있으면 while문을 비정상적으로 크게 반복하여 존재하지 않는 메모리에 접근하려다 메모리 커럽션이 발생한다. 드라이버에서 발생한 취약점이기 때문에 블루스크린이 발생하면서 윈도우 서버가 종료된다.

# 동적 분석
동적 분석은 Windbg를 통해 진행했다. 브레이크 포인터를 걸기 위해 “bu srv!srvFind2Loop+0xD65A” 명령어를 실행 후에 SMB 패킷을 재전송하였다. 그러면 브레이크가 걸리고 레지스터 RCX를 확인하면 0x10007을 볼 수 있다. 이것은 임의의 패킷을 생성할 때 GeaList의 SizeOfListInBytes를 설정한 값이다.
1: kd> g
Breakpoint 1 hit
srv! ?? ::NNGAKEGL::`string'+0x58e2:
fffff880`0534023a 0fb701 movzx eax,word ptr [rcx]
0: kd> dd @rcx
fffff8a0`1f053124 00010007 00000000 00000000 00000000
fffff8a0`1f053134 00000000 00000000 00000000 00000000
fffff8a0`1f053144 00000000 00000000 00000000 00000000
fffff8a0`1f053154 00000000 00000000 00000000 00000000
fffff8a0`1f053164 00000000 00000000 00000000 00000000
fffff8a0`1f053174 00000000 00000000 00000000 00000000
fffff8a0`1f053184 00000000 00000000 00000000 00000000
fffff8a0`1f053194 00000000 00000000 00000000 00000000
한줄을 실행 후 레지스터를 확인하면 WORD로 잘못된 캐스팅이 되어 0x7로 변경된 것을 알 수 있다.
0: kd> p
0: kd> r
rax=0000000000000007 rbx=fffff8a01eebc060 rcx=fffff8a01f053124
rdx=0000000000000204 rsi=fffff8a01f053010 rdi=0000000000000003
rip=fffff8800534023d rsp=fffff880051bb880 rbp=fffff8a01f053000
r8=0000000000000000 r9=0000000000000000 r10=fffffa8032db4010
r11=0000000000000000 r12=fffff8a01f050200 r13=0000000000000000
r14=fffff8a01f05332a r15=0000000000000000
정상적으로 프로그램이 이 데이터를 처리하려면 DWORD로 인식되어 0x10007의 MAX 값보다 크다고 판단되어 함수가 종료되어야 한다. 그러나 WORD로 변경되기 때문에 cmp ax,6 및 cmp eax,edx를 우회하여 SrvOs2GeaListToNt로 진입이 가능하다.
srv! ?? ::NNGAKEGL::`string'+0x58e5:
fffff880`0534023d 6683f806 cmp ax,6
fffff880`05340241 0f829b000000 jb srv! ?? ::NNGAKEGL::`string'+0x599a (fffff880`053402e2)
fffff880`05340247 0fb7c0 movzx eax,ax
fffff880`0534024a 3bc2 cmp eax,edx
fffff880`0534024c 0f8790000000 ja srv! ?? ::NNGAKEGL::`string'+0x599a (fffff880`053402e2)
fffff880`05340252 4c8d8c24a8000000 lea r9,[rsp+0A8h]
fffff880`0534025a 4c8d842420010000 lea r8,[rsp+120h]
fffff880`05340262 488d942418010000 lea rdx,[rsp+118h]
fffff880`0534026a e831cb0000 call srv!SrvOs2GeaListToNt (fffff880`0534cda0)
디버깅을 진행하기 위해 “bu srv!SrvOs2GeaListSizeToNt+0xa” 명령어를 실행했다. GEAList의 크기인 0x10007의 값에 GEAList의 시작포인터를 구하여 GEAList의 End Point를 구한다.
0: kd> r
rax=0000000000000007 rbx=fffff8a01f053124 rcx=fffff8a01f053124
rdx=fffff8a01f053128 rsi=fffff880051bb9a0 rdi=0000000000000000
rip=fffff8800534ab4a rsp=fffff880051bb828 rbp=fffff880051bb998
r8=0000000000010007 r9=0000000000000000 r10=fffffa8032db4010
r11=0000000000000000 r12=fffff8a01f050200 r13=fffff880051bb928
r14=fffff8a01f05332a r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000246
srv!SrvOs2GeaListSizeToNt+0xa:
fffff880`0534ab4a 4c03c1 add r8,rcx # -> end point of GeaList
0: kd> p
0: kd> r
rax=0000000000000007 rbx=fffff8a01f053124 rcx=fffff8a01f053124
rdx=fffff8a01f053128 rsi=fffff880051bb9a0 rdi=0000000000000000
rip=fffff8800534ab4d rsp=fffff880051bb828 rbp=fffff880051bb998
r8=fffff8a01f06312b r9=0000000000000000 r10=fffffa8032db4010 #<-- r8
r11=0000000000000000 r12=fffff8a01f050200 r13=fffff880051bb928
r14=fffff8a01f05332a r15=0000000000000000
iopl=0 nv up ei ng nz na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000286
srv!SrvOs2GeaListSizeToNt+0xd:
fffff880`0534ab4d eb24 jmp srv!SrvOs2GeaListSizeToNt+0x33 (fffff880`0534ab73)
그리고 End Point까지 반복문을 돌면서 GEA를 순회하여 NT Size를 구하게 되는데 비정상적인 0x10007 값이 더해졌으므로 접근 권한이 없는 메모리까지 접근하여 커널 패닉이 발생한다.
TRAP_FRAME: fffff880051bb690 -- (.trap 0xfffff880051bb690)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=fffff8a020981003 rbx=0000000000000000 rcx=fffff8a02097b124
rdx=fffff8a020981001 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8800534ab58 rsp=fffff880051bb828 rbp=fffff880051bb998
r8=fffff8a02098b12b r9=00000000000172ec r10=0000000000000000
r11=0000000000000000 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei ng nz ac po cy
srv!SrvOs2GeaListSizeToNt+0x18:
fffff880`0534ab58 440fb612 movzx r10d,byte ptr [rdx] ds:1020:1001=??
안타깝게도 exploitable 하지는 않은 버그였다.