metasploit-framework/lib/net/ssh/transport/state.rb

202 lines
6.1 KiB
Ruby

require 'zlib'
require 'net/ssh/transport/cipher_factory'
require 'net/ssh/transport/hmac'
module Net; module SSH; module Transport
# Encapsulates state information about one end of an SSH connection. Such
# state includes the packet sequence number, the algorithms in use, how
# many packets and blocks have been processed since the last reset, and so
# forth. This class will never be instantiated directly, but is used as
# part of the internal state of the PacketStream module.
class State
# The socket object that owns this state object.
attr_reader :socket
# The next packet sequence number for this socket endpoint.
attr_reader :sequence_number
# The hmac algorithm in use for this endpoint.
attr_reader :hmac
# The compression algorithm in use for this endpoint.
attr_reader :compression
# The compression level to use when compressing data (or nil, for the default).
attr_reader :compression_level
# The number of packets processed since the last call to #reset!
attr_reader :packets
# The number of data blocks processed since the last call to #reset!
attr_reader :blocks
# The cipher algorithm in use for this socket endpoint.
attr_reader :cipher
# The role that this state plays (either :client or :server)
attr_reader :role
# The maximum number of packets that this endpoint wants to process before
# needing a rekey.
attr_accessor :max_packets
# The maximum number of blocks that this endpoint wants to process before
# needing a rekey.
attr_accessor :max_blocks
# The user-specified maximum number of bytes that this endpoint ought to
# process before needing a rekey.
attr_accessor :rekey_limit
# Creates a new state object, belonging to the given socket. Initializes
# the algorithms to "none".
def initialize(socket, role)
@socket = socket
@role = role
@sequence_number = @packets = @blocks = 0
@cipher = CipherFactory.get("none")
@hmac = HMAC.get("none")
@compression = nil
@compressor = @decompressor = nil
@next_iv = ""
end
# A convenience method for quickly setting multiple values in a single
# command.
def set(values)
values.each do |key, value|
instance_variable_set("@#{key}", value)
end
reset!
end
def update_cipher(data)
result = cipher.update(data)
update_next_iv(role == :client ? result : data)
return result
end
def final_cipher
result = cipher.final
update_next_iv(role == :client ? result : "", true)
return result
end
# Increments the counters. The sequence number is incremented (and remapped
# so it always fits in a 32-bit integer). The number of packets and blocks
# are also incremented.
def increment(packet_length)
@sequence_number = (@sequence_number + 1) & 0xFFFFFFFF
@packets += 1
@blocks += (packet_length + 4) / cipher.block_size
end
# The compressor object to use when compressing data. This takes into account
# the desired compression level.
def compressor
@compressor ||= Zlib::Deflate.new(compression_level || Zlib::DEFAULT_COMPRESSION)
end
# The decompressor object to use when decompressing data.
def decompressor
@decompressor ||= Zlib::Inflate.new(nil)
end
# Returns true if data compression/decompression is enabled. This will
# return true if :standard compression is selected, or if :delayed
# compression is selected and the :authenticated hint has been received
# by the socket.
def compression?
compression == :standard || (compression == :delayed && socket.hints[:authenticated])
end
# Compresses the data. If no compression is in effect, this will just return
# the data unmodified, otherwise it uses #compressor to compress the data.
def compress(data)
data = data.to_s
return data unless compression?
compressor.deflate(data, Zlib::SYNC_FLUSH)
end
# Deompresses the data. If no compression is in effect, this will just return
# the data unmodified, otherwise it uses #decompressor to decompress the data.
def decompress(data)
data = data.to_s
return data unless compression?
decompressor.inflate(data)
end
# Resets the counters on the state object, but leaves the sequence_number
# unchanged. It also sets defaults for and recomputes the max_packets and
# max_blocks values.
def reset!
@packets = @blocks = 0
@max_packets ||= 1 << 31
if max_blocks.nil?
# cargo-culted from openssh. the idea is that "the 2^(blocksize*2)
# limit is too expensive for 3DES, blowfish, etc., so enforce a 1GB
# limit for small blocksizes."
if cipher.block_size >= 16
@max_blocks = 1 << (cipher.block_size * 2)
else
@max_blocks = (1 << 30) / cipher.block_size
end
# if a limit on the # of bytes has been given, convert that into a
# minimum number of blocks processed.
if rekey_limit
@max_blocks = [@max_blocks, rekey_limit / cipher.block_size].min
end
end
cleanup
end
# Closes any the compressor and/or decompressor objects that have been
# instantiated.
def cleanup
if @compressor
@compressor.finish if !@compressor.finished?
@compressor.close
end
if @decompressor
# we call reset here so that we don't get warnings when we try to
# close the decompressor
@decompressor.reset
@decompressor.close
end
@compressor = @decompressor = nil
end
# Returns true if the number of packets processed exceeds the maximum
# number of packets, or if the number of blocks processed exceeds the
# maximum number of blocks.
def needs_rekey?
max_packets && packets > max_packets ||
max_blocks && blocks > max_blocks
end
private
def update_next_iv(data, reset=false)
@next_iv << data
@next_iv = @next_iv[-cipher.iv_len..-1]
if reset
cipher.reset
cipher.iv = @next_iv
end
return data
end
end
end; end; end