# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi """ NetBIOS over TCP/IP [RFC 1001/1002] """ import struct from scapy.arch import get_if_addr from scapy.base_classes import Net from scapy.ansmachine import AnsweringMachine from scapy.compat import bytes_encode from scapy.config import conf from scapy.packet import Packet, bind_bottom_up, bind_layers, bind_top_down from scapy.fields import ( BitEnumField, BitField, ByteEnumField, ByteField, FieldLenField, FlagsField, IPField, IntField, NetBIOSNameField, PacketListField, ShortEnumField, ShortField, StrFixedLenField, XShortField, XStrFixedLenField ) from scapy.interfaces import _GlobInterfaceType from scapy.sendrecv import sr1 from scapy.layers.inet import IP, UDP, TCP from scapy.layers.l2 import Ether, SourceMACField # Typing imports from typing import ( List, Union, ) class NetBIOS_DS(Packet): name = "NetBIOS datagram service" fields_desc = [ ByteEnumField("type", 17, {17: "direct_group"}), ByteField("flags", 0), XShortField("id", 0), IPField("src", "127.0.0.1"), ShortField("sport", 138), ShortField("len", None), ShortField("ofs", 0), NetBIOSNameField("srcname", ""), NetBIOSNameField("dstname", ""), ] def post_build(self, p, pay): p += pay if self.len is None: tmp_len = len(p) - 14 p = p[:10] + struct.pack("!H", tmp_len) + p[12:] return p # ShortField("length",0), # ShortField("Delimiter",0), # ByteField("command",0), # ByteField("data1",0), # ShortField("data2",0), # ShortField("XMIt",0), # ShortField("RSPCor",0), # StrFixedLenField("dest","",16), # StrFixedLenField("source","",16), # # ] # # NetBIOS _NETBIOS_SUFFIXES = { 0x4141: "workstation", 0x4141 + 0x03: "messenger service", 0x4141 + 0x200: "file server service", 0x4141 + 0x10b: "domain master browser", 0x4141 + 0x10c: "domain controller", 0x4141 + 0x10e: "browser election service" } _NETBIOS_QRTYPES = { 0x20: "NB", 0x21: "NBSTAT" } _NETBIOS_QRCLASS = { 1: "INTERNET" } _NETBIOS_RNAMES = { 0xC00C: "Label String Pointer to QUESTION_NAME" } _NETBIOS_OWNER_MODE_TYPES = { 0: "B node", 1: "P node", 2: "M node", 3: "H node" } _NETBIOS_GNAMES = { 0: "Unique name", 1: "Group name" } class NBNSHeader(Packet): name = "NBNS Header" fields_desc = [ ShortField("NAME_TRN_ID", 0), BitField("RESPONSE", 0, 1), BitField("OPCODE", 0, 4), FlagsField("NM_FLAGS", 0, 7, ["B", "res1", "res0", "RA", "RD", "TC", "AA"]), BitField("RCODE", 0, 4), ShortField("QDCOUNT", 0), ShortField("ANCOUNT", 0), ShortField("NSCOUNT", 0), ShortField("ARCOUNT", 0), ] def hashret(self): return b"NBNS" + struct.pack("!B", self.OPCODE) # Name Query Request # RFC1002 sect 4.2.12 class NBNSQueryRequest(Packet): name = "NBNS query request" fields_desc = [NetBIOSNameField("QUESTION_NAME", "windows"), ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL", 0), ShortEnumField("QUESTION_TYPE", 0x20, _NETBIOS_QRTYPES), ShortEnumField("QUESTION_CLASS", 1, _NETBIOS_QRCLASS)] def mysummary(self): return "NBNSQueryRequest who has '\\\\%s'" % ( self.QUESTION_NAME.decode(errors="backslashreplace") ) bind_layers(NBNSHeader, NBNSQueryRequest, OPCODE=0x0, NM_FLAGS=0x11, QDCOUNT=1) # Name Query Response # RFC1002 sect 4.2.13 class NBNS_ADD_ENTRY(Packet): fields_desc = [ BitEnumField("G", 0, 1, _NETBIOS_GNAMES), BitEnumField("OWNER_NODE_TYPE", 00, 2, _NETBIOS_OWNER_MODE_TYPES), BitEnumField("UNUSED", 0, 13, {0: "Unused"}), IPField("NB_ADDRESS", "127.0.0.1") ] class NBNSQueryResponse(Packet): name = "NBNS query response" fields_desc = [NetBIOSNameField("RR_NAME", "windows"), ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL", 0), ShortEnumField("QUESTION_TYPE", 0x20, _NETBIOS_QRTYPES), ShortEnumField("QUESTION_CLASS", 1, _NETBIOS_QRCLASS), IntField("TTL", 0x493e0), FieldLenField("RDLENGTH", None, length_of="ADDR_ENTRY"), PacketListField("ADDR_ENTRY", [NBNS_ADD_ENTRY()], NBNS_ADD_ENTRY, length_from=lambda pkt: pkt.RDLENGTH) ] def mysummary(self): if not self.ADDR_ENTRY or \ not isinstance(self.ADDR_ENTRY[0], NBNS_ADD_ENTRY): return "NBNSQueryResponse" return "NBNSQueryResponse '\\\\%s' is at %s" % ( self.RR_NAME.decode(errors="backslashreplace"), self.ADDR_ENTRY[0].NB_ADDRESS ) def answers(self, other): return ( isinstance(other, NBNSQueryRequest) and other.QUESTION_NAME == self.RR_NAME ) bind_layers(NBNSHeader, NBNSQueryResponse, # RD+AA OPCODE=0x0, NM_FLAGS=0x50, RESPONSE=1, ANCOUNT=1) for _flg in [0x58, 0x70, 0x78]: bind_bottom_up(NBNSHeader, NBNSQueryResponse, OPCODE=0x0, NM_FLAGS=_flg, RESPONSE=1, ANCOUNT=1) # Node Status Request # RFC1002 sect 4.2.17 class NBNSNodeStatusRequest(NBNSQueryRequest): name = "NBNS status request" QUESTION_NAME = b"*" + b"\x00" * 14 QUESTION_TYPE = 0x21 def mysummary(self): return "NBNSNodeStatusRequest who has '\\\\%s'" % ( self.QUESTION_NAME.decode(errors="backslashreplace") ) bind_bottom_up(NBNSHeader, NBNSNodeStatusRequest, OPCODE=0x0, NM_FLAGS=0, QDCOUNT=1) bind_layers(NBNSHeader, NBNSNodeStatusRequest, OPCODE=0x0, NM_FLAGS=1, QDCOUNT=1) # Node Status Response # RFC1002 sect 4.2.18 class NBNSNodeStatusResponseService(Packet): name = "NBNS Node Status Response Service" fields_desc = [StrFixedLenField("NETBIOS_NAME", "WINDOWS ", 15), ByteEnumField("SUFFIX", 0, {0: "workstation", 0x03: "messenger service", 0x20: "file server service", 0x1b: "domain master browser", 0x1c: "domain controller", 0x1e: "browser election service" }), ByteField("NAME_FLAGS", 0x4), ByteEnumField("UNUSED", 0, {0: "unused"})] def default_payload_class(self, payload): return conf.padding_layer class NBNSNodeStatusResponse(Packet): name = "NBNS Node Status Response" fields_desc = [NetBIOSNameField("RR_NAME", "windows"), ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL", 0), ShortEnumField("RR_TYPE", 0x21, _NETBIOS_QRTYPES), ShortEnumField("RR_CLASS", 1, _NETBIOS_QRCLASS), IntField("TTL", 0), ShortField("RDLENGTH", 83), FieldLenField("NUM_NAMES", None, fmt="B", count_of="NODE_NAME"), PacketListField("NODE_NAME", [NBNSNodeStatusResponseService()], NBNSNodeStatusResponseService, count_from=lambda pkt: pkt.NUM_NAMES), SourceMACField("MAC_ADDRESS"), XStrFixedLenField("STATISTICS", b"", 46)] def answers(self, other): return ( isinstance(other, NBNSNodeStatusRequest) and other.QUESTION_NAME == self.RR_NAME ) bind_layers(NBNSHeader, NBNSNodeStatusResponse, OPCODE=0x0, NM_FLAGS=0x40, RESPONSE=1, ANCOUNT=1) # Name Registration Request # RFC1002 sect 4.2.2 class NBNSRegistrationRequest(Packet): name = "NBNS registration request" fields_desc = [ NetBIOSNameField("QUESTION_NAME", "Windows"), ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL", 0), ShortEnumField("QUESTION_TYPE", 0x20, _NETBIOS_QRTYPES), ShortEnumField("QUESTION_CLASS", 1, _NETBIOS_QRCLASS), ShortEnumField("RR_NAME", 0xC00C, _NETBIOS_RNAMES), ShortEnumField("RR_TYPE", 0x20, _NETBIOS_QRTYPES), ShortEnumField("RR_CLASS", 1, _NETBIOS_QRCLASS), IntField("TTL", 0), ShortField("RDLENGTH", 6), BitEnumField("G", 0, 1, _NETBIOS_GNAMES), BitEnumField("OWNER_NODE_TYPE", 00, 2, _NETBIOS_OWNER_MODE_TYPES), BitEnumField("UNUSED", 0, 13, {0: "Unused"}), IPField("NB_ADDRESS", "127.0.0.1") ] def mysummary(self): return self.sprintf("Register %G% %QUESTION_NAME% at %NB_ADDRESS%") bind_bottom_up(NBNSHeader, NBNSRegistrationRequest, OPCODE=0x5) bind_layers(NBNSHeader, NBNSRegistrationRequest, OPCODE=0x5, NM_FLAGS=0x11, QDCOUNT=1, ARCOUNT=1) # Wait for Acknowledgement Response # RFC1002 sect 4.2.16 class NBNSWackResponse(Packet): name = "NBNS Wait for Acknowledgement Response" fields_desc = [NetBIOSNameField("RR_NAME", "windows"), ShortEnumField("SUFFIX", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL", 0), ShortEnumField("RR_TYPE", 0x20, _NETBIOS_QRTYPES), ShortEnumField("RR_CLASS", 1, _NETBIOS_QRCLASS), IntField("TTL", 2), ShortField("RDLENGTH", 2), BitField("RDATA", 10512, 16)] # 10512=0010100100010000 bind_layers(NBNSHeader, NBNSWackResponse, OPCODE=0x7, NM_FLAGS=0x40, RESPONSE=1, ANCOUNT=1) # NetBIOS DATAGRAM HEADER class NBTDatagram(Packet): name = "NBT Datagram Packet" fields_desc = [ByteField("Type", 0x10), ByteField("Flags", 0x02), ShortField("ID", 0), IPField("SourceIP", "127.0.0.1"), ShortField("SourcePort", 138), ShortField("Length", None), ShortField("Offset", 0), NetBIOSNameField("SourceName", "windows"), ShortEnumField("SUFFIX1", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL1", 0), NetBIOSNameField("DestinationName", "windows"), ShortEnumField("SUFFIX2", 0x4141, _NETBIOS_SUFFIXES), ByteField("NULL2", 0)] def post_build(self, pkt, pay): if self.Length is None: length = len(pay) + 68 pkt = pkt[:10] + struct.pack("!H", length) + pkt[12:] return pkt + pay # SESSION SERVICE PACKETS class NBTSession(Packet): name = "NBT Session Packet" MAXLENGTH = 0x3ffff fields_desc = [ByteEnumField("TYPE", 0, {0x00: "Session Message", 0x81: "Session Request", 0x82: "Positive Session Response", 0x83: "Negative Session Response", 0x84: "Retarget Session Response", 0x85: "Session Keepalive"}), BitField("RESERVED", 0x00, 7), BitField("LENGTH", None, 17)] def post_build(self, pkt, pay): if self.LENGTH is None: length = len(pay) & self.MAXLENGTH pkt = pkt[:1] + struct.pack("!I", length)[1:] return pkt + pay def extract_padding(self, s): return s[:self.LENGTH], s[self.LENGTH:] @classmethod def tcp_reassemble(cls, data, *args, **kwargs): if len(data) < 4: return None length = struct.unpack("!I", data[:4])[0] & cls.MAXLENGTH if len(data) >= length + 4: return cls(data) bind_bottom_up(UDP, NBNSHeader, dport=137) bind_bottom_up(UDP, NBNSHeader, sport=137) bind_top_down(UDP, NBNSHeader, sport=137, dport=137) bind_bottom_up(UDP, NBTDatagram, dport=138) bind_bottom_up(UDP, NBTDatagram, sport=138) bind_top_down(UDP, NBTDatagram, sport=138, dport=138) bind_bottom_up(TCP, NBTSession, dport=445) bind_bottom_up(TCP, NBTSession, sport=445) bind_bottom_up(TCP, NBTSession, dport=139) bind_bottom_up(TCP, NBTSession, sport=139) bind_layers(TCP, NBTSession, dport=139, sport=139) _nbns_cache = conf.netcache.new_cache("nbns_cache", 300) @conf.commands.register def nbns_resolve( qname: str, iface: Union[_GlobInterfaceType, List[_GlobInterfaceType]] = None, raw: bool = False, timeout: int = 3, **kwargs, ) -> List[str]: """ Perform a simple NBNS (NetBios Name Services) resolution with caching :param qname: the name to query :param iface: the interfaces to use. (default: all) :param raw: return the whole netbios packet (default False) :param timeout: seconds until timeout (per server) :raise TimeoutError: if no DNS servers were reached in time. """ kwargs.setdefault("verbose", 0) # Unify types (for caching) qname = NBNSQueryRequest.QUESTION_NAME.any2i(None, qname) # Check cache cache_ident = qname + b"raw" if raw else b"" result = _nbns_cache.get(cache_ident) if result: return result if iface is None: ifaces = [ x for name, x in conf.ifaces.items() if x.is_valid() and name != conf.loopback_name ] elif isinstance(iface, list): ifaces = iface else: ifaces = [iface] # Builds a request for each broadcast address of each interface requests = [] for iface in ifaces: for bdcst in conf.route.get_if_bcast(iface): if bdcst == "255.255.255.255": continue requests.append( IP(dst=bdcst) / UDP() / NBNSHeader() / NBNSQueryRequest(QUESTION_NAME=qname) ) if not requests: return None # Perform requests, get the first response try: old_checkIPAddr = conf.checkIPaddr conf.checkIPaddr = False res = sr1( requests, timeout=timeout, first=True, **kwargs, ) finally: conf.checkIPaddr = old_checkIPAddr if res is not None: if raw: # Raw result = res else: # Get IP result = [x.NB_ADDRESS for x in res.ADDR_ENTRY] if result: # Cache it _nbns_cache[cache_ident] = result return result else: raise TimeoutError class NBNS_am(AnsweringMachine): function_name = "nbnsd" filter = "udp port 137" sniff_options = {"store": 0} def parse_options(self, server_name=None, from_ip=None, ip=None): """ NBNS answering machine :param server_name: the netbios server name to match :param from_ip: an IP (can have a netmask) to filter on :param ip: the IP to answer with """ self.ServerName = bytes_encode(server_name or "") self.ip = ip if isinstance(from_ip, str): self.from_ip = Net(from_ip) else: self.from_ip = from_ip def is_request(self, req): if self.from_ip and IP in req and req[IP].src not in self.from_ip: return False return NBNSQueryRequest in req and ( not self.ServerName or req[NBNSQueryRequest].QUESTION_NAME.strip() == self.ServerName ) def make_reply(self, req): # type: (Packet) -> Packet resp = Ether( dst=req[Ether].src, src=None if req[Ether].dst == "ff:ff:ff:ff:ff:ff" else req[Ether].dst, ) / IP(dst=req[IP].src) / UDP( sport=req.dport, dport=req.sport, ) address = self.ip or get_if_addr(self.optsniff.get("iface", conf.iface)) resp /= NBNSHeader() / NBNSQueryResponse( RR_NAME=self.ServerName or req.QUESTION_NAME, SUFFIX=req.SUFFIX, ADDR_ENTRY=[NBNS_ADD_ENTRY(NB_ADDRESS=address)] ) resp.NAME_TRN_ID = req.NAME_TRN_ID return resp