| 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 | """ |
|---|
| 22 | A 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') |
|---|
| 26 | MAC: 06:05:04:03:02:01 (UNKNW) IP: 127.0.0.1 1 service(s) (0 accounts for port 21) |
|---|
| 27 | dissector.ftp.info FTP : 127.0.0.1:21 -> USER: anonymous PASS: guest@example.com |
|---|
| 28 | MAC: 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') |
|---|
| 31 | fingerprint.notice 127.0.0.1 is running Novell NetWare 3.12 - 5.00 (nearest) |
|---|
| 32 | MAC: 01:02:03:04:05:06 (UNKNW) IP: 127.0.0.1 OS: Novell NetWare 3.12 - 5.00 (nearest) |
|---|
| 33 | fingerprint.notice 127.0.0.1 is running Novell NetWare 3.12 - 5.00 (nearest) |
|---|
| 34 | MAC: 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) |
|---|
| 35 | dissector.ftp.info FTP : 127.0.0.1:21 -> USER: anonymous PASS: guest@example.com |
|---|
| 36 | MAC: 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 | |
|---|
| 39 | import os.path |
|---|
| 40 | |
|---|
| 41 | from umit.pm.core.i18n import _ |
|---|
| 42 | from umit.pm.gui.core.app import PMApp |
|---|
| 43 | from umit.pm.core.atoms import defaultdict |
|---|
| 44 | from umit.pm.gui.plugins.engine import Plugin |
|---|
| 45 | from umit.pm.manager.auditmanager import * |
|---|
| 46 | |
|---|
| 47 | from umit.pm.core.bus import unbind_function, implements |
|---|
| 48 | from 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 | |
|---|
| 57 | Account = AccountProvider |
|---|
| 58 | |
|---|
| 59 | class 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 | |
|---|
| 69 | class 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') |
|---|
| 103 | class 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 | ) |
|---|