383 lines
9.5 KiB
Ruby
383 lines
9.5 KiB
Ruby
|
# This file is part of Metasm, the Ruby assembly manipulation suite
|
||
|
# Copyright (C) 2007 Yoann GUILLOT
|
||
|
#
|
||
|
# Licence is LGPL, see LICENCE in the top-level directory
|
||
|
|
||
|
#
|
||
|
# this exemple illustrates the use of the PTrace32 class to implement a pytstop-like functionnality
|
||
|
# Works on linux/x86
|
||
|
#
|
||
|
|
||
|
require 'metasm'
|
||
|
|
||
|
class Rubstop < Metasm::PTrace32
|
||
|
EFLAGS = {0 => 'c', 2 => 'p', 4 => 'a', 6 => 'z', 7 => 's', 9 => 'i', 10 => 'd', 11 => 'o'}
|
||
|
# define accessors for registers
|
||
|
%w[eax ebx ecx edx ebp esp edi esi eip orig_eax eflags dr0 dr1 dr2 dr3 dr6 dr7 cs ds es fs gs].each { |reg|
|
||
|
define_method(reg) { peekusr(REGS_I386[reg.upcase]) & 0xffffffff }
|
||
|
define_method(reg+'=') { |v|
|
||
|
@regs_cache[reg] = v
|
||
|
v = [v].pack('L').unpack('l').first if v >= 0x8000_0000
|
||
|
pokeusr(REGS_I386[reg.upcase], v)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
def cont(signal=0)
|
||
|
@ssdontstopbp = nil
|
||
|
singlestep(true) if @wantbp
|
||
|
super(signal)
|
||
|
::Process.waitpid(@pid)
|
||
|
return if child.exited?
|
||
|
@oldregs.update @regs_cache
|
||
|
readregs
|
||
|
checkbp
|
||
|
end
|
||
|
|
||
|
def singlestep(justcheck=false)
|
||
|
super()
|
||
|
::Process.waitpid(@pid)
|
||
|
return if child.exited?
|
||
|
case @wantbp
|
||
|
when ::Integer; bpx @wantbp ; @wantbp = nil
|
||
|
when ::String; self.dr7 |= 1 << (2*@wantbp[2, 1].to_i) ; @wantbp = nil
|
||
|
end
|
||
|
return if justcheck
|
||
|
@oldregs.update @regs_cache
|
||
|
readregs
|
||
|
checkbp
|
||
|
end
|
||
|
|
||
|
def stepover
|
||
|
i = curinstr.instruction if curinstr
|
||
|
if i and (i.opname == 'call' or (i.prefix and i.prefix[:rep]))
|
||
|
eaddr = @regs_cache['eip'] + curinstr.bin_length
|
||
|
bpx eaddr, true
|
||
|
cont
|
||
|
else
|
||
|
singlestep
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def stepout
|
||
|
# XXX @regs_cache..
|
||
|
stepover until curinstr.opcode.name == 'ret'
|
||
|
singlestep
|
||
|
end
|
||
|
|
||
|
def syscall
|
||
|
@ssdontstopbp = nil
|
||
|
singlestep(true) if @wantbp
|
||
|
super()
|
||
|
::Process.waitpid(@pid)
|
||
|
return if child.exited?
|
||
|
@oldregs.update @regs_cache
|
||
|
readregs
|
||
|
checkbp
|
||
|
end
|
||
|
|
||
|
attr_accessor :pgm, :regs_cache, :breakpoints, :singleshot, :wantbp,
|
||
|
:symbols, :symbols_len, :filemap, :has_pax, :oldregs
|
||
|
def initialize(*a)
|
||
|
super(*a)
|
||
|
@pgm = Metasm::ExeFormat.new Metasm::Ia32.new
|
||
|
@pgm.encoded = Metasm::EncodedData.new Metasm::LinuxRemoteString.new(@pid)
|
||
|
@pgm.encoded.data.ptrace = self
|
||
|
@regs_cache = {}
|
||
|
@oldregs = {}
|
||
|
readregs
|
||
|
@oldregs.update @regs_cache
|
||
|
@breakpoints = {}
|
||
|
@singleshot = {}
|
||
|
@wantbp = nil
|
||
|
@symbols = {}
|
||
|
@symbols_len = {}
|
||
|
@filemap = {}
|
||
|
@has_pax = false
|
||
|
|
||
|
stack = self[regs_cache['esp'], 0x1000].to_str.unpack('L*')
|
||
|
stack.shift # argc
|
||
|
stack.shift until stack.empty? or stack.first == 0 # argv
|
||
|
stack.shift
|
||
|
stack.shift until stack.empty? or stack.first == 0 # envp
|
||
|
stack.shift
|
||
|
stack.shift until stack.empty? or stack.shift == 3 # find PHDR ptr in auxv
|
||
|
if phdr = stack.shift
|
||
|
phdr &= 0xffff_f000
|
||
|
loadsyms phdr, phdr.to_s(16)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def readregs
|
||
|
%w[eax ebx ecx edx esi edi esp ebp eip orig_eax eflags dr0 dr1 dr2 dr3 dr6 dr7 cs ds].each { |r| @regs_cache[r] = send(r) }
|
||
|
@curinstr = nil if @regs_cache['eip'] != @oldregs['eip']
|
||
|
@pgm.encoded.data.invalidate
|
||
|
end
|
||
|
|
||
|
def curinstr
|
||
|
@curinstr ||= mnemonic_di
|
||
|
end
|
||
|
|
||
|
def child
|
||
|
$?
|
||
|
end
|
||
|
|
||
|
def checkbp
|
||
|
::Process::waitpid(@pid, ::Process::WNOHANG) if not child
|
||
|
return if not child
|
||
|
if not child.stopped?
|
||
|
if child.exited?; log "process exited with status #{child.exitstatus}"
|
||
|
elsif child.signaled?; log "process exited due to signal #{child.termsig} (#{Signal.list.index child.termsig})"
|
||
|
else log "process in unknown status #{child.inspect}"
|
||
|
end
|
||
|
return
|
||
|
elsif child.stopsig != ::Signal.list['TRAP']
|
||
|
log "process stopped due to signal #{child.stopsig} (#{Signal.list.index child.stopsig})"
|
||
|
return # do not check 0xcc at eip-1 ! ( if curinstr.bin_length == 1 )
|
||
|
end
|
||
|
ccaddr = @regs_cache['eip']-1
|
||
|
if @breakpoints[ccaddr] and self[ccaddr] == 0xcc
|
||
|
if @ssdontstopbp != ccaddr
|
||
|
self[ccaddr] = @breakpoints.delete ccaddr
|
||
|
self.eip = ccaddr
|
||
|
@wantbp = ccaddr if not @singleshot.delete ccaddr
|
||
|
@ssdontstopbp = ccaddr
|
||
|
else
|
||
|
@ssdontstopbp = nil
|
||
|
end
|
||
|
elsif @regs_cache['dr6'] & 15 != 0
|
||
|
dr = (0..3).find { |dr_| @regs_cache['dr6'] & (1 << dr_) != 0 }
|
||
|
@wantbp = "dr#{dr}" if not @singleshot.delete @regs_cache['eip']
|
||
|
self.dr6 = 0
|
||
|
self.dr7 = @regs_cache['dr7'] & (0xffff_ffff ^ (3 << (2*dr)))
|
||
|
readregs
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def bpx(addr, singleshot=false)
|
||
|
@singleshot[addr] = singleshot
|
||
|
return if @breakpoints[addr]
|
||
|
if @has_pax
|
||
|
set_hwbp 'x', addr
|
||
|
else
|
||
|
begin
|
||
|
@breakpoints[addr] = self[addr]
|
||
|
self[addr] = 0xcc
|
||
|
rescue Errno::EIO
|
||
|
log 'i/o error when setting breakpoint, switching to PaX mode'
|
||
|
@has_pax = true
|
||
|
@breakpoints.delete addr
|
||
|
bpx(addr, singleshot)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def mnemonic_di(addr = eip)
|
||
|
@pgm.encoded.ptr = addr
|
||
|
di = @pgm.cpu.decode_instruction(@pgm.encoded, addr)
|
||
|
@curinstr = di if addr == @regs_cache['eip']
|
||
|
di
|
||
|
end
|
||
|
|
||
|
def mnemonic(addr=eip)
|
||
|
mnemonic_di(addr).instruction
|
||
|
end
|
||
|
|
||
|
def regs_dump
|
||
|
[%w[eax ebx ecx edx orig_eax], %w[ebp esp edi esi eip]].map { |l|
|
||
|
l.map { |reg| "#{reg}=#{'%08x' % @regs_cache[reg]}" }.join(' ')
|
||
|
}.join("\n")
|
||
|
end
|
||
|
|
||
|
def findfilemap(s)
|
||
|
@filemap.keys.find { |k| @filemap[k][0] <= s and @filemap[k][1] > s } || '???'
|
||
|
end
|
||
|
|
||
|
def findsymbol(k)
|
||
|
file = findfilemap(k) + '!'
|
||
|
if s = @symbols[k] ? k : @symbols.keys.find { |s_| s_ < k and s_ + @symbols_len[s_].to_i > k }
|
||
|
file + @symbols[s] + (s == k ? '' : "+#{(k-s).to_s(16)}")
|
||
|
else
|
||
|
file + ('%08x' % k)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def set_hwbp(type, addr, len=1)
|
||
|
dr = (0..3).find { |dr_| @regs_cache['dr7'] & (1 << (2*dr_)) == 0 and @wantbp != "dr#{dr}" }
|
||
|
if not dr
|
||
|
log 'no debug reg available :('
|
||
|
return false
|
||
|
end
|
||
|
@regs_cache['dr7'] &= 0xffff_ffff ^ (0xf << (16+4*dr))
|
||
|
case type
|
||
|
when 'x'; addr += 0x6000_0000 if @has_pax
|
||
|
when 'r'; @regs_cache['dr7'] |= (((len-1)<<2)|3) << (16+4*dr)
|
||
|
when 'w'; @regs_cache['dr7'] |= (((len-1)<<2)|1) << (16+4*dr)
|
||
|
end
|
||
|
send("dr#{dr}=", addr)
|
||
|
self.dr6 = 0
|
||
|
self.dr7 = @regs_cache['dr7'] | (1 << (2*dr))
|
||
|
readregs
|
||
|
true
|
||
|
end
|
||
|
|
||
|
def clearbreaks
|
||
|
@wantbp = nil if @wantbp == @regs_cache['eip']
|
||
|
@breakpoints.each { |addr, oct| self[addr, 1] = oct }
|
||
|
@breakpoints.clear
|
||
|
if @regs_cache['dr7'] & 0xff != 0
|
||
|
self.dr7 = 0
|
||
|
readregs
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def loadsyms(baseaddr, name)
|
||
|
@loadedsyms ||= {}
|
||
|
return if @loadedsyms[name] or self[baseaddr, 4] != "\x7fELF"
|
||
|
@loadedsyms[name] = true
|
||
|
|
||
|
set_status " loading symbols from #{name}..."
|
||
|
e = Metasm::LoadedELF.load self[baseaddr, 0x100_0000]
|
||
|
e.load_address = baseaddr
|
||
|
begin
|
||
|
e.decode
|
||
|
#e = Metasm::ELF.decode_file name rescue return # read from disk
|
||
|
rescue
|
||
|
log "failed to load symbols from #{name}: #$!"
|
||
|
($!.backtrace - caller).each { |l| log l.chomp }
|
||
|
@filemap[baseaddr.to_s(16)] = [baseaddr, baseaddr+0x1000]
|
||
|
return
|
||
|
end
|
||
|
|
||
|
if e.tag['SONAME']
|
||
|
name = e.tag['SONAME']
|
||
|
return if name and @loadedsyms[name]
|
||
|
@loadedsyms[name] = true
|
||
|
end
|
||
|
|
||
|
last_s = e.segments.reverse.find { |s| s.type == 'LOAD' }
|
||
|
vlen = last_s.vaddr + last_s.memsz
|
||
|
vlen -= baseaddr if e.header.type == 'EXEC'
|
||
|
@filemap[name] = [baseaddr, baseaddr + vlen]
|
||
|
|
||
|
oldsyms = @symbols.length
|
||
|
e.symbols.each { |s|
|
||
|
next if not s.name or s.shndx == 'UNDEF'
|
||
|
sname = s.name
|
||
|
sname = 'weak_'+sname if s.bind == 'WEAK'
|
||
|
sname = 'local_'+sname if s.bind == 'LOCAL'
|
||
|
v = s.value
|
||
|
v = baseaddr + v if v < baseaddr
|
||
|
@symbols[v] = sname
|
||
|
@symbols_len[v] = s.size
|
||
|
}
|
||
|
if e.header.type == 'EXEC'
|
||
|
@symbols[e.header.entry] = 'entrypoint'
|
||
|
end
|
||
|
set_status nil
|
||
|
log "loaded #{@symbols.length-oldsyms} symbols from #{name} at #{'%08x' % baseaddr}"
|
||
|
end
|
||
|
|
||
|
def loadallsyms
|
||
|
File.read("/proc/#{@pid}/maps").each { |l|
|
||
|
name = l.split[5]
|
||
|
loadsyms l.to_i(16), name if name and name[0] == ?/
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def loadmap(mapfile)
|
||
|
# file fmt: addr type name eg 'c01001ba t setup_idt'
|
||
|
minaddr = maxaddr = nil
|
||
|
File.read(mapfile).each { |l|
|
||
|
addr, type, name = l.chomp.split
|
||
|
addr = addr.to_i(16)
|
||
|
minaddr = addr if not minaddr or minaddr > addr
|
||
|
maxaddr = addr if not maxaddr or maxaddr < addr
|
||
|
@symbols[addr] = name
|
||
|
}
|
||
|
if minaddr
|
||
|
@filemap[minaddr.to_s(16)] = [minaddr, maxaddr+1]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def scansyms
|
||
|
addr = 0
|
||
|
fd = @pgm.encoded.data.readfd
|
||
|
while addr <= 0xffff_f000
|
||
|
addr = 0xc000_0000 if @has_pax and addr == 0x6000_0000
|
||
|
log "scansym: #{'%08x' % addr}" if addr & 0x0fff_ffff == 0
|
||
|
fd.pos = addr
|
||
|
loadsyms(addr, '%08x'%addr) if (fd.read(4) == "\x7fELF" rescue false)
|
||
|
addr += 0x1000
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def backtrace
|
||
|
s = findsymbol(@regs_cache['eip'])
|
||
|
if block_given?
|
||
|
yield s
|
||
|
else
|
||
|
bt = []
|
||
|
bt << s
|
||
|
end
|
||
|
fp = @regs_cache['ebp']
|
||
|
while fp >= @regs_cache['esp'] and fp <= @regs_cache['esp']+0x10000
|
||
|
s = findsymbol(self[fp+4, 4].unpack('L').first)
|
||
|
if block_given?
|
||
|
yield s
|
||
|
else
|
||
|
bt << s
|
||
|
end
|
||
|
fp = self[fp, 4].unpack('L').first
|
||
|
end
|
||
|
bt
|
||
|
end
|
||
|
|
||
|
def [](addr, len=nil)
|
||
|
@pgm.encoded.data[addr, len]
|
||
|
end
|
||
|
def []=(addr, len, str=nil)
|
||
|
@pgm.encoded.data[addr, len] = str
|
||
|
end
|
||
|
|
||
|
attr_accessor :logger
|
||
|
def log(s)
|
||
|
@logger ||= $stdout
|
||
|
@logger.puts s
|
||
|
end
|
||
|
|
||
|
# set a temporary status info (nil for default value)
|
||
|
def set_status(s)
|
||
|
@logger ||= $stdout
|
||
|
if @logger != $stdout
|
||
|
@logger.statusline = s
|
||
|
else
|
||
|
s ||= ' '*72
|
||
|
@logger.print s + "\r"
|
||
|
@logger.flush
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if $0 == __FILE__
|
||
|
# start debugging
|
||
|
rs = Rubstop.new(ARGV.shift)
|
||
|
|
||
|
begin
|
||
|
while rs.child.stopped? and rs.child.stopsig == Signal.list['TRAP']
|
||
|
if $VERBOSE
|
||
|
puts "#{'%08x' % rs.eip} #{rs.mnemonic}"
|
||
|
rs.singlestep
|
||
|
else
|
||
|
rs.syscall ; rs.syscall # wait return of syscall
|
||
|
puts "#{rs.orig_eax.to_s.ljust(3)} #{Rubstop::SYSCALLNR.index rs.orig_eax}"
|
||
|
end
|
||
|
end
|
||
|
p rs.child
|
||
|
puts rs.regs_dump
|
||
|
rescue Interrupt
|
||
|
rs.detach rescue nil
|
||
|
puts 'interrupted!'
|
||
|
rescue Errno::ESRCH
|
||
|
end
|
||
|
end
|