root/pm/trunk/audits/passive/profiler/sources/main.py @ 5517

Revision 5517, 13.1 kB (checked in by nopper, 7 months ago)

Fixing profiler audit

Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# Copyright (C) 2008 Adriano Monteiro Marques
4#
5# Author: Francesco Piccinno <stack.box@gmail.com>
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
21"""
22A traffic profiler and collector module.
23
24>>> from umit.pm.core.auditutils import audit_unittest
25>>> audit_unittest('-f ethernet,ip,tcp,ftp,profiler', 'ftp-login.pcap')
26MAC: 06:05:04:03:02:01 (UNKNW) IP: 127.0.0.1 1 service(s) (0 accounts for port 21)
27dissector.ftp.info FTP : 127.0.0.1:21 -> USER: anonymous PASS: guest@example.com
28MAC: 06:05:04:03:02:01 (UNKNW) IP: 127.0.0.1 1 service(s) (1 accounts for port 21)
29
30>>> audit_unittest('-f ethernet,ip,tcp,ftp,fingerprint,profiler', 'ftp-login.pcap')
31fingerprint.notice 127.0.0.1 is running Novell NetWare 3.12 - 5.00 (nearest)
32MAC: 01:02:03:04:05:06 (UNKNW) IP: 127.0.0.1 OS: Novell NetWare 3.12 - 5.00 (nearest)
33fingerprint.notice 127.0.0.1 is running Novell NetWare 3.12 - 5.00 (nearest)
34MAC: 06:05:04:03:02:01 (UNKNW) IP: 127.0.0.1 OS: Novell NetWare 3.12 - 5.00 (nearest) 1 service(s) (0 accounts for port 21)
35dissector.ftp.info FTP : 127.0.0.1:21 -> USER: anonymous PASS: guest@example.com
36MAC: 06:05:04:03:02:01 (UNKNW) IP: 127.0.0.1 OS: Novell NetWare 3.12 - 5.00 (nearest) 1 service(s) (1 accounts for port 21)
37"""
38
39import os.path
40
41from umit.pm.core.i18n import _
42from umit.pm.gui.core.app import PMApp
43from umit.pm.core.atoms import defaultdict
44from umit.pm.gui.plugins.engine import Plugin
45from umit.pm.manager.auditmanager import *
46
47from umit.pm.core.bus import unbind_function, implements
48from umit.pm.core.providers import AccountProvider, PortProvider, \
49     ProfileProvider, \
50     UNKNOWN_TYPE, HOST_LOCAL_TYPE, HOST_NONLOCAL_TYPE, \
51     GATEWAY_TYPE, ROUTER_TYPE
52
53################################################################################
54# Provider implementation
55################################################################################
56
57Account = AccountProvider
58
59class Port(PortProvider):
60    def get_account(self, user, pwd):
61        for a in self.accounts:
62            if a.username == user and a.password == pwd:
63                return a
64
65        a = Account()
66        self.accounts.append(a)
67        return a
68
69class Profile(ProfileProvider):
70    def get_port(self, proto, port):
71        for p in self.ports:
72            if p.proto == proto and p.port == port:
73                return p
74
75        p = Port()
76        p.port = port
77        p.proto = proto
78        self.ports.append(p)
79        return p
80
81    def __str__(self):
82        s = ''
83
84        if self.l2_addr:
85            s += "MAC: %s " % self.l2_addr
86        if self.vendor:
87            s += "(%s) " % self.vendor
88        if self.l3_addr:
89            s += "IP: %s " % self.l3_addr
90        if self.distance:
91            s += "%d hop(s) " % self.distance
92        if self.fingerprint:
93            s += "OS: %s " % self.fingerprint
94        if self.ports:
95            s += "%d service(s) " % len(self.ports)
96
97            for p in self.ports:
98                s += "(%d accounts for port %d) " % (len(p.accounts), p.port)
99
100        return s[:-1]
101
102@implements('pm.hostlist')
103class Profiler(Plugin, PassiveAudit):
104    def start(self, reader):
105        # We see profile with l3_addr as key (IP address)
106        # and the overflowed items as a list. So we use a defaultdict
107        self.profiles = defaultdict(list)
108
109        conf = AuditManager().get_configuration('passive.profiler')
110
111        self.maxnum = max(conf['cleanup_hit'], 10)
112        self.keep_local = conf['keep_local']
113
114        if conf['mac_fingerprint']:
115            if reader:
116                contents = reader.file.read('data/finger.mac.db')
117            else:
118                contents = open(os.path.join('passive', 'profiler', 'data',
119                                             'finger.mac.db'), 'r').read()
120
121            self.macdb = {}
122
123            for line in contents.splitlines():
124                if not line or line[0] == '#':
125                    continue
126
127                try:
128                    mac_pref, vendor = line.split(' ', 1)
129                    self.macdb[mac_pref] = vendor
130                except:
131                    continue
132
133            log.info('Loaded %d MAC fingerprints.' % len(self.macdb))
134        else:
135            self.macdb = None
136
137        if reader:
138            self.debug = False
139        else:
140            self.debug = True
141
142    @unbind_function('pm.hostlist', ('get', 'info', 'populate', 'get_target'))
143    def stop(self):
144        try:
145            manager.add_decoder_hook(PROTO_LAYER, NL_TYPE_TCP,
146                                     self._parse_tcp, 1)
147        except:
148            pass
149
150        try:
151            manager.add_decoder_hook(NET_LAYER, LL_TYPE_ARP,
152                                     self._parse_arp, 1)
153        except:
154            pass
155
156        try:
157            manager.add_decoder_hook(PROTO_LAYER, NL_TYPE_ICMP,
158                                     self._parse_icmp, 1)
159        except:
160            pass
161
162    def __impl_info(self, intf, ip, mac):
163        """
164        @return a ProfileProvider object or None if not found
165        """
166
167        for prof in self.profiles[ip]:
168            if prof.l2_addr == mac:
169                return prof
170
171    def __impl_populate(self, interface):
172        # This signal is triggered when the user change the interface
173        # combobox selection and we have to repopulate the tree
174
175        log.debug('Profiler is going to repopulate the hostlist for %s intf' % \
176                  interface)
177
178        ret = []
179
180        for ip in self.profiles:
181            for prof in self.profiles[ip]:
182                ret.append((ip, prof.l2_addr, prof.hostname))
183
184        return ret
185
186    def __impl_get(self):
187        return self.profiles
188
189    def __impl_get_target(self, **kwargs):
190        ret = []
191        l2_addr, l3_addr, hostname, netmask = None, None, None, None
192
193        if 'l2_addr' in kwargs:
194            l2_addr = kwargs.pop('l2_addr')
195        if 'l3_addr' in kwargs:
196            l3_addr = kwargs.pop('l3_addr')
197        if 'hostname' in kwargs:
198            hostname = kwargs.pop('hostname')
199        if 'netmask' in kwargs:
200            netmask = kwargs.pop('netmask')
201
202        log.debug('Looking for a profile matching l2_addr=%s l3_addr=%s '
203                  'hostname=%s netmask=%s' % \
204                  (l2_addr, l3_addr, hostname, netmask))
205
206        check_validity = lambda prof: \
207               (not l2_addr or (l2_addr and prof.l2_addr == l2_addr)) and \
208               (not hostname or (hostname and prof.hostname == hostname))
209
210        if l3_addr:
211            if l3_addr not in self.profiles:
212                return None
213
214            for prof in self.profiles[l3_addr]:
215                if check_validity(prof):
216                    ret.append(prof)
217        else:
218            if netmask:
219                valid_ip = filter(netmask.match_strict, self.profiles.keys())
220            else:
221                valid_ip = self.profiles.keys()
222
223            for ip in valid_ip:
224                for prof in self.profiles[ip]:
225                    if check_validity(prof):
226                        ret.append(prof)
227
228        log.debug('Returning %s' % ret)
229        return ret
230
231    def register_hooks(self):
232        manager = AuditManager()
233
234        # TODO: also handle UDP when UDP dissectors will be ready.
235        manager.add_decoder_hook(PROTO_LAYER, NL_TYPE_TCP,
236                                 self._parse_tcp, 1)
237
238        manager.add_decoder_hook(NET_LAYER, LL_TYPE_ARP,
239                                 self._parse_arp, 1)
240
241        manager.add_decoder_hook(PROTO_LAYER, NL_TYPE_ICMP,
242                                 self._parse_icmp, 1)
243
244    def _parse_tcp(self, mpkt):
245        if mpkt.flags & MPKT_FORWARDED or \
246           mpkt.flags & MPKT_IGNORE:
247            return
248
249        sport = mpkt.l4_src
250        dport = mpkt.l4_dst
251        tcpflags = mpkt.l4_flags
252
253        if not tcpflags:
254            return
255
256        prof = None
257        port = None
258
259        # Simple open port
260
261        if (tcpflags & TH_SYN and tcpflags & TH_ACK):
262            prof = self.get_or_create(mpkt)
263            port = prof.get_port(APP_LAYER_TCP, sport)
264
265        # Dissector exposed banner
266
267        banner = mpkt.cfields.get('banner', None)
268
269        if banner:
270            if not prof:
271                prof = self.get_or_create(mpkt)
272                port = prof.get_port(APP_LAYER_TCP, sport)
273
274            port.banner = banner
275
276        # Fingerprint of fingerprint plugin
277
278        fingerprint = mpkt.cfields.get('remote_os', None)
279
280        if fingerprint:
281            if not prof:
282                prof = self.get_or_create(mpkt)
283
284            prof.fingerprint = fingerprint
285
286        # Username or password exposed by a dissector
287
288        username = mpkt.cfields.get('username', None)
289        password = mpkt.cfields.get('password', None)
290
291        if username or password:
292            if not prof:
293                prof = self.get_or_create(mpkt, True)
294                port = prof.get_port(APP_LAYER_TCP, dport)
295
296            account = port.get_account(username, password)
297            account.username = username
298            account.password = password
299
300        if self.debug and prof:
301            print prof
302
303    def _parse_arp(self, mpkt):
304        if mpkt.context:
305            mpkt.context.check_forwarded(mpkt)
306
307        if mpkt.flags & MPKT_FORWARDED or \
308           mpkt.flags & MPKT_IGNORE:
309            return
310
311        prof = self.get_or_create(mpkt)
312        prof.type = HOST_LOCAL_TYPE
313        prof.distance = 1 # we are in LAN so distance is 1
314
315        # TODO: we have to check the interface ip with the ip of mpkt
316        # and if equal set distance to 0
317
318    def _parse_icmp(self, mpkt):
319        if mpkt.flags & MPKT_FORWARDED or \
320           mpkt.flags & MPKT_IGNORE:
321            return
322
323        prof = self.get_or_create(mpkt)
324
325        icmp_type = mpkt.get_field('icmp.type')
326
327        if icmp_type == ICMP_TYPE_DEST_UNREACH:
328            icmp_code = mpkt.get_field('icmp.code')
329
330            if icmp_code == ICMP_CODE_HOST_UNREACH or \
331               icmp_code == ICMP_CODE_NET_UNREACH:
332
333                prof.type = ROUTER_TYPE
334
335        elif icmp_type == ICMP_TYPE_REDIRECT or \
336             icmp_type == ICMP_TYPE_TIME_EXCEEDED:
337
338            prof.type = ROUTER_TYPE
339
340    def cleanup(self):
341        # Yes this is really a mess. But it is for performance :)
342        log.info('Cleaning up all collected profiles')
343
344        if self.keep_local:
345            not_interesting = lambda p: p.type != HOST_LOCAL_TYPE
346        else:
347            not_interesting = lambda p: True
348
349        ipidx = 0
350        ips = self.profiles.keys()
351
352        while ipidx < len(ips):
353            ipkey = ips[ipidx]
354            profiles = self.profiles[ipkey]
355
356            profidx = 0
357            proflen = len(profiles)
358
359            while profidx < proflen:
360                profile = profiles[profidx]
361
362                if not_interesting(profile):
363                    if profile.fingerprint or profile.ports:
364                        AuditManager().user_msg(str(profile), 6, 'profiler')
365
366                    # Delete the profile
367                    profile = None
368                    del profiles[profidx]
369                    proflen -= 1
370                else:
371                    profidx += 1
372
373            if not profiles:
374                del self.profiles[ipkey]
375
376            ipidx += 1
377
378    def get_or_create(self, mpkt, clientside=False):
379        if len(self.profiles) >= self.maxnum:
380            self.cleanup()
381
382        if not clientside:
383            ip = mpkt.l3_src
384            mac = mpkt.l2_src
385        else:
386            ip = mpkt.l3_dst
387            mac = mpkt.l2_dst
388
389        for prof in self.profiles[ip]:
390            if not mac:
391                return prof
392            elif prof.l2_addr == mac:
393                return prof
394        else:
395            prof = Profile()
396            prof.l2_addr = mac
397            prof.l3_addr = ip
398
399            self.profiles[ip].append(prof)
400
401            if self.macdb:
402                prof.vendor = self.macdb.get(mac[:8].replace(':', '').upper(),
403                                             _('UNKNW'))
404
405            log.info('Adding a new profile -> %s' % prof)
406
407            return prof
408
409__plugins__ = [Profiler]
410__plugins_deps__ = [('Profiler', ['=TCPDecoder-1.0'], ['=Profiler-1.0'], [])]
411
412__audit_type__ = 0
413__protocols__ = (('icmp', None), ('eth', None))
414__configurations__ = (('passive.profiler', {
415    'mac_fingerprint' : [True, 'Enable MAC lookup into DB to report NIC '
416                         'vendor'],
417    'keep_local' : [True, 'Keep only reserved addresses (127./172./10.)'],
418    'cleanup_hit' : [60, 'Purge local cache after cleanup_timeout seconds.' \
419                     'All sensible information will be printed before real '
420                     'deletion. Values should be >= 10'],
421    }),
422)
Note: See TracBrowser for help on using the browser.