#!/usr/bin/env ruby
# -*- coding: binary -*-

require 'rex/post/process'
require 'rex/post/meterpreter/packet'
require 'rex/post/meterpreter/client'
require 'rex/post/meterpreter/channels/pools/stream_pool'
require 'rex/post/meterpreter/extensions/stdapi/stdapi'

require 'rex/post/meterpreter/extensions/stdapi/sys/process_subsystem/image'
require 'rex/post/meterpreter/extensions/stdapi/sys/process_subsystem/io'
require 'rex/post/meterpreter/extensions/stdapi/sys/process_subsystem/memory'
require 'rex/post/meterpreter/extensions/stdapi/sys/process_subsystem/thread'

module Rex
module Post
module Meterpreter
module Extensions
module Stdapi
module Sys

##
#
# This class implements the Rex::Post::Process interface.
#
##
class Process < Rex::Post::Process

	include Rex::Post::Meterpreter::ObjectAliasesContainer

	##
	#
	# Class methods
	#
	##

	class << self
		attr_accessor :client
	end

	#
	# Returns the process identifier of the process supplied in key if it's
	# valid.
	#
	def Process.[](key)
		each_process { |p|
			if (p['name'].downcase == key.downcase)
				return p['pid']
			end
		}

		return nil
	end

	#
	# Attachs to the supplied process with a given set of permissions.
	#
	def Process.open(pid = nil, perms = nil)
		real_perms = 0

		if (perms == nil)
			perms = PROCESS_ALL
		end

		if (perms & PROCESS_READ)
			real_perms |= PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION
		end

		if (perms & PROCESS_WRITE)
			real_perms |= PROCESS_SET_SESSIONID | PROCESS_VM_WRITE | PROCESS_DUP_HANDLE | PROCESS_SET_QUOTA | PROCESS_SET_INFORMATION
		end

		if (perms & PROCESS_EXECUTE)
			real_perms |= PROCESS_TERMINATE | PROCESS_CREATE_THREAD | PROCESS_CREATE_PROCESS | PROCESS_SUSPEND_RESUME
		end

		return _open(pid, real_perms)
	end

	#
	# Low-level process open.
	#
	def Process._open(pid, perms, inherit = false)
		request = Packet.create_request('stdapi_sys_process_attach')

		if (pid == nil)
			pid = 0
		end

		# Populate the request
		request.add_tlv(TLV_TYPE_PID, pid)
		request.add_tlv(TLV_TYPE_PROCESS_PERMS, perms)
		request.add_tlv(TLV_TYPE_INHERIT, inherit)

		# Transmit the request
		response = self.client.send_request(request)
		handle   = response.get_tlv_value(TLV_TYPE_HANDLE)

		# If the handle is valid, allocate a process instance and return it
		if (handle != nil)
			return self.new(pid, handle)
		end

		return nil
	end

	#
	# Executes an application using the arguments provided
	#
	# Hash arguments supported:
	#
	#   Hidden      => true/false
	#   Channelized => true/false
	#   Suspended   => true/false
	#   InMemory    => true/false
	#
	def Process.execute(path, arguments = nil, opts = nil)
		request = Packet.create_request('stdapi_sys_process_execute')
		flags   = 0

		# If we were supplied optional arguments...
		if (opts != nil)
			if (opts['Hidden'])
				flags |= PROCESS_EXECUTE_FLAG_HIDDEN
			end
			if (opts['Channelized'])
				flags |= PROCESS_EXECUTE_FLAG_CHANNELIZED
			end
			if (opts['Suspended'])
				flags |= PROCESS_EXECUTE_FLAG_SUSPENDED
			end
			if (opts['UseThreadToken'])
				flags |= PROCESS_EXECUTE_FLAG_USE_THREAD_TOKEN
			end
			if (opts['Desktop'])
				flags |= PROCESS_EXECUTE_FLAG_DESKTOP
			end
			if (opts['Session'])
				flags |= PROCESS_EXECUTE_FLAG_SESSION
				request.add_tlv( TLV_TYPE_PROCESS_SESSION, opts['Session'] )
			end
			inmem = opts['InMemory']
			if inmem

				# add the file contents into the tlv
				f = ::File.new(path, 'rb')
				request.add_tlv(TLV_TYPE_VALUE_DATA, f.read(f.stat.size))
				f.close

				# replace the path with the "dummy"
				path = inmem.kind_of?(String) ? inmem : 'cmd'
			end
		end

		request.add_tlv(TLV_TYPE_PROCESS_PATH, client.unicode_filter_decode( path ));

		# If process arguments were supplied
		if (arguments != nil)
			request.add_tlv(TLV_TYPE_PROCESS_ARGUMENTS, arguments);
		end

		request.add_tlv(TLV_TYPE_PROCESS_FLAGS, flags);

		response = client.send_request(request)

		# Get the response parameters
		pid        = response.get_tlv_value(TLV_TYPE_PID)
		handle     = response.get_tlv_value(TLV_TYPE_PROCESS_HANDLE)
		channel_id = response.get_tlv_value(TLV_TYPE_CHANNEL_ID)
		channel    = nil

		# If we were creating a channel out of this
		if (channel_id != nil)
			channel = Rex::Post::Meterpreter::Channels::Pools::StreamPool.new(client,
					channel_id, "stdapi_process", CHANNEL_FLAG_SYNCHRONOUS)
		end

		# Return a process instance
		return self.new(pid, handle, channel)
	end

	#
	# Kills one or more processes.
	#
	def Process.kill(*args)
		request = Packet.create_request('stdapi_sys_process_kill')

		args.each { |id|
			request.add_tlv(TLV_TYPE_PID, id)
		}

		client.send_request(request)

		return true
	end

	#
	# Gets the process id that the remote side is executing under.
	#
	def Process.getpid
		request = Packet.create_request('stdapi_sys_process_getpid')

		response = client.send_request(request)

		return response.get_tlv_value(TLV_TYPE_PID)
	end

	#
	# Enumerates all of the elements in the array returned by get_processes.
	#
	def Process.each_process(&block)
		self.get_processes.each(&block)
	end

	#
	# Returns a ProcessList of processes as Hash objects with keys for 'pid',
	# 'ppid', 'name', 'path', 'user', 'session' and 'arch'.
	#
	def Process.get_processes
		request   = Packet.create_request('stdapi_sys_process_get_processes')
		processes = ProcessList.new

		response = client.send_request(request)

		response.each(TLV_TYPE_PROCESS_GROUP) { |p|
		arch = ""

		pa = p.get_tlv_value( TLV_TYPE_PROCESS_ARCH )
		if( pa != nil )
			if pa == 1 # PROCESS_ARCH_X86
				arch = ARCH_X86
			elsif pa == 2 # PROCESS_ARCH_X64
				arch = ARCH_X86_64
			end
		end

		processes <<
				{
					'pid'      => p.get_tlv_value(TLV_TYPE_PID),
					'ppid'     => p.get_tlv_value(TLV_TYPE_PARENT_PID),
					'name'     => client.unicode_filter_encode( p.get_tlv_value(TLV_TYPE_PROCESS_NAME) ),
					'path'     => client.unicode_filter_encode( p.get_tlv_value(TLV_TYPE_PROCESS_PATH) ),
					'session'  => p.get_tlv_value(TLV_TYPE_PROCESS_SESSION),
					'user'     => client.unicode_filter_encode( p.get_tlv_value(TLV_TYPE_USER_NAME) ),
					'arch'     => arch
				}
		}

		return processes
	end

	#
	# An alias for get_processes.
	#
	def Process.processes
		self.get_processes
	end

	##
	#
	# Instance methods
	#
	##

	#
	# Initializes the process instance and its aliases.
	#
	def initialize(pid, handle, channel = nil)
		self.client  = self.class.client
		self.handle  = handle
		self.channel = channel

		# If the process identifier is zero, then we must lookup the current
		# process identifier
		if (pid == 0)
			self.pid = client.sys.process.getpid
		else
			self.pid = pid
		end

		initialize_aliases(
			{
				'image'  => Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessSubsystem::Image.new(self),
				'io'     => Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessSubsystem::IO.new(self),
				'memory' => Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessSubsystem::Memory.new(self),
				'thread' => Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessSubsystem::Thread.new(self),
			})

		ObjectSpace.define_finalizer( self, self.class.finalize(self.client, self.handle) )
	end

	def self.finalize(client,handle)
		proc { self.close(client,handle) }
	end

	#
	# Returns the executable name of the process.
	#
	def name
		return get_info()['name']
	end

	#
	# Returns the path to the process' executable.
	#
	def path
		return get_info()['path']
	end

	#
	# Closes the handle to the process that was opened.
	#
	def self.close(client, handle)
		request = Packet.create_request('stdapi_sys_process_close')
		request.add_tlv(TLV_TYPE_HANDLE, handle)
		response = client.send_request(request, nil)
		handle = nil;
		return true
	end

	#
	# Instance method
	#
	def close(handle=self.handle)
		self.class.close(self.client, handle)
	end

	#
	# Block until this process terminates on the remote side.
	# By default we choose not to allow a packet responce timeout to
	# occur as we may be waiting indefinatly for the process to terminate.
	#
	def wait( timeout = -1 )
		request = Packet.create_request('stdapi_sys_process_wait')

		request.add_tlv(TLV_TYPE_HANDLE, self.handle)

		response = self.client.send_request(request, timeout)

		self.handle = nil

		return true
	end

	attr_reader   :client, :handle, :channel, :pid # :nodoc:
protected
	attr_writer   :client, :handle, :channel, :pid # :nodoc:

	#
	# Gathers information about the process and returns a hash.
	#
	def get_info
		request = Packet.create_request('stdapi_sys_process_get_info')
		info    = {}

		request.add_tlv(TLV_TYPE_HANDLE, handle)

		# Send the request
		response = client.send_request(request)

		# Populate the hash
		info['name'] = client.unicode_filter_encode( response.get_tlv_value(TLV_TYPE_PROCESS_NAME) )
		info['path'] = client.unicode_filter_encode( response.get_tlv_value(TLV_TYPE_PROCESS_PATH) )

		return info
	end

end

#
# Simple wrapper class for storing processes
#
class ProcessList < Array

	#
	# Create a Rex::Ui::Text::Table out of the processes stored in this list
	#
	# +opts+ is passed on to Rex::Ui::Text::Table.new, mostly unmolested
	#
	# Note that this output is affected by Rex::Post::Meterpreter::Client#unicode_filter_encode
	#
	def to_table(opts={})
		if empty?
			return Rex::Ui::Text::Table.new(opts)
		end

		cols = [ "PID", "PPID", "Name", "Arch", "Session", "User", "Path" ]
		# Arch and Session are specific to native Windows, PHP and Java can't do
		# ppid.  Cut columns from the list if they aren't there.  It is conceivable
		# that processes might have different columns, but for now assume that the
		# first one is representative.
		cols.delete_if { |c| !( first.has_key?(c.downcase) ) or first[c.downcase].nil? }

		opts = {
			"Header"  => "Process List",
			"Columns" => cols
		}.merge(opts)

		tbl = Rex::Ui::Text::Table.new(opts)
		each { |process|
			tbl << cols.map {|c| process[c.downcase] }.compact
		}

		tbl
	end
end

end; end; end; end; end; end

