class Metasm::LinDebugger

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

Attributes

cached_waitpid[RW]

ptrace is per-process or per-thread ?

callback_branch[RW]
callback_exec[RW]
callback_syscall[RW]
continuesignal[RW]

ptrace is per-process or per-thread ?

has_pax_mprotect[RW]

ptrace is per-process or per-thread ?

ptrace[RW]

ptrace is per-process or per-thread ?

target_syscall[RW]

ptrace is per-process or per-thread ?

Public Class Methods

new(pidpath=nil, &b) click to toggle source
Calls superclass method Metasm::Debugger::new
# File metasm/os/linux.rb, line 1101
def initialize(pidpath=nil, &b)
        super()
        @pid_stuff_list << :has_pax_mprotect << :ptrace       << :breaking << :os_process
        @tid_stuff_list << :continuesignal << :saved_csig << :ctx << :target_syscall

        # by default, break on all signals except SIGWINCH (terminal resize notification)
        @pass_all_exceptions = lambda { |e| e[:signal] == 'WINCH' }

        @callback_syscall = lambda { |i| log "syscall #{i[:syscall]}" }
        @callback_exec = lambda { |i| log "execve #{os_process.path}" }
        @cached_waitpid = []

        return if not pidpath

        t = begin; Integer(pidpath)
            rescue ArgumentError, TypeError
            end
        t ? attach(t) : create_process(pidpath, &b)
end

Public Instance Methods

attach(pid, do_attach=:attach) click to toggle source

attach to a running process and all its threads

# File metasm/os/linux.rb, line 1124
def attach(pid, do_attach=:attach)
        pt = PTrace.new(pid, do_attach)
        set_context(pt.pid, pt.pid)   # swapout+init_newpid
        log "attached #@pid"
        list_threads.each { |tid| attach_thread(tid) if tid != @pid }
        set_tid @pid
end
attach_thread(tid) click to toggle source

attach a thread of the current process

# File metasm/os/linux.rb, line 1200
def attach_thread(tid)
        set_tid tid
        @ptrace.pid = tid
        @ptrace.attach
        @state = :stopped
        # store this waitpid so that we can return it in a future check_target
        ::Process.waitpid(tid, ::Process::WALL)
        # XXX can $? be safely stored?
        @cached_waitpid << [tid, $?.dup]
        log "attached thread #{tid}"
        set_thread_options
rescue Errno::ESRCH
        # raced, thread quitted already
        del_tid
end
bpx(addr, *a, &b) click to toggle source
Calls superclass method Metasm::Debugger#bpx
# File metasm/os/linux.rb, line 1556
def bpx(addr, *a, &b)
        return hwbp(addr, :x, 1, *a, &b) if @has_pax_mprotect
        super(addr, *a, &b)
end
break(&b) click to toggle source
# File metasm/os/linux.rb, line 1499
def break(&b)
        @breaking = b || true
        kill 'STOP'
end
check_pid(pid) click to toggle source
# File metasm/os/linux.rb, line 1173
def check_pid(pid)
        LinOS.check_process(pid)
end
create_process(path, &b) click to toggle source

create a process and debug it if given a block, the block is run in the context of the ruby subprocess after the fork() and before exec()ing the target binary you can use it to eg tweak file descriptors:

tg_stdin_r, tg_stdin_w = IO.pipe
create_process('/bin/cat') { tg_stdin_w.close ; $stdin.reopen(tg_stdin_r) }
tg_stdin_w.write 'lol'
# File metasm/os/linux.rb, line 1139
def create_process(path, &b)
        pt = PTrace.new(path, :create, &b)
        # TODO save path, allow restart etc
        set_context(pt.pid, pt.pid)   # swapout+init_newpid
        log "attached #@pid"
end
ctx() click to toggle source

current thread register values accessor

# File metasm/os/linux.rb, line 1235
def ctx
        @ctx ||= case @ptrace.host_csn
                 when 'ia32'; PTraceContext_Ia32.new(@ptrace, @tid)
                 when 'x64'; PTraceContext_X64.new(@ptrace, @tid)
                 else raise '8==D'
                 end
end
detach() click to toggle source

stop debugging the current process

# File metasm/os/linux.rb, line 1531
def detach
        if @state == :running
                # must be stopped so we can rm bps
                self.break { detach }
                mypid = @pid
                wait_target

                # after syscall(), wait will return once for interrupted syscall,
                # and we need to wait more for the break callback to kick in
                if @pid == mypid and @state == :stopped and @info =~ /syscall/
                        do_continue
                        check_target
                end

                return
        end
        del_all_breakpoints
        each_tid {
                @ptrace.pid = @tid
                @ptrace.detach rescue nil
                @delete_thread = true
        }
        del_pid
end
do_check_target() click to toggle source
# File metasm/os/linux.rb, line 1377
def do_check_target
        if @cached_waitpid.empty?
                t = ::Process.waitpid(-1, ::Process::WNOHANG | ::Process::WALL)
                st = $?
        else
                t, st = @cached_waitpid.shift
        end
        return if not t
        set_tid_findpid t
        update_waitpid st
        true
rescue ::Errno::ECHILD
end
do_continue() click to toggle source
# File metasm/os/linux.rb, line 1403
def do_continue
        @state = :running
        @ptrace.pid = tid
        @ptrace.cont(@continuesignal)
end
do_enable_bp(b) click to toggle source

handles exceptions from PaX-style mprotect restrictions on bpx, transmute them to hwbp on the fly

Calls superclass method Metasm::Debugger#do_enable_bp
# File metasm/os/linux.rb, line 1563
def do_enable_bp(b)
        super(b)
rescue ::Errno::EIO
        if b.type == :bpx
                @memory[b.address, 1]        # check if we can read
                # didn't raise: it's a PaX-style config
                @has_pax_mprotect = true
                b.del
                hwbp(b.address, :x, 1, b.oneshot, b.condition, &b.action)
                log 'PaX: bpx->hwbp'
        else raise
        end
end
do_singlestep(*a) click to toggle source
# File metasm/os/linux.rb, line 1409
def do_singlestep(*a)
        @state = :running
        @ptrace.pid = tid
        @ptrace.singlestep(@continuesignal)
end
do_trace_children() click to toggle source

update the current pid relative to tracing children (@trace_children only effects newly traced pid/tid)

# File metasm/os/linux.rb, line 1225
def do_trace_children
        each_tid { set_thread_options }
end
do_wait_target() click to toggle source
# File metasm/os/linux.rb, line 1391
def do_wait_target
        if @cached_waitpid.empty?
                t = ::Process.waitpid(-1, ::Process::WALL)
                st = $?
        else
                t, st = @cached_waitpid.shift
        end
        set_tid_findpid t
        update_waitpid st
rescue ::Errno::ECHILD
end
evt_branch(info={}) click to toggle source

SIGTRAP + SIGINFO_TRAP_BRANCH = ?

# File metasm/os/linux.rb, line 1480
def evt_branch(info={})
        @state = :stopped
        @info = "branch"

        callback_branch[info] if callback_branch
end
evt_exec(info={}) click to toggle source

called during sys_execve in the new process

# File metasm/os/linux.rb, line 1488
def evt_exec(info={})
        @state = :stopped
        @info = "#{info[:exec]} execve"

        initialize_newpid
        # XXX will receive a SIGTRAP, could hide it..

        callback_exec[info] if callback_exec
        # calling continue() here will loop back to TRAP+INFO_EXEC
end
evt_syscall(info={}) click to toggle source

woke up from a PT_SYSCALL

# File metasm/os/linux.rb, line 1466
def evt_syscall(info={})
        @state = :stopped
        @info = "syscall #{info[:syscall]}"

        callback_syscall[info] if callback_syscall

        if @target_syscall and info[:syscall] !~ /^#@target_syscall$/i
                resume_badbreak
        else
                @target_syscall = nil
        end
end
get_reg_value(r) click to toggle source
# File metasm/os/linux.rb, line 1243
def get_reg_value(r)
        return 0 if @state != :stopped
        ctx.get_reg(r)
rescue Errno::ESRCH
        0
end
hack_x64_32() click to toggle source

We're a 32bit process debugging a 64bit target the ptrace kernel interface we use only allow us a 32bit-like target access With this we advertize the cpu as having eax..edi registers (the only one we can access), while still decoding x64 instructions (whose addr < 4G)

# File metasm/os/linux.rb, line 1189
def hack_x64_32
        log "WARNING: debugging a 64bit process from a 32bit debugger is a very bad idea !"
        ia32 = Ia32.new
        @cpu.instance_variable_set('@dbg_register_pc', ia32.dbg_register_pc)
        @cpu.instance_variable_set('@dbg_register_sp', ia32.dbg_register_sp)
        @cpu.instance_variable_set('@dbg_register_flags', ia32.dbg_register_flags)
        @cpu.instance_variable_set('@dbg_register_list', ia32.dbg_register_list)
        @cpu.instance_variable_set('@dbg_register_size', ia32.dbg_register_size)
end
initialize_cpu() click to toggle source
# File metasm/os/linux.rb, line 1146
def initialize_cpu
        @cpu = os_process.cpu
        # need to init @ptrace here, before init_dasm calls gui.swapin        XXX this stinks
        @ptrace = PTrace.new(@pid, false)
        if @cpu.size == 64 and @ptrace.reg_off['EAX']
                hack_x64_32
        end
        set_tid @pid
        set_thread_options
end
initialize_memory() click to toggle source
# File metasm/os/linux.rb, line 1157
def initialize_memory
        @memory = os_process.memory = LinuxRemoteString.new(@pid, 0, nil, self)
end
invalidate() click to toggle source
Calls superclass method Metasm::Debugger#invalidate
# File metasm/os/linux.rb, line 1229
def invalidate
        @ctx = nil
        super()
end
kill(sig=nil) click to toggle source
# File metasm/os/linux.rb, line 1504
def kill(sig=nil)
        return if not tid
        # XXX tkill ?
        ::Process.kill(sig2signr(sig), tid)
rescue Errno::ESRCH
end
list_processes() click to toggle source
# File metasm/os/linux.rb, line 1169
def list_processes
        LinOS.list_processes
end
list_threads() click to toggle source
# File metasm/os/linux.rb, line 1165
def list_threads
        os_process.threads
end
mappings() click to toggle source
# File metasm/os/linux.rb, line 1177
def mappings
        os_process.mappings
end
modules() click to toggle source
# File metasm/os/linux.rb, line 1181
def modules
        os_process.modules
end
os_process() click to toggle source
# File metasm/os/linux.rb, line 1161
def os_process
        @os_process ||= LinOS.open_process(@pid)
end
pass_current_exception(bool=true) click to toggle source
# File metasm/os/linux.rb, line 1511
def pass_current_exception(bool=true)
        if bool
                @continuesignal = @saved_csig
        else
                @continuesignal = 0
        end
end
set_reg_value(r, v) click to toggle source
# File metasm/os/linux.rb, line 1249
def set_reg_value(r, v)
        ctx.set_reg(r, v)
end
set_thread_options() click to toggle source

set the debugee ptrace options (notify clone/exec/exit, and fork/vfork depending on @trace_children)

# File metasm/os/linux.rb, line 1217
def set_thread_options
        opts  = %w[TRACESYSGOOD TRACECLONE TRACEEXEC TRACEEXIT]
        opts += %w[TRACEFORK TRACEVFORK TRACEVFORKDONE] if trace_children
        @ptrace.pid = @tid
        @ptrace.setoptions(*opts)
end
set_tid_findpid(tid) click to toggle source
# File metasm/os/linux.rb, line 1365
def set_tid_findpid(tid)
        return if tid == @tid
        if tid != @pid and !@tid_stuff[tid]
                if kv = @pid_stuff.find { |k, v| v[:tid_stuff] and v[:tid_stuff][tid] }
                        set_pid kv[0]
                elsif pr = list_processes.find { |p| p.threads.include?(tid) }
                        set_pid pr.pid
                end
        end
        set_tid tid
end
shortname() click to toggle source
# File metasm/os/linux.rb, line 1121
def shortname; 'lindbg'; end
sig2signr(sig) click to toggle source
# File metasm/os/linux.rb, line 1519
def sig2signr(sig)
        case sig
        when nil, ''; 9
        when Integer; sig
        when String
                sig = sig.upcase.sub(/^SIG_?/, '')
                PTrace::SIGNAL[sig] || Integer(sig)
        else raise "unhandled signal #{sig.inspect}"
        end
end
singleblock() click to toggle source

use the PT_SINGLEBLOCK to execute until the next branch

# File metasm/os/linux.rb, line 1442
def singleblock
        # record as singlestep to avoid evt_singlestep -> evt_exception
        # step or block doesn't matter much here anyway
        if b = check_breakpoint_cause and b.hash_shared.find { |bb| bb.state == :active }
                singlestep_bp(b) {
                        next if not check_pre_run(:singlestep)
                        @state = :running
                        @ptrace.pid = @tid
                        @ptrace.singleblock(@continuesignal)
                }
        else
                return if not check_pre_run(:singlestep)
                @state = :running
                @ptrace.pid = @tid
                @ptrace.singleblock(@continuesignal)
        end
end
singleblock_wait(*a, &b) click to toggle source
# File metasm/os/linux.rb, line 1460
def singleblock_wait(*a, &b)
        singleblock(*a, &b)
        wait_target
end
syscall(arg=nil) click to toggle source

use the PT_SYSCALL to break on next syscall regexp allowed to wait a specific syscall

# File metasm/os/linux.rb, line 1417
def syscall(arg=nil)
        arg = nil if arg and arg.strip == ''
        if b = check_breakpoint_cause and b.hash_shared.find { |bb| bb.state == :active }
                singlestep_bp(b) {
                        next if not check_pre_run(:syscall, arg)
                        @target_syscall = arg
                        @state = :running
                        @ptrace.pid = @tid
                        @ptrace.syscall(@continuesignal)
                }
        else
                return if not check_pre_run(:syscall, arg)
                @target_syscall = arg
                @state = :running
                @ptrace.pid = @tid
                @ptrace.syscall(@continuesignal)
        end
end
syscall_wait(*a, &b) click to toggle source
# File metasm/os/linux.rb, line 1436
def syscall_wait(*a, &b)
        syscall(*a, &b)
        wait_target
end
ui_command_setup(ui) click to toggle source
# File metasm/os/linux.rb, line 1577
def ui_command_setup(ui)
        ui.new_command('syscall', 'waits for the target to do a syscall using PT_SYSCALL') { |arg| ui.wrap_run { syscall arg } }
        ui.keyboard_callback[:f6] = lambda { ui.wrap_run { syscall } }

        ui.new_command('signal_cont', 'set/get the continue signal (0 == unset)') { |arg|
                case arg.to_s.strip
                when ''; log "#{@continuesignal} (#{PTrace::SIGNAL[@continuesignal]})"
                else @continuesignal = sig2signr(arg)
                end
        }
end
update_waitpid(status) click to toggle source
# File metasm/os/linux.rb, line 1253
def update_waitpid(status)
        invalidate
        @continuesignal = 0
        @state = :stopped     # allow get_reg (for eg pt_syscall)
        info = { :status => status }
        if status.exited?
                info.update :exitcode => status.exitstatus
                if @tid == @pid              # XXX
                        evt_endprocess info
                else
                        evt_endthread info
                end
        elsif status.signaled?
                info.update :signal => (PTrace::SIGNAL[status.termsig] || status.termsig)
                if @tid == @pid
                        evt_endprocess info
                else
                        evt_endthread info
                end
        elsif status.stopped?
                sig = status.stopsig & 0x7f
                signame = PTrace::SIGNAL[sig]
                if signame == 'TRAP'
                        if status.stopsig & 0x80 > 0
                                # XXX int80 in x64 => syscallnr32 ?
                                evt_syscall info.update(:syscall => @ptrace.syscallnr[get_reg_value(@ptrace.syscallreg)])

                        elsif (status >> 16) > 0
                                case PTrace::WAIT_EXTENDEDRESULT[status >> 16]
                                when 'EVENT_FORK', 'EVENT_VFORK'
                                        # parent notification of a fork
                                        # child receives STOP (may have already happened)
                                        #cld = @ptrace.geteventmsg
                                        resume_badbreak

                                when 'EVENT_CLONE'
                                        #cld = @ptrace.geteventmsg
                                        resume_badbreak

                                when 'EVENT_EXIT'
                                        @ptrace.pid = @tid
                                        info.update :exitcode => @ptrace.geteventmsg
                                        if @tid == @pid
                                                evt_endprocess info
                                        else
                                                evt_endthread info
                                        end

                                when 'EVENT_VFORKDONE'
                                        resume_badbreak

                                when 'EVENT_EXEC'
                                        evt_exec info
                                end

                        else
                                @ptrace.pid = @tid
                                si = @ptrace.getsiginfo
                                case si.si_code
                                when PTrace::SIGINFO['BRKPT'],
                                     PTrace::SIGINFO['KERNEL']     # \xCC prefer KERNEL to BRKPT
                                        evt_bpx
                                when PTrace::SIGINFO['TRACE']
                                        evt_singlestep    # singlestep/singleblock
                                when PTrace::SIGINFO['BRANCH']
                                        evt_branch        # XXX BTS?
                                when PTrace::SIGINFO['HWBKPT']
                                        evt_hwbp
                                else
                                        @saved_csig = @continuesignal = sig
                                        info.update :signal => signame, :type => "SIG#{signame}"
                                        evt_exception info
                                end
                        end

                elsif signame == 'STOP' and @info == 'new'
                        # new thread break on creation (eg after fork + TRACEFORK)
                        if @pid == @tid
                                attach(@pid, false)
                                evt_newprocess info
                        else
                                evt_newthread info
                        end

                elsif signame == 'STOP' and @breaking
                        @state = :stopped
                        @info = 'break'
                        @breaking.call if @breaking.kind_of? Proc
                        @breaking = nil

                else
                        @saved_csig = @continuesignal = sig
                        info.update :signal => signame, :type => "SIG#{signame}"
                        if signame == 'SEGV'
                                # need more data on access violation (for bpm)
                                info.update :type => 'access violation'
                                @ptrace.pid = @tid
                                si = @ptrace.getsiginfo
                                access = case si.si_code
                                         when PTrace::SIGINFO['MAPERR']; :r       # XXX write access to unmapped => ?
                                         when PTrace::SIGINFO['ACCERR']; :w
                                         end
                                info.update :fault_addr => si.si_addr, :fault_access => access
                        end
                        evt_exception info
                end
        else
                log "unknown wait status #{status.inspect}"
                evt_exception info.update(:type => "unknown wait #{status.inspect}")
        end
end