# -*- coding: binary -*-

require 'msf/core/exploit/tcp'

module Msf

#
# Implement some helpers for communicating with a remote gdb instance.
#
# More info on the gdb protocol can be found here:
# https://sourceware.org/gdb/current/onlinedocs/gdb/Overview.html#Overview
#

module Exploit::Remote::Gdb

  include Msf::Exploit::Remote::Tcp

  # thrown when an expected ACK packet is never received
  class BadAckError < RuntimeError; end

  # thrown when a response is incorrect
  class BadResponseError < RuntimeError; end

  # thrown when a checksum is invalid
  class BadChecksumError < RuntimeError; end

  # Default list of supported GDB features to send them to the target
  GDB_FEATURES = 'qSupported:multiprocess+;qRelocInsn+;qvCont+;'

  # Maps index of register in GDB that holds $PC to architecture
  PC_REGISTERS = {
    '08' => ARCH_X86,
    '10' => ARCH_X64
  }

  # Send an ACK packet
  def send_ack
    sock.put('+')
    vprint_status('Sending ack...')
  end

  # Reads an ACK packet from the wire
  # @raise [BadAckError] if a bad ACK is received
  def read_ack
    unless sock.get_once(1) == '+'
      raise BadAckError
    end
    vprint_status('Received ack...')
  end

  # Sends a command and receives an ACK from the remote.
  # @param cmd [String] the gdb command to run. The command is will be
  #   wrapped '$' prefix and checksum.
  def send_cmd(cmd)
    full_cmd = '$' + cmd + '#' + checksum(cmd)
    vprint_status('Sending cmd: '+full_cmd)
    sock.put(full_cmd)
    read_ack
  end

  # Reads (and possibly decodes) from the socket and sends an ACK to verify receipt
  # @param opts [Hash] the options hash
  # @option opts :decode [Boolean] rle decoding should be applied to the response
  # @option opts :verify [Boolean] verify the response's checksum
  # @return [String] the response
  # @raise [BadResponseError] if the expected response is missing
  # @raise [BadChecksumError] if the checksum is invalid
  def read_response(opts={})
    decode, verify = opts.fetch(:decode, false), opts.fetch(:verify, true)
    res = sock.get_once
    raise BadResponseError if res.nil?
    raise BadChecksumError if (verify && !verify_checksum(res))
    res = decode_rle(res) if decode
    vprint_status('Result: '+res)
    send_ack
    res
  end

  # Implements decoding of gdbserver's Run-Length-Encoding that is applied
  # on some hex values to collapse repeated characters.
  #
  # https://sourceware.org/gdb/current/onlinedocs/gdb/Overview.html#Binary-Data
  #
  # @param msg [String] the message to decode
  # @return [String] the decoded result
  def decode_rle(msg)
    vprint_status "Before decoding: #{msg}"
    msg.gsub /.\*./ do |match|
      match.bytes.to_a.first.chr * (match.bytes.to_a.last - 29 + 1)
    end
  end

  # The two-digit checksum is computed as the modulo 256 sum of all characters
  # between the leading ‘$’ and the trailing ‘#’ (an eight bit unsigned checksum).
  # @param str [String] the string to calculate the checksum of
  # @return [String] hex string containing checksum
  def checksum(str)
    "%02x" % str.bytes.inject(0) { |b, sum| (sum+b)%256 }
  end

  # Verifies a response's checksum
  # @param res [String] the response to check
  # @return [Boolean] whether the checksum is valid
  def verify_checksum(res)
    msg, chksum = res.match(/^\$(.*)#(\h{2})$/)[1..2]
    checksum(msg) == chksum
  end

  # Writes the buffer +buf+ to the address +addr+ in the remote process's memory
  # @param buf [String] the buffer to write
  # @param addr [String] the hex-encoded address to write to
  def write(buf, addr)
    hex = Rex::Text.to_hex(buf, '')
    send_cmd "M#{addr},#{buf.length.to_s(16)}:#{hex}"
    read_response
  end

  # Steps execution and finds $PC pointer and architecture
  # @return [Hash] with :arch and :pc keys containing architecture and PC pointer
  # @raise [BadResponseError] if necessary data is missing
  def process_info
    data = step
    pc_data = data.split(';')[2]
    raise BadResponseError if pc_data.nil?
    pc_data = pc_data.split(':')
    my_arch = PC_REGISTERS[pc_data[0]]
    pc = pc_data[1]

    if my_arch.nil?
      raise RuntimeError, "Could not detect a supported arch from response to step:\n#{pc_data}"
    end

    {
      arch: my_arch,
      pc: Rex::Text.to_hex(Rex::Arch.pack_addr(my_arch, Integer(pc, 16)), ''),
      pc_raw: Integer(pc, 16)
    }
  end

  # Continues execution of the remote process
  # @param opts [Hash] the options hash
  # @option opts :read [Boolean] read the response
  def continue(opts={})
    send_cmd 'vCont;c'
    read_response if opts.fetch(:read, true)
  end

  # Detaches from the remote process
  # @param opts [Hash] the options hash
  # @option opts :read [Boolean] read the response
  def detach(opts={})
    send_cmd 'D'
    read_response if opts.fetch(:read, true)
  end

  # Executes one instruction on the remote process
  #
  # The results of running "step" will look like:
  # x86: $T0505:00000000;04:a0f7ffbf;08:d2f0fdb7;thread:p2d39.2d39;core:0;#53
  # x64: $T0506:0000000000000000;07:b0587f9fff7f0000;10:d3e29d03057f0000;thread:p8bf9.8bf9;core:0;#df
  # The third comma-separated field will contain EIP, and the register index
  # will let us deduce the remote architecture (through PC_REGISTERS lookup)
  #
  # @return [String] a list of key/value pairs, including current PC
  def step
    send_cmd 'vCont;s'
    read_response(decode: true)
  end

  def run(filename)
    send_cmd "vRun;#{Rex::Text.to_hex(filename, '')}"
    read_response
  end

  def enable_extended_mode
    send_cmd("!")
    read_response
  end

  # Performs a handshake packet exchange
  # @param features [String] the list of supported features to tell the remote
  #   host that the client supports (defaults to +DEFAULT_GDB_FEATURES+)
  def handshake(features=GDB_FEATURES)
    send_cmd features
    read_response # lots of flags, nothing interesting
  end

end

end
