import os, sys, struct, types, logging, binascii, time from io import StringIO from .smb_constants import * # Set to True if you want to enable support for extended security. Required for Windows Vista and later SUPPORT_EXTENDED_SECURITY = True # Set to True if you want to enable SMB2 protocol. SUPPORT_SMB2 = True # Supported dialects DIALECTS = [ ] for i, ( name, dialect ) in enumerate([ ( 'NT_LAN_MANAGER_DIALECT', b'NT LM 0.12' ), ]): DIALECTS.append(dialect) globals()[name] = i DIALECTS2 = [ ] for i, ( name, dialect ) in enumerate([ ( 'SMB2_DIALECT', b'SMB 2.002' ) ]): DIALECTS2.append(dialect) globals()[name] = i + len(DIALECTS) class UnsupportedFeature(Exception): """ Raised when an supported feature is present/required in the protocol but is not currently supported by pysmb """ pass class ProtocolError(Exception): def __init__(self, message, data_buf = None, smb_message = None): self.message = message self.data_buf = data_buf self.smb_message = smb_message def __str__(self): b = StringIO() b.write(self.message + os.linesep) if self.smb_message: b.write('=' * 20 + ' SMB Message ' + '=' * 20 + os.linesep) b.write(str(self.smb_message)) if self.data_buf: b.write('=' * 20 + ' SMB Data Packet (hex) ' + '=' * 20 + os.linesep) b.write(str(binascii.hexlify(self.data_buf))) b.write(os.linesep) return b.getvalue() class SMB2ProtocolHeaderError(ProtocolError): def __init__(self): ProtocolError.__init__(self, "Packet header belongs to SMB2") class OperationFailure(Exception): def __init__(self, message, smb_messages): self.message = message self.smb_messages = smb_messages def __str__(self): b = StringIO() b.write(self.message + os.linesep) for idx, m in enumerate(self.smb_messages): b.write('=' * 20 + ' SMB Message %d ' % idx + '=' * 20 + os.linesep) b.write('SMB Header:' + os.linesep) b.write('-----------' + os.linesep) b.write(str(m)) b.write('SMB Data Packet (hex):' + os.linesep) b.write('----------------------' + os.linesep) b.write(str(binascii.hexlify(m.raw_data))) b.write(os.linesep) return b.getvalue() class SMBError: def __init__(self): self.reset() def reset(self): self.internal_value = 0 self.is_ntstatus = True def __str__(self): if self.is_ntstatus: return 'NTSTATUS=0x%08X' % self.internal_value else: return 'ErrorClass=0x%02X ErrorCode=0x%04X' % ( self.internal_value >> 24, self.internal_value & 0xFFFF ) @property def hasError(self): return self.internal_value != 0 class SMBMessage: HEADER_STRUCT_FORMAT = "<4sBIBHHQxxHHHHB" HEADER_STRUCT_SIZE = struct.calcsize(HEADER_STRUCT_FORMAT) log = logging.getLogger('SMB.SMBMessage') protocol = 1 def __init__(self, payload = None): self.reset() if payload: self.payload = payload self.payload.initMessage(self) def __str__(self): b = StringIO() b.write('Command: 0x%02X (%s) %s' % ( self.command, SMB_COMMAND_NAMES.get(self.command, ''), os.linesep )) b.write('Status: %s %s' % ( str(self.status), os.linesep )) b.write('Flags: 0x%02X %s' % ( self.flags, os.linesep )) b.write('Flags2: 0x%04X %s' % ( self.flags2, os.linesep )) b.write('PID: %d %s' % ( self.pid, os.linesep )) b.write('UID: %d %s' % ( self.uid, os.linesep )) b.write('MID: %d %s' % ( self.mid, os.linesep )) b.write('TID: %d %s' % ( self.tid, os.linesep )) b.write('Security: 0x%016X %s' % ( self.security, os.linesep )) b.write('Parameters: %d bytes %s%s %s' % ( len(self.parameters_data), os.linesep, str(binascii.hexlify(self.parameters_data)), os.linesep )) b.write('Data: %d bytes %s%s %s' % ( len(self.data), os.linesep, str(binascii.hexlify(self.data)), os.linesep )) return b.getvalue() def reset(self): self.raw_data = b'' self.command = 0 self.status = SMBError() self.flags = 0 self.flags2 = 0 self.pid = 0 self.tid = 0 self.uid = 0 self.mid = 0 self.security = 0 self.parameters_data = b'' self.data = b'' self.payload = None @property def isAsync(self): return bool(self.flags & SMB2_FLAGS_ASYNC_COMMAND) @property def isReply(self): return bool(self.flags & SMB_FLAGS_REPLY) @property def hasExtendedSecurity(self): return bool(self.flags2 & SMB_FLAGS2_EXTENDED_SECURITY) def encode(self): """ Encode this SMB message into a series of bytes suitable to be embedded with a NetBIOS session message. AssertionError will be raised if this SMB message has not been initialized with a Payload instance @return: a string containing the encoded SMB message """ assert self.payload self.pid = os.getpid() self.payload.prepare(self) parameters_len = len(self.parameters_data) assert parameters_len % 2 == 0 headers_data = struct.pack(self.HEADER_STRUCT_FORMAT, b'\xFFSMB', self.command, self.status.internal_value, self.flags, self.flags2, (self.pid >> 16) & 0xFFFF, self.security, self.tid, self.pid & 0xFFFF, self.uid, self.mid, int(parameters_len / 2)) return headers_data + self.parameters_data + struct.pack(' 0 and buf_len < (datalen_offset + 2 + body_len): # Not enough data in buf to decode body raise ProtocolError('Not enough data. Body decoding failed', buf) self.parameters_data = buf[offset:datalen_offset] if body_len > 0: self.data = buf[datalen_offset+2:datalen_offset+2+body_len] self.raw_data = buf self._decodePayload() return self.HEADER_STRUCT_SIZE + params_count * 2 + 2 + body_len def _decodePayload(self): if self.command == SMB_COM_READ_ANDX: self.payload = ComReadAndxResponse() elif self.command == SMB_COM_WRITE_ANDX: self.payload = ComWriteAndxResponse() elif self.command == SMB_COM_TRANSACTION: self.payload = ComTransactionResponse() elif self.command == SMB_COM_TRANSACTION2: self.payload = ComTransaction2Response() elif self.command == SMB_COM_OPEN_ANDX: self.payload = ComOpenAndxResponse() elif self.command == SMB_COM_NT_CREATE_ANDX: self.payload = ComNTCreateAndxResponse() elif self.command == SMB_COM_TREE_CONNECT_ANDX: self.payload = ComTreeConnectAndxResponse() elif self.command == SMB_COM_ECHO: self.payload = ComEchoResponse() elif self.command == SMB_COM_SESSION_SETUP_ANDX: self.payload = ComSessionSetupAndxResponse() elif self.command == SMB_COM_NEGOTIATE: self.payload = ComNegotiateResponse() if self.payload: self.payload.decode(self) class Payload: DEFAULT_ANDX_PARAM_HEADER = b'\xFF\x00\x00\x00' DEFAULT_ANDX_PARAM_SIZE = 4 def initMessage(self, message): # SMB_FLAGS2_UNICODE must always be enabled. Without this, almost all the Payload subclasses will need to be # rewritten to check for OEM/Unicode strings which will be tedious. Fortunately, almost all tested CIFS services # support SMB_FLAGS2_UNICODE by default. assert message.payload == self message.flags = SMB_FLAGS_CASE_INSENSITIVE | SMB_FLAGS_CANONICALIZED_PATHS message.flags2 = SMB_FLAGS2_UNICODE | SMB_FLAGS2_NT_STATUS | SMB_FLAGS2_IS_LONG_NAME | SMB_FLAGS2_LONG_NAMES if SUPPORT_EXTENDED_SECURITY: message.flags2 |= SMB_FLAGS2_EXTENDED_SECURITY def prepare(self, message): raise NotImplementedError def decode(self, message): raise NotImplementedError class ComNegotiateRequest(Payload): """ References: =========== - [MS-CIFS]: 2.2.4.52.1 - [MS-SMB]: 2.2.4.5.1 """ def initMessage(self, message): Payload.initMessage(self, message) message.command = SMB_COM_NEGOTIATE def prepare(self, message): assert message.payload == self message.parameters_data = b'' if SUPPORT_SMB2: message.data = b''.join([b'\x02'+s+b'\x00' for s in DIALECTS + DIALECTS2]) else: message.data = b''.join([b'\x02'+s+b'\x00' for s in DIALECTS]) class ComNegotiateResponse(Payload): """ Contains information on the SMB_COM_NEGOTIATE response from server After calling the decode method, each instance will contain the following attributes, - security_mode (integer) - max_mpx_count (integer) - max_number_vcs (integer) - max_buffer_size (long) - max_raw_size (long) - session_key (long) - capabilities (long) - system_time (long) - server_time_zone (integer) - challenge_length (integer) If the underlying SMB message's flag2 does not have SMB_FLAGS2_EXTENDED_SECURITY bit enabled, then the instance will have the following additional attributes, - challenge (string) - domain (unicode) If the underlying SMB message's flags2 has SMB_FLAGS2_EXTENDED_SECURITY bit enabled, then the instance will have the following additional attributes, - server_guid (string) - security_blob (string) References: =========== - [MS-SMB]: 2.2.4.5.2.1 - [MS-CIFS]: 2.2.4.52.2 """ PAYLOAD_STRUCT_FORMAT = ' 0: if data_len >= self.challenge_length: self.challenge = message.data[:self.challenge_length] s = b'' offset = self.challenge_length while offset < data_len: _s = message.data[offset:offset+2] if _s == b'\0\0': self.domain = s.decode('UTF-16LE') break else: s += _s offset += 2 else: raise ProtocolError('Not enough data to decode SMB_COM_NEGOTIATE (without security extensions) Challenge field', message.raw_data, message) else: if data_len < 16: raise ProtocolError('Not enough data to decode SMB_COM_NEGOTIATE (with security extensions) ServerGUID field', message.raw_data, message) self.server_guid = message.data[:16] self.security_blob = message.data[16:] @property def supportsExtendedSecurity(self): return bool(self.capabilities & CAP_EXTENDED_SECURITY) class ComSessionSetupAndxRequest__WithSecurityExtension(Payload): """ References: =========== - [MS-SMB]: 2.2.4.6.1 """ PAYLOAD_STRUCT_FORMAT = ' 0: params_bytes_offset = offset offset += params_bytes_len else: params_bytes_offset = 0 padding2 = b'' if offset % 4 != 0: padding2 = b'\0'*(4-offset%4) offset += (4-offset%4) if data_bytes_len > 0: data_bytes_offset = offset else: data_bytes_offset = 0 message.parameters_data = \ struct.pack(self.PAYLOAD_STRUCT_FORMAT, self.total_params_count, self.total_data_count, self.max_params_count, self.max_data_count, self.max_setup_count, 0x00, # Reserved1. Must be 0x00 self.flags, self.timeout, 0x0000, # Reserved2. Must be 0x0000 params_bytes_len, params_bytes_offset, data_bytes_len, data_bytes_offset, int(setup_bytes_len / 2)) + \ self.setup_bytes message.data = padding0 + name + padding1 + self.params_bytes + padding2 + self.data_bytes class ComTransactionResponse(Payload): """ Contains information about a SMB_COM_TRANSACTION response from the server After decoding, each instance contains the following attributes: - total_params_count (integer) - total_data_count (integer) - setup_bytes (string) - data_bytes (string) - params_bytes (string) References: =========== - [MS-CIFS]: 2.2.4.33.2 """ PAYLOAD_STRUCT_FORMAT = ' 0: setup_bytes_len = setup_count * 2 if len(message.parameters_data) < self.PAYLOAD_STRUCT_SIZE + setup_bytes_len: raise ProtocolError('Not enough data to decode SMB_COM_TRANSACTION parameters', message.raw_data, message) self.setup_bytes = message.parameters_data[self.PAYLOAD_STRUCT_SIZE:self.PAYLOAD_STRUCT_SIZE+setup_bytes_len] else: self.setup_bytes = '' offset = message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count * 2 + 2 # constant 2 is for the ByteCount field in the SMB header (i.e. field which indicates number of data bytes after the SMB parameters) if params_bytes_len > 0: self.params_bytes = message.data[params_bytes_offset-offset:params_bytes_offset-offset+params_bytes_len] else: self.params_bytes = '' if data_bytes_len > 0: self.data_bytes = message.data[data_bytes_offset-offset:data_bytes_offset-offset+data_bytes_len] else: self.data_bytes = '' class ComTransaction2Request(Payload): """ References: =========== - [MS-CIFS]: 2.2.4.46.1 """ PAYLOAD_STRUCT_FORMAT = 'HHHHBBHIHHHHHH' PAYLOAD_STRUCT_SIZE = struct.calcsize(PAYLOAD_STRUCT_FORMAT) def __init__(self, max_params_count, max_data_count, max_setup_count, total_params_count = 0, total_data_count = 0, params_bytes = b'', data_bytes = b'', setup_bytes = b'', flags = 0, timeout = 0): self.total_params_count = total_params_count or len(params_bytes) self.total_data_count = total_data_count or len(data_bytes) self.max_params_count = max_params_count self.max_data_count = max_data_count self.max_setup_count = max_setup_count self.flags = flags self.timeout = timeout self.params_bytes = params_bytes self.data_bytes = data_bytes self.setup_bytes = setup_bytes def initMessage(self, message): Payload.initMessage(self, message) message.command = SMB_COM_TRANSACTION2 def prepare(self, message): setup_bytes_len = len(self.setup_bytes) params_bytes_len = len(self.params_bytes) data_bytes_len = len(self.data_bytes) name = b'\0\0' padding0 = b'' offset = message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_bytes_len + 2 # constant 2 is for the ByteCount field in the SMB header (i.e. field which indicates number of data bytes after the SMB parameters) if offset % 2 != 0: padding0 = b'\0' offset += 1 offset += 2 # For the name field padding1 = b'' if offset % 4 != 0: padding1 = b'\0'*(4-offset%4) if params_bytes_len > 0: params_bytes_offset = offset offset += params_bytes_len else: params_bytes_offset = 0 padding2 = b'' if offset % 4 != 0: padding2 = b'\0'*(4-offset%4) if data_bytes_len > 0: data_bytes_offset = offset else: data_bytes_offset = 0 message.parameters_data = \ struct.pack(self.PAYLOAD_STRUCT_FORMAT, self.total_params_count, self.total_data_count, self.max_params_count, self.max_data_count, self.max_setup_count, 0x00, # Reserved1. Must be 0x00 self.flags, self.timeout, 0x0000, # Reserved2. Must be 0x0000 params_bytes_len, params_bytes_offset, data_bytes_len, data_bytes_offset, int(setup_bytes_len / 2)) + \ self.setup_bytes message.data = padding0 + name + padding1 + self.params_bytes + padding2 + self.data_bytes class ComTransaction2Response(Payload): """ Contains information about a SMB_COM_TRANSACTION2 response from the server After decoding, each instance contains the following attributes: - total_params_count (integer) - total_data_count (integer) - setup_bytes (string) - data_bytes (string) - params_bytes (string) References: =========== - [MS-CIFS]: 2.2.4.46.2 """ PAYLOAD_STRUCT_FORMAT = ' 0: setup_bytes_len = setup_count * 2 if len(message.parameters_data) < self.PAYLOAD_STRUCT_SIZE + setup_bytes_len: raise ProtocolError('Not enough data to decode SMB_COM_TRANSACTION parameters', message.raw_data, message) self.setup_bytes = message.parameters_data[self.PAYLOAD_STRUCT_SIZE:self.PAYLOAD_STRUCT_SIZE+setup_bytes_len] else: self.setup_bytes = '' offset = message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count * 2 + 2 # constant 2 is for the ByteCount field in the SMB header (i.e. field which indicates number of data bytes after the SMB parameters) if params_bytes_len > 0: self.params_bytes = message.data[params_bytes_offset-offset:params_bytes_offset-offset+params_bytes_len] else: self.params_bytes = '' if data_bytes_len > 0: self.data_bytes = message.data[data_bytes_offset-offset:data_bytes_offset-offset+data_bytes_len] else: self.data_bytes = '' class ComCloseRequest(Payload): """ References: =========== - [MS-CIFS]: 2.2.4.5.1 """ PAYLOAD_STRUCT_FORMAT = '> 32) # OffsetHigh field defined in [MS-SMB]: 2.2.4.3.1 message.data = b'\0' + self.data_bytes class ComWriteAndxResponse(Payload): """ References: =========== - [MS-CIFS]: 2.2.4.43.2 - [MS-SMB]: 2.2.4.3.2 """ PAYLOAD_STRUCT_FORMAT = '> 32), # Note that in [MS-SMB]: 2.2.4.2.1, this field can also act as MaxCountHigh field self.remaining, # In [MS-CIFS]: 2.2.4.42.1, this field must be set to 0x0000 self.offset >> 32) message.data = b'' class ComReadAndxResponse(Payload): """ References: =========== - [MS-CIFS]: 2.2.4.42.2 - [MS-SMB]: 2.2.4.2.2 """ PAYLOAD_STRUCT_FORMAT = ' 0: params_bytes_offset = offset else: params_bytes_offset = 0 offset += params_bytes_len padding1 = b'' if offset % 4 != 0: padding1 = b'\0'*(4-offset%4) offset += (4-offset%4) if data_bytes_len > 0: data_bytes_offset = offset else: data_bytes_offset = 0 message.parameters_data = \ struct.pack(self.PAYLOAD_STRUCT_FORMAT, self.max_setup_count, 0x00, # Reserved1. Must be 0x00 self.total_params_count, self.total_data_count, self.max_params_count, self.max_data_count, params_bytes_len, params_bytes_offset, data_bytes_len, data_bytes_offset, int(setup_bytes_len / 2), self.function) + \ self.setup_bytes message.data = padding0 + self.params_bytes + padding1 + self.data_bytes class ComNTTransactResponse(Payload): """ Contains information about a SMB_COM_NT_TRANSACT response from the server After decoding, each instance contains the following attributes: - total_params_count (integer) - total_data_count (integer) - setup_bytes (string) - data_bytes (string) - params_bytes (string) References: =========== - [MS-CIFS]: 2.2.4.62.2 """ PAYLOAD_STRUCT_FORMAT = '<3sIIIIIIIIBH' PAYLOAD_STRUCT_SIZE = struct.calcsize(PAYLOAD_STRUCT_FORMAT) def decode(self, message): assert message.command == SMB_COM_NT_TRANSACT if not message.status.hasError: _, self.total_params_count, self.total_data_count, \ params_count, params_offset, params_displ, \ data_count, data_offset, data_displ, setup_count = struct.unpack(self.PAYLOAD_STRUCT_FORMAT, message.parameters_data[:self.PAYLOAD_STRUCT_SIZE]) self.setup_bytes = message.parameters_data[self.PAYLOAD_STRUCT_SIZE:self.PAYLOAD_STRUCT_SIZE+setup_count*2] if params_count > 0: params_offset -= message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count*2 + 2 self.params_bytes = message.data[params_offset:params_offset+params_count] else: self.params_bytes = b'' if data_count > 0: data_offset -= message.HEADER_STRUCT_SIZE + self.PAYLOAD_STRUCT_SIZE + setup_count*2 + 2 self.data_bytes = message.data[data_offset:data_offset+data_count] else: self.data_bytes = b''