170 lines
4.7 KiB
Ruby
170 lines
4.7 KiB
Ruby
|
# This file is part of Metasm, the Ruby assembly manipulation suite
|
||
|
# Copyright (C) 2007 Yoann GUILLOT
|
||
|
#
|
||
|
# Licence is LGPL, see LICENCE in the top-level directory
|
||
|
|
||
|
|
||
|
#
|
||
|
# in this exemple we will patch a process specified on the commandline (pid or part of the image name)
|
||
|
# we will retrieve the user32.dll library mapped, and hook every exported function.
|
||
|
# each hook will redirect the code flow to our shellcode, which will display the hooked function
|
||
|
# name in a messagebox.
|
||
|
# The hook is this time a real hook: we overwrite the first instructions with a jump to our code,
|
||
|
# and run those overwritten instruction again before giving control back to original function.
|
||
|
#
|
||
|
# usage: ruby w32hook-advance.rb notepad
|
||
|
# use ruby -d to impress your friends :)
|
||
|
#
|
||
|
# XXX obsolete, should replace all virtallocex etc by WinOS calls
|
||
|
|
||
|
require 'metasm'
|
||
|
|
||
|
include Metasm
|
||
|
|
||
|
# open target
|
||
|
WinOS.get_debug_privilege
|
||
|
if not pr = WinOS.find_process(ARGV.first)
|
||
|
# display list of running processes and exit
|
||
|
puts WinOS.list_processes.sort_by { |pr_| pr_.pid }
|
||
|
exit
|
||
|
end
|
||
|
raise 'cannot open target process' if not pr.handle
|
||
|
|
||
|
# the main shellcode
|
||
|
sc = Shellcode.assemble Ia32.new, <<EOS
|
||
|
main_hook:
|
||
|
pushfd ; save registers
|
||
|
pushad
|
||
|
|
||
|
mov eax, dword ptr [in_hook] ; check if we are in the hook (yay threadsafe)
|
||
|
test eax, eax
|
||
|
jnz main_hook_done
|
||
|
mov dword ptr [in_hook], 1
|
||
|
|
||
|
mov eax, dword ptr [esp+4+4*9] ; get the function name (1st argument)
|
||
|
|
||
|
push 0
|
||
|
push eax
|
||
|
push eax
|
||
|
push 0
|
||
|
call messageboxw
|
||
|
|
||
|
mov dword ptr [in_hook], 0
|
||
|
main_hook_done:
|
||
|
popad
|
||
|
popfd
|
||
|
ret 4
|
||
|
|
||
|
.align 4
|
||
|
in_hook dd 0 ; l33t mutex
|
||
|
EOS
|
||
|
|
||
|
# this is where we store every function hook
|
||
|
hooks = {}
|
||
|
prepare_hook = lambda { |mpe, base, export|
|
||
|
hooklabel = sc.new_label('hook')
|
||
|
namelabel = sc.new_label('name')
|
||
|
|
||
|
# this will overwrite the function entrypoint
|
||
|
target = base + export.target
|
||
|
hooks[target] = Shellcode.new(sc.cpu).share_namespace(sc).parse("jmp #{hooklabel}").assemble.encoded
|
||
|
|
||
|
# backup the overwritten instructions
|
||
|
# retrieve instructions until their length is >= our hook length
|
||
|
mpe.encoded.ptr = export.target
|
||
|
sz = 0
|
||
|
overwritten = []
|
||
|
while sz < hooks[target].length
|
||
|
di = sc.cpu.decode_instruction mpe.encoded, target
|
||
|
if not di or not di.opcode or not di.instruction
|
||
|
puts "W: unknown instruction in #{export.name} !"
|
||
|
break
|
||
|
end
|
||
|
overwritten << di.instruction
|
||
|
sz += di.bin_length
|
||
|
end
|
||
|
puts "overwritten at #{export.name}:", overwritten, '' if $DEBUG
|
||
|
resumeaddr = target + sz
|
||
|
|
||
|
# append the call-specific shellcode to the main hook code
|
||
|
sc.cursource << Label.new(hooklabel)
|
||
|
sc.parse <<EOS
|
||
|
push #{namelabel}
|
||
|
call main_hook ; log the call
|
||
|
; rerun the overwritten instructions
|
||
|
#{overwritten.join("\n")}
|
||
|
jmp #{resumeaddr} ; get back to original code flow
|
||
|
EOS
|
||
|
sc.cursource << Label.new(namelabel)
|
||
|
sc.parse "dw #{export.name.inspect}, 0"
|
||
|
}
|
||
|
|
||
|
msgboxw = nil
|
||
|
# decode interesting libraries from address space
|
||
|
pr.modules[1..-1].each { |m|
|
||
|
# search for messageboxw
|
||
|
if m.path =~ /user32/i
|
||
|
mpe = LoadedPE.load pr.memory[m.addr, 0x1000000]
|
||
|
mpe.decode_header
|
||
|
mpe.decode_exports
|
||
|
mpe.export.exports.each { |e| msgboxw = m.addr + mpe.label_rva(e.target) if e.name == 'MessageBoxW' }
|
||
|
end
|
||
|
# prepare hooks
|
||
|
next if m.path !~ /user32/i # filter interesting libraries
|
||
|
puts "handling #{File.basename m.path}" if $VERBOSE
|
||
|
|
||
|
if not mpe
|
||
|
mpe = LoadedPE.load pr.memory[m.addr, 0x1000000]
|
||
|
mpe.decode_header
|
||
|
mpe.decode_exports
|
||
|
end
|
||
|
next if not mpe.export or not mpe.export.exports
|
||
|
|
||
|
# discard exported data
|
||
|
text = mpe.sections.find { |s| s.name == '.text' }
|
||
|
mpe.export.exports.each { |e|
|
||
|
next if not e.target or not e.name
|
||
|
next if e.name =~ /(?:Translate|Get|Dispatch)Message|CallNextHookEx|TranslateAccelerator/
|
||
|
|
||
|
# ensure we have an offset and not a label name
|
||
|
e.target = mpe.label_rva(e.target)
|
||
|
|
||
|
# ensure the exported thing is in the .text section
|
||
|
next if e.target < text.virtaddr or e.target >= text.virtaddr + text.virtsize
|
||
|
|
||
|
# prepare the hook
|
||
|
prepare_hook[mpe, m.addr, e]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
raise 'Did not find MessageBoxW !' if not msgboxw
|
||
|
|
||
|
puts 'linking...'
|
||
|
sc.assemble
|
||
|
puts 'done'
|
||
|
|
||
|
# allocate memory for our code
|
||
|
raise 'remote allocation failed' if not injected_addr = WinAPI.virtualallocex(pr.handle, 0, sc.encoded.length, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
|
||
|
|
||
|
puts "Injecting hooks at #{'%x' % injected_addr}"
|
||
|
|
||
|
# fixup & inject our code
|
||
|
binding = { 'messageboxw' => msgboxw }
|
||
|
hooks.each { |addr, edata| binding.update edata.binding(addr) }
|
||
|
binding.update sc.encoded.binding(injected_addr)
|
||
|
|
||
|
# fixup
|
||
|
sc.encoded.fixup(binding)
|
||
|
# inject
|
||
|
pr.memory[injected_addr, sc.encoded.data.length] = sc.encoded.data
|
||
|
|
||
|
# now overwrite entry points
|
||
|
hooks.each { |addr, edata|
|
||
|
edata.fixup(binding)
|
||
|
pr.memory[addr, edata.data.length] = edata.data
|
||
|
}
|
||
|
|
||
|
puts 'done'
|
||
|
|
||
|
WinAPI.closehandle(pr.handle)
|