320 lines
8.1 KiB
Ruby
320 lines
8.1 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
|
|
::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
|
|
if curinstr.opcode and curinstr.opcode.name == 'call'
|
|
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
|
|
@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
|
|
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.keys.find { |s| s <= k and s + @symbols_len[s] > 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 loadsyms(baseaddr, name)
|
|
@loadedsyms ||= {}
|
|
return if @loadedsyms[name] or self[baseaddr, 4] != "\x7fELF"
|
|
@loadedsyms[name] = true
|
|
|
|
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'
|
|
@symbols[baseaddr + s.value] = sname
|
|
@symbols_len[baseaddr + s.value] = s.size
|
|
}
|
|
if e.header.type == 'EXEC'
|
|
@symbols[e.header.entry] = 'entrypoint'
|
|
@symbols_len[e.header.entry] = 1
|
|
end
|
|
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 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
|
|
bt = []
|
|
bt << findsymbol(@regs_cache['eip'])
|
|
fp = @regs_cache['ebp']
|
|
while fp >= @regs_cache['esp'] and fp <= @regs_cache['esp']+0x10000
|
|
bt << findsymbol(self[fp+4, 4].unpack('L').first)
|
|
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
|
|
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
|