├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── core ├── __init__.py ├── attribdict.py ├── common.py └── settings.py ├── misc └── precommit-hook ├── requirements.txt ├── sensor.conf └── sensor.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py text eol=lf 2 | *.txt text eol=lf 3 | *.csv text eol=lf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *~ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Miroslav Stampar (@stamparm) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ``` 4 | sudo su 5 | apt-get install python python-pcapy git 6 | cd /root 7 | git clone https://github.com/stamparm/ipnoise.git 8 | (crontab -l ; echo '*/1 * * * * if [ -n "$(ps -ef | grep -v grep | grep ipnoise/sensor.py)" ]; then : ; else python /root/ipnoise/sensor.py; fi') | crontab - 9 | ``` 10 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2016 Miroslav Stampar (@stamparm) 5 | See the file 'LICENSE' for copying permission 6 | """ 7 | 8 | pass 9 | -------------------------------------------------------------------------------- /core/attribdict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2016 Miroslav Stampar (@stamparm) 5 | See the file 'LICENSE' for copying permission 6 | """ 7 | 8 | class AttribDict(dict): 9 | def __getattr__(self, name): 10 | return self[name] if name in self else None 11 | 12 | def __setattr__(self, name, value): 13 | self[name] = value -------------------------------------------------------------------------------- /core/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2016 Miroslav Stampar (@stamparm) 5 | See the file 'LICENSE' for copying permission 6 | """ 7 | 8 | import os 9 | import subprocess 10 | 11 | def addr_to_int(value): 12 | _ = value.split('.') 13 | return (long(_[0]) << 24) + (long(_[1]) << 16) + (long(_[2]) << 8) + long(_[3]) 14 | 15 | def int_to_addr(value): 16 | return '.'.join(str(value >> n & 0xFF) for n in (24, 16, 8, 0)) 17 | 18 | def make_mask(bits): 19 | return 0xffffffff ^ (1 << 32 - bits) - 1 20 | 21 | def check_sudo(): 22 | retval = None 23 | 24 | if not subprocess.mswindows: 25 | if getattr(os, "geteuid"): 26 | retval = os.geteuid() == 0 27 | else: 28 | import ctypes 29 | retval = ctypes.windll.shell32.IsUserAnAdmin() 30 | 31 | return retval 32 | -------------------------------------------------------------------------------- /core/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2016 Miroslav Stampar (@stamparm) 5 | See the file 'LICENSE' for copying permission 6 | """ 7 | 8 | import os 9 | import re 10 | import socket 11 | import stat 12 | import subprocess 13 | 14 | from core.attribdict import AttribDict 15 | 16 | config = AttribDict() 17 | 18 | NAME = "ipnoise" 19 | VERSION = "0.1.6" 20 | SNAP_LEN = 2000 21 | IPPROTO = 8 22 | ETH_LENGTH = 14 23 | IPPROTO_LUT = dict(((getattr(socket, _), _.replace("IPPROTO_", "")) for _ in dir(socket) if _.startswith("IPPROTO_"))) 24 | LOCAL_NETWORKS = [] 25 | HOST_ADDRESSES = set() 26 | DATE_FORMAT = "%Y-%m-%d" 27 | TIME_FORMAT = "%Y-%m-%d %H:%M:%S" 28 | SYSTEM_LOG_DIRECTORY = "/var/log" if not subprocess.mswindows else "C:\\Windows\\Logs" 29 | LOG_DIRECTORY = os.path.join(SYSTEM_LOG_DIRECTORY, NAME) 30 | DEFAULT_LOG_PERMISSIONS = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH 31 | CSV_HEADER = "proto src_ip dst_ip dst_port first_seen last_seen count" 32 | ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 33 | SENSOR_CONFIG_FILE = os.path.join(ROOT_DIR, "sensor.conf") 34 | MAX_IP_FILTER_RANGE = 2 ** 16 35 | MAX_PUT_SIZE = 5 * 1024 * 1024 36 | 37 | # Reference: https://sixohthree.com/media/2003/06/26/lock_your_doors/portscan.txt 38 | MISC_PORTS = { 17: "qotd", 53: "dns", 135: "dcom-rpc", 502: "modbus", 623: "ipmi", 1433: "mssql", 1723: "pptp", 1900: "upnp", 3128: "squid", 3389: "rdesktop", 5351: "nat-pmp", 5357: "wsdapi", 5631: "pc-anywhere", 5800: "vnc", 5900: "vnc", 5901: "vnc-1", 5902: "vnc-2", 5903: "vnc-3", 6379: "redis", 7547: "cwmp", 8118: "privoxy", 8338: "maltrail", 8339: "tsusen", 8443: "https-alt", 9200: "wap-wsp", 11211: "memcached", 17185: "vxworks", 27017: "mongo", 53413: "netis" } 39 | 40 | def read_config(config_file): 41 | global config 42 | 43 | if not os.path.isfile(config_file): 44 | exit("[!] missing configuration file '%s'" % config_file) 45 | 46 | config.clear() 47 | 48 | try: 49 | array = None 50 | content = open(config_file, "rb").read() 51 | 52 | for line in content.split("\n"): 53 | line = re.sub(r"#.+", "", line) 54 | if not line.strip(): 55 | continue 56 | 57 | if line.count(' ') == 0: 58 | array = line.upper() 59 | config[array] = [] 60 | continue 61 | 62 | if array and line.startswith(' '): 63 | config[array].append(line.strip()) 64 | continue 65 | else: 66 | array = None 67 | try: 68 | name, value = line.strip().split(' ', 1) 69 | except ValueError: 70 | name = line.strip() 71 | value = "" 72 | finally: 73 | name = name.upper() 74 | value = value.strip("'\"") 75 | 76 | if name.startswith("USE_"): 77 | value = value.lower() in ("1", "true") 78 | elif value.isdigit(): 79 | value = int(value) 80 | else: 81 | for match in re.finditer(r"\$([A-Z0-9_]+)", value): 82 | if match.group(1) in globals(): 83 | value = value.replace(match.group(0), globals()[match.group(1)]) 84 | else: 85 | value = value.replace(match.group(0), os.environ.get(match.group(1), match.group(0))) 86 | if subprocess.mswindows and "://" not in value: 87 | value = value.replace("/", "\\") 88 | 89 | config[name] = value 90 | 91 | except (IOError, OSError): 92 | pass 93 | 94 | for option in ("MONITOR_INTERFACE",): 95 | if not option in config: 96 | exit("[!] missing mandatory option '%s' in configuration file '%s'" % (option, config_file)) 97 | -------------------------------------------------------------------------------- /misc/precommit-hook: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Reference: http://jeffreysambells.com/2010/10/22/a-git-pre-commit-hook-to-update-androidmanifest-xml-versioncode 4 | 5 | SETTINGS="../core/settings.py" 6 | 7 | declare -x SCRIPTPATH="${0}" 8 | 9 | FULLPATH=${SCRIPTPATH%/*}/$SETTINGS 10 | 11 | if [ -f $FULLPATH ] 12 | then 13 | LINE=$(grep -o ${FULLPATH} -e 'VERSION = "[0-9.]*"'); 14 | declare -a LINE; 15 | INCREMENTED=$(python -c "import re, sys; version = re.search('\"([0-9.]*)\"', sys.argv[1]).group(1); _ = version.split('.'); _.append(0) if len(_) < 3 else _; _[-1] = str(int(_[-1]) + 1); print sys.argv[1].replace(version, '.'.join(_))" "$LINE") 16 | if [ -n "$INCREMENTED" ] 17 | then 18 | sed "s/${LINE}/${INCREMENTED}/" $FULLPATH > $FULLPATH.tmp && mv $FULLPATH.tmp $FULLPATH 19 | echo "Updated ${INCREMENTED} in ${FULLPATH}"; 20 | else 21 | echo "Something went wrong in VERSION increment" 22 | exit 1 23 | fi 24 | fi; 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pcapy 2 | -------------------------------------------------------------------------------- /sensor.conf: -------------------------------------------------------------------------------- 1 | # Network capture filter (e.g. tcp) 2 | # Note(s): more info about filters can be found at: https://danielmiessler.com/study/tcpdump/ 3 | CAPTURE_FILTER not tcp or tcp[tcpflags] == tcp-syn 4 | 5 | # Interface used for monitoring 6 | MONITOR_INTERFACE any 7 | 8 | # Flush-write results to log CSV files every seconds 9 | WRITE_PERIOD 120 10 | 11 | # Addresses to be ignored in captures 12 | IGNORE_ADDRESSES 255.255.255.255 127.0.0.1 0.0.0.0 13 | 14 | # Ports to ignore in captures 15 | IGNORE_PORTS 8338 8339 16 | 17 | # Show debug messages 18 | SHOW_DEBUG true -------------------------------------------------------------------------------- /sensor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2016 Miroslav Stampar (@stamparm) 5 | See the file 'LICENSE' for copying permission 6 | """ 7 | 8 | import csv 9 | import datetime 10 | import optparse 11 | import glob 12 | import os 13 | import re 14 | import socket 15 | import stat 16 | import struct 17 | import subprocess 18 | import sys 19 | import time 20 | import traceback 21 | 22 | sys.dont_write_bytecode = True 23 | 24 | from core.common import addr_to_int 25 | from core.common import make_mask 26 | from core.common import check_sudo 27 | from core.settings import config 28 | from core.settings import CSV_HEADER 29 | from core.settings import DATE_FORMAT 30 | from core.settings import DEFAULT_LOG_PERMISSIONS 31 | from core.settings import ETH_LENGTH 32 | from core.settings import HOST_ADDRESSES 33 | from core.settings import IPPROTO 34 | from core.settings import IPPROTO_LUT 35 | from core.settings import LOCAL_NETWORKS 36 | from core.settings import LOG_DIRECTORY 37 | from core.settings import NAME 38 | from core.settings import read_config 39 | from core.settings import SENSOR_CONFIG_FILE 40 | from core.settings import SNAP_LEN 41 | from core.settings import SYSTEM_LOG_DIRECTORY 42 | from core.settings import VERSION 43 | 44 | try: 45 | import pcapy 46 | except ImportError: 47 | if subprocess.mswindows: 48 | exit("[!] please install Pcapy (e.g. 'https://breakingcode.wordpress.com/?s=pcapy') and WinPcap (e.g. 'http://www.winpcap.org/install/')") 49 | else: 50 | exit("[!] please install Pcapy (e.g. 'apt-get install python-pcapy')") 51 | 52 | _auxiliary = {} 53 | _cap = None 54 | _datalink = None 55 | _traffic = {} 56 | 57 | LAST_FILENAME = None 58 | LAST_WRITE = None 59 | 60 | def _get_sys_whitelist(): 61 | retval = set() 62 | for filename in glob.glob(os.path.join(SYSTEM_LOG_DIRECTORY, "auth.log*")): 63 | with open(filename, "rb") as f: 64 | for line in f: 65 | if "]: Accepted" in line: 66 | match = re.search(r"from ([\d.]+) port", line) 67 | if match: 68 | retval.add(match.group(1)) 69 | 70 | _ = "/etc/resolv.conf" 71 | if os.path.isfile(_): 72 | with open(_, "rb") as f: 73 | for line in f: 74 | match = re.search(r"nameserver\s+([\d.]+)", line) 75 | if match: 76 | retval.add(match.group(1)) 77 | 78 | return retval 79 | 80 | def _log_write(force=False, filename=None): 81 | global LAST_FILENAME 82 | global LAST_WRITE 83 | 84 | current = time.time() 85 | filename = filename or os.path.join(LOG_DIRECTORY, "%s.csv" % datetime.datetime.utcnow().strftime(DATE_FORMAT)) 86 | whitelist = _get_sys_whitelist() 87 | 88 | if LAST_WRITE is None: 89 | LAST_WRITE = current 90 | 91 | if LAST_FILENAME is None: 92 | LAST_FILENAME = filename 93 | 94 | if force or (current - LAST_WRITE) > config.WRITE_PERIOD: 95 | if not os.path.exists(filename): 96 | open(filename, "w+").close() 97 | os.chmod(filename, DEFAULT_LOG_PERMISSIONS) 98 | 99 | with open(filename, "w+b") as f: 100 | results = [] 101 | f.write("%s\n" % CSV_HEADER) 102 | 103 | for dst_key in _traffic: 104 | proto, dst_ip, dst_port = dst_key.split(":") 105 | for src_ip in _traffic[dst_key]: 106 | if src_ip in whitelist: 107 | continue 108 | stat_key = "%s:%s" % (dst_key, src_ip) 109 | first_seen, last_seen, count = _auxiliary[stat_key] 110 | results.append((proto, src_ip, dst_ip, dst_port, first_seen, last_seen, count)) 111 | 112 | for entry in sorted(results): 113 | f.write("%s\n" % " ".join(str(_) for _ in entry)) 114 | 115 | LAST_WRITE = current 116 | 117 | if LAST_FILENAME != filename: 118 | if not force and LAST_WRITE != current: 119 | _log_write(True, LAST_FILENAME) 120 | 121 | LAST_FILENAME = filename 122 | 123 | _traffic.clear() 124 | _auxiliary.clear() 125 | 126 | def _process_packet(packet, sec, usec): 127 | try: 128 | if _datalink == pcapy.DLT_LINUX_SLL: 129 | packet = packet[2:] 130 | 131 | eth_header = struct.unpack("!HH8sH", packet[:ETH_LENGTH]) 132 | eth_protocol = socket.ntohs(eth_header[3]) 133 | 134 | if eth_protocol == IPPROTO: # IP 135 | ip_header = struct.unpack("!BBHHHBBH4s4s", packet[ETH_LENGTH:ETH_LENGTH + 20]) 136 | ip_length = ip_header[2] 137 | packet = packet[:ETH_LENGTH + ip_length] # truncate 138 | iph_length = (ip_header[0] & 0xF) << 2 139 | 140 | protocol = ip_header[6] 141 | src_ip = socket.inet_ntoa(ip_header[8]) 142 | dst_ip = socket.inet_ntoa(ip_header[9]) 143 | 144 | proto = IPPROTO_LUT.get(protocol) 145 | 146 | local_src = False 147 | for prefix, mask in LOCAL_NETWORKS: 148 | if addr_to_int(src_ip) & mask == prefix: 149 | local_src = True 150 | break 151 | 152 | if proto is None or any(_ in (config.IGNORE_ADDRESSES or "") for _ in (src_ip, dst_ip)): 153 | return 154 | 155 | if HOST_ADDRESSES and dst_ip not in HOST_ADDRESSES: 156 | return 157 | 158 | # only process SYN packets 159 | if protocol == socket.IPPROTO_TCP: # TCP 160 | if local_src: 161 | return 162 | 163 | i = iph_length + ETH_LENGTH 164 | src_port, dst_port, _, _, _, flags = struct.unpack("!HHLLBB", packet[i:i + 14]) 165 | 166 | if any(str(_) in (config.IGNORE_PORTS or "") for _ in (src_port, dst_port)): 167 | return 168 | 169 | dst_key = "%s:%s:%s" % (proto, dst_ip, dst_port) 170 | stat_key = "%s:%s" % (dst_key, src_ip) 171 | 172 | if flags == 2: # SYN set (only) 173 | if dst_key not in _traffic: 174 | _traffic[dst_key] = set() 175 | 176 | _traffic[dst_key].add(src_ip) 177 | 178 | if stat_key not in _auxiliary: 179 | _auxiliary[stat_key] = [sec, sec, 1] 180 | else: 181 | _auxiliary[stat_key][1] = sec 182 | _auxiliary[stat_key][2] += 1 183 | 184 | else: 185 | if protocol == socket.IPPROTO_UDP: # UDP 186 | i = iph_length + ETH_LENGTH 187 | _ = packet[i:i + 4] 188 | if len(_) < 4: 189 | return 190 | 191 | src_port, dst_port = struct.unpack("!HH", _) 192 | if src_port < 1024 and dst_port > 1024 and dst_ip in HOST_ADDRESSES: # potential reflection/amplification (junk) responses (e.g. service response to fake src_ip) 193 | return 194 | else: # non-TCP/UDP (e.g. ICMP) 195 | src_port, dst_port = '-', '-' 196 | 197 | if any(str(_) in (config.IGNORE_PORTS or "") for _ in (src_port, dst_port)): 198 | return 199 | 200 | dst_key = "%s:%s:%s" % (proto, dst_ip, dst_port) 201 | stat_key = "%s:%s" % (dst_key, src_ip) 202 | 203 | flow = tuple(sorted((addr_to_int(src_ip), src_port, addr_to_int(dst_ip), dst_port))) 204 | 205 | if flow not in _auxiliary: 206 | _auxiliary[flow] = True 207 | 208 | if local_src: 209 | return 210 | 211 | if dst_key not in _traffic: 212 | _traffic[dst_key] = set() 213 | 214 | _traffic[dst_key].add(src_ip) 215 | _auxiliary[stat_key] = [sec, sec, 1] 216 | 217 | elif stat_key in _auxiliary: 218 | _auxiliary[stat_key][1] = sec 219 | _auxiliary[stat_key][2] += 1 220 | 221 | except: 222 | if config.SHOW_DEBUG: 223 | traceback.print_exc() 224 | 225 | finally: 226 | _log_write() 227 | 228 | def packet_handler(header, packet): 229 | try: 230 | sec, usec = header.getts() 231 | _process_packet(packet, sec, usec) 232 | except socket.timeout: 233 | pass 234 | 235 | def init_sensor(): 236 | global _cap 237 | global _datalink 238 | 239 | items = [] 240 | 241 | for cmd, regex in (("/sbin/ifconfig", r"inet addr:([\d.]+) .*Mask:([\d.]+)"), ("ipconfig", r"IPv4 Address[^\n]+([\d.]+)\s+Subnet Mask[^\n]+([\d.]+)")): 242 | try: 243 | items = re.findall(regex, subprocess.check_output(cmd)) 244 | break 245 | except OSError: 246 | pass 247 | 248 | for ip, mask in items: 249 | LOCAL_NETWORKS.append((addr_to_int(ip) & addr_to_int(mask), addr_to_int(mask))) 250 | HOST_ADDRESSES.add(ip) 251 | 252 | try: 253 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 254 | s.connect(("google.com", 0)) 255 | HOST_ADDRESSES.add(s.getsockname()[0]) 256 | except: 257 | pass 258 | 259 | try: 260 | if not os.path.isdir(LOG_DIRECTORY): 261 | os.makedirs(LOG_DIRECTORY) 262 | except Exception, ex: 263 | if "Permission denied" in str(ex): 264 | exit("[x] please run with sudo/Administrator privileges") 265 | else: 266 | raise 267 | 268 | print "[i] using '%s' for log storage" % LOG_DIRECTORY 269 | 270 | print "[i] opening interface '%s'" % config.MONITOR_INTERFACE 271 | 272 | try: 273 | _cap = pcapy.open_live(config.MONITOR_INTERFACE, SNAP_LEN, True, 0) 274 | except socket.error, ex: 275 | if "permitted" in str(ex): 276 | exit("\n[x] please run with sudo/Administrator privileges") 277 | elif "No such device" in str(ex): 278 | exit("\n[x] no such device '%s'" % config.MONITOR_INTERFACE) 279 | else: 280 | raise 281 | except Exception, ex: 282 | if "Operation not permitted" in str(ex): 283 | exit("[x] please run with sudo/Administrator privileges") 284 | else: 285 | raise 286 | 287 | if config.CAPTURE_FILTER: 288 | print "[i] setting filter '%s'" % config.CAPTURE_FILTER 289 | _cap.setfilter(config.CAPTURE_FILTER) 290 | 291 | _datalink = _cap.datalink() 292 | if _datalink not in (pcapy.DLT_EN10MB, pcapy.DLT_LINUX_SLL): 293 | exit("[x] datalink type '%s' not supported" % _datalink) 294 | 295 | filename = os.path.join(LOG_DIRECTORY, "%s.csv" % datetime.datetime.utcnow().strftime(DATE_FORMAT)) 296 | if os.path.exists(filename): 297 | with open(filename, "rb") as f: 298 | reader = csv.DictReader(f, delimiter=' ') 299 | for row in reader: 300 | dst_key = "%s:%s:%s" % (row["proto"], row["dst_ip"], row["dst_port"]) 301 | stat_key = "%s:%s" % (dst_key, row["src_ip"]) 302 | 303 | if dst_key not in _traffic: 304 | _traffic[dst_key] = set() 305 | 306 | _traffic[dst_key].add(row["src_ip"]) 307 | _auxiliary[stat_key] = [int(row["first_seen"]), int(row["last_seen"]), int(row["count"])] 308 | 309 | def start_sensor(): 310 | try: 311 | _cap.loop(-1, packet_handler) 312 | except SystemError: 313 | pass 314 | finally: 315 | _log_write(True) 316 | 317 | def main(): 318 | """ 319 | Main function 320 | """ 321 | 322 | print "%s (sensor) #v%s\n" % (NAME, VERSION) 323 | 324 | parser = optparse.OptionParser(version=VERSION) 325 | parser.add_option("-c", dest="config_file", default=SENSOR_CONFIG_FILE, help="Configuration file (default: '%s')" % os.path.split(SENSOR_CONFIG_FILE)[-1]) 326 | options, _ = parser.parse_args() 327 | 328 | if not check_sudo(): 329 | exit("[x] please run with sudo/Administrator privileges") 330 | 331 | read_config(options.config_file) 332 | init_sensor() 333 | 334 | try: 335 | start_sensor() 336 | except KeyboardInterrupt: 337 | print "\r[x] stopping (Ctrl-C pressed)" 338 | except: 339 | if config.SHOW_DEBUG: 340 | traceback.print_exc() 341 | finally: 342 | os._exit(0) 343 | 344 | if __name__ == "__main__": 345 | main() 346 | --------------------------------------------------------------------------------