lib/ruby_smb/client/negotiation.rb
require 'securerandom'
module RubySMB
class Client
# This module holds all of the methods backing the {RubySMB::Client#negotiate} method
module Negotiation
# Handles the entire SMB Multi-Protocol Negotiation from the
# Client to the Server. It sets state on the client appropriate
# to the protocol and capabilities negotiated during the exchange.
# It also keeps track of the negotiated dialect.
#
# @return [void]
def negotiate
request_packet = negotiate_request
raw_response = send_recv(request_packet)
response_packet = negotiate_response(raw_response)
# The list of dialect identifiers sent to the server is stored
# internally to be able to retrieve the negotiated dialect later on.
# This is only valid for SMB1.
response_packet.dialects = request_packet.dialects if response_packet.respond_to? :dialects=
version = parse_negotiate_response(response_packet)
if @dialect == '0x0311'
update_preauth_hash(request_packet)
update_preauth_hash(response_packet)
end
# If the response contains an SMB2 dialect and the request was SMB1;
# it indicates that the server supports SMB2 and wants to upgrade the
# connection. The server expects the client to send a subsequent SMB2
# Negotiate request to negotiate the actual SMB 2 Protocol revision to
# be used. The wildcard revision number is sent only in response to a
# multi-protocol negotiate request with the "SMB 2.???" dialect string.
if request_packet.packet_smb_version == 'SMB1' && RubySMB::Dialect[@dialect]&.order == RubySMB::Dialect::ORDER_SMB2
self.smb2_message_id += 1
version = negotiate
end
version
rescue RubySMB::Error::InvalidPacket, Errno::ECONNRESET, RubySMB::Error::CommunicationError => e
version = request_packet.packet_smb_version
version = 'SMB3' if version == 'SMB2' && !@smb2 && @smb3
version = 'SMB2 or SMB3' if version == 'SMB2' && @smb2 && @smb3
error = "Unable to negotiate #{version} with the remote host: #{e.message}"
raise RubySMB::Error::NegotiationFailure, error
end
# Creates the first Negotiate Request Packet according to the SMB version
# used.
#
# @return [RubySMB::SMB1::Packet::NegotiateRequest] a SMB1 Negotiate Request packet if SMB1 is used
# @return [RubySMB::SMB1::Packet::NegotiateRequest] a SMB2 Negotiate Request packet if SMB2 is used
def negotiate_request
if smb1
smb1_negotiate_request
else
smb2_3_negotiate_request
end
end
# Takes the raw response data from the server and tries
# parse it into a valid Response packet object.
#
# @param raw_data [String] the raw binary response from the server
# @return [RubySMB::SMB1::Packet::NegotiateResponseExtended] when the response is an SMB1 Extended Security Negotiate Response Packet
# @return [RubySMB::SMB1::Packet::NegotiateResponse] when the response is an SMB1 Negotiate Response Packet
# @return [RubySMB::SMB2::Packet::NegotiateResponse] when the response is an SMB2 Negotiate Response Packet
def negotiate_response(raw_data)
response = nil
if smb1
packet = RubySMB::SMB1::Packet::NegotiateResponseExtended.read raw_data
unless packet.valid?
packet = RubySMB::SMB1::Packet::NegotiateResponse.read raw_data
end
response = packet if packet.valid?
end
if (smb2 || smb3) && response.nil?
packet = RubySMB::SMB2::Packet::NegotiateResponse.read raw_data
response = packet if packet.valid?
end
if response.nil?
if packet.packet_smb_version == 'SMB1'
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
expected_cmd: RubySMB::SMB1::Packet::NegotiateResponse::COMMAND,
packet: packet
)
elsif packet.packet_smb_version == 'SMB2'
raise RubySMB::Error::InvalidPacket.new(
expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
expected_cmd: RubySMB::SMB2::Packet::NegotiateResponse::COMMAND,
packet: packet
)
else
raise RubySMB::Error::InvalidPacket, 'Unknown SMB protocol version'
end
end
response
end
# Sets the supported SMB Protocol and whether or not
# Signing is enabled based on the Negotiate Response Packet.
# It also stores the negotiated dialect.
#
# @param packet [RubySMB::SMB1::Packet::NegotiateResponseExtended] if SMB1 was negotiated
# @param packet [RubySMB::SMB2::Packet::NegotiateResponse] if SMB2 was negotiated
# @return [String] The SMB version as a string ('SMB1', 'SMB2')
def parse_negotiate_response(packet)
case packet
when RubySMB::SMB1::Packet::NegotiateResponseExtended
self.smb1 = true
self.smb2 = false
self.smb3 = false
self.signing_required = packet.parameter_block.security_mode.security_signatures_required == 1
self.dialect = packet.negotiated_dialect.to_s
# MaxBufferSize is largest message server will receive, measured from start of the SMB header. Subtract 260
# for protocol overhead. Then this value can be used for max read/write size without having to factor in
# protocol overhead every time.
self.server_max_buffer_size = packet.parameter_block.max_buffer_size - 260
self.negotiated_smb_version = 1
self.session_encrypt_data = false
self.negotiation_security_buffer = packet.data_block.security_blob
'SMB1'
when RubySMB::SMB2::Packet::NegotiateResponse
self.smb1 = false
unless packet.dialect_revision.to_i == RubySMB::SMB2::SMB2_WILDCARD_REVISION
self.smb2 = packet.dialect_revision.to_i >= 0x0200 && packet.dialect_revision.to_i < 0x0300
self.smb3 = packet.dialect_revision.to_i >= 0x0300 && packet.dialect_revision.to_i < 0x0400
end
self.signing_required = packet.security_mode.signing_required == 1 if self.smb2 || self.smb3
self.dialect = "0x%04x" % packet.dialect_revision
self.server_max_read_size = packet.max_read_size
self.server_max_write_size = packet.max_write_size
self.server_max_transact_size = packet.max_transact_size
# This value is used in SMB1 only but calculate a valid value anyway
self.server_max_buffer_size = [self.server_max_read_size, self.server_max_write_size, self.server_max_transact_size].min
self.negotiated_smb_version = self.smb2 ? 2 : 3
self.server_guid = packet.server_guid
self.server_start_time = packet.server_start_time.to_time if packet.server_start_time != 0
self.server_system_time = packet.system_time.to_time if packet.system_time != 0
self.server_supports_multi_credit = self.dialect != '0x0202' && packet&.capabilities&.large_mtu == 1
self.negotiation_security_buffer = packet.security_buffer
case self.dialect
when '0x02ff'
when '0x0300', '0x0302'
if packet&.capabilities&.encryption == 1
self.encryption_algorithm = RubySMB::SMB2::EncryptionCapabilities::ENCRYPTION_ALGORITHM_MAP[RubySMB::SMB2::EncryptionCapabilities::AES_128_CCM]
end
self.session_encrypt_data = self.session_encrypt_data && !self.encryption_algorithm.nil?
when '0x0311'
parse_smb3_capabilities(packet)
self.session_encrypt_data = self.session_encrypt_data && !self.encryption_algorithm.nil?
else
self.session_encrypt_data = false
end
return "SMB#{self.negotiated_smb_version}"
else
error = 'Unable to negotiate with remote host'
if packet.status_code == WindowsError::NTStatus::STATUS_NOT_SUPPORTED
error << ", SMB2" if @smb2
error << ", SMB3" if @smb3
error << ' not supported'
end
raise RubySMB::Error::NegotiationFailure, error
end
end
def parse_smb3_capabilities(response_packet)
nc = response_packet.find_negotiate_context(
RubySMB::SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES
)
@preauth_integrity_hash_algorithm = RubySMB::SMB2::PreauthIntegrityCapabilities::HASH_ALGORITM_MAP[nc&.data&.hash_algorithms&.first]
unless @preauth_integrity_hash_algorithm
raise RubySMB::Error::EncryptionError.new(
'Unable to retrieve the Preauth Integrity Hash Algorithm from the Negotiate response'
)
end
# Set the encryption the client will use, prioritizing AES_128_GCM over AES_128_CCM
nc = response_packet.find_negotiate_context(
RubySMB::SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES
)
@server_encryption_algorithms = nc&.data&.ciphers&.to_ary
if @server_encryption_algorithms.nil? || @server_encryption_algorithms.empty?
raise RubySMB::Error::EncryptionError.new(
'Unable to retrieve the encryption cipher list supported by the server from the Negotiate response'
)
end
if @server_encryption_algorithms.include?(RubySMB::SMB2::EncryptionCapabilities::AES_128_GCM)
@encryption_algorithm = RubySMB::SMB2::EncryptionCapabilities::ENCRYPTION_ALGORITHM_MAP[RubySMB::SMB2::EncryptionCapabilities::AES_128_GCM]
else
@encryption_algorithm = RubySMB::SMB2::EncryptionCapabilities::ENCRYPTION_ALGORITHM_MAP[@server_encryption_algorithms.first]
end
unless @encryption_algorithm
raise RubySMB::Error::EncryptionError.new(
'Unable to retrieve the encryption cipher list supported by the server from the Negotiate response'
)
end
nc = response_packet.find_negotiate_context(
RubySMB::SMB2::NegotiateContext::SMB2_COMPRESSION_CAPABILITIES
)
@server_compression_algorithms = nc&.data&.compression_algorithms&.to_ary || []
end
# Create a {RubySMB::SMB1::Packet::NegotiateRequest} packet with the
# dialects filled in based on the protocol options set on the Client.
#
# @return [RubySMB::SMB1::Packet::NegotiateRequest] a completed SMB1 Negotiate Request packet
def smb1_negotiate_request
packet = RubySMB::SMB1::Packet::NegotiateRequest.new
# Default to always enabling Extended Security. It simplifies the Negotiation process
# while being guaranteed to work with any modern Windows system. We can get more sophisticated
# with switching this on and off at a later date if the need arises.
packet.smb_header.flags2.extended_security = 1
# Recent Mac OS X requires the unicode flag to be set on the Negotiate
# SMB Header request, even if this packet does not contain string fields
# (see Flags2 SMB_FLAGS2_UNICODE definition in "2.2.3.1 The SMB Header"
# documentation:
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/69a29f73-de0c-45a6-a1aa-8ceeea42217f
packet.smb_header.flags2.unicode = 1
# There is no real good reason to ever send an SMB1 Negotiate packet
# to Negotiate strictly SMB2, but the protocol WILL support it
packet.add_dialect(SMB1_DIALECT_SMB1_DEFAULT) if smb1
packet.add_dialect(SMB1_DIALECT_SMB2_DEFAULT) if smb2
packet.add_dialect(SMB1_DIALECT_SMB2_WILDCARD) if smb2 || smb3
packet
end
# Create a {RubySMB::SMB2::Packet::NegotiateRequest} packet with
# the default dialect added. This will never be used when we
# may want to communicate over SMB1
#
# @ return [RubySMB::SMB2::Packet::NegotiateRequest] a completed SMB2 Negotiate Request packet
def smb2_3_negotiate_request
packet = RubySMB::SMB2::Packet::NegotiateRequest.new
packet.security_mode.signing_enabled = 1
packet.client_guid = SecureRandom.random_bytes(16)
packet.set_dialects(SMB2_DIALECT_DEFAULT.map {|d| d.to_i(16)}) if smb2
packet = add_smb3_to_negotiate_request(packet) if smb3
packet
end
# This adds SMBv3 specific information: SMBv3 supported dialects,
# encryption capability, Negotiate Contexts if the dialect requires them
#
# @param packet [RubySMB::SMB2::Packet::NegotiateRequest] the NegotiateRequest
# to add SMB3 specific info to
# @param dialects [Array<String>] the dialects to negotiate. This must be
# an array of strings. Default is SMB3_DIALECT_DEFAULT
# @return [RubySMB::SMB2::Packet::NegotiateRequest] a completed SMB3 Negotiate Request packet
# @raise [ArgumentError] if dialects is not an array of strings
def add_smb3_to_negotiate_request(packet, dialects = SMB3_DIALECT_DEFAULT)
dialects.each do |dialect|
raise ArgumentError, 'Must be an array of strings' unless dialect.is_a? String
packet.add_dialect(dialect.to_i(16))
end
packet.capabilities.encryption = @session_encrypt_data ? 1 : 0
if packet.dialects.include?(0x0311)
nc = RubySMB::SMB2::NegotiateContext.new(
context_type: RubySMB::SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES
)
nc.data.hash_algorithms << RubySMB::SMB2::PreauthIntegrityCapabilities::SHA_512
nc.data.salt = SecureRandom.random_bytes(32)
packet.add_negotiate_context(nc)
@preauth_integrity_hash_value = "\x00" * 64
nc = RubySMB::SMB2::NegotiateContext.new(
context_type: RubySMB::SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES
)
nc.data.ciphers << RubySMB::SMB2::EncryptionCapabilities::AES_256_GCM
nc.data.ciphers << RubySMB::SMB2::EncryptionCapabilities::AES_256_CCM
nc.data.ciphers << RubySMB::SMB2::EncryptionCapabilities::AES_128_GCM
nc.data.ciphers << RubySMB::SMB2::EncryptionCapabilities::AES_128_CCM
packet.add_negotiate_context(nc)
nc = RubySMB::SMB2::NegotiateContext.new(
context_type: RubySMB::SMB2::NegotiateContext::SMB2_COMPRESSION_CAPABILITIES
)
# Adding all possible compression algorithm even if we don't support
# them yet. This will force the server to disclose the supported
# algorithms in the response.
nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::LZNT1
nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::LZ77
nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::LZ77_Huffman
nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::Pattern_V1
packet.add_negotiate_context(nc)
end
packet
end
end
end
end