# 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_ is defined, it is called whenever the function is entered, via a bpx (int3) # if post_ 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