metasploit-framework/lib/rex/post/meterpreter/packet_dispatcher.rb

545 lines
12 KiB
Ruby

#!/usr/bin/env ruby
# -*- coding: binary -*-
require 'rex/post/meterpreter/packet_response_waiter'
require 'rex/logging'
require 'rex/exceptions'
module Rex
module Post
module Meterpreter
###
#
# Exception thrown when a request fails.
#
###
class RequestError < ArgumentError
def initialize(method, einfo, ecode=nil)
@method = method
@result = einfo
@code = ecode || einfo
end
def to_s
"#{@method}: Operation failed: #{@result}"
end
# The method that failed.
attr_reader :method
# The error result that occurred, typically a windows error message.
attr_reader :result
# The error result that occurred, typically a windows error code.
attr_reader :code
end
###
#
# Handles packet transmission, reception, and correlation,
# and processing
#
###
module PacketDispatcher
PacketTimeout = 600
##
#
# Synchronization
#
##
attr_accessor :comm_mutex
##
#
#
# Passive Dispatching
#
##
attr_accessor :passive_service, :send_queue, :recv_queue
def initialize_passive_dispatcher
self.send_queue = []
self.recv_queue = []
self.waiters = []
self.alive = true
self.passive_service = self.passive_dispatcher
self.passive_service.remove_resource("/" + self.conn_id + "/")
self.passive_service.add_resource("/" + self.conn_id + "/",
'Proc' => Proc.new { |cli, req| on_passive_request(cli, req) },
'VirtualDirectory' => true
)
end
def shutdown_passive_dispatcher
return if not self.passive_service
self.passive_service.remove_resource("/" + self.conn_id + "/")
self.alive = false
self.send_queue = []
self.recv_queue = []
self.waiters = []
self.passive_service = nil
end
def on_passive_request(cli, req)
begin
resp = Rex::Proto::Http::Response.new(200, "OK")
resp['Content-Type'] = 'application/octet-stream'
resp['Connection'] = 'close'
# If the first 4 bytes are "RECV", return the oldest packet from the outbound queue
if req.body[0,4] == "RECV"
rpkt = send_queue.pop
resp.body = rpkt || ''
begin
cli.send_response(resp)
rescue ::Exception => e
send_queue.unshift(rpkt) if rpkt
elog("Exception sending a reply to the reader request: #{cli.inspect} #{e.class} #{e} #{e.backtrace}")
end
else
resp.body = ""
if req.body and req.body.length > 0
packet = Packet.new(0)
packet.from_r(req.body)
dispatch_inbound_packet(packet)
end
cli.send_response(resp)
end
# Force a closure for older WinInet implementations
self.passive_service.close_client( cli )
rescue ::Exception => e
elog("Exception handling request: #{cli.inspect} #{req.inspect} #{e.class} #{e} #{e.backtrace}")
end
end
##
#
# Transmission
#
##
#
# Sends a packet without waiting for a response.
#
def send_packet(packet, completion_routine = nil, completion_param = nil)
if (completion_routine)
add_response_waiter(packet, completion_routine, completion_param)
end
bytes = 0
raw = packet.to_r
err = nil
# Short-circuit send when using a passive dispatcher
if self.passive_service
send_queue.push(raw)
return raw.size # Lie!
end
if (raw)
# This mutex is used to lock out new commands during an
# active migration.
self.comm_mutex.synchronize do
begin
bytes = self.sock.write(raw)
rescue ::Exception => e
err = e
end
end
if bytes.to_i == 0
# Mark the session itself as dead
self.alive = false
# Indicate that the dispatcher should shut down too
@finish = true
# Reraise the error to the top-level caller
raise err if err
end
end
return bytes
end
#
# Sends a packet and waits for a timeout for the given time interval.
#
def send_request(packet, t = self.response_timeout)
if not t
send_packet(packet)
return nil
end
response = send_packet_wait_response(packet, t)
if (response == nil)
raise TimeoutError.new("Send timed out")
elsif (response.result != 0)
einfo = lookup_error(response.result)
e = RequestError.new(packet.method, einfo, response.result)
e.set_backtrace(caller)
raise e
end
return response
end
#
# Transmits a packet and waits for a response.
#
def send_packet_wait_response(packet, t)
# First, add the waiter association for the supplied packet
waiter = add_response_waiter(packet)
# Transmit the packet
if (send_packet(packet).to_i <= 0)
# Remove the waiter if we failed to send the packet.
remove_response_waiter(waiter)
return nil
end
# Wait for the supplied time interval
waiter.wait(t)
# Remove the waiter from the list of waiters in case it wasn't
# removed
remove_response_waiter(waiter)
# Return the response packet, if any
return waiter.response
end
##
#
# Reception
#
##
#
# Monitors the PacketDispatcher's sock for data in its own
# thread context and parsers all inbound packets.
#
def monitor_socket
# Skip if we are using a passive dispatcher
return if self.passive_service
self.comm_mutex = ::Mutex.new
self.waiters = []
@pqueue = []
@finish = false
@last_recvd = Time.now
@ping_sent = false
self.alive = true
# Spawn a thread for receiving packets
self.receiver_thread = Rex::ThreadFactory.spawn("MeterpreterReceiver", false) do
while (self.alive)
begin
rv = Rex::ThreadSafe.select([ self.sock.fd ], nil, nil, 0.25)
ping_time = 60
# If there's nothing to read, and it's been awhile since we
# saw a packet, we need to send a ping. We wait
# ping_time*2 seconds before deciding a session is dead.
if (not rv and self.send_keepalives and Time.now - @last_recvd > ping_time)
# If the queue is empty and we've already sent a
# keepalive without getting a reply, then this
# session is hosed, and we should give up on it.
if @ping_sent and @pqueue.empty? and (Time.now - @last_recvd > ping_time * 2)
dlog("No response to ping, session #{self.sid} is dead", LEV_3)
self.alive = false
@finish = true
break
end
# Let the packet queue processor finish up before
# we send a ping.
if not @ping_sent and @pqueue.empty?
# Our 'ping' is actually just a check for eof on
# channel id 0. This method has no side effects
# and always returns an answer (regardless of the
# existence of chan 0), which is all that's
# needed for a liveness check. The answer itself
# is unimportant and is ignored.
pkt = Packet.create_request('core_channel_eof')
pkt.add_tlv(TLV_TYPE_CHANNEL_ID, 0)
waiter = Proc.new { |response, param|
@ping_sent = false
@last_recvd = Time.now
}
send_packet(pkt, waiter)
@ping_sent = true
end
next
end
next if not rv
packet = receive_packet
@pqueue << packet if packet
@last_recvd = Time.now
rescue ::Exception
dlog("Exception caught in monitor_socket: #{$!}", 'meterpreter', LEV_1)
@finish = true
self.alive = false
break
end
end
end
# Spawn a new thread that monitors the socket
self.dispatcher_thread = Rex::ThreadFactory.spawn("MeterpreterDispatcher", false) do
begin
# Whether we're finished or not is determined by the receiver
# thread above.
while(not @finish)
if(@pqueue.empty?)
::IO.select(nil, nil, nil, 0.10)
next
end
incomplete = []
backlog = []
while(@pqueue.length > 0)
backlog << @pqueue.shift
end
#
# Prioritize message processing here
# 1. Close should always be processed at the end
# 2. Command responses always before channel data
#
tmp_command = []
tmp_channel = []
tmp_close = []
backlog.each do |pkt|
if(pkt.response?)
tmp_command << pkt
next
end
if(pkt.method == "core_channel_close")
tmp_close << pkt
next
end
tmp_channel << pkt
end
backlog = []
backlog.push(*tmp_command)
backlog.push(*tmp_channel)
backlog.push(*tmp_close)
#
# Process the message queue
#
backlog.each do |pkt|
begin
if ! dispatch_inbound_packet(pkt)
# Only requeue packets newer than the timeout
if (::Time.now.to_i - pkt.created_at.to_i < PacketTimeout)
incomplete << pkt
end
end
rescue ::Exception => e
dlog("Dispatching exception with packet #{pkt}: #{e} #{e.backtrace}", 'meterpreter', LEV_1)
end
end
# If the backlog and incomplete arrays are the same, it means
# dispatch_inbound_packet wasn't able to handle any of the
# packets. When that's the case, we can get into a situation
# where @pqueue is not empty and, since nothing else bounds this
# loop, we spin CPU trying to handle packets that can't be
# handled. Sleep here to treat that situation as though the
# queue is empty.
if (backlog.length > 0 && backlog.length == incomplete.length)
::IO.select(nil, nil, nil, 0.10)
end
@pqueue.unshift(*incomplete)
if(@pqueue.length > 100)
dlog("Backlog has grown to over 100 in monitor_socket, dropping older packets: #{@pqueue[0 .. 25].map{|x| x.inspect}.join(" - ")}", 'meterpreter', LEV_1)
@pqueue = @pqueue[25 .. 100]
end
end
rescue ::Exception => e
dlog("Exception caught in monitor_socket dispatcher: #{e.class} #{e} #{e.backtrace}", 'meterpreter', LEV_1)
ensure
self.receiver_thread.kill if self.receiver_thread
end
end
end
#
# Parses data from the dispatcher's sock and returns a Packet context
# once a full packet has been received.
#
def receive_packet
return parser.recv(self.sock)
end
#
# Stop the monitor
#
def monitor_stop
if(self.receiver_thread)
self.receiver_thread.kill
self.receiver_thread = nil
end
if(self.dispatcher_thread)
self.dispatcher_thread.kill
self.dispatcher_thread = nil
end
end
##
#
# Waiter registration
#
##
#
# Adds a waiter association with the supplied request packet.
#
def add_response_waiter(request, completion_routine = nil, completion_param = nil)
waiter = PacketResponseWaiter.new(request.rid, completion_routine, completion_param)
self.waiters << waiter
return waiter
end
#
# Notifies a whomever is waiting for a the supplied response,
# if anyone.
#
def notify_response_waiter(response)
self.waiters.each() { |waiter|
if (waiter.waiting_for?(response))
waiter.notify(response)
remove_response_waiter(waiter)
break
end
}
end
#
# Removes a waiter from the list of waiters.
#
def remove_response_waiter(waiter)
self.waiters.delete(waiter)
end
##
#
# Dispatching
#
##
#
# Initializes the inbound handlers.
#
def initialize_inbound_handlers
@inbound_handlers = []
end
#
# Dispatches and processes an inbound packet. If the packet is a
# response that has an associated waiter, the waiter is notified.
# Otherwise, the packet is passed onto any registered dispatch
# handlers until one returns success.
#
def dispatch_inbound_packet(packet, client = nil)
handled = false
# If no client context was provided, return self as PacketDispatcher
# is a mixin for the Client instance
if (client == nil)
client = self
end
# If the packet is a response, try to notify any potential
# waiters
if ((resp = packet.response?))
if (notify_response_waiter(packet))
return true
end
end
# Enumerate all of the inbound packet handlers until one handles
# the packet
@inbound_handlers.each { |handler|
handled = nil
begin
if ! resp
handled = handler.request_handler(client, packet)
else
handled = handler.response_handler(client, packet)
end
rescue ::Exception => e
dlog("Exception caught in dispatch_inbound_packet: handler=#{handler} #{e.class} #{e} #{e.backtrace}", 'meterpreter', LEV_1)
return true
end
if (handled)
break
end
}
return handled
end
#
# Registers an inbound packet handler that implements the
# InboundPacketHandler interface.
#
def register_inbound_handler(handler)
@inbound_handlers << handler
end
#
# Deregisters a previously registered inbound packet handler.
#
def deregister_inbound_handler(handler)
@inbound_handlers.delete(handler)
end
protected
attr_accessor :receiver_thread # :nodoc:
attr_accessor :dispatcher_thread # :nodoc:
attr_accessor :waiters # :nodoc:
end
end; end; end