File: netbios.py

package info (click to toggle)
python-dpkt 1.9.8-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,104 kB
  • sloc: python: 14,911; makefile: 23
file content (328 lines) | stat: -rw-r--r-- 10,061 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# $Id: netbios.py 23 2006-11-08 15:45:33Z dugsong $
# -*- coding: utf-8 -*-
"""Network Basic Input/Output System."""
from __future__ import absolute_import

import struct

from . import dpkt
from . import dns
from .compat import compat_ord


def encode_name(name):
    """
    Return the NetBIOS first-level encoded name.

    14.1.  FIRST LEVEL ENCODING

    The first level representation consists of two parts:

     -  NetBIOS name
     -  NetBIOS scope identifier

    The 16 byte NetBIOS name is mapped into a 32 byte wide field using a
    reversible, half-ASCII, biased encoding.  Each half-octet of the
    NetBIOS name is encoded into one byte of the 32 byte field.  The
    first half octet is encoded into the first byte, the second half-
    octet into the second byte, etc.

    Each 4-bit, half-octet of the NetBIOS name is treated as an 8-bit,
    right-adjusted, zero-filled binary number.  This number is added to
    value of the ASCII character 'A' (hexadecimal 41).  The resulting 8-
    bit number is stored in the appropriate byte.  The following diagram
    demonstrates this procedure:


                         0 1 2 3 4 5 6 7
                        +-+-+-+-+-+-+-+-+
                        |a b c d|w x y z|          ORIGINAL BYTE
                        +-+-+-+-+-+-+-+-+
                            |       |
                   +--------+       +--------+
                   |                         |     SPLIT THE NIBBLES
                   v                         v
            0 1 2 3 4 5 6 7           0 1 2 3 4 5 6 7
           +-+-+-+-+-+-+-+-+         +-+-+-+-+-+-+-+-+
           |0 0 0 0 a b c d|         |0 0 0 0 w x y z|
           +-+-+-+-+-+-+-+-+         +-+-+-+-+-+-+-+-+
                   |                         |
                   +                         +     ADD 'A'
                   |                         |
            0 1 2 3 4 5 6 7           0 1 2 3 4 5 6 7
           +-+-+-+-+-+-+-+-+         +-+-+-+-+-+-+-+-+
           |0 1 0 0 0 0 0 1|         |0 1 0 0 0 0 0 1|
           +-+-+-+-+-+-+-+-+         +-+-+-+-+-+-+-+-+

    This encoding results in a NetBIOS name being represented as a
    sequence of 32 ASCII, upper-case characters from the set
    {A,B,C...N,O,P}.

    The NetBIOS scope identifier is a valid domain name (without a
    leading dot).

    An ASCII dot (2E hexadecimal) and the scope identifier are appended
    to the encoded form of the NetBIOS name, the result forming a valid
    domain name.
    """
    l_ = []
    for c in struct.pack('16s', name.encode()):
        c = compat_ord(c)
        l_.append(chr((c >> 4) + 0x41))
        l_.append(chr((c & 0xf) + 0x41))
    return ''.join(l_)


def decode_name(nbname):
    """
    Return the NetBIOS first-level decoded nbname.

    """
    if len(nbname) != 32:
        return nbname

    l_ = []
    for i in range(0, 32, 2):
        l_.append(
            chr(
                ((ord(nbname[i]) - 0x41) << 4) |
                ((ord(nbname[i + 1]) - 0x41) & 0xf)
            )
        )
    return ''.join(l_).split('\x00', 1)[0]


# RR types
NS_A = 0x01  # IP address
NS_NS = 0x02  # Name Server
NS_NULL = 0x0A  # NULL
NS_NB = 0x20  # NetBIOS general Name Service
NS_NBSTAT = 0x21  # NetBIOS NODE STATUS

# RR classes
NS_IN = 1

# NBSTAT name flags
NS_NAME_G = 0x8000  # group name (as opposed to unique)
NS_NAME_DRG = 0x1000  # deregister
NS_NAME_CNF = 0x0800  # conflict
NS_NAME_ACT = 0x0400  # active
NS_NAME_PRM = 0x0200  # permanent

# NBSTAT service names
nbstat_svcs = {
    # (service, unique): list of ordered (name prefix, service name) tuples
    (0x00, 0): [('', 'Domain Name')],
    (0x00, 1): [('IS~', 'IIS'), ('', 'Workstation Service')],
    (0x01, 0): [('__MSBROWSE__', 'Master Browser')],
    (0x01, 1): [('', 'Messenger Service')],
    (0x03, 1): [('', 'Messenger Service')],
    (0x06, 1): [('', 'RAS Server Service')],
    (0x1B, 1): [('', 'Domain Master Browser')],
    (0x1C, 0): [('INet~Services', 'IIS'), ('', 'Domain Controllers')],
    (0x1D, 1): [('', 'Master Browser')],
    (0x1E, 0): [('', 'Browser Service Elections')],
    (0x1F, 1): [('', 'NetDDE Service')],
    (0x20, 1): [('Forte_$ND800ZA', 'DCA IrmaLan Gateway Server Service'),
                ('', 'File Server Service')],
    (0x21, 1): [('', 'RAS Client Service')],
    (0x22, 1): [('', 'Microsoft Exchange Interchange(MSMail Connector)')],
    (0x23, 1): [('', 'Microsoft Exchange Store')],
    (0x24, 1): [('', 'Microsoft Exchange Directory')],
    (0x2B, 1): [('', 'Lotus Notes Server Service')],
    (0x2F, 0): [('IRISMULTICAST', 'Lotus Notes')],
    (0x30, 1): [('', 'Modem Sharing Server Service')],
    (0x31, 1): [('', 'Modem Sharing Client Service')],
    (0x33, 0): [('IRISNAMESERVER', 'Lotus Notes')],
    (0x43, 1): [('', 'SMS Clients Remote Control')],
    (0x44, 1): [('', 'SMS Administrators Remote Control Tool')],
    (0x45, 1): [('', 'SMS Clients Remote Chat')],
    (0x46, 1): [('', 'SMS Clients Remote Transfer')],
    (0x4C, 1): [('', 'DEC Pathworks TCPIP service on Windows NT')],
    (0x52, 1): [('', 'DEC Pathworks TCPIP service on Windows NT')],
    (0x87, 1): [('', 'Microsoft Exchange MTA')],
    (0x6A, 1): [('', 'Microsoft Exchange IMC')],
    (0xBE, 1): [('', 'Network Monitor Agent')],
    (0xBF, 1): [('', 'Network Monitor Application')]
}


def node_to_service_name(name_service_flags):
    name, service, flags = name_service_flags
    try:
        unique = int(flags & NS_NAME_G == 0)
        for namepfx, svcname in nbstat_svcs[(service, unique)]:
            if name.startswith(namepfx):
                return svcname
    except KeyError:
        pass
    return ''


class NS(dns.DNS):
    """
    NetBIOS Name Service.

    RFC1002: https://tools.ietf.org/html/rfc1002
    """

    class Q(dns.DNS.Q):
        pass

    class RR(dns.DNS.RR):
        """NetBIOS resource record.

        RFC1001: 14.  REPRESENTATION OF NETBIOS NAMES

        NetBIOS names as seen across the client interface to NetBIOS are
        exactly 16 bytes long.  Within the NetBIOS-over-TCP protocols, a
        longer representation is used.

        There are two levels of encoding.  The first level maps a NetBIOS
        name into a domain system name.  The second level maps the domain
        system name into the "compressed" representation required for
        interaction with the domain name system.

        Except in one packet, the second level representation is the only
        NetBIOS name representation used in NetBIOS-over-TCP packet formats.
        The exception is the RDATA field of a NODE STATUS RESPONSE packet.
        """

        _node_name_struct = struct.Struct('>15s B H')
        _node_name_len = _node_name_struct.size

        def unpack_rdata(self, buf, off):
            if self.type == NS_A:
                self.ip = self.rdata
            elif self.type == NS_NBSTAT:
                num_names = compat_ord(self.rdata[0])

                self.nodenames = [
                    self._node_name_struct.unpack_from(
                        self.rdata, 1+idx*self._node_name_len
                    ) for idx in range(num_names)
                ]
                # XXX - skip stats


class Session(dpkt.Packet):
    """NetBIOS Session Service."""
    __hdr__ = (
        ('type', 'B', 0),
        ('flags', 'B', 0),
        ('len', 'H', 0)
    )


SSN_MESSAGE = 0
SSN_REQUEST = 1
SSN_POSITIVE = 2
SSN_NEGATIVE = 3
SSN_RETARGET = 4
SSN_KEEPALIVE = 5


class Datagram(dpkt.Packet):
    """NetBIOS Datagram Service."""
    __hdr__ = (
        ('type', 'B', 0),
        ('flags', 'B', 0),
        ('id', 'H', 0),
        ('src', 'I', 0),
        ('sport', 'H', 0),
        ('len', 'H', 0),
        ('off', 'H', 0)
    )


DGRAM_UNIQUE = 0x10
DGRAM_GROUP = 0x11
DGRAM_BROADCAST = 0x12
DGRAM_ERROR = 0x13
DGRAM_QUERY = 0x14
DGRAM_POSITIVE = 0x15
DGRAM_NEGATIVE = 0x16


def test_encode_name():
    assert encode_name('The NetBIOS name') == 'FEGIGFCAEOGFHEECEJEPFDCAGOGBGNGF'
    # rfc1002
    assert encode_name('FRED            ') == 'EGFCEFEECACACACACACACACACACACACA'
    # https://github.com/kbandla/dpkt/issues/458
    assert encode_name('*') == 'CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'


def test_decode_name():
    assert decode_name('FEGIGFCAEOGFHEECEJEPFDCAGOGBGNGF') == 'The NetBIOS name'
    # original botched example from rfc1001
    assert decode_name('FEGHGFCAEOGFHEECEJEPFDCAHEGBGNGF') == 'Tge NetBIOS tame'
    assert decode_name('CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') == '*'

    # decode a name which is not 32 chars long
    assert decode_name('CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABB') == 'CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABB'


def test_node_to_service_name():
    svcname = node_to_service_name(("ISS", 0x00, 0x0800))
    assert svcname == "Workstation Service"


def test_node_to_service_name_keyerror():
    svcname = node_to_service_name(("ISS", 0xff, 0x0800))
    assert svcname == ""


def test_rr():
    import pytest
    from binascii import unhexlify

    rr = NS.RR()
    with pytest.raises(NotImplementedError):
        len(rr)

    buf = unhexlify(''.join([
        '01',        # A record
        '0001',      # DNS_IN
        '00000000',  # TTL
        '0000',      # rlen
    ]))
    rr.unpack_rdata(buf, 0)
    assert rr.ip == rr.rdata


def test_rr_nbstat():
    from binascii import unhexlify
    buf = unhexlify(''.join([
        '41' * 1025,  # Name
        '0033',       # NS_NBSTAT
        '0001',       # DNS_IN
        '00000000',   # TTL
        '0004',       # rlen
    ]))
    rdata = (
        b'\x02'  # NUM_NAMES
        b'ABCDEFGHIJKLMNO\x2f\x01\x02'
        b'PQRSTUVWXYZABCD\x43\x03\x04'
    )
    rr = NS.RR(
        type=NS_NBSTAT,
        rdata=rdata,
    )

    assert rr.type == NS_NBSTAT
    rr.unpack_rdata(buf, 0)
    assert rr.nodenames == [
        (b'ABCDEFGHIJKLMNO', 0x2f, 0x0102),
        (b'PQRSTUVWXYZABCD', 0x43, 0x0304),
    ]


def test_ns():
    from binascii import unhexlify
    ns = NS()
    correct = unhexlify(
        '0000'
        '0100'
        '0000000000000000'
    )
    assert bytes(ns) == correct