# 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 # # 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::PTrace::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 rescue '' 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 clear breakpoints gdb_send('D') 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