# 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 require 'metasm/os/main' require 'metasm/debug' module Metasm class PTrace attr_accessor :buf, :pid def self.open(target) ptrace = new(target) return ptrace if not block_given? begin yield ptrace ensure ptrace.detach end end # calls PTRACE_TRACEME on the current (ruby) process def self.traceme new(::Process.pid, false).traceme end # creates a ptraced process (target = path) # or opens a running process (target = pid) # values for do_attach: # :create => always fork+traceme+exec+wait # :attach => always attach # false/nil => same as attach, without actually calling PT_ATTACH (useful when the ruby process is already tracing pid) # default/anything else: try to attach if pid is numeric, else create def initialize(target, do_attach=true, &b) case do_attach when :create init_create(target, &b) when :attach init_attach(target) when :dup raise ArgumentError unless target.kind_of?(PTrace) @pid = target.pid tweak_for_pid(@pid, target.tgcpu) # avoid re-parsing /proc/self/exe when nil, false @pid = Integer(target) tweak_for_pid(@pid) else t = begin; Integer(target) rescue ArgumentError, TypeError end t ? init_attach(t) : init_create(target, &b) end end def init_attach(target) @pid = Integer(target) tweak_for_pid(@pid) attach wait puts "Ptrace: attached to #@pid" if $DEBUG end def init_create(target, &b) if not @pid = ::Process.fork tweak_for_pid(::Process.pid) traceme b.call if b ::Process.exec(*target) exit!(0) end wait raise "could not exec #{target}" if $?.exited? tweak_for_pid(@pid) puts "Ptrace: attached to new #@pid" if $DEBUG end def wait ::Process.wait(@pid, ::Process::WALL) end attr_accessor :reg_off, :intsize, :syscallnr, :syscallreg attr_accessor :packint, :packuint, :host_intsize, :host_syscallnr attr_accessor :tgcpu @@sys_ptrace = {} # setup variables according to the target (ptrace interface, syscall nrs, ...) def tweak_for_pid(pid=@pid, tgcpu=nil) # use these for our syscalls PTRACE @@host_csn ||= LinOS.open_process(::Process.pid).cpu.shortname case @@host_csn when 'ia32' @packint = 'l' @packuint = 'L' @host_intsize = 4 @host_syscallnr = SYSCALLNR_I386 @reg_off = REGS_I386 when 'x64' @packint = 'q' @packuint = 'Q' @host_intsize = 8 @host_syscallnr = SYSCALLNR_X86_64 @reg_off = REGS_X86_64 else raise 'unsupported architecture' end @tgcpu = tgcpu || LinOS.open_process(pid).cpu # use these to interpret the child state case @tgcpu.shortname when 'ia32' @syscallreg = 'ORIG_EAX' @syscallnr = SYSCALLNR_I386 @intsize = 4 when 'x64' @syscallreg = 'ORIG_RAX' @syscallnr = SYSCALLNR_X86_64 @intsize = 8 else raise 'unsupported target architecture' end # buffer used in ptrace syscalls @buf = [0].pack(@packint) @sys_ptrace = @@sys_ptrace[@host_syscallnr['ptrace']] ||= setup_sys_ptrace(@host_syscallnr['ptrace']) end def setup_sys_ptrace(sysnr) moo = Class.new(DynLdr) case @@host_csn when 'ia32' # XXX compat lin2.4 ? asm = < 0 off -= decal peekdata(off) off += @host_intsize buf << @buf[decal...@host_intsize] end offend = off + len while off < offend peekdata(off) buf << @buf[0, @host_intsize] off += @host_intsize end buf[0, len] end def writemem(off, str) str.force_encoding('binary') if str.respond_to?(:force_encoding) decal = off % @host_intsize if decal > 0 off -= decal peekdata(off) str = @buf[0...decal] + str end decal = str.length % @host_intsize if decal > 0 peekdata(off+str.length-decal) str += @buf[decal...@host_intsize] end i = 0 while i < str.length pokedata(off+i, str[i, @host_intsize]) i += @host_intsize end true end # linux/ptrace.h COMMAND = { :TRACEME => 0, :PEEKTEXT => 1, :PEEKDATA => 2, :PEEKUSR => 3, :POKETEXT => 4, :POKEDATA => 5, :POKEUSR => 6, :CONT => 7, :KILL => 8, :SINGLESTEP => 9, :ATTACH => 16, :DETACH => 17, :SYSCALL => 24, # arch/x86/include/ptrace-abi.h :GETREGS => 12, :SETREGS => 13, :GETFPREGS => 14, :SETFPREGS => 15, :GETFPXREGS => 18, :SETFPXREGS => 19, :OLDSETOPTIONS => 21, :GET_THREAD_AREA => 25, :SET_THREAD_AREA => 26, :ARCH_PRCTL => 30, :SYSEMU => 31, :SYSEMU_SINGLESTEP=> 32, :SINGLEBLOCK => 33, # 0x4200-0x4300 are reserved for architecture-independent additions. :SETOPTIONS => 0x4200, :GETEVENTMSG => 0x4201, :GETSIGINFO => 0x4202, :SETSIGINFO => 0x4203 } OPTIONS = { # options set using PTRACE_SETOPTIONS 'TRACESYSGOOD' => 0x01, 'TRACEFORK' => 0x02, 'TRACEVFORK' => 0x04, 'TRACECLONE' => 0x08, 'TRACEEXEC' => 0x10, 'TRACEVFORKDONE'=> 0x20, 'TRACEEXIT' => 0x40, 'TRACESECCOMP' => 0x80, } WAIT_EXTENDEDRESULT = { # Wait extended result codes for the above trace options. 'EVENT_FORK' => 1, 'EVENT_VFORK' => 2, 'EVENT_CLONE' => 3, 'EVENT_EXEC' => 4, 'EVENT_VFORK_DONE' => 5, 'EVENT_EXIT' => 6, 'EVENT_SECCOMP' => 7, } WAIT_EXTENDEDRESULT.update WAIT_EXTENDEDRESULT.invert # block trace BTS_O = { 'TRACE' => 1, 'SCHED' => 2, 'SIGNAL' => 4, 'ALLOC' => 8 } BTS = { 'CONFIG' => 40, 'STATUS' => 41, 'SIZE' => 42, 'GET' => 43, 'CLEAR' => 44, 'DRAIN' => 45 } REGS_I386 = { 'EBX' => 0, 'ECX' => 1, 'EDX' => 2, 'ESI' => 3, 'EDI' => 4, 'EBP' => 5, 'EAX' => 6, 'DS' => 7, 'ES' => 8, 'FS' => 9, 'GS' => 10, 'ORIG_EAX' => 11, 'EIP' => 12, 'CS' => 13, 'EFL' => 14, 'UESP'=> 15, 'EFLAGS' => 14, 'ESP' => 15, 'SS' => 16, # from ptrace.c in kernel source & asm-i386/user.h 'DR0' => 63, 'DR1' => 64, 'DR2' => 65, 'DR3' => 66, 'DR4' => 67, 'DR5' => 68, 'DR6' => 69, 'DR7' => 70 } REGS_X86_64 = { 'R15' => 0, 'R14' => 1, 'R13' => 2, 'R12' => 3, 'RBP' => 4, 'RBX' => 5, 'R11' => 6, 'R10' => 7, 'R9' => 8, 'R8' => 9, 'RAX' => 10, 'RCX' => 11, 'RDX' => 12, 'RSI' => 13, 'RDI' => 14, 'ORIG_RAX' => 15, 'RIP' => 16, 'CS' => 17, 'RFLAGS' => 18, 'RSP' => 19, 'SS' => 20, 'FS_BASE' => 21, 'GS_BASE' => 22, 'DS' => 23, 'ES' => 24, 'FS' => 25, 'GS' => 26, #'FP_VALID' => 27, #'387_XWD' => 28, '387_RIP' => 29, '387_RDP' => 30, '387_MXCSR' => 31, #'FP0' => 32, 'FP1' => 34, 'FP2' => 36, 'FP3' => 38, #'FP4' => 40, 'FP5' => 42, 'FP6' => 44, 'FP7' => 46, #'XMM0' => 48, 'XMM1' => 52, 'XMM2' => 56, 'XMM3' => 60, #'XMM4' => 64, 'XMM5' => 68, 'XMM6' => 72, 'XMM7' => 76, #'FPAD0' => 80, 'FPAD11' => 91, #'TSZ' => 92, 'DSZ' => 93, 'SSZ' => 94, 'CODE' => 95, #'STK' => 96, 'SIG' => 97, 'PAD' => 98, 'U_AR0' => 99, #'FPPTR' => 100, 'MAGIC' => 101, 'COMM0' => 102, 'COMM1' => 103, #'COMM2' => 104, 'COMM3' => 105, 'DR0' => 106, 'DR1' => 107, 'DR2' => 108, 'DR3' => 109, 'DR4' => 110, 'DR5' => 111, 'DR6' => 112, 'DR7' => 113, #'ERROR_CODE' => 114, 'FAULT_ADDR' => 115 } SYSCALLNR_I386 = %w[restart_syscall exit fork read write open close waitpid creat link unlink execve chdir time mknod chmod lchown break oldstat lseek getpid mount umount setuid getuid stime ptrace alarm oldfstat pause utime stty gtty access nice ftime sync kill rename mkdir rmdir dup pipe times prof brk setgid getgid signal geteuid getegid acct umount2 lock ioctl fcntl mpx setpgid ulimit oldolduname umask chroot ustat dup2 getppid getpgrp setsid sigaction sgetmask ssetmask setreuid setregid sigsuspend sigpending sethostname setrlimit getrlimit getrusage gettimeofday settimeofday getgroups setgroups select symlink oldlstat readlink uselib swapon reboot readdir mmap munmap truncate ftruncate fchmod fchown getpriority setpriority profil statfs fstatfs ioperm socketcall syslog setitimer getitimer stat lstat fstat olduname iopl vhangup idle vm86old wait4 swapoff sysinfo ipc fsync sigreturn clone setdomainname uname modify_ldt adjtimex mprotect sigprocmask create_module init_module delete_module get_kernel_syms quotactl getpgid fchdir bdflush sysfs personality afs_syscall setfsuid setfsgid _llseek getdents _newselect flock msync readv writev getsid fdatasync _sysctl mlock munlock mlockall munlockall sched_setparam sched_getparam sched_setscheduler sched_getscheduler sched_yield sched_get_priority_max sched_get_priority_min sched_rr_get_interval nanosleep mremap setresuid getresuid vm86 query_module poll nfsservctl setresgid getresgid prctl rt_sigreturn rt_sigaction rt_sigprocmask rt_sigpending rt_sigtimedwait rt_sigqueueinfo rt_sigsuspend pread64 pwrite64 chown getcwd capget capset sigaltstack sendfile getpmsg putpmsg vfork ugetrlimit mmap2 truncate64 ftruncate64 stat64 lstat64 fstat64 lchown32 getuid32 getgid32 geteuid32 getegid32 setreuid32 setregid32 getgroups32 setgroups32 fchown32 setresuid32 getresuid32 setresgid32 getresgid32 chown32 setuid32 setgid32 setfsuid32 setfsgid32 pivot_root mincore madvise getdents64 fcntl64 sys_222 sys_223 gettid readahead setxattr lsetxattr fsetxattr getxattr lgetxattr fgetxattr listxattr llistxattr flistxattr removexattr lremovexattr fremovexattr tkill sendfile64 futex sched_setaffinity sched_getaffinity set_thread_area get_thread_area io_setup io_destroy io_getevents io_submit io_cancel fadvise64 sys_251 exit_group lookup_dcookie epoll_create epoll_ctl epoll_wait remap_file_pages set_tid_address timer_create timer_settime timer_gettime timer_getoverrun timer_delete clock_settime clock_gettime clock_getres clock_nanosleep statfs64 fstatfs64 tgkill utimes fadvise64_64 vserver mbind get_mempolicy set_mempolicy mq_open mq_unlink mq_timedsend mq_timedreceive mq_notify mq_getsetattr kexec_load waitid sys_setaltroot add_key request_key keyctl ioprio_set ioprio_get inotify_init inotify_add_watch inotify_rm_watch migrate_pages openat mkdirat mknodat fchownat futimesat fstatat64 unlinkat renameat linkat symlinkat readlinkat fchmodat faccessat pselect6 ppoll unshare set_robust_list get_robust_list splice sync_file_range tee vmsplice move_pages getcpu epoll_pwait utimensat signalfd timerfd eventfd fallocate timerfd_settime timerfd_gettime signalfd4 eventfd2 epoll_create1 dup3 pipe2 inotify_init1 preadv pwritev rt_tg_sigqueueinfo perf_counter_open].inject({}) { |h, sc| h.update sc => h.length } SYSCALLNR_I386.update SYSCALLNR_I386.invert SYSCALLNR_X86_64 = %w[read write open close stat fstat lstat poll lseek mmap mprotect munmap brk rt_sigaction rt_sigprocmask rt_sigreturn ioctl pread64 pwrite64 readv writev access pipe select sched_yield mremap msync mincore madvise shmget shmat shmctl dup dup2 pause nanosleep getitimer alarm setitimer getpid sendfile socket connect accept sendto recvfrom sendmsg recvmsg shutdown bind listen getsockname getpeername socketpair setsockopt getsockopt clone fork vfork execve exit wait4 kill uname semget semop semctl shmdt msgget msgsnd msgrcv msgctl fcntl flock fsync fdatasync truncate ftruncate getdents getcwd chdir fchdir rename mkdir rmdir creat link unlink symlink readlink chmod fchmod chown fchown lchown umask gettimeofday getrlimit getrusage sysinfo times ptrace getuid syslog getgid setuid setgid geteuid getegid setpgid getppid getpgrp setsid setreuid setregid getgroups setgroups setresuid getresuid setresgid getresgid getpgid setfsuid setfsgid getsid capget capset rt_sigpending rt_sigtimedwait rt_sigqueueinfo rt_sigsuspend sigaltstack utime mknod uselib personality ustat statfs fstatfs sysfs getpriority setpriority sched_setparam sched_getparam sched_setscheduler sched_getscheduler sched_get_priority_max sched_get_priority_min sched_rr_get_interval mlock munlock mlockall munlockall vhangup modify_ldt pivot_root _sysctl prctl arch_prctl adjtimex setrlimit chroot sync acct settimeofday mount umount2 swapon swapoff reboot sethostname setdomainname iopl ioperm create_module init_module delete_module get_kernel_syms query_module quotactl nfsservctl getpmsg putpmsg afs_syscall tuxcall security gettid readahead setxattr lsetxattr fsetxattr getxattr lgetxattr fgetxattr listxattr llistxattr flistxattr removexattr lremovexattr fremovexattr tkill time futex sched_setaffinity sched_getaffinity set_thread_area io_setup io_destroy io_getevents io_submit io_cancel get_thread_area lookup_dcookie epoll_create epoll_ctl_old epoll_wait_old remap_file_pages getdents64 set_tid_address restart_syscall semtimedop fadvise64 timer_create timer_settime timer_gettime timer_getoverrun timer_delete clock_settime clock_gettime clock_getres clock_nanosleep exit_group epoll_wait epoll_ctl tgkill utimes vserver mbind set_mempolicy get_mempolicy mq_open mq_unlink mq_timedsend mq_timedreceive mq_notify mq_getsetattr kexec_load waitid add_key request_key keyctl ioprio_set ioprio_get inotify_init inotify_add_watch inotify_rm_watch migrate_pages openat mkdirat mknodat fchownat futimesat newfstatat unlinkat renameat linkat symlinkat readlinkat fchmodat faccessat pselect6 ppoll unshare set_robust_list get_robust_list splice tee sync_file_range vmsplice move_pages utimensat epoll_pwait signalfd timerfd_create eventfd fallocate timerfd_settime timerfd_gettime accept4 signalfd4 eventfd2 epoll_create1 dup3 pipe2 inotify_init1 preadv pwritev rt_tgsigqueueinfo perf_counter_open].inject({}) { |h, sc| h.update sc => h.length } SYSCALLNR_X86_64.update SYSCALLNR_X86_64.invert SIGNAL = Signal.list.dup SIGNAL.delete SIGNAL.index(0) SIGNAL['TRAP'] ||= 5 # windows+gdbremote SIGNAL.update SIGNAL.invert # include/asm-generic/errno-base.h ERRNO = %w[ERR0 EPERM ENOENT ESRCH EINTR EIO ENXIO E2BIG ENOEXEC EBADF ECHILD EAGAIN ENOMEM EACCES EFAULT ENOTBLK EBUSY EEXIST EXDEV ENODEV ENOTDIR EISDIR EINVAL ENFILE EMFILE ENOTTY ETXTBSY EFBIG ENOSPC ESPIPE EROFS EMLINK EPIPE EDOM ERANGE].inject({}) { |h, e| h.update e => h.length } ERRNO.update ERRNO.invert SIGINFO = { # user-generated signal 'DETHREAD' => -7, # execve killing threads 'TKILL' => -6, 'SIGIO' => -5, 'ASYNCIO' => -4, 'MESGQ' => -3, 'TIMER' => -2, 'QUEUE' => -1, 'USER' => 0, 'KERNEL' => 0x80, # ILL 'ILLOPC' => 1, 'ILLOPN' => 2, 'ILLADR' => 3, 'ILLTRP' => 4, 'PRVOPC' => 5, 'PRVREG' => 6, 'COPROC' => 7, 'BADSTK' => 8, # FPE 'INTDIV' => 1, 'INTOVF' => 2, 'FLTDIV' => 3, 'FLTOVF' => 4, 'FLTUND' => 5, 'FLTRES' => 6, 'FLTINV' => 7, 'FLTSUB' => 8, # SEGV 'MAPERR' => 1, 'ACCERR' => 2, # BUS 'ADRALN' => 1, 'ADRERR' => 2, 'OBJERR' => 3, 'MCEERR_AR' => 4, 'MCEERR_AO' => 5, # TRAP 'BRKPT' => 1, 'TRACE' => 2, 'BRANCH' => 3, 'HWBKPT' => 4, # CHLD 'EXITED' => 1, 'KILLED' => 2, 'DUMPED' => 3, 'TRAPPED' => 4, 'STOPPED' => 5, 'CONTINUED' => 6, # POLL 'POLL_IN' => 1, 'POLL_OUT' => 2, 'POLL_MSG' => 3, 'POLL_ERR' => 4, 'POLL_PRI' => 5, 'POLL_HUP' => 6 } SIGINFO_C = <= 128b */ struct { /* kill(). */ __pid_t si_pid; /* Sending process ID. */ __uid_t si_uid; /* Real user ID of sending process. */ } _kill; struct { /* POSIX.1b timers. */ int si_tid; /* Timer ID. */ int si_overrun; /* Overrun count. */ sigval_t si_sigval; /* Signal value. */ } _timer; struct { /* POSIX.1b signals. */ __pid_t si_pid; /* Sending process ID. */ __uid_t si_uid; /* Real user ID of sending process. */ sigval_t si_sigval; /* Signal value. */ } _rt; struct { /* SIGCHLD. */ __pid_t si_pid; /* Which child. */ __uid_t si_uid; /* Real user ID of sending process. */ int si_status; /* Exit value or signal. */ __clock_t si_utime; __clock_t si_stime; } _sigchld; struct { /* SIGILL, SIGFPE, SIGSEGV, SIGBUS. */ uintptr_t si_addr; /* Faulting insn/memory ref. */ } _sigfault; struct { /* SIGPOLL. */ long int si_band; /* Band event for SIGPOLL. */ int si_fd; } _sigpoll; struct { /* SIGSYS under SECCOMP */ uintptr_t si_calladdr; /* calling insn address */ int si_syscall; /* triggering syscall nr */ int si_arch; /* AUDIT_ARCH_* for syscall */ } _sigsys; }; }; EOS def sys_ptrace(req, pid, addr, data) ret = @sys_ptrace.ptrace(req, pid, addr, data) if ret < 0 and ret > -256 raise SystemCallError.new("ptrace #{COMMAND.index(req) || req}", -ret) end ret end def traceme sys_ptrace(COMMAND[:TRACEME], 0, 0, 0) end def peektext(addr) sys_ptrace(COMMAND[:PEEKTEXT], @pid, addr, @buf) @buf end def peekdata(addr) sys_ptrace(COMMAND[:PEEKDATA], @pid, addr, @buf) @buf end def peekusr(addr) sys_ptrace(COMMAND[:PEEKUSR], @pid, @host_intsize*addr, @buf) @peekmask ||= (1 << ([@host_intsize, @intsize].min*8)) - 1 bufval & @peekmask end def poketext(addr, data) sys_ptrace(COMMAND[:POKETEXT], @pid, addr, data.unpack(@packint).first) end def pokedata(addr, data) sys_ptrace(COMMAND[:POKEDATA], @pid, addr, data.unpack(@packint).first) end def pokeusr(addr, data) sys_ptrace(COMMAND[:POKEUSR], @pid, @host_intsize*addr, data) end def getregs(buf=nil) buf = buf.str if buf.respond_to?(:str) # AllocCStruct buf ||= [0].pack('C')*512 sys_ptrace(COMMAND[:GETREGS], @pid, 0, buf) buf end def setregs(buf) buf = buf.str if buf.respond_to?(:str) sys_ptrace(COMMAND[:SETREGS], @pid, 0, buf) end def getfpregs(buf=nil) buf = buf.str if buf.respond_to?(:str) buf ||= [0].pack('C')*1024 sys_ptrace(COMMAND[:GETFPREGS], @pid, 0, buf) buf end def setfpregs(buf) buf = buf.str if buf.respond_to?(:str) sys_ptrace(COMMAND[:SETFPREGS], @pid, 0, buf) end def getfpxregs(buf=nil) buf = buf.str if buf.respond_to?(:str) buf ||= [0].pack('C')*512 sys_ptrace(COMMAND[:GETFPXREGS], @pid, 0, buf) buf end def setfpxregs(buf) buf = buf.str if buf.respond_to?(:str) sys_ptrace(COMMAND[:SETFPXREGS], @pid, 0, buf) end def get_thread_area(addr) sys_ptrace(COMMAND[:GET_THREAD_AREA], @pid, addr, @buf) bufval end def set_thread_area(addr, data) sys_ptrace(COMMAND[:SET_THREAD_AREA], @pid, addr, data) end def prctl(addr, data) sys_ptrace(COMMAND[:ARCH_PRCTL], @pid, addr, data) end def cont(sig = nil) sig ||= 0 sys_ptrace(COMMAND[:CONT], @pid, 0, sig) end def kill sys_ptrace(COMMAND[:KILL], @pid, 0, 0) end def singlestep(sig = nil) sig ||= 0 sys_ptrace(COMMAND[:SINGLESTEP], @pid, 0, sig) end def singleblock(sig = nil) sig ||= 0 sys_ptrace(COMMAND[:SINGLEBLOCK], @pid, 0, sig) end def syscall(sig = nil) sig ||= 0 sys_ptrace(COMMAND[:SYSCALL], @pid, 0, sig) end def attach sys_ptrace(COMMAND[:ATTACH], @pid, 0, 0) end def detach sys_ptrace(COMMAND[:DETACH], @pid, 0, 0) end def setoptions(*opt) opt = opt.inject(0) { |b, o| b |= o.kind_of?(Integer) ? o : OPTIONS[o] } sys_ptrace(COMMAND[:SETOPTIONS], @pid, 0, opt) end # retrieve pid of cld for EVENT_CLONE/FORK, exitcode for EVENT_EXIT def geteventmsg sys_ptrace(COMMAND[:GETEVENTMSG], @pid, 0, @buf) bufval end def cp @cp ||= @tgcpu.new_cparser end def siginfo @siginfo ||= ( cp.parse SIGINFO_C if not cp.toplevel.struct['siginfo'] cp.alloc_c_struct('siginfo') ) end def getsiginfo sys_ptrace(COMMAND[:GETSIGINFO], @pid, 0, siginfo.str) siginfo end def setsiginfo(si=siginfo) si = si.str if si.respond_to?(:str) sys_ptrace(COMMAND[:SETSIGINFO], @pid, 0, si) end end class LinOS < OS class Process < OS::Process # returns/create a LinuxRemoteString def memory @memory ||= LinuxRemoteString.new(pid) end attr_writer :memory def debugger @debugger ||= LinDebugger.new(@pid) end attr_writer :debugger # returns the list of loaded Modules, incl start address & path # read from /proc/pid/maps def modules list = [] seen = {} File.readlines("/proc/#{pid}/maps").each { |l| # 08048000-08064000 r-xp 000000 08:01 4234 /usr/bin/true l = l.split next if l.length < 6 or seen[l[-1]] seen[l[-1]] = true m = Module.new m.addr = l[0].to_i(16) m.path = l[-1] list << m } list rescue [] end # return a list of [addr_start, length, perms, file] def mappings list = [] File.readlines("/proc/#{pid}/maps").each { |l| l = l.split addrstart, addrend = l[0].split('-').map { |i| i.to_i 16 } list << [addrstart, addrend-addrstart, l[1], l[5]] } list rescue [] end # returns a list of threads sharing this process address space # read from /proc/pid/task/ def threads Dir.entries("/proc/#{pid}/task/").grep(/^\d+$/).map { |tid| tid.to_i } rescue # TODO handle pthread stuff (eg 2.4 kernels) [pid] end # return the invocation commandline, from /proc/pid/cmdline # this is manipulable by the target itself def cmdline @cmdline ||= File.read("/proc/#{pid}/cmdline") rescue '' end attr_writer :cmdline def path cmdline.split(0.chr)[0] end # returns the address size of the process, based on its #cpu def addrsz cpu.size end # returns the CPU for the process, by reading /proc/pid/exe def cpu e = ELF.load_file("/proc/#{pid}/exe") # dont decode shdr/phdr, this is 2x faster for repeated debugger spawn e.decode_header(0, false, false) e.cpu end def terminate kill end def kill(signr=9) ::Process.kill(signr, @pid) end end class << self # returns an array of Processes, with pid/module listing def list_processes Dir.entries('/proc').grep(/^\d+$/).map { |pid| Process.new(pid.to_i) } end # return a Process for the specified pid if it exists in /proc def open_process(pid) Process.new(pid) if check_process(pid) end def check_process(pid) File.directory?("/proc/#{pid}") end # create a LinDebugger on the target pid/binary def create_debugger(path) LinDebugger.new(path) end end # class << self end class LinuxRemoteString < VirtualString attr_accessor :pid, :readfd attr_accessor :dbg # returns a virtual string proxying the specified process memory range # reads are cached (4096 aligned bytes read at once), from /proc/pid/mem # writes are done directly by ptrace def initialize(pid, addr_start=0, length=nil, dbg=nil) @pid = pid length ||= 1 << (dbg ? dbg.cpu.size : (LinOS.open_process(@pid).addrsz rescue 32)) @readfd = File.open("/proc/#@pid/mem", 'rb') rescue nil @dbg = dbg if dbg super(addr_start, length) end def dup(addr = @addr_start, len = @length) self.class.new(@pid, addr, len, dbg) end def do_ptrace(needproc) if dbg dbg.switch_context(@pid) { st = dbg.state next if st != :stopped if needproc # we will try to access /proc/pid/mem # if the main thread is still running, fallback to ptrace.readmem instead pst = (dbg.tid == @pid ? st : dbg.tid_stuff[@pid][:state]) if pst != :stopped savedreadfd = @readfd @readfd = nil begin yield dbg.ptrace ensure @readfd = savedreadfd end else yield dbg.ptrace end else yield dbg.ptrace end } else PTrace.open(@pid) { |ptrace| yield ptrace } end end def rewrite_at(addr, data) # target must be stopped wr = do_ptrace(false) { |ptrace| ptrace.writemem(addr, data) } raise "couldn't ptrace_write at #{'%x' % addr}" if not wr end def get_page(addr, len=@pagelength) do_ptrace(true) { |ptrace| begin if readfd and addr < (1<<63) # 1<<63: ruby seek = 'too big to fit longlong', linux read = EINVAL @readfd.pos = addr @readfd.read len elsif addr < (1<<(ptrace.host_intsize*8)) # can reach 1<<64 with peek_data only if ptrace accepts 64bit args ptrace.readmem(addr, len) end rescue Errno::EIO, Errno::ESRCH nil end } end end class PTraceContext_Ia32 < PTrace C_STRUCT = < true } @gpr_peek = @@gpr_peek_ia32 ||= (0..7).inject({}) { |h, i| h.update "dr#{i}".to_sym => REGS_I386["DR#{i}"] } @gpr_sub = @@gpr_sub_ia32 ||= gpr_sub_init @xmm = @@xmm_ia32 ||= [:cwd, :swd, :twd, :fop, :fip, :fcs, :foo, :fos, :mxcsr].inject({}) { |h, r| h.update r => true } @cp.parse C_STRUCT if not @cp.toplevel.struct['user_regs_struct_ia32'] @gpr_st = @xmm_st = nil end # :bh => [:ebx, 0xff, 8] # XXX similar to Reg.symbolic... DRY def gpr_sub_init ret = {} %w[a b c d].each { |r| b = "e#{r}x".to_sym ret["#{r}x".to_sym] = [b, 0xffff] ret["#{r}l".to_sym] = [b, 0xff] ret["#{r}h".to_sym] = [b, 0xff, 8] } %w[sp bp si di].each { |r| b = "e#{r}".to_sym ret[r.to_sym] = [b, 0xffff] } ret[:orig_rax] = [:orig_eax, 0xffff_ffff] ret end def do_getregs st = cp.alloc_c_struct('user_regs_struct_ia32') getregs(st) st end def do_setregs(st=@gpr_st) setregs(st) end def do_getxmm st = cp.alloc_c_struct('user_fxsr_struct_ia32') getfpxregs(st) st end def do_setxmm(st=@xmm_st) setfpxregs(st) end def get_reg(r) r = r.downcase if r == 'ORIG_EAX' or r == 'ORIG_RAX' rs = r.to_sym if @gpr[rs] @gpr_st ||= do_getregs @gpr_st[rs] elsif o = @gpr_peek[rs] peekusr(o) elsif o = @gpr_sub[rs] v = get_reg(o[0]) v >>= o[2] if o[2] v &= o[1] elsif @xmm[rs] @xmm_st ||= do_getxmm @xmm_st[rs] else case r.to_s when /^st(\d?)$/i i = $1.to_i @xmm_st ||= do_getxmm fu = @xmm_st.st_space [fu[4*i], fu[4*i+1], fu[4*i+2]].pack('L*').unpack('D').first # XXX when /^mmx?(\d)$/i i = $1.to_i @xmm_st ||= do_getxmm fu = @xmm_st.st_space fu[4*i] | (fu[4*i + 1] << 32) when /^xmm(\d+)$/i i = $1.to_i @xmm_st ||= do_getxmm fu = @xmm_st.xmm_space fu[4*i] | (fu[4*i + 1] << 32) | (fu[4*i + 2] << 64) | (fu[4*i + 3] << 96) # TODO when /^ymm(\d+)$/i else raise "unknown register name #{r}" end end end def set_reg(r, v) r = r.downcase if r == 'ORIG_EAX' or r == 'ORIG_RAX' rs = r.to_sym if @gpr[rs] @gpr_st ||= do_getregs @gpr_st[rs] = v do_setregs elsif o = @gpr_peek[rs] pokeusr(o, v) elsif o = @gpr_sub[rs] vo = get_reg(o[0]) msk = o[1] v &= o[1] if o[2] msk <<= o[2] v <<= o[2] end v |= vo & ~msk set_reg(o[0], v) elsif @xmm[rs] @xmm_st ||= do_getxmm @xmm_st[rs] = v do_setxmm else case r.to_s when /^st(\d?)$/i i = $1.to_i @xmm_st ||= do_getxmm fu = @xmm_st.st_space fu[4*i], fu[4*i+1], fu[4*i+2] = [v, -1].pack('DL').unpack('L*') # XXX do_setxmm when /^mmx?(\d)$/i i = $1.to_i @xmm_st ||= do_getxmm fu = @xmm_st.st_space fu[4*i] = v & 0xffff_ffff fu[4*i + 1] = (v >> 32) & 0xffff_ffff do_setxmm when /^xmm(\d+)$/i i = $1.to_i @xmm_st ||= do_getxmm fu = @xmm_st.xmm_space fu[4*i] = v & 0xffff_ffff fu[4*i + 1] = (v >> 32) & 0xffff_ffff fu[4*i + 2] = (v >> 64) & 0xffff_ffff fu[4*i + 3] = (v >> 96) & 0xffff_ffff do_setxmm # TODO when /^ymm(\d+)$/i else raise "unknown register name #{r}" end end end end class PTraceContext_X64 < PTraceContext_Ia32 C_STRUCT = < true } @gpr_peek = @@gpr_peek_x64 ||= (0..7).inject({}) { |h, i| h.update "dr#{i}".to_sym => REGS_X86_64["DR#{i}"] } @gpr_sub = @@gpr_sub_x64 ||= gpr_sub_init @xmm = @@xmm_x64 ||= [:cwd, :swd, :twd, :fop, :rip, :rdp, :mxcsr, :mxcsr_mask].inject({}) { |h, r| h.update r => true } @cp.parse C_STRUCT if not @cp.toplevel.struct['user_regs_struct_x64'] @gpr_st = @xmm_st = nil end def gpr_sub_init ret = {} %w[a b c d].each { |r| b = "r#{r}x".to_sym ret["e#{r}x".to_sym] = [b, 0xffff_ffff] ret[ "#{r}x".to_sym] = [b, 0xffff] ret[ "#{r}l".to_sym] = [b, 0xff] ret[ "#{r}h".to_sym] = [b, 0xff, 8] } %w[sp bp si di].each { |r| b = "r#{r}".to_sym ret["e#{r}".to_sym] = [b, 0xffff_ffff] ret[ "#{r}".to_sym] = [b, 0xffff] ret["#{r}l".to_sym] = [b, 0xff] } (8..15).each { |i| b = "r#{i}".to_sym ret["r#{i}d"] = [b, 0xffff_ffff] ret["r#{i}w"] = [b, 0xffff] ret["r#{i}b"] = [b, 0xff] } ret[:eip] = [:rip, 0xffff_ffff] ret[:eflags] = [:rflags, 0xffff_ffff] ret[:orig_eax] = [:orig_rax, 0xffff_ffff] ret end def do_getregs st = cp.alloc_c_struct('user_regs_struct_x64') getregs(st) st end def do_setregs(st=@gpr_st) setregs(st) end def do_getxmm st = cp.alloc_c_struct('user_i387_struct_x64') getfpregs(st) st end def do_setxmm(st=@xmm_st) setfpregs(st) end end module ::Process WALL = 0x40000000 if not defined? WALL WCLONE = 0x80000000 if not defined? WCLONE end # this class implements a high-level API over the ptrace debugging primitives class LinDebugger < Debugger # ptrace is per-process or per-thread ? attr_accessor :ptrace, :continuesignal, :has_pax_mprotect, :target_syscall, :cached_waitpid attr_accessor :callback_syscall, :callback_branch, :callback_exec 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 def shortname; 'lindbg'; end # attach to a running process and all its threads 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 # 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' 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 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 def initialize_memory @memory = os_process.memory = LinuxRemoteString.new(@pid, 0, nil, self) end def os_process @os_process ||= LinOS.open_process(@pid) end def list_threads os_process.threads end def list_processes LinOS.list_processes end def check_pid(pid) LinOS.check_process(pid) end def mappings os_process.mappings end def modules os_process.modules 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) 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 # attach a thread of the current process 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 # set the debugee ptrace options (notify clone/exec/exit, and fork/vfork depending on @trace_children) 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 # update the current pid relative to tracing children (@trace_children only effects newly traced pid/tid) def do_trace_children each_tid { set_thread_options } end def invalidate @ctx = nil super() end # current thread register values accessor 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 def get_reg_value(r) return 0 if @state != :stopped ctx.get_reg(r) rescue Errno::ESRCH 0 end def set_reg_value(r, v) ctx.set_reg(r, v) end 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 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 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 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 def do_continue @state = :running @ptrace.pid = tid @ptrace.cont(@continuesignal) end def do_singlestep(*a) @state = :running @ptrace.pid = tid @ptrace.singlestep(@continuesignal) end # use the PT_SYSCALL to break on next syscall # regexp allowed to wait a specific syscall 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 def syscall_wait(*a, &b) syscall(*a, &b) wait_target end # use the PT_SINGLEBLOCK to execute until the next branch 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 def singleblock_wait(*a, &b) singleblock(*a, &b) wait_target end # woke up from a PT_SYSCALL 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 # SIGTRAP + SIGINFO_TRAP_BRANCH = ? def evt_branch(info={}) @state = :stopped @info = "branch" callback_branch[info] if callback_branch end # called during sys_execve in the new process 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 def break(&b) @breaking = b || true kill 'STOP' end def kill(sig=nil) return if not tid # XXX tkill ? ::Process.kill(sig2signr(sig), tid) rescue Errno::ESRCH end def pass_current_exception(bool=true) if bool @continuesignal = @saved_csig else @continuesignal = 0 end end 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 # stop debugging the current process 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 def bpx(addr, *a, &b) return hwbp(addr, :x, 1, *a, &b) if @has_pax_mprotect super(addr, *a, &b) end # handles exceptions from PaX-style mprotect restrictions on bpx, # transmute them to hwbp on the fly 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 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 end end