Newer
Older

Pavel Kácha
committed
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
import sys
import re
import time
import optparse
import signal
import contextlib
import uuid
import json
import socket
import resource
import atexit
import logging
import logging.handlers

Pavel Kácha
committed
import os.path as pth

Pavel Kácha
committed
from collections import namedtuple
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict

Pavel Kácha
committed
class WindowContextMgr(object):
def __init__(self, window=60*10, timeout=60*5, ideagen=None):
self.contexts = {}
self.update_timestamp = 0
self.window = window
self.timeout = timeout
self.ideagen = ideagen
self.first_update_queue = OrderedDict()
self.last_update_queue = OrderedDict()

Pavel Kácha
committed
def expire_queue(self, queue, window):

Pavel Kácha
committed
aggr_events = []
ctx_to_del = []
for ctx, timestamp in queue.iteritems():
if timestamp >= self.update_timestamp - window:

Pavel Kácha
committed
break
closed = self.ctx_close(self.contexts[ctx])
if closed is not None:
aggr_events.append(closed)
ctx_to_del.append(ctx)
for ctx in ctx_to_del:
del self.contexts[ctx]
del self.first_update_queue[ctx]
del self.last_update_queue[ctx]

Pavel Kácha
committed
return aggr_events

Pavel Kácha
committed
def process(self, event=None, timestamp=None):
if timestamp > self.update_timestamp:
self.update_timestamp = timestamp

Pavel Kácha
committed
aggr_events = []
aggr_events.extend(self.expire_queue(self.first_update_queue, self.window))
aggr_events.extend(self.expire_queue(self.last_update_queue, self.timeout))

Pavel Kácha
committed
if event is not None:
ctx = self.match(event)
if ctx is not None:
if ctx not in self.contexts:
self.contexts[ctx] = self.ctx_create(event)
self.first_update_queue[ctx] = self.update_timestamp
self.last_update_queue[ctx] = self.update_timestamp

Pavel Kácha
committed
else:
self.ctx_append(self.contexts[ctx], event)
del self.last_update_queue[ctx]
self.last_update_queue[ctx] = self.update_timestamp

Pavel Kácha
committed
return aggr_events
def close(self, timestamp):
if timestamp is not None and timestamp > self.update_timestamp:
self.update_timestamp = timestamp

Pavel Kácha
committed
aggr_events = []
for context in self.contexts.values():
closed = self.ctx_close(context)
if closed is not None:
aggr_events.append(closed)
self.contexts = {}
self.first_update_queue = OrderedDict()
self.last_update_queue = OrderedDict()

Pavel Kácha
committed
return aggr_events
class PingContextMgr(WindowContextMgr):
def match(self, event):
return event.src_ip if 'Ping' in event.message else None
def ctx_create(self, event):
ctx = dict(
src_ip=event.src_ip,
tgt_ips=set([event.tgt_ip]),
count=1,
first_update=self.update_timestamp,
last_update=self.update_timestamp

Pavel Kácha
committed
)
return ctx
def ctx_append(self, ctx, event):
ctx["tgt_ips"].add(event.tgt_ip)
ctx["count"] += 1
ctx["last_update"] = self.update_timestamp
def ctx_close(self, ctx):
return self.ideagen.gen_idea(
src=ctx["src_ip"],
src_ports=None,
targets=[(ip, []) for ip in ctx["tgt_ips"]],
detect_time=self.update_timestamp,
event_time=ctx["first_update"],
cease_time=ctx["last_update"],
count=ctx["count"],
template="ping"

Pavel Kácha
committed
)
class ConnectContextMgr(WindowContextMgr):
def match(self, event):
return event.src_ip if 'Connect' in event.message else None
def ctx_create(self, event):
ctx = dict(
src_ip=event.src_ip,
src_ports=set([event.src_port]),
tgt_ips_ports={event.tgt_ip: set([event.tgt_port])},
count=1,
first_update=self.update_timestamp,
last_update=self.update_timestamp

Pavel Kácha
committed
)
return ctx
def ctx_append(self, ctx, event):
tgt_ports = ctx["tgt_ips_ports"].setdefault(event.tgt_ip, set())
tgt_ports.add(event.tgt_port)
ctx["src_ports"].add(event.src_port)
ctx["count"] += 1
ctx["last_update"] = self.update_timestamp
def ctx_close(self, ctx):
return self.ideagen.gen_idea(
src=ctx["src_ip"],
src_ports=ctx["src_ports"],
targets=ctx["tgt_ips_ports"].items(),
detect_time=self.update_timestamp,
event_time=ctx["first_update"],
cease_time=ctx["last_update"],
count=ctx["count"],
template="connect"

Pavel Kácha
committed
)
class InboundContextMgr(ConnectContextMgr):
def match(self, event):
return event.src_ip if 'Inbound' in event.message else None
def ctx_close(self, ctx):
return self.ideagen.gen_idea(
src=ctx["src_ip"],
src_ports=ctx["src_ports"],
targets=ctx["tgt_ips_ports"].items(),
detect_time=self.update_timestamp,
event_time=ctx["first_update"],
cease_time=ctx["last_update"],
count=ctx["count"],
template="synack"

Pavel Kácha
committed
)
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
class FileWatcher(object):
def __init__(self, filename, tail=True):
self.filename = filename
self.open()
self.line_buffer = ""
if tail and self.f:
self.f.seek(0, os.SEEK_END)
def open(self):
try:
self.f = open(self.filename, "r")
st = os.fstat(self.f.fileno())
self.inode, self.size = st.st_ino, st.st_size
except IOError:
self.f = None
self.inode = -1
self.size = -1
def _check_reopen(self):
try:
st = os.stat(self.filename)
cur_inode, cur_size = st.st_ino, st.st_size
except OSError as e:
cur_inode = -1
cur_size = -1
if cur_inode != self.inode or cur_size < self.size:
self.close()
self.open()
def readline(self):
if not self.f:
self.open()
if not self.f:
return self.line_buffer
res = self.f.readline()
if not res:
self._check_reopen()
if not self.f:
return self.line_buffer
res = self.f.readline()
if not res.endswith("\n"):
self.line_buffer += res
else:
res = self.line_buffer + res
self.line_buffer = ""
return res
def close(self):
try:
if self.f:
self.f.close()
except IOError:
pass
self.inode = -1
self.size = -1
def __repr__(self):
return '%s("%s")' % (type(self).__name__, self.filename)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return False
def __iter__(self):
return self
def next(self):
return self.readline()

Pavel Kácha
committed
class IdeaGen(object):
def __init__(self, name, test=False):
self.name = name
self.test = test
self.template = {
"connect": {
"category": ["Recon.Scanning"],

Pavel Kácha
committed
"template": "labrea-001",
"note": "Connections from remote host to never assigned IP",
"proto": ["tcp"]

Pavel Kácha
committed
},
"ping": {
"category": ["Recon.Scanning"],
"description": "Ping scan",
"template": "labrea-002",
"note": "Ping requests from remote host to never assigned IP",
"proto": ["icmp"]

Pavel Kácha
committed
},
"synack": {
"category": ["Availability.DoS"],
"description": "Unsolicited TCP SYN/ACK connections/scan",

Pavel Kácha
committed
"template": "labrea-003",
"note": "Unsolicited SYN/ACK packet received from remote host to never assigned IP",
"source_type": ["Backscatter"],
"source_to_target": True,
"proto": ["tcp"]

Pavel Kácha
committed
}
}
def format_timestamp(self, epoch):
return "%04d-%02d-%02dT%02d:%02d:%02dZ" % time.gmtime(epoch)[:6]
def gen_idea(self, src, src_ports, targets, detect_time, event_time, cease_time, count, template):
tmpl = self.template[template]
isource = {
"IP6" if ":" in src else "IP4": [src],

Pavel Kácha
committed
}
if "source_type" in tmpl:
isource["Type"] = tmpl["source_type"]

Pavel Kácha
committed
if src_ports:
isource["Port"] = [int(port) for port in src_ports]
# Fold multiple IPs with the same portset
folded_tgt = {}
for tgt, ports in targets:
folded_tgt.setdefault(frozenset(ports), []).append(tgt)
itargets = []
for ports, tgt in folded_tgt.items():

Pavel Kácha
committed
tgts4 = [ip for ip in tgt if ":" not in ip]
tgts6 = [ip for ip in tgt if ":" in ip]
if tgts4:
itarget["IP4"] = tgts4
if tgts6:
itarget["IP6"] = tgts6
if ports:
itarget["Port"] = [int(port) for port in ports]
itargets.append(itarget)
inode = {
"SW": ["LaBrea"],
"Type": ["Connection", "Tarpit"],
"Name": self.name
}
idea = {
"Format": "IDEA0",
"ID": str(uuid.uuid4()),
"Category": tmpl["category"] + (["Test"] if self.test else []),

Pavel Kácha
committed
"Description": tmpl["description"],
"DetectTime": self.format_timestamp(detect_time),
"EventTime": self.format_timestamp(event_time),
"CeaseTime": self.format_timestamp(cease_time),

Pavel Kácha
committed
"Note": tmpl["note"],
"_CESNET": {
"EventTemplate": tmpl["template"],
},
"Target": itargets,
"Node": [inode]
}
if tmpl.get("source_to_target", False):
idea["Target"].append(isource)
else:
idea["Source"] = [isource]
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
class Filer(object):
def __init__(self, directory):
self.basedir = self._ensure_path(directory)
self.tmp = self._ensure_path(pth.join(self.basedir, "tmp"))
self.incoming = self._ensure_path(pth.join(self.basedir, "incoming"))
self.hostname = socket.gethostname()
self.pid = os.getpid()
def _ensure_path(self, p):
try:
os.mkdir(p)
except OSError:
if not pth.isdir(p):
raise
return p
def _get_new_name(self, fd=None):
(inode, device) = os.fstat(fd)[1:3] if fd else (0, 0)
return "%s.%d.%f.%d.%d" % (
self.hostname, self.pid, time.time(), device, inode)
def create_unique_file(self):
# First find and open name unique within tmp
tmpname = None
while not tmpname:
tmpname = self._get_new_name()
try:
fd = os.open(pth.join(self.tmp, tmpname), os.O_CREAT | os.O_RDWR | os.O_EXCL)
except OSError as e:
if e.errno != errno.EEXIST:
raise # other errors than duplicates should get noticed
tmpname = None
# Now we know the device/inode, rename to make unique within system
newname = self._get_new_name(fd)
os.rename(pth.join(self.tmp, tmpname), pth.join(self.tmp, newname))
nf = os.fdopen(fd, "w")
return nf, newname
def publish_file(self, short_name):
os.rename(pth.join(self.tmp, short_name), pth.join(self.incoming, short_name))

Pavel Kácha
committed
def daemonize(
work_dir=None, chroot_dir=None,
umask=None, uid=None, gid=None,
pidfile=None, files_preserve=[], signals={}):

Pavel Kácha
committed
# Dirs, limits, users
if chroot_dir is not None:
os.chdir(chroot_dir)
os.chroot(chroot_dir)
if umask is not None:
os.umask(umask)
if work_dir is not None:
os.chdir(work_dir)
if gid is not None:
os.setgid(gid)
if uid is not None:
os.setuid(uid)
# Doublefork, split session

Pavel Kácha
committed
os._exit(0)
try:
os.setsid()
except OSError:
pass

Pavel Kácha
committed
os._exit(0)
# Setup signal handlers
for (signum, handler) in signals.items():
signal.signal(signum, handler)
# Close descriptors
descr_preserve = set(f.fileno() for f in files_preserve)
maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
if maxfd == resource.RLIM_INFINITY:

Pavel Kácha
committed
maxfd = 65535
for fd in range(maxfd, 3, -1): # 3 means omit stdin, stdout, stderr
if fd not in descr_preserve:
try:
os.close(fd)
except Exception:
pass
# Redirect stdin, stdout, stderr to /dev/null
devnull = os.open(os.devnull, os.O_RDWR)
for fd in range(3):
os.dup2(devnull, fd)
# PID file
if pidfile is not None:
pidd = os.open(pidfile, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_TRUNC)

Pavel Kácha
committed
os.write(pidd, str(os.getpid())+"\n")
os.close(pidd)
# Define and setup atexit closure

Pavel Kácha
committed
@atexit.register
def unlink_pid():
try:
os.unlink(pidfile)
except Exception:
pass
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
def save_events(aggr, filer):
for event in aggr:
f, name = filer.create_unique_file()
with f:
f.write(json.dumps(event, ensure_ascii=True))
filer.publish_file(name)
logging.info(
"Saved %s %s \"%s\" (%d) as file %s" % (
event["ID"], "+".join(event["Category"]), event["Description"], event["ConnCount"], name))
RE_LIST = (
# 1493035442 Initial Connect - tarpitting: 89.163.242.15 56736 -> 195.113.254.182 9898
# 1493037991 Inbound SYN/ACK: 185.62.190.15 21001 -> 195.113.252.222 15584
(
re.compile(r'([0-9]+) ([^:]*:) ([^ ]+) ([0-9]+) -> ([^ ]+) ([0-9]+).*'),
namedtuple("connect_tuple", ("timestamp", "message", "src_ip", "src_port", "tgt_ip", "tgt_port"))
),
# 1493035442 Responded to a Ping: 88.86.96.25 -> 195.113.253.87 *
(
re.compile(r'([0-9]+) ([^:]*:) ([^ ]+) -> ([^ ]+).*'),
namedtuple("ping_tuple", ("timestamp", "message", "src_ip", "tgt_ip"))
)
)
def match_event(line):
for labrea_re, event_tuple in RE_LIST:
match = labrea_re.match(line)
if match:
return event_tuple(*match.groups())
logging.info("Unmatched line: \"%s\"" % line.replace("\n", r"\n"))
return None

Pavel Kácha
committed
def get_args():
optp = optparse.OptionParser(
usage="usage: %prog [options] logfile ...",
description="Watch LaBrea logfiles and generate Idea events into directory")
optp.add_option(
"-w", "--window",

Pavel Kácha
committed
default=900,
dest="window",
type="int",
action="store",
help="max detection window (default: %default)")
optp.add_option(
"-t", "--timeout",

Pavel Kácha
committed
default=300,
dest="timeout",
type="int",
action="store",
help="detection timeout (default: %default)")
optp.add_option(
"-n", "--name",

Pavel Kácha
committed
default=None,
dest="name",
type="string",
action="store",
help="Warden client name")
optp.add_option(
"--test",

Pavel Kácha
committed
default=False,
dest="test",
action="store_true",
help="Add Test category")
optp.add_option(
"-o", "--oneshot",

Pavel Kácha
committed
default=False,
dest="oneshot",
action="store_true",
help="process files and quit (do not daemonize)")
optp.add_option(
"--poll",
default=1,
dest="poll",
type="int",
action="store",
help="log file polling interval")
optp.add_option(
"-d", "--dir",
default=None,
dest="dir",
type="string",
action="store",
help="Target directory (mandatory)"),
optp.add_option(
"-p", "--pid",

Pavel Kácha
committed
default=pth.join("/var/run", pth.splitext(pth.basename(sys.argv[0]))[0] + ".pid"),
dest="pid",
type="string",
action="store",
help="create PID file with this name (default: %default)")
optp.add_option(
"-u", "--uid",
default=None,
dest="uid",
type="int",
action="store",
help="user id to run under")
optp.add_option(
"-g", "--gid",
default=None,
dest="gid",
type="int",
action="store",
help="group id to run under")
optp.add_option(
"-v", "--verbose",
dest="verbose",
action="store_true",
help="turn on debug logging")
optp.add_option(
"--log",
default="local7",
dest="log",
type="string",
action="store",
help="syslog facility or log file name (default: %default)")
optp.add_option(
"--realtime",

Pavel Kácha
committed
default=True,
dest="realtime",
action="store_true",
help="use system time along with log timestamps (default)")
optp.add_option(
"--norealtime",

Pavel Kácha
committed
dest="realtime",
action="store_false",
help="don't system time, use solely log timestamps")
return optp
running_flag = True
reload_flag = False

Pavel Kácha
committed
def terminate_me(signum, frame):
global running_flag
running_flag = False

Pavel Kácha
committed
def reload_me(signum, frame):
global reload_flag
reload_flag = True

Pavel Kácha
committed
def main():
global reload_flag
optp = get_args()
opts, args = optp.parse_args()
if not args or opts.name is None or opts.dir is None:

Pavel Kácha
committed
optp.print_help()
sys.exit()
logformat = "%(filename)s[%(process)d] %(message)s"
logger = logging.getLogger()

Pavel Kácha
committed
if opts.oneshot:
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s " + logformat))

Pavel Kácha
committed
else:
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
if "/" in opts.log:
handler = logging.handlers.WatchedFileHandler(opts.log)
handler.setFormatter(logging.Formatter("%(asctime)s " + logformat))
else:
handler = logging.handlers.SysLogHandler(address="/dev/log", facility=opts.log)
handler.setFormatter(logging.Formatter(logformat))
logger.addHandler(handler)
logger.setLevel(logging.DEBUG if opts.verbose else logging.INFO)
try:
if opts.oneshot:
signal.signal(signal.SIGINT, terminate_me)
signal.signal(signal.SIGTERM, terminate_me)
files = [open(arg) for arg in args]
else:
daemonize(
pidfile=opts.pid,
uid=opts.uid,
gid=opts.gid,
signals={
signal.SIGINT: terminate_me,
signal.SIGTERM: terminate_me,
signal.SIGHUP: reload_me
})
files = [FileWatcher(arg) for arg in args]

Pavel Kácha
committed
ideagen = IdeaGen(name=opts.name, test=opts.test)
contexts = [
context(window=opts.window, timeout=opts.timeout, ideagen=ideagen)
for context in [PingContextMgr, ConnectContextMgr, InboundContextMgr]
]
with contextlib.nested(*files):
for line_set in izip(*files):
for line in line_set:
if not line:
break
event = match_event(line)
if event or opts.realtime:
timestamp = int(event.timestamp) if event else int(time.time())

Pavel Kácha
committed
for context in contexts:
save_events(context.process(event, timestamp), filer)
if not running_flag:
break
if reload_flag:
for f in files:
f.close()
f.open()

Pavel Kácha
committed
reload_flag = False
if not any(line_set):
time.sleep(opts.poll)
for context in contexts:
timestamp = int(time.time()) if opts.realtime else None
save_events(context.close(timestamp), filer)
except Exception:
logging.exception("Exception")