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

require 'rex/post/meterpreter/client'
require 'rex/post/meterpreter/extensions/stdapi/constants'

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

###
#
# Provides an interface to allocate, free, read, write, query,
# protect, lock, and unlock memory in the context of a given
# process.
#
###
class Memory

	# Page protection translation hash
	@@page_protection_map =
	{
		PROT_NONE                => PAGE_NOACCESS,
		PROT_EXEC                => PAGE_EXECUTE,
		PROT_EXEC | PROT_READ    => PAGE_EXECUTE_READ,
		PROT_EXEC | PROT_READ |
			PROT_WRITE            => PAGE_EXECUTE_READWRITE,
		PROT_EXEC | PROT_READ |
			PROT_WRITE | PROT_COW => PAGE_EXECUTE_WRITECOPY,
		PROT_READ                => PAGE_READONLY,
		PROT_READ | PROT_WRITE   => PAGE_READWRITE,
		PROT_READ | PROT_WRITE |
			PROT_COW              => PAGE_WRITECOPY,
		PROT_WRITE               => PAGE_READWRITE
	}

	##
	#
	# Constructor
	#
	##

	#
	# Initializes a memory modification instance with the supplied process
	# instance.
	#
	def initialize(process)
		self.process = process
	end

	#
	# Allocate storage of the supplied length and returns the
	# address at which the memory was allocated.
	#
	def allocate(length, protection = nil, base = nil)
		allocation_type = MEM_COMMIT

		# If no protection was supplied, default to the most flexible
		if (protection == nil)
			protection = PAGE_EXECUTE_READWRITE
		else
			protection = gen_prot_to_specific(protection)
		end

		# If the preferred base is non-nil, set the reserve flag
		if (base != nil)
			allocation_type |= MEM_RESERVE
		end

		return _allocate(base, length, allocation_type, protection)
	end

	#
	# Low-level memory allocation.
	#
	def _allocate(base, length, allocation_type, protection)
		request = Packet.create_request('stdapi_sys_process_memory_allocate')

		# Populate the request
		if (base != nil)
			request.add_tlv(TLV_TYPE_BASE_ADDRESS, base)
		end

		request.add_tlv(TLV_TYPE_HANDLE, process.handle)
		request.add_tlv(TLV_TYPE_LENGTH, length)
		request.add_tlv(TLV_TYPE_ALLOCATION_TYPE, allocation_type)
		request.add_tlv(TLV_TYPE_PROTECTION, protection)

		# Transmit the request
		response = process.client.send_request(request);

		return response.get_tlv_value(TLV_TYPE_BASE_ADDRESS)
	end

	#
	# Deallocate a region of memory in the context of a process.
	#
	def free(base, length = 0)
		return _free(base, length)
	end

	#
	# Low-level memory deallocation.
	#
	def _free(base, length)
		request = Packet.create_request('stdapi_sys_process_memory_free')

		request.add_tlv(TLV_TYPE_HANDLE, process.handle)
		request.add_tlv(TLV_TYPE_BASE_ADDRESS, base)
		request.add_tlv(TLV_TYPE_LENGTH, length)

		response = process.client.send_request(request)

		return true
	end

	#
	# Read memory from the context of a process and return the buffer.
	#
	def read(base, length)
		request = Packet.create_request('stdapi_sys_process_memory_read')

		request.add_tlv(TLV_TYPE_HANDLE, process.handle)
		request.add_tlv(TLV_TYPE_BASE_ADDRESS, base)
		request.add_tlv(TLV_TYPE_LENGTH, length)

		response = process.client.send_request(request)

		return response.get_tlv_value(TLV_TYPE_PROCESS_MEMORY)
	end

	#
	# Write memory to the context of a process and return the number of bytes
	# actually written.
	#
	def write(base, data)
		request = Packet.create_request('stdapi_sys_process_memory_write')

		request.add_tlv(TLV_TYPE_HANDLE, process.handle)
		request.add_tlv(TLV_TYPE_BASE_ADDRESS, base)
		request.add_tlv(TLV_TYPE_PROCESS_MEMORY, data)

		response = process.client.send_request(request)

		return response.get_tlv_value(TLV_TYPE_LENGTH)
	end

	#
	# Queries an address for information about its state.
	#
	def query(base)
		request = Packet.create_request('stdapi_sys_process_memory_query')

		request.add_tlv(TLV_TYPE_HANDLE, process.handle)
		request.add_tlv(TLV_TYPE_BASE_ADDRESS, base)

		response = process.client.send_request(request)

		# Build out the hash from the response information
		info = {}

		info['BaseAddress']       = response.get_tlv_value(TLV_TYPE_BASE_ADDRESS)
		info['AllocationBase']    = response.get_tlv_value(TLV_TYPE_ALLOC_BASE_ADDRESS)
		info['AllocationProtect'] = specific_prot_to_gen(response.get_tlv_value(TLV_TYPE_ALLOC_PROTECTION))
		info['RegionSize']        = response.get_tlv_value(TLV_TYPE_LENGTH)

		# Translate the memory state
		state = response.get_tlv_value(TLV_TYPE_MEMORY_STATE)

		if (state == MEM_FREE)
			info['Available'] = true
		elsif (state == MEM_COMMIT)
			info['Available'] = false
		elsif (state == MEM_RESERVE)
			info['Reserved'] = true
		end

		# Translate the region protections
		info['Protect'] = specific_prot_to_gen(response.get_tlv_value(TLV_TYPE_PROTECTION))

		# Translate the memory type
		type = response.get_tlv_value(TLV_TYPE_MEMORY_TYPE)

		if (type == MEM_IMAGE)
			info['ImageMapping'] = true
		elsif (type == MEM_MAPPED)
			info['MemoryMapping'] = true
		elsif (type == MEM_PRIVATE)
			info['PrivateMapping'] = true
		end

		return info
	end

	#
	# Change the protection masks on the region supplied in base.
	#
	def protect(base, length = nil, protection = nil)
		request = Packet.create_request('stdapi_sys_process_memory_protect')

		if (length == nil)
			length = 4096
		end

		# If no protection was supplied, default to the most flexible
		if (protection == nil)
			protection = PAGE_EXECUTE_READWRITE
		else
			protection = gen_prot_to_specific(protection)
		end

		request.add_tlv(TLV_TYPE_HANDLE, process.handle)
		request.add_tlv(TLV_TYPE_BASE_ADDRESS, base)
		request.add_tlv(TLV_TYPE_LENGTH, length)
		request.add_tlv(TLV_TYPE_PROTECTION, protection)

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

		# Return the old protection to the caller
		return specific_prot_to_gen(response.get_tlv_value(TLV_TYPE_PROTECTION))
	end

	#
	# Lock a region of memory into physical memory so that it can't be
	# swapped to disk.  This can only be done in the context of the
	# process that is running the meterpreter server.  The instance's
	# handle is ignored.
	#
	def lock(base, length)
		request = Packet.create_request('stdapi_sys_process_memory_lock')

		request.add_tlv(TLV_TYPE_BASE_ADDRESS, base)
		request.add_tlv(TLV_TYPE_LENGTH, length)

		response = process.client.send_request(request)

		return true
	end

	#
	# Unloock a region of memory into physical memory so that it can be
	# swapped to disk.  This can only be done in the context of the
	# process that is running the meterpreter server.  The instance's
	# handle is ignored.
	#
	def unlock(base, length)
		request = Packet.create_request('stdapi_sys_process_memory_unlock')

		request.add_tlv(TLV_TYPE_BASE_ADDRESS, base)
		request.add_tlv(TLV_TYPE_LENGTH, length)

		response = process.client.send_request(request)

		return true
	end


	##
	#
	# Conditionals
	#
	##

	#
	# Check to see if an address is readable.
	#
	def readable?(base)
		info = nil

		begin
			info = query(base)
		rescue
		end

		if ((info != nil) &&
		    (info['Available'] == false) &&
		    (info['Protect'] & PROT_READ == PROT_READ))
			return true
		end

		return false
	end

	#
	# Check to see if an address is writable.
	#
	def writable?(base)
		info = nil

		begin
			info = query(base)
		rescue
		end

		if ((info != nil) &&
		    (info['Available'] == false) &&
		    (info['Protect'] & PROT_WRITE == PROT_WRITE))
			return true
		end

		return false
	end

protected

	#
	# Translates general protection flags to specific protection flags.
	#
	def gen_prot_to_specific(prot)
		if (prot == nil)
			return PAGE_READ
		end

		return @@page_protection_map[prot]
	end

	#
	# Translates specific protection flags to general protection flags.
	#
	def specific_prot_to_gen(prot)

		if (prot == nil)
			return PAGE_READONLY
		end

		return @@page_protection_map.invert[prot]
	end

	attr_accessor :process # :nodoc:
end

end; end; end; end; end; end; end
