class Metasm::WinDebugger

this class implements a high-level API over the Windows debugging primitives

Attributes

auto_fix_fs_bug[RW]
callback_debugstring[RW]
callback_ripevent[RW]
callback_unloadlibrary[RW]
continuecode[RW]
os_process[RW]
os_thread[RW]

Public Class Methods

new(pidpath=nil) click to toggle source
Calls superclass method Metasm::Debugger.new
# File metasm/os/windows.rb, line 1740
def initialize(pidpath=nil)
        super()
        @pid_stuff_list << :os_process
        @tid_stuff_list << :os_thread << :ctx << :continuecode

        @auto_fix_fs_bug = false

        return if not pidpath

        begin
                npid = Integer(pidpath)
                attach(npid)
        rescue ArgumentError
                create_process(pidpath)
        end

        check_target until pid
end

Public Instance Methods

attach(npid) click to toggle source
# File metasm/os/windows.rb, line 1761
def attach(npid)
        WinAPI.debugactiveprocess(npid)
        WinAPI.debugsetprocesskillonexit(0) if WinAPI.respond_to?(:debugsetprocesskillonexit)
        100.times {
                check_target
                break if pid
        }
        raise "attach failed" if not pid
end
break() click to toggle source
# File metasm/os/windows.rb, line 2076
def break
        return if @state != :running
        # debugbreak() will create a new thread to 0xcc, but wont touch existing threads
        suspend
end
check_pid(pid) click to toggle source
# File metasm/os/windows.rb, line 1838
def check_pid(pid)
        WinOS.check_process(pid)
end
check_tid(tid) click to toggle source
Calls superclass method Metasm::Debugger#check_tid
# File metasm/os/windows.rb, line 1842
def check_tid(tid)
        # dont raise() on the first set_context when os_proc is not set yet
        return true if not os_process
        super(tid)
end
create_process(target) click to toggle source
# File metasm/os/windows.rb, line 1771
def create_process(target)
        startupinfo = WinAPI.alloc_c_struct('STARTUPINFOA', :cb => :size)
        processinfo = WinAPI.alloc_c_struct('PROCESS_INFORMATION')
        flags  = WinAPI::DEBUG_PROCESS
        flags |= WinAPI::DEBUG_ONLY_THIS_PROCESS if not trace_children
        target = target.dup if target.frozen? # eg ARGV
        h = WinAPI.createprocessa(nil, target, nil, nil, 0, flags, nil, nil, startupinfo, processinfo)
        raise "CreateProcess: #{WinAPI.last_error_msg}" if not h

        set_context(processinfo.dwprocessid, processinfo.dwthreadid)
        @os_process = WinOS::Process.new(processinfo.dwprocessid, processinfo.hprocess)
        @os_thread  = WinOS::Thread.new(processinfo.dwthreadid, processinfo.hthread, @os_process)
        initialize_osprocess
        check_target
end
ctx() click to toggle source
# File metasm/os/windows.rb, line 1848
def ctx
        if not @ctx
                # swapin_tid => gui.swapin_tid => getreg before we init os_thread in EventCreateThread
                return Hash.new(0) if not os_thread
                @ctx = os_thread.context
                @ctx.update
        end
        @ctx
end
del_pid() click to toggle source
Calls superclass method Metasm::Debugger#del_pid
# File metasm/os/windows.rb, line 2070
def del_pid
        # tell Windows to release the PROCESS object
        WinAPI.debugactiveprocessstop(@pid) if WinAPI.respond_to?(:debugactiveprocessstop)
        super()
end
del_tid() click to toggle source
Calls superclass method Metasm::Debugger#del_tid
# File metasm/os/windows.rb, line 2059
def del_tid
        # tell Windows to release the THREAD object
        WinAPI.continuedebugevent(@pid, @tid, @continuecode)
        super()
end
del_tid_notid() click to toggle source

do nothing, windows will send us a EXIT_PROCESS event

# File metasm/os/windows.rb, line 2066
def del_tid_notid
        nil while do_waitfordebug(10) and !@tid
end
detach() click to toggle source
# File metasm/os/windows.rb, line 2096
def detach
        del_all_breakpoints
        if not WinAPI.respond_to? :debugactiveprocessstop
                raise 'detach not supported'
        end
        # handle pending bp events
        # TODO check_target needs the Breakpoint objects...
        #pid = @pid ; 50.times { check_target } ; self.pid = pid

        # if we detach after a dbgevt and before calling continuedbgevent, the thread
        # may receive unhandled exceptions (eg BPX) and crash the process right after detach
        each_tid { do_continue if @state == :stopped }
        del_pid
end
do_check_target() click to toggle source
# File metasm/os/windows.rb, line 2043
def do_check_target
        do_waitfordebug(0)
end
do_continue(*a) click to toggle source
# File metasm/os/windows.rb, line 1877
def do_continue(*a)
        @cpu.dbg_disable_singlestep(self)
        if @continuecode == :suspended
                resume
        else
                @state = :running
                WinAPI.continuedebugevent(@pid, @tid, @continuecode)
        end
end
do_disable_bpm(bp) click to toggle source
# File metasm/os/windows.rb, line 1904
def do_disable_bpm(bp)
        @bpm_info ||= WinAPI.alloc_c_struct("MEMORY_BASIC_INFORMATION#{WinAPI.host_cpu.size}")
        WinAPI.virtualqueryex(os_process.handle, bp.address, @bpm_info, @bpm_info.sizeof)
        WinAPI.virtualprotectex(os_process.handle, bp.address, bp.internal[:len], @bpm_info[:protect] & ~WinAPI::PAGE_GUARD, @bpm_info)
end
do_enable_bpm(bp) click to toggle source
# File metasm/os/windows.rb, line 1897
def do_enable_bpm(bp)
        @bpm_info ||= WinAPI.alloc_c_struct("MEMORY_BASIC_INFORMATION#{WinAPI.host_cpu.size}")
        WinAPI.virtualqueryex(os_process.handle, bp.address, @bpm_info, @bpm_info.sizeof)
        # TODO save original page perms, check bpm type (:w -> vprotect(PAGE_READONLY)), handle multiple bpm on same page
        WinAPI.virtualprotectex(os_process.handle, bp.address, bp.internal[:len], @bpm_info[:protect] | WinAPI::PAGE_GUARD, @bpm_info)
end
do_singlestep(*a) click to toggle source
# File metasm/os/windows.rb, line 1887
def do_singlestep(*a)
        @cpu.dbg_enable_singlestep(self)
        if @continuecode == :suspended
                resume
        else
                @state = :running
                WinAPI.continuedebugevent(@pid, @tid, @continuecode)
        end
end
do_wait_target() click to toggle source
# File metasm/os/windows.rb, line 2047
def do_wait_target
        do_waitfordebug(WinAPI::INFINITE)
end
do_waitfordebug(timeout) click to toggle source
# File metasm/os/windows.rb, line 2051
def do_waitfordebug(timeout)
        @dbg_eventstruct ||= WinAPI.alloc_c_struct('_DEBUG_EVENT')
        if WinAPI.waitfordebugevent(@dbg_eventstruct, timeout) != 0
                update_dbgev(@dbg_eventstruct)
                true
        end
end
evt_debugstring(info={}) click to toggle source
# File metasm/os/windows.rb, line 2013
def evt_debugstring(info={})
        @state = :stopped
        @info = "debugstring"

        log "Debugstring: #{info[:string].inspect}"

        callback_debugstring[info] if callback_debugstring

        # allow callback to skip this call to continue() by setting info[:nocontinue] = true
        continue unless info[:nocontinue]
end
evt_ripevent(info={}) click to toggle source
# File metasm/os/windows.rb, line 2034
def evt_ripevent(info={})
        @state = :stopped
        @info = "rip_event"   # wtf?

        callback_ripevent[info] if callback_ripevent

        continue unless info[:nocontinue]
end
evt_unloadlibrary(info={}) click to toggle source
# File metasm/os/windows.rb, line 2025
def evt_unloadlibrary(info={})
        @state = :stopped
        @info = "unload library"

        callback_unloadlibrary[info] if callback_unloadlibrary

        continue unless info[:nocontinue]
end
get_reg_value(r) click to toggle source
# File metasm/os/windows.rb, line 1863
def get_reg_value(r)
        ctx[r]
end
initialize_cpu() click to toggle source
# File metasm/os/windows.rb, line 1806
def initialize_cpu
        # wait until we receive the CREATE_PROCESS_DBGEVT message
        return if not @os_process
        case WinAPI.host_cpu.shortname
        when 'ia32', 'x64'
                @cpu = Ia32.new(os_process.cpusz)
        else
                raise 'unsupported architecture'
        end
end
initialize_memory() click to toggle source
# File metasm/os/windows.rb, line 1817
def initialize_memory
        return if not @os_process
        @memory = os_process.memory
end
initialize_newpid() click to toggle source
Calls superclass method Metasm::Debugger#initialize_newpid
# File metasm/os/windows.rb, line 1794
def initialize_newpid
        raise "non-existing pid #@pid" if pid and not WinOS.check_process(@pid)
        super()
        # os_process etc wait for CREATE_THREAD_DBGEVT
end
initialize_newtid() click to toggle source
Calls superclass method Metasm::Debugger#initialize_newtid
# File metasm/os/windows.rb, line 1800
def initialize_newtid
        super()
        # os_thread etc wait for CREATE_THREAD_DBGEVT
        @continuecode = WinAPI::DBG_CONTINUE  #WinAPI::DBG_EXCEPTION_NOT_HANDLED
end
initialize_osprocess() click to toggle source

called whenever we receive a handle to a new process being debugged, after initialisation of @os_process

# File metasm/os/windows.rb, line 1788
def initialize_osprocess
        initialize_cpu
        initialize_memory
        initialize_disassembler
end
invalidate() click to toggle source
Calls superclass method Metasm::Debugger#invalidate
# File metasm/os/windows.rb, line 1858
def invalidate
        @ctx = nil
        super()
end
kill(exitcode=0) click to toggle source
# File metasm/os/windows.rb, line 2111
def kill(exitcode=0)
        os_process.terminate(exitcode)
end
list_processes() click to toggle source
# File metasm/os/windows.rb, line 1830
def list_processes
        WinOS.list_processes
end
list_threads() click to toggle source
# File metasm/os/windows.rb, line 1834
def list_threads
        os_process.threads
end
mappings() click to toggle source
# File metasm/os/windows.rb, line 1822
def mappings
        os_process.mappings
end
modules() click to toggle source
# File metasm/os/windows.rb, line 1826
def modules
        os_process.modules
end
pass_current_exception(doit = true) click to toggle source
# File metasm/os/windows.rb, line 2115
def pass_current_exception(doit = true)
        return if @continuecode == :suspended
        @continuecode = (doit ? WinAPI::DBG_EXCEPTION_NOT_HANDLED : WinAPI::DBG_CONTINUE)
end
resume() click to toggle source
# File metasm/os/windows.rb, line 2090
def resume
        @state = :running
        @info = nil
        os_thread.resume
end
set_reg_value(r, v) click to toggle source
# File metasm/os/windows.rb, line 1867
def set_reg_value(r, v)
        if @state == :running
                suspend
                ctx[r] = v
                resume
        else
                ctx[r] = v
        end
end
shortname() click to toggle source
# File metasm/os/windows.rb, line 1759
def shortname; 'windbg'; end
suspend() click to toggle source
# File metasm/os/windows.rb, line 2082
def suspend
        os_thread.suspend
        invalidate
        @state = :stopped
        @info = 'thread suspended'
        @continuecode = :suspended
end
update_dbgev(ev) click to toggle source
# File metasm/os/windows.rb, line 1910
def update_dbgev(ev)
        # XXX ev is static, copy all necessary values before yielding to something that may call check_target
        set_context ev.dwprocessid, ev.dwthreadid
        invalidate
        @continuecode = WinAPI::DBG_CONTINUE

        # XXX reinterpret ev as struct32/64 depending on os_process.addrsz ?
        case ev.dwdebugeventcode
        when WinAPI::EXCEPTION_DEBUG_EVENT
                st = ev.exception
                str = st.exceptionrecord
                stf = st.dwfirstchance       # non-zero = first chance

                @state = :stopped
                @info = "exception"

                # DWORD ExceptionCode;
                # DWORD ExceptionFlags;
                # struct _EXCEPTION_RECORD *ExceptionRecord;
                # PVOID ExceptionAddress;
                # DWORD NumberParameters;
                # ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
                case str.exceptioncode
                when WinAPI::STATUS_ACCESS_VIOLATION, WinAPI::STATUS_GUARD_PAGE_VIOLATION
                        if @auto_fix_fs_bug and ctx.fs != 0x3b
                                # fix bug in xpsp1 where fs would get a random value in a debugee
                                log "wdbg: #{pid}:#{tid} fix fs bug" if $DEBUG
                                ctx.fs = 0x3b
                                resume_badbreak
                                return
                        end
                        mode = case str.exceptioninformation[0]
                               when 0; :r
                               when 1; :w
                               when 8; :x
                               end
                        addr = str.exceptioninformation[1]
                        evt_exception(:type => 'access violation', :st => str, :firstchance => stf,
                                      :fault_addr => addr, :fault_access => mode)
                when WinAPI::STATUS_BREAKPOINT, WinAPI::STATUS_WX86_BREAKPOINT
                        # we must ack ntdll interrupts on process start
                        # but we should not mask process-generated exceptions by default..
                        evt_bpx
                when WinAPI::STATUS_SINGLE_STEP, WinAPI::STATUS_WX86_SINGLE_STEP
                        evt_hwbp_singlestep
                else
                        @status_name ||= WinAPI.cp.lexer.definition.keys.grep(/^STATUS_/).
                                        sort.inject({}) { |h, c| h.update WinAPI.const_get(c) => c }
                        type = @status_name[str.exceptioncode] || str.exceptioncode.to_s(16)
                        evt_exception(:type => type, :st => str, :firstchance => stf)
                end

        when WinAPI::CREATE_THREAD_DEBUG_EVENT
                st = ev.createthread
                @os_thread ||= WinOS::Thread.new(@tid, st.hthread, os_process)
                @os_thread.teb_base = st.lpthreadlocalbase if st.lpthreadlocalbase.to_i != 0
                evt_newthread(:st => st)

        when WinAPI::CREATE_PROCESS_DEBUG_EVENT
                # XXX 32 vs 64 struct undecidable before we get hprocess..
                st = ev.createprocess
                if not @os_process
                        @os_process = WinOS::Process.new(@pid, st.hprocess)
                        @os_thread ||= WinOS::Thread.new(@tid, st.hthread, os_process)
                        initialize_osprocess
                else
                        @os_thread ||= WinOS::Thread.new(@tid, st.hthread, os_process)
                end
                @os_thread.teb_base = st.lpthreadlocalbase if st.lpthreadlocalbase.to_i != 0
                hfile = st.hfile
                evt_newprocess(:st => st)
                WinAPI.closehandle(hfile)

        when WinAPI::EXIT_THREAD_DEBUG_EVENT
                st = ev.exitthread
                evt_endthread(:exitcode => st.dwexitcode)

        when WinAPI::EXIT_PROCESS_DEBUG_EVENT
                st = ev.exitprocess
                evt_endprocess(:exitcode => st.dwexitcode)

        when WinAPI::LOAD_DLL_DEBUG_EVENT
                st = ev.loaddll
                hfile = st.hfile
                evt_loadlibrary(:address => st.lpbaseofdll, :st => st)
                WinAPI.closehandle(hfile)

        when WinAPI::UNLOAD_DLL_DEBUG_EVENT
                st = ev.unloaddll
                evt_unloadlibrary(:address => st.lpbaseofdll)

        when WinAPI::OUTPUT_DEBUG_STRING_EVENT
                st = ev.debugstring
                str = WinAPI.decode_c_ary("__int#{st.funicode != 0 ? 16 : 8}", st.ndebugstringlength, @memory, st.lpdebugstringdata) if st.lpdebugstringdata
                str = str.to_array.pack('C*') rescue str.to_array.pack('v*')
                evt_debugstring(:string => str, :st => str)

        when WinAPI::RIP_EVENT
                st = ev.ripinfo
                evt_ripevent(:st => st)
        end
end