# Copyright (c) 2003-2006 CORE Security Technologies # # This software is provided under under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # # $Id: nmb.py,v 1.4 2006/05/23 21:19:25 gera Exp $ # # -*- mode: python; tab-width: 4 -*- # # Copyright (C) 2001 Michael Teo # nmb.py - NetBIOS library # # This software is provided 'as-is', without any express or implied warranty. # In no event will the author be held liable for any damages arising from the # use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # # 3. This notice cannot be removed or altered from any source distribution. # # Altered source done by Alberto Solino import os, sys, socket, string, re, select, errno from random import randint from struct import * CVS_REVISION = '$Revision: 1.4 $' # Taken from socket module reference INADDR_ANY = '' BROADCAST_ADDR = '' # Default port for NetBIOS name service NETBIOS_NS_PORT = 137 # Default port for NetBIOS session service NETBIOS_SESSION_PORT = 139 # Default port for SMB session service SMB_SESSION_PORT = 445 # Owner Node Type Constants NODE_B = 0x0000 NODE_P = 0x2000 NODE_M = 0x4000 NODE_RESERVED = 0x6000 NODE_GROUP = 0x8000 NODE_UNIQUE = 0x0 # Name Type Constants TYPE_UNKNOWN = 0x01 TYPE_WORKSTATION = 0x00 TYPE_CLIENT = 0x03 TYPE_SERVER = 0x20 TYPE_DOMAIN_MASTER = 0x1B TYPE_MASTER_BROWSER = 0x1D TYPE_BROWSER = 0x1E TYPE_NETDDE = 0x1F # Opcodes values OPCODE_QUERY = 0 OPCODE_REGISTRATION = 0x5 OPCODE_RELEASE = 0x6 OPCODE_WACK = 0x7 OPCODE_REFRESH = 0x8 OPCODE_REQUEST = 0 OPCODE_RESPONSE = 0x10 # NM_FLAGS NM_FLAGS_BROADCAST = 0x1 NM_FLAGS_UNICAST = 0 NM_FLAGS_RA = 0x8 NM_FLAGS_RD = 0x10 NM_FLAGS_TC = 0x20 NM_FLAGS_AA = 0x40 # QUESTION_TYPE QUESTION_TYPE_NB = 0x20 # NetBIOS general Name Service Resource Record QUESTION_TYPE_NBSTAT = 0x21 # NetBIOS NODE STATUS Resource Record # QUESTION_CLASS QUESTION_CLASS_IN = 0x1 # Internet class # RR_TYPE Resource Record Type code RR_TYPE_A = 0x1 # IP address Resource Record RR_TYPE_NS = 0x2 # Name Server Resource Record RR_TYPE_NULL = 0xA # NULL Resource Record RR_TYPE_NB = 0x20 # NetBIOS general Name Service Resource Record RR_TYPE_NBSTAT = 0x21 # NetBIOS NODE STATUS Resource Record # Resource Record Class RR_CLASS_IN = 1 # Internet class # RCODE values RCODE_FMT_ERR = 0x1 # Format Error. Request was invalidly formatted. RCODE_SRV_ERR = 0x2 # Server failure. Problem with NBNS, cannot process name. RCODE_IMP_ERR = 0x4 # Unsupported request error. Allowable only for challenging NBNS when gets an Update type # registration request. RCODE_RFS_ERR = 0x5 # Refused error. For policy reasons server will not register this name from this host. RCODE_ACT_ERR = 0x6 # Active error. Name is owned by another node. RCODE_CFT_ERR = 0x7 # Name in conflict error. A UNIQUE name is owned by more than one node. # NAME_FLAGS NAME_FLAGS_PRM = 0x0200 # Permanent Name Flag. If one (1) then entry is for the permanent node name. Flag is zero # (0) for all other names. NAME_FLAGS_ACT = 0x0400 # Active Name Flag. All entries have this flag set to one (1). NAME_FLAG_CNF = 0x0800 # Conflict Flag. If one (1) then name on this node is in conflict. NAME_FLAG_DRG = 0x1000 # Deregister Flag. If one (1) then this name is in the process of being deleted. NAME_TYPES = { TYPE_UNKNOWN: 'Unknown', TYPE_WORKSTATION: 'Workstation', TYPE_CLIENT: 'Client', TYPE_SERVER: 'Server', TYPE_MASTER_BROWSER: 'Master Browser', TYPE_BROWSER: 'Browser Server', TYPE_DOMAIN_MASTER: 'Domain Master' , TYPE_NETDDE: 'NetDDE Server'} # NetBIOS Session Types NETBIOS_SESSION_MESSAGE = 0x0 NETBIOS_SESSION_REQUEST = 0x81 NETBIOS_SESSION_POSITIVE_RESPONSE = 0x82 NETBIOS_SESSION_NEGATIVE_RESPONSE = 0x83 NETBIOS_SESSION_RETARGET_RESPONSE = 0x84 NETBIOS_SESSION_KEEP_ALIVE = 0x85 def strerror(errclass, errcode): if errclass == ERRCLASS_OS: return 'OS Error', os.strerror(errcode) elif errclass == ERRCLASS_QUERY: return 'Query Error', QUERY_ERRORS.get(errcode, 'Unknown error') elif errclass == ERRCLASS_SESSION: return 'Session Error', SESSION_ERRORS.get(errcode, 'Unknown error') else: return 'Unknown Error Class', 'Unknown Error' class NetBIOSError(Exception): pass class NetBIOSTimeout(Exception): def __init__(self, message = 'The NETBIOS connection with the remote host timed out.'): Exception.__init__(self, message) class NBResourceRecord: def __init__(self, data = 0): self._data = data try: if self._data: self.rr_name = (re.split('\x00',data))[0] offset = len(self.rr_name)+1 self.rr_type = unpack('>H', self._data[offset:offset+2])[0] self.rr_class = unpack('>H', self._data[offset+2: offset+4])[0] self.ttl = unpack('>L',self._data[offset+4:offset+8])[0] self.rdlength = unpack('>H', self._data[offset+8:offset+10])[0] self.rdata = data[offset+10:self.rdlength] offset = self.rdlength - 2 self.unit_id = data[offset:offset+6] else: self.rr_name = '' self.rr_type = 0 self.rr_class = 0 self.ttl = 0 self.rdlength = 0 self.rdata = '' self.unit_id = '' except Exception,e: raise NetBIOSError( 'Wrong packet format ' ) def set_rr_name(self, name): self.rr_name = name def set_rr_type(self, name): self.rr_type = name def set_rr_class(self,cl): self_rr_class = cl def set_ttl(self,ttl): self.ttl = ttl def set_rdata(self,rdata): self.rdata = rdata self.rdlength = len(rdata) def get_unit_id(self): return self.unit_id def get_rr_name(self): return self.rr_name def get_rr_class(self): return self.rr_class def get_ttl(self): return self.ttl def get_rdlength(self): return self.rdlength def get_rdata(self): return self.rdata def rawData(self): return self.rr_name + pack('!HHLH',self.rr_type, self.rr_class, self.ttl, self.rdlength) + self.rdata class NBNodeStatusResponse(NBResourceRecord): def __init__(self, data = 0): NBResourceRecord.__init__(self,data) self.num_names = 0 self.node_names = [ ] self.statstics = '' self.mac = '00-00-00-00-00-00' try: if data: self._data = self.get_rdata() self.num_names = unpack('>B',self._data[:1])[0] offset = 1 for i in range(0, self.num_names): name = self._data[offset:offset + 15] type,flags = unpack('>BH', self._data[offset + 15: offset + 18]) offset += 18 self.node_names.append(NBNodeEntry(name, type ,flags)) self.set_mac_in_hexa(self.get_unit_id()) except Exception,e: raise NetBIOSError( 'Wrong packet format ' ) def set_mac_in_hexa(self, data): data_aux = '' for d in data: if data_aux == '': data_aux = '%02x' % ord(d) else: data_aux += '-%02x' % ord(d) self.mac = string.upper(data_aux) def get_num_names(self): return self.num_names def get_mac(self): return self.mac def set_num_names(self, num): self.num_names = num def get_node_names(self): return self.node_names def add_node_name(self,node_names): self.node_names.append(node_names) self.num_names += 1 def rawData(self): res = pack('!B', self.num_names ) for i in range(0, self.num_names): res += self.node_names[i].rawData() class NBPositiveNameQueryResponse(NBResourceRecord): def __init__(self,data = 0): NBResourceRecord.__init__(self,data) self.add_entries = [ ] if data: self._data = self.get_rdata() class NetBIOSPacket: """ This is a packet as defined in RFC 1002 """ def __init__(self, data = 0): self.name_trn_id = 0x0 # Transaction ID for Name Service Transaction. # Requestor places a unique value for each active # transaction. Responder puts NAME_TRN_ID value # from request packet in response packet. self.opcode = 0 # Packet type code self.nm_flags = 0 # Flags for operation self.rcode = 0 # Result codes of request. self.qdcount = 0 # Unsigned 16 bit integer specifying the number of entries in the question section of a Name self.ancount = 0 # Unsigned 16 bit integer specifying the number of # resource records in the answer section of a Name # Service packet. self.nscount = 0 # Unsigned 16 bit integer specifying the number of # resource records in the authority section of a # Name Service packet. self.arcount = 0 # Unsigned 16 bit integer specifying the number of # resource records in the additional records # section of a Name Service packeT. self.questions = '' self.answers = '' if data == 0: self._data = '' else: try: self._data = data self.opcode = ord(data[2]) >> 3 self.nm_flags = ((ord(data[2]) & 0x3) << 4) | ((ord(data[3]) & 0xf0) >> 4) self.name_trn_id = unpack('>H', self._data[:2])[0] self.rcode = ord(data[3]) & 0x0f self.qdcount = unpack('>H', self._data[4:6])[0] self.ancount = unpack('>H', self._data[6:8])[0] self.nscount = unpack('>H', self._data[8:10])[0] self.arcount = unpack('>H', self._data[10:12])[0] self.answers = self._data[12:] except Exception,e: raise NetBIOSError( 'Wrong packet format ' ) def set_opcode(self, opcode): self.opcode = opcode def set_trn_id(self, trn): self.name_trn_id = trn def set_nm_flags(self, nm_flags): self.nm_flags = nm_flags def set_rcode(self, rcode): self.rcode = rcode def addQuestion(self, question, qtype, qclass): self.qdcount = self.qdcount + 1 self.questions += question + pack('!HH',qtype,qclass) def get_trn_id(self): return self.name_trn_id def get_rcode(self): return self.rcode def get_nm_flags(self): return self.name_trn_id def get_opcode(self): return self.opcode def get_qdcount(self): return self.qdcount def get_ancount(self): return self.ancount def get_nscount(self): return self.nscount def get_arcount(self): return self.arcount def rawData(self): secondWord = self.opcode << 11 secondWord = secondWord | (self.nm_flags << 4) secondWord = secondWord | self.rcode data = pack('!HHHHHH', self.name_trn_id, secondWord , self.qdcount, self.ancount, self.nscount, self.arcount) + self.questions + self.answers return data def get_answers(self): return self.answers class NBHostEntry: def __init__(self, nbname, nametype, ip): self.__nbname = nbname self.__nametype = nametype self.__ip = ip def get_nbname(self): return self.__nbname def get_nametype(self): return self.__nametype def get_ip(self): return self.__ip def __repr__(self): return '' class NBNodeEntry: def __init__(self, nbname, nametype, flags): self.__nbname = string.ljust(nbname,17) self.__nametype = nametype self.__flags = flags self.__isgroup = flags & 0x8000 self.__nodetype = flags & 0x6000 self.__deleting = flags & 0x1000 self.__isconflict = flags & 0x0800 self.__isactive = flags & 0x0400 self.__ispermanent = flags & 0x0200 def get_nbname(self): return self.__nbname def get_nametype(self): return self.__nametype def is_group(self): return self.__isgroup def get_nodetype(self): return self.__nodetype def is_deleting(self): return self.__deleting def is_conflict(self): return self.__isconflict def is_active(self): return self.__isactive def is_permanent(self): return self.__ispermanent def set_nbname(self, name): self.__nbname = string.ljust(name,17) def set_nametype(self, type): self.__nametype = type def set_flags(self,flags): self.__flags = flags def __repr__(self): s = ' 15: name = name[:15] + chr(type) else: name = string.ljust(name, 15) + chr(type) encoded_name = chr(len(name) * 2) + re.sub('.', _do_first_level_encoding, name) if scope: encoded_scope = '' for s in string.split(scope, '.'): encoded_scope = encoded_scope + chr(len(s)) + s return encoded_name + encoded_scope + '\0' else: return encoded_name + '\0' # Internal method for use in encode_name() def _do_first_level_encoding(m): s = ord(m.group(0)) return string.uppercase[s >> 4] + string.uppercase[s & 0x0f] def decode_name(name): name_length = ord(name[0]) assert name_length == 32 decoded_name = re.sub('..', _do_first_level_decoding, name[1:33]) if name[33] == '\0': return 34, decoded_name, '' else: decoded_domain = '' offset = 34 while 1: domain_length = ord(name[offset]) if domain_length == 0: break decoded_domain = '.' + name[offset:offset + domain_length] offset = offset + domain_length return offset + 1, decoded_name, decoded_domain def _do_first_level_decoding(m): s = m.group(0) return chr(((ord(s[0]) - ord('A')) << 4) | (ord(s[1]) - ord('A'))) class NetBIOSSessionPacket: def __init__(self, data = 0): self.type = 0x0 self.flags = 0x0 self.length = 0x0 if data == 0: self._trailer = '' else: try: self.type = ord(data[0]) self.flags = ord(data[1]) self.length = unpack('!H', data[2:4])[0] self._trailer = data[4:] except: raise NetBIOSError( 'Wrong packet format ' ) def set_type(self, type): self.type = type def get_type(self): return self.type def rawData(self): data = pack('!BBH',self.type,self.flags,self.length) + self._trailer return data def set_trailer(self,data): self._trailer = data self.length = len(data) def get_length(self): return self.length def get_trailer(self): return self._trailer class NetBIOSSession: def __init__(self, myname, remote_name, remote_host, remote_type = TYPE_SERVER, sess_port = NETBIOS_SESSION_PORT, timeout = None, local_type = TYPE_WORKSTATION): if len(myname) > 15: self.__myname = string.upper(myname[:15]) else: self.__myname = string.upper(myname) assert remote_name if len(remote_name) > 15: self.__remote_name = string.upper(remote_name[:15]) else: self.__remote_name = string.upper(remote_name) self.__remote_host = remote_host self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self.__sock.connect(( remote_host, sess_port )) except socket.error, ex: raise ex if sess_port == NETBIOS_SESSION_PORT: self.__request_session(remote_type, local_type, timeout) def get_myname(self): return self.__myname def get_remote_host(self): return self.__remote_host def get_remote_name(self): return self.__remote_name def close(self): self.__sock.close() def send_packet(self, data): p = NetBIOSSessionPacket() p.set_type(NETBIOS_SESSION_MESSAGE) p.set_trailer(data) self.__sock.send(p.rawData()) def recv_packet(self, timeout = None): data = self.__read(timeout) return NetBIOSSessionPacket(data) def __request_session(self, remote_type, local_type, timeout = None): p = NetBIOSSessionPacket() remote_name = encode_name(self.__remote_name, remote_type, '') myname = encode_name(self.__myname, local_type, '') p.set_type(NETBIOS_SESSION_REQUEST) p.set_trailer(remote_name + myname) self.__sock.send(p.rawData()) while 1: p = self.recv_packet(timeout) if p.get_type() == NETBIOS_SESSION_NEGATIVE_RESPONSE: raise NetBIOSError, ( 'Cannot request session', ERRCLASS_SESSION, ord(p.get_trailer()[0]) ) elif p.get_type() == NETBIOS_SESSION_POSITIVE_RESPONSE: break else: # Ignore all other messages, most probably keepalive messages pass def __read(self, timeout = None): read_len = 4 data = '' while read_len > 0: try: ready, _, _ = select.select([self.__sock.fileno() ], [ ], [ ], timeout) if not ready: raise NetBIOSTimeout received = self.__sock.recv(read_len) if len(received)==0: raise NetBIOSError, ( 'Error while reading from remote', ERRCLASS_OS, None) data = data + received read_len = 4 - len(data) except select.error, ex: if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: raise NetBIOSError, ( 'Error occurs while reading from remote', ERRCLASS_OS, ex[0] ) type, flags, length = unpack('>ccH', data) if ord(flags) & 0x01: length = length | 0x10000 read_len = length data2='' while read_len > 0: try: ready, _, _ = select.select([ self.__sock.fileno() ], [ ], [ ], timeout) if not ready: raise NetBIOSTimeout received = self.__sock.recv(read_len) if len(received)==0: raise NetBIOSError, ( 'Error while reading from remote', ERRCLASS_OS, None) data2 = data2 + received read_len = length - len(data2) except select.error, ex: if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN: raise NetBIOSError, ( 'Error while reading from remote', ERRCLASS_OS, ex[0] ) return data + data2 def get_socket(self): return self.__sock ERRCLASS_QUERY = 0x00 ERRCLASS_SESSION = 0xf0 ERRCLASS_OS = 0xff QUERY_ERRORS = { 0x01: 'Request format error. Please file a bug report.', 0x02: 'Internal server error', 0x03: 'Name does not exist', 0x04: 'Unsupported request', 0x05: 'Request refused' } SESSION_ERRORS = { 0x80: 'Not listening on called name', 0x81: 'Not listening for calling name', 0x82: 'Called name not present', 0x83: 'Sufficient resources', 0x8f: 'Unspecified error' } def main(): print if __name__ == '__main__': main()