metasploit-framework/lib/metasm/samples/dbg-apihook.rb

244 lines
6.7 KiB
Ruby

# This file is part of Metasm, the Ruby assembly manipulation suite
# Copyright (C) 2006-2009 Yoann GUILLOT
#
# Licence is LGPL, see LICENCE in the top-level directory
#
# This sample defines an ApiHook class, that you can subclass to easily hook functions
# in a debugged process. Your custom function will get called whenever an API function is,
# giving you access to the arguments, you can also take control just before control returns
# to the caller.
# See the example in the end for more details.
# As a standalone application, it hooks WriteFile in the 'notepad' process, and make it
# skip the first two bytes of the buffer.
#
require 'metasm'
class ApiHook
attr_accessor :dbg
# rewrite this function to list the hooks you want
# return an array of hashes
def setup
#[{ :function => 'WriteFile', :abi => :stdcall }, # standard function hook
# { :module => 'Foo.dll', :rva => 0x2433, # arbitrary code hook
# :abi => :fastcall, :hookname => 'myhook' }] # hooks named pre_myhook/post_myhook
end
# initialized from a Debugger or a process description that will be debugged
# sets the hooks up, then run_forever
def initialize(dbg)
if not dbg.kind_of? Metasm::Debugger
process = Metasm::OS.current.find_process(dbg)
raise 'no such process' if not process
dbg = process.debugger
end
@dbg = dbg
begin
setup.each { |h| setup_hook(h) }
init_prerun if respond_to?(:init_prerun) # allow subclass to do stuff before main loop
@dbg.run_forever
rescue Interrupt
@dbg.detach #rescue nil
end
end
# setup one function hook
def setup_hook(h)
@las ||= false
if not h[:lib] and not @las
@dbg.loadallsyms
@las = false
elsif h[:lib]
# avoid loadallsyms if specified (regexp against pathname, not exported lib name)
@dbg.loadsyms(h[:lib])
end
pre = "pre_#{h[:hookname] || h[:function]}"
post = "post_#{h[:hookname] || h[:function]}"
nargs = h[:nargs] || method(pre).arity if respond_to?(pre)
if target = h[:address]
elsif target = h[:rva]
modbase = @dbg.modulemap[h[:module]]
raise "cant find module #{h[:module]} in #{@dbg.modulemap.join(', ')}" if not modbase
target += modbase[0]
else
target = h[:function]
end
@dbg.bpx(target, false, h[:condition]) {
@nargs = nargs
catch(:finish) {
@cur_abi = h[:abi]
@ret_longlong = h[:ret_longlong]
if respond_to? pre
args = read_arglist
send pre, *args
end
if respond_to? post
@dbg.bpx(@dbg.func_retaddr, true) {
retval = read_ret
send post, retval, args
}
end
}
}
end
# retrieve the arglist at func entry, from @nargs & @cur_abi
def read_arglist
nr = @nargs
args = []
if (@cur_abi == :fastcall or @cur_abi == :thiscall) and nr > 0
args << @dbg.get_reg_value(:ecx)
nr -= 1
end
if @cur_abi == :fastcall and nr > 0
args << @dbg.get_reg_value(:edx)
nr -= 1
end
nr.times { |i| args << @dbg.func_arg(i) }
args
end
# retrieve the function returned value
def read_ret
ret = @dbg.func_retval
if @ret_longlong
ret = (ret & 0xffffffff) | (@dbg[:edx] << 32)
end
ret
end
# patch the value of an argument
# only valid in pre_hook
# nr starts at 0
def patch_arg(nr, value)
case @cur_abi
when :fastcall
case nr
when 0
@dbg.set_reg_value(:ecx, value)
return
when 1
@dbg.set_reg_value(:edx, value)
return
else
nr -= 2
end
when :thiscall
case nr
when 0
@dbg.set_reg_value(:ecx, value)
return
else
nr -= 1
end
end
@dbg.func_arg_set(nr, value)
end
# patch the function return value
# only valid post_hook
def patch_ret(val)
if @ret_longlong
@dbg.set_reg_value(:edx, (val >> 32) & 0xffffffff)
val &= 0xffffffff
end
@dbg.func_retval_set(val)
end
# skip the function call
# only valid in pre_hook
def finish(retval)
patch_ret(retval)
@dbg.ip = @dbg.func_retaddr
case @cur_abi
when :fastcall
@dbg[:esp] += 4*(@nargs-2) if @nargs > 2
when :thiscall
@dbg[:esp] += 4*(@nargs-1) if @nargs > 1
when :stdcall
@dbg[:esp] += 4*@nargs
end
@dbg.sp += @dbg.cpu.size/8
throw :finish
end
end
if __FILE__ == $0
# this is the class you have to define to hook
#
# setup() defines the list of hooks as an array of hashes
# for exported functions, simply use :function => function name
# for arbitrary hook, :module => 'module.dll', :rva => 0x1234, :hookname => 'myhook' (call pre_myhook/post_myhook)
# :abi can be :stdcall (windows standard export), :fastcall or :thiscall, leave empty for cdecl
# if pre_<function> is defined, it is called whenever the function is entered, via a bpx (int3)
# if post_<function> is defined, it is called whenever the function exists, with a bpx on the return address setup at func entry
# the pre_hook receives all arguments to the original function
# change them with patch_arg(argnr, newval)
# read memory with @dbg.memory_read_int(ptr), or @dbg.memory[ptr, length]
# skip the function call with finish(fake_retval) (!) needs a correct :abi & param count !
# the post_hook receives the function return value
# change it with patch_ret(newval)
class MyHook < ApiHook
def setup
[{ :function => 'WriteFile', :abi => :stdcall }]
end
def init_prerun
puts "hooks ready, save a file in notepad"
end
def pre_WriteFile(handle, pbuf, size, pwritten, overlap)
# we can skip the function call with this
#finish(28)
# spy on the api / trace calls
bufdata = @dbg.memory[pbuf, size]
puts "writing #{bufdata.inspect}"
# but we can also mess with the args
# ex: skip first 2 bytes of the buffer
patch_arg(1, pbuf+2)
patch_arg(2, size-2)
end
def post_WriteFile(retval, arglistcopy)
# we can patch the API return value with this
#patch_retval(42)
# finish messing with the args: fake the nrofbyteswritten
#handle, pbuf, size, pwritten, overlap = arglistcopy
size, pwritten = arglistcopy.values_at(2, 3)
written = @dbg.memory_read_int(pwritten)
if written == size
# if written everything, patch the value so that the program dont detect our intervention
@dbg.memory_write_int(pwritten, written+2)
end
puts "write retval: #{retval}, written: #{written} bytes"
end
end
Metasm::OS.current.get_debug_privilege if Metasm::OS.current.respond_to? :get_debug_privilege
# run our Hook engine on a running 'notepad' instance
MyHook.new('notepad')
# the script ends when notepad exits
end