583 lines
13 KiB
Ruby
583 lines
13 KiB
Ruby
# This file is part of Metasm, the Ruby assembly manipulation suite
|
|
# Copyright (C) 2009 Yoann GUILLOT
|
|
#
|
|
# Licence is LGPL, see LICENCE in the top-level directory
|
|
|
|
#
|
|
# this is a rubstop-api compatible Gdb stub
|
|
# it can connect to a gdb server and interface with the lindebug frontend
|
|
# linux/x86 only
|
|
#
|
|
|
|
require 'socket'
|
|
require 'metasm'
|
|
|
|
class GdbRemoteString < Metasm::VirtualString
|
|
attr_accessor :gdbg
|
|
|
|
def initialize(gdbg, addr_start=0, length=0xffff_ffff)
|
|
@gdbg = gdbg
|
|
@pagelength = 512
|
|
super(addr_start, length)
|
|
end
|
|
|
|
def dup(addr=@addr_start, len=@length)
|
|
self.class.new(@gdbg, addr, len)
|
|
end
|
|
|
|
def rewrite_at(addr, data)
|
|
len = data.length
|
|
off = 0
|
|
while len > @pagelength
|
|
@gdbg.setmem(addr+off, data[off, @pagelength])
|
|
off += @pagelength
|
|
len -= @pagelength
|
|
end
|
|
@gdbg.setmem(addr+off, data[off, len])
|
|
end
|
|
|
|
def get_page(addr)
|
|
@gdbg.getmem(addr, @pagelength)
|
|
end
|
|
end
|
|
|
|
class Rubstop
|
|
EFLAGS = {0 => 'c', 2 => 'p', 4 => 'a', 6 => 'z', 7 => 's', 9 => 'i', 10 => 'd', 11 => 'o'}
|
|
GDBREGS = %w[eax ecx edx ebx esp ebp esi edi eip eflags cs ss ds es fs gs] # XXX [77] = 'orig_eax'
|
|
# define accessors for registers
|
|
GDBREGS.compact.each { |reg|
|
|
define_method(reg) { regs_cache[reg] }
|
|
define_method(reg + '=') { |v| regs_cache[reg] = v ; regs_dirty }
|
|
}
|
|
|
|
# compute the hex checksum used in gdb protocol
|
|
def gdb_csum(buf)
|
|
'%02x' % (buf.unpack('C*').inject(0) { |cs, c| cs + c } & 0xff)
|
|
end
|
|
|
|
# send the buffer, waits ack
|
|
# return true on success
|
|
def gdb_send(cmd, buf='')
|
|
buf = cmd + buf
|
|
buf = '$' << buf << '#' << gdb_csum(buf)
|
|
log "gdb_send(#{buf[0, 32].inspect}#{'...' if buf.length > 32})" if $DEBUG
|
|
|
|
5.times {
|
|
@io.write buf
|
|
loop do
|
|
if not IO.select([@io], nil, nil, 1)
|
|
break
|
|
end
|
|
raise Errno::EPIPE if not ack = @io.read(1)
|
|
case ack
|
|
when '+'
|
|
return true
|
|
when '-'
|
|
log "gdb_send: ack neg" if $DEBUG
|
|
break
|
|
when nil; return
|
|
end
|
|
end
|
|
}
|
|
log "send error #{cmd.inspect} (no ack)"
|
|
false
|
|
end
|
|
|
|
# return buf, or nil on error / csum error
|
|
def gdb_readresp
|
|
state = :nosync
|
|
buf = ''
|
|
cs = ''
|
|
while state != :done
|
|
# XXX timeout etc
|
|
raise Errno::EPIPE if not c = @io.read(1)
|
|
case state
|
|
when :nosync
|
|
if c == '$'
|
|
state = :data
|
|
end
|
|
when :data
|
|
if c == '#'
|
|
state = :csum1
|
|
else
|
|
buf << c
|
|
end
|
|
when :csum1
|
|
cs << c
|
|
state = :csum2
|
|
when :csum2
|
|
cs << c
|
|
state = :done
|
|
if cs.downcase != gdb_csum(buf).downcase
|
|
log "transmit error"
|
|
@io.write '-'
|
|
return
|
|
end
|
|
end
|
|
end
|
|
@io.write '+'
|
|
|
|
if buf =~ /^E(..)$/
|
|
e = $1.to_i(16)
|
|
log "error #{e} (#{Metasm::PTrace32::ERRNO.index(e)})"
|
|
return
|
|
end
|
|
log "gdb_readresp: got #{buf[0, 64].inspect}#{'...' if buf.length > 64}" if $DEBUG
|
|
|
|
buf
|
|
end
|
|
|
|
def gdb_msg(*a)
|
|
if gdb_send(*a)
|
|
gdb_readresp
|
|
end
|
|
end
|
|
|
|
# rle: build the regexp that will match repetitions of a character, skipping counts leading to invalid char
|
|
rng = [3..(125-29)]
|
|
[?+, ?-, ?#, ?$].sort.each { |invalid|
|
|
invalid -= 29
|
|
rng.each_with_index { |r, i|
|
|
if r.include? invalid
|
|
replace = [r.begin..invalid-1, invalid+1..r.end]
|
|
replace.delete_if { |r_| r_.begin > r_.end }
|
|
rng[i, 1] = replace
|
|
end
|
|
}
|
|
}
|
|
repet = rng.reverse.map { |r| "\\1{#{r.begin},#{r.end}}" }.join('|')
|
|
RLE_RE = /(.)(#{repet})/
|
|
|
|
# rle-compress a buffer
|
|
# a character followed by '*' followed by 'x' is asc(x)-28 repetitions of the char
|
|
# eg '0* ' => '0' * (asc(' ') - 28) = '0000'
|
|
# for the count character, it must be 32 <= char < 126 and not be '+' '-' '#' or '$'
|
|
def rle(buf)
|
|
buf.gsub(RLE_RE) {
|
|
chr, len = $1, $2.length+1
|
|
chr + '*' + (len+28).chr
|
|
}
|
|
end
|
|
# decompress rle-encoded data
|
|
def unrle(buf) buf.gsub(/(.)\*(.)/) { $1 * ($2[0]-28) } end
|
|
# send an integer as a long hex packed with leading 0 stripped
|
|
def hexl(int) [int].pack('N').unpack('H*').first.gsub(/^0+(.)/, '\1') end
|
|
# send a binary buffer as a rle hex-encoded
|
|
def hex(buf) buf.unpack('H*').first end
|
|
# decode an rle hex-encoded buffer
|
|
def unhex(buf)
|
|
buf = buf[/^[a-fA-F0-9]*/]
|
|
buf = '0' + buf if buf.length % 1 == 1
|
|
[buf].pack('H*')
|
|
end
|
|
|
|
# on-demand local cache of registers
|
|
def regs_cache
|
|
readregs if @regs_cache.empty?
|
|
@regs_cache
|
|
end
|
|
|
|
# retrieve remote regs
|
|
def readregs
|
|
sync_regs
|
|
if buf = gdb_msg('g')
|
|
regs = unhex(unrle(buf))
|
|
if regs.length < GDBREGS.length*4
|
|
# retry once, was probably a response to something else
|
|
puts "bad regs size!" if $DEBUG
|
|
buf = gdb_msg('g')
|
|
regs = unhex(unrle(buf)) if buf
|
|
if not buf or regs.length < GDBREGS.length*4
|
|
raise "regs buffer recv is too short !"
|
|
end
|
|
end
|
|
@regs_dirty = false
|
|
@regs_cache = Hash[GDBREGS.zip(regs.unpack('L*'))]
|
|
end
|
|
@curinstr = nil if @regs_cache['eip'] != @oldregs['eip']
|
|
end
|
|
|
|
# mark local cache of regs as modified, need to send it before continuing execution
|
|
def regs_dirty
|
|
@regs_dirty = true
|
|
end
|
|
|
|
# send the local copy of regs if dirty
|
|
def sync_regs
|
|
if not @regs_cache.empty? and @regs_dirty
|
|
send_regs
|
|
end
|
|
end
|
|
|
|
# send the local copy of regs
|
|
def send_regs
|
|
return if @regs_cache.empty?
|
|
regs = @regs_cache.values_at(*GDBREGS)
|
|
@regs_dirty = false
|
|
gdb_msg('G', hex(regs.pack('L*')))
|
|
end
|
|
|
|
# read memory (small blocks prefered)
|
|
def getmem(addr, len)
|
|
return '' if len == 0
|
|
if mem = gdb_msg('m', hexl(addr) << ',' << hexl(len))
|
|
unhex(unrle(mem))
|
|
end
|
|
end
|
|
|
|
# write memory (small blocks prefered)
|
|
def setmem(addr, data)
|
|
len = data.length
|
|
return if len == 0
|
|
raise 'writemem error' if not gdb_msg('M', hexl(addr) << ',' << hexl(len) << ':' << rle(hex(data)))
|
|
end
|
|
|
|
# read arbitrary blocks of memory (chunks to getmem)
|
|
def [](addr, len)
|
|
@pgm.encoded[addr, len].data
|
|
end
|
|
|
|
# write arbitrary blocks of memory (chunks to getmem)
|
|
def []=(addr, len, str)
|
|
@pgm.encoded[addr, len] = str
|
|
end
|
|
|
|
def curinstr
|
|
@curinstr ||= mnemonic_di
|
|
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 pre_run
|
|
@oldregs = regs_cache.dup
|
|
sync_regs
|
|
end
|
|
|
|
def post_run
|
|
@regs_cache.clear
|
|
@curinstr = nil
|
|
@mem.invalidate
|
|
end
|
|
|
|
def quiet
|
|
@quiet = true
|
|
begin
|
|
yield
|
|
ensure
|
|
@quiet = false
|
|
end
|
|
end
|
|
|
|
def log_stopped(msg)
|
|
return if @quiet ||= false
|
|
case msg[0]
|
|
when ?T
|
|
sig = [msg[1, 2]].pack('H*')[0]
|
|
misc = msg[3..-1].split(';').inject({}) { |h, s| k, v = s.split(':', 2) ; h.update k => (v || true) }
|
|
str = "stopped by signal #{sig}"
|
|
str = "thread #{[misc['thread']].pack('H*').unpack('N').first} #{str}" if misc['thread']
|
|
log str
|
|
when ?S
|
|
sig = [msg[1, 2]].pack('H*')[0]
|
|
log "stopped by signal #{sig}"
|
|
end
|
|
end
|
|
|
|
def cont
|
|
pre_run
|
|
do_singlestep if @wantbp
|
|
rmsg = gdb_msg('c')
|
|
post_run
|
|
ccaddr = eip-1
|
|
if @breakpoints[ccaddr] and self[ccaddr, 1] == "\xcc"
|
|
self[ccaddr, 1] = @breakpoints.delete ccaddr
|
|
mem.invalidate
|
|
self.eip = ccaddr
|
|
@wantbp = ccaddr if not @singleshot.delete ccaddr
|
|
sync_regs
|
|
end
|
|
log_stopped rmsg
|
|
end
|
|
|
|
def singlestep
|
|
pre_run
|
|
do_singlestep
|
|
post_run
|
|
end
|
|
|
|
def do_singlestep
|
|
gdb_msg('s')
|
|
if @wantbp
|
|
self[@wantbp, 1] = "\xcc"
|
|
@wantbp = nil
|
|
end
|
|
end
|
|
|
|
def stepover
|
|
i = curinstr.instruction if curinstr
|
|
if i and (i.opname == 'call' or (i.prefix and i.prefix[:rep]))
|
|
eaddr = eip + curinstr.bin_length
|
|
bpx eaddr, true
|
|
quiet { cont }
|
|
else
|
|
singlestep
|
|
end
|
|
end
|
|
|
|
def stepout
|
|
stepover until curinstr and curinstr.opcode.name == 'ret'
|
|
singlestep
|
|
rescue Interrupt
|
|
log 'interrupted'
|
|
end
|
|
|
|
def bpx(addr, singleshot=false)
|
|
return if @breakpoints[addr]
|
|
@singleshot[addr] = true if singleshot
|
|
@breakpoints[addr] = self[addr, 1]
|
|
self[addr, 1] = "\xcc"
|
|
end
|
|
|
|
|
|
def kill
|
|
gdb_send('k')
|
|
end
|
|
|
|
def detach
|
|
# TODO
|
|
end
|
|
|
|
attr_accessor :pgm, :breakpoints, :singleshot, :wantbp,
|
|
:symbols, :symbols_len, :filemap, :oldregs, :io, :mem
|
|
def initialize(io)
|
|
case io
|
|
when IO; @io = io
|
|
when /^udp:([^:]*):(\d+)$/; @io = UDPSocket.new ; @io.connect($1, $2)
|
|
when /^(?:tcp:)?([^:]*):(\d+)$/; @io = TCPSocket.open($1, $2)
|
|
else raise "unknown target #{io.inspect}"
|
|
end
|
|
@pgm = Metasm::ExeFormat.new Metasm::Ia32.new
|
|
@mem = GdbRemoteString.new self
|
|
@pgm.encoded = Metasm::EncodedData.new @mem
|
|
@regs_cache = {}
|
|
@regs_dirty = nil
|
|
@oldregs = {}
|
|
@breakpoints = {}
|
|
@singleshot = {}
|
|
@wantbp = nil
|
|
@symbols = {}
|
|
@symbols_len = {}
|
|
@filemap = {}
|
|
|
|
gdb_setup
|
|
end
|
|
|
|
def gdb_setup
|
|
#gdb_msg('q', 'Supported')
|
|
#gdb_msg('Hc', '-1')
|
|
#gdb_msg('qC')
|
|
if not gdb_msg('?')
|
|
log "nobody on the line, waiting for someone to wake up"
|
|
IO.select([@io], nil, nil, nil)
|
|
log "who's there ?"
|
|
end
|
|
end
|
|
|
|
def set_hwbp(type, addr, len=1, set=true)
|
|
set = (set ? 'Z' : 'z')
|
|
type = { 'r' => '3', 'w' => '2', 'x' => '1', 's' => '0' }[type] || raise("invalid hwbp type #{type}")
|
|
gdb_msg(set, type << ',' << hexl(addr) << ',' << hexl(len))
|
|
true
|
|
end
|
|
|
|
def unset_hwbp(type, addr, len=1)
|
|
set_hwbp(type, addr, len, false)
|
|
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 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
|
|
rescue Interrupt
|
|
log "interrupted"
|
|
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' and e.header.entry >= baseaddr and e.header.entry < baseaddr + vlen
|
|
@symbols[e.header.entry] = 'entrypoint'
|
|
end
|
|
set_status nil
|
|
log "loaded #{@symbols.length-oldsyms} symbols from #{name} at #{'%08x' % baseaddr}"
|
|
end
|
|
|
|
# scan val at the beginning of each page (custom gdb msg)
|
|
def pageheadsearch(val)
|
|
resp = gdb_msg('qy', hexl(val))
|
|
unhex(resp).unpack('L*')
|
|
end
|
|
|
|
def scansyms
|
|
# TODO use qSymbol or something
|
|
pageheadsearch("\x7fELF".unpack('L').first).each { |addr| loadsyms(addr, '%08x'%addr) }
|
|
end
|
|
|
|
# use qSymbol to retrieve a symbol value (uint)
|
|
def request_symbol(name)
|
|
resp = gdb_msg('qSymbol:', hex(name))
|
|
if resp and a = resp.split(':')[1]
|
|
unhex(a).unpack('N').first
|
|
end
|
|
end
|
|
|
|
def loadallsyms
|
|
# kgdb: read kernel symbols from 'module_list'
|
|
# too bad module_list is not in ksyms
|
|
if mod = request_symbol('module_list')
|
|
int_at = lambda { |addr, off| @mem[addr+off, 4].unpack('L').first }
|
|
mod_size = lambda { int_at[mod, 0] }
|
|
mod_next = lambda { int_at[mod, 4] }
|
|
mod_nsym = lambda { int_at[mod, 0x18] } # most portable. yes.
|
|
mod_syms = lambda { int_at[mod, 0x20] }
|
|
|
|
read_strz = lambda { |addr|
|
|
if i = @mem.index(0, addr)
|
|
@mem[addr...i]
|
|
end
|
|
}
|
|
|
|
while mod != 0
|
|
symtab = [[]]
|
|
|
|
@mem[mod_syms[], mod_nsym[]*8].to_str.unpack('L*').each { |i|
|
|
# make a list of couples
|
|
if symtab.last.length < 2
|
|
symtab.last << i
|
|
else
|
|
symtab << [i]
|
|
end
|
|
}
|
|
|
|
symtab.each { |v, n|
|
|
n = read_strz[n]
|
|
# ||= to keep symbol precedence order (1st match wins)
|
|
@symbols[v] ||= n
|
|
}
|
|
|
|
mod = mod_next[]
|
|
end
|
|
end
|
|
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 backtrace
|
|
s = findsymbol(eip)
|
|
if block_given?
|
|
yield s
|
|
else
|
|
bt = []
|
|
bt << s
|
|
end
|
|
fp = ebp
|
|
while fp >= esp and fp <= esp+0x100000
|
|
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
|
|
|
|
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
|
|
|
|
def checkbp ; end
|
|
end
|