class Metasm::LinDebugger
this class implements a high-level API over the ptrace debugging primitives
Attributes
ptrace is per-process or per-thread ?
ptrace is per-process or per-thread ?
ptrace is per-process or per-thread ?
ptrace is per-process or per-thread ?
ptrace is per-process or per-thread ?
Public Class Methods
# 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 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 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
# 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
# File metasm/os/linux.rb, line 1499 def break(&b) @breaking = b || true kill 'STOP' end
# File metasm/os/linux.rb, line 1173 def check_pid(pid) LinOS.check_process(pid) end
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
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
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
# 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
# File metasm/os/linux.rb, line 1403 def do_continue @state = :running @ptrace.pid = tid @ptrace.cont(@continuesignal) end
handles exceptions from PaX-style mprotect restrictions on bpx, transmute them to hwbp on the fly
# 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
# File metasm/os/linux.rb, line 1409 def do_singlestep(*a) @state = :running @ptrace.pid = tid @ptrace.singlestep(@continuesignal) end
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
# 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
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
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
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
# 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
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
# 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
# File metasm/os/linux.rb, line 1157 def initialize_memory @memory = os_process.memory = LinuxRemoteString.new(@pid, 0, nil, self) end
# File metasm/os/linux.rb, line 1229 def invalidate @ctx = nil super() end
# 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
# File metasm/os/linux.rb, line 1169 def list_processes LinOS.list_processes end
# File metasm/os/linux.rb, line 1165 def list_threads os_process.threads end
# File metasm/os/linux.rb, line 1177 def mappings os_process.mappings end
# File metasm/os/linux.rb, line 1181 def modules os_process.modules end
# File metasm/os/linux.rb, line 1161 def os_process @os_process ||= LinOS.open_process(@pid) end
# File metasm/os/linux.rb, line 1511 def pass_current_exception(bool=true) if bool @continuesignal = @saved_csig else @continuesignal = 0 end end
# File metasm/os/linux.rb, line 1249 def set_reg_value(r, v) ctx.set_reg(r, v) end
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
# 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
# File metasm/os/linux.rb, line 1121 def shortname; 'lindbg'; end
# 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
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
# File metasm/os/linux.rb, line 1460 def singleblock_wait(*a, &b) singleblock(*a, &b) wait_target end
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
# File metasm/os/linux.rb, line 1436 def syscall_wait(*a, &b) syscall(*a, &b) wait_target end
# 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
# 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