/lib/rex/post/meterpreter/packet_dispatcher.rb
Ruby | 544 lines | 300 code | 95 blank | 149 comment | 45 complexity | 908c0a7aed894df186a3c84da79ed2b6 MD5 | raw file
- #!/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