#    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 exemple illustrates the use of the PTrace class to implement a pytstop-like functionnality
# Works on linux/x86
#

require 'metasm'

class Rubstop < Metasm::PTrace
	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

	def state; :stopped end
	def ptrace; self 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.dbg = 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
