mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 06:38:20 +00:00
1036 lines
30 KiB
Python
Executable file
1036 lines
30 KiB
Python
Executable file
#!/usr/bin/python
|
|
|
|
import sys
|
|
import re
|
|
import getopt
|
|
import os.path
|
|
import struct
|
|
|
|
# FIXME: Not all of the implemented Snort options are really tested...
|
|
|
|
snortcmd = re.compile( "(preprocessor|include|var|config|alert|log|pass|activate|dynamic|output)\s*:?\s*(.*)" )
|
|
snortrule = re.compile( "(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+\((.*)\)" )
|
|
snortopt = re.compile( r"([a-zA-Z_]+)\s*(:\s*(([^;]|(?<=\\);)+)|);" )
|
|
|
|
snortipre = "(\d+\.\d+\.\d+\.\d+(/\d+)?)"
|
|
snortip = re.compile( "(!)?%s" % snortipre )
|
|
snortiplist = re.compile( "(!)?\[(%s(,%s)*)\]" % ( snortipre,snortipre ) )
|
|
snortport = re.compile( "(!)?(\d+)" )
|
|
snortportrange = re.compile( "(!)?(\d+)?:(\d+)?" )
|
|
snortval = re.compile( "([<>=!]?) *(\d+)" )
|
|
snortvalrange = re.compile( "(\d+)? *- *(\d+)?" )
|
|
snortbytecode = re.compile( r"\\\|\s*(([a-fA-F0-9]{2}\s*)+)\s*\\\|" ) # "|" are quoted when we do the match
|
|
snorthex = re.compile( "(..) *" )
|
|
snortquote = re.compile( r"\\(.)" )
|
|
snortalpha = re.compile( r"(\\x[a-fA-F0-9]{2}|[A-Za-z])" )
|
|
snortrpc = re.compile( r"((\d)+|\*) *, *((\d)+|\*) *, *((\d)+|\*)" )
|
|
|
|
snortsid = re.compile( "sid: *(\d+)" )
|
|
snortrev = re.compile( "rev: *(\d+)" )
|
|
|
|
# Mapping of Snort's variables to Bro's
|
|
# <snort> -> ( <bro>, <invert> )
|
|
MapVars = {
|
|
"external_net": ( "local_nets", 1 ),
|
|
"home_net": ( "local_nets", 0 ),
|
|
"http_servers": ( "http_servers", 0 ),
|
|
"http_ports": ( "http_ports", 0 ),
|
|
"oracle_ports": ( "oracle_ports", 0 ),
|
|
"smtp_servers": ( "smtp_servers", 0 ),
|
|
"sql_servers": ( "sql_servers", 0 ),
|
|
"telnet_servers": ( "telnet_servers", 0 ),
|
|
"aim_servers": ( "aim_servers", 0 ),
|
|
"shellcode_ports": ( "non_shellcode_ports", 1 ),
|
|
}
|
|
|
|
# Mapping of variables to content
|
|
SnortVars = {}
|
|
|
|
# List of tuples (file,linenr) for all not fully processed input files:
|
|
Inputs = []
|
|
|
|
# Last input line read
|
|
RawInputLine = ""
|
|
|
|
# Counts Snort rules without SID
|
|
UnknownCount = 0
|
|
|
|
# There may be some rules for which it don't make sense to translate them. We ignore them.
|
|
IgnoreSIDs = {}
|
|
|
|
# Always include these signatures, even if they would be ignored with option -c.
|
|
AlwaysIncludeSIDs = {}
|
|
|
|
def error( str ):
|
|
if Inputs:
|
|
if RawInputLine:
|
|
print >>sys.stderr, ">>", RawInputLine,
|
|
if RawInputLine[-1] != "\n":
|
|
print >>sys.stderr
|
|
print >>sys.stderr, "Error in %s, line %d: %s" % ( Inputs[0][0].name, Inputs[0][1], str )
|
|
|
|
else:
|
|
print >>sys.stderr, "Error:", str
|
|
sys.exit( 1 )
|
|
|
|
def warning( str ):
|
|
if Inputs:
|
|
if RawInputLine:
|
|
print >>sys.stderr, ">>", RawInputLine,
|
|
if RawInputLine[-1] != "\n":
|
|
print >>sys.stderr
|
|
print >>sys.stderr, "Warning in %s, line %d: %s" % ( Inputs[0][0].name, Inputs[0][1], str )
|
|
else:
|
|
print >>sys.stderr, "Warning:", str
|
|
|
|
# Converts Snort-like globs into regexp chars
|
|
def replaceGlobs( str ):
|
|
# Note that the globs are quoted when we see them here
|
|
# FIXME: If the original pattern contains a "\?" or "\*" it
|
|
# will be converted as well.
|
|
str = str.replace( "\\*", ".*" )
|
|
str = str.replace( "\\?", "." )
|
|
return str
|
|
|
|
# Replaces Snort-like bytecodes by \x.. sequences
|
|
def replaceByteCodes( str ):
|
|
return snortbytecode.sub( lambda bytecode: snorthex.sub( "\\x\\1", bytecode.group( 1 ) ), str )
|
|
|
|
# Removes Snort's \ quotations
|
|
def removeQuotes( str ):
|
|
return snortquote.sub( "\\1", str )
|
|
|
|
# Quotes all '"'
|
|
def quoteStr( str ):
|
|
return str.replace( "\"", "\\\"" )
|
|
|
|
# Translates all alphabetical characters to case-insensitive classes
|
|
def makeCaseInsensitive( str ):
|
|
|
|
def replaceby( match ):
|
|
s = match.group( 1 )
|
|
# Do not change hex codes
|
|
if s.startswith( "\\x" ):
|
|
return s
|
|
return "[%s%s]" % ( s.lower(), s.upper() )
|
|
|
|
str = snortalpha.sub( replaceby , str )
|
|
return str
|
|
|
|
# Insert some special variants/character classes for URIs
|
|
# - "/ -> [/\]"
|
|
def makeURIPattern( str ):
|
|
str = str.replace( "\\/", "[\\/\\\\]" )
|
|
return str
|
|
|
|
# Escapes all regex control characters so that it can be safely used as a regex
|
|
def escapeCtrl( str ):
|
|
re = ""
|
|
for i in str:
|
|
if "^$|.*+?[](){}/\"\\".find( i ) >= 0:
|
|
i = "\\" + i
|
|
re += i
|
|
return re
|
|
|
|
# Converts a Snort pattern into a RE
|
|
# If icase==1, constructs an case-insensitive regex.
|
|
# If uri==1, creates some special things for URIs (see above)
|
|
# If glob==1, the pattern is a Snort-like "regex" glob
|
|
# If negate==1, the pattern is negated
|
|
# If neglen>0, it's the number of character which are *not* allowed to match the negated pattern
|
|
#
|
|
# FIXME: We should use a more sophisticated parser here
|
|
#
|
|
def patternToRE( str, icase, uri, globs, negate, neglen ):
|
|
|
|
if negate:
|
|
if patternLength( str ) > 1:
|
|
warning( "Can\'t negate patterns with more than one character" )
|
|
return "<willnevermatch>"
|
|
str = "[^%s]" % replaceByteCodes( escapeCtrl( str ) )
|
|
|
|
if neglen > 0:
|
|
return str + "{%d}" % neglen
|
|
else:
|
|
return str + "*"
|
|
|
|
str = removeQuotes( str )
|
|
str = escapeCtrl( str )
|
|
if globs:
|
|
str = replaceGlobs( str )
|
|
str = replaceByteCodes( str )
|
|
|
|
if uri:
|
|
str = makeURIPattern( str )
|
|
|
|
if icase:
|
|
str = makeCaseInsensitive( str )
|
|
|
|
return str
|
|
|
|
# Counts the number chars in the Snort pattern
|
|
def patternLength( str ):
|
|
str = removeQuotes( str )
|
|
str = escapeCtrl( str )
|
|
str = replaceByteCodes( str )
|
|
|
|
count = 0
|
|
esc = 0
|
|
|
|
for c in str:
|
|
if c == "\\" and not esc:
|
|
esc = 1
|
|
continue
|
|
|
|
if esc and c == "x":
|
|
count -= 2
|
|
|
|
esc = 0
|
|
count += 1
|
|
|
|
return count
|
|
|
|
# Parse a Snort variable declaration
|
|
def parseVar( decl ):
|
|
( key, value ) = decl.split()
|
|
SnortVars[ "$"+key ] = value
|
|
|
|
# Perform Snort's variable expansion
|
|
def expandVars( str ):
|
|
for ( key, value ) in SnortVars.items():
|
|
str = str.replace( key, value )
|
|
return str
|
|
|
|
# Parse Snort's rule options
|
|
def parseRuleOptions( str ):
|
|
|
|
options = {}
|
|
|
|
m = snortopt.findall( str )
|
|
|
|
lastcontent = None
|
|
depth = distance = negate = nocase = offset = regex = within = -1
|
|
|
|
negpattern = -1
|
|
|
|
# print str
|
|
|
|
for ( b1, b2, b3, b4 ) in m:
|
|
if b2:
|
|
val = b3
|
|
if val[0] == "!":
|
|
negpattern = 1
|
|
val = val[1:]
|
|
if len( val ) >= 2:
|
|
if val[0] == '"' and val[-1] == '"':
|
|
val = val[1:-1]
|
|
else:
|
|
val = ""
|
|
|
|
# Note that there may be more than one depth/offset per rule.
|
|
# Each depth/offset/regex affects exactly that content which precedes it.
|
|
# It's illegal to have a depth/offset/regex before a content.
|
|
# That is all undocumented.
|
|
# ... !@#$%^&* ...
|
|
|
|
option = b1.lower()
|
|
|
|
if option == "content":
|
|
|
|
if not lastcontent:
|
|
lastcontent = val
|
|
continue
|
|
|
|
oldval = val
|
|
val = ( lastcontent, depth, distance, negate, nocase, offset, regex, within )
|
|
depth = distance = negate = nocase = offset = regex = within = -1
|
|
negate = negpattern
|
|
lastcontent = oldval
|
|
|
|
elif option == "depth":
|
|
depth = int( val )
|
|
continue
|
|
|
|
elif option == "distance":
|
|
distance = int( val )
|
|
continue
|
|
|
|
elif option == "within":
|
|
within = int( val )
|
|
continue
|
|
|
|
elif option == "offset":
|
|
offset = int( val )
|
|
continue
|
|
|
|
elif option == "regex":
|
|
regex = 1
|
|
continue
|
|
|
|
elif option == "nocase":
|
|
nocase = 1
|
|
continue
|
|
|
|
try:
|
|
options[ option ] += [ val ]
|
|
except LookupError:
|
|
options[ option ] = [ val ]
|
|
|
|
if lastcontent:
|
|
try:
|
|
options[ "content" ] += [ ( lastcontent, depth, distance, negate, nocase, offset, regex, within ) ]
|
|
except LookupError:
|
|
options[ "content" ] = [ ( lastcontent, depth, distance, negate, nocase, offset, regex, within ) ]
|
|
|
|
return options
|
|
|
|
# Parses a list of IPs in Snort's format
|
|
def parseIP( ip ):
|
|
|
|
# See if it's an unexpanded var.
|
|
if ip.startswith( "$" ):
|
|
try:
|
|
( brovar, neg ) = MapVars[ ip[1:] ]
|
|
return ( neg, [brovar] )
|
|
except LookupError:
|
|
error( "Unknown variable " + ip )
|
|
|
|
m = snortip.match( ip )
|
|
if m:
|
|
ips = ( m.group( 2 ), )
|
|
else:
|
|
m = snortiplist.match( ip )
|
|
if m:
|
|
ips = m.group( 2 ).split( "," )
|
|
else:
|
|
error( "Can\'t parse IP " + ip )
|
|
|
|
if m.group( 1 ) != "!":
|
|
return( 0, ips )
|
|
else:
|
|
return( 1, ips )
|
|
|
|
# Parses a list of ports in Snort's format
|
|
def parsePort( port ):
|
|
|
|
# See if it's an unexpanded var.
|
|
if port.startswith( "$" ):
|
|
try:
|
|
( brovar, neg ) = MapVars[ port[1:] ]
|
|
return ( neg, brovar, brovar )
|
|
except LookupError:
|
|
error( "Unknown variable " + port )
|
|
|
|
m = snortportrange.match( port )
|
|
if m:
|
|
try:
|
|
min = int( m.group( 2 ) )
|
|
except TypeError:
|
|
min = 0
|
|
try:
|
|
max = int( m.group( 3 ) )
|
|
except TypeError:
|
|
max = 65535
|
|
else:
|
|
m = snortport.match( port )
|
|
if m:
|
|
min = max = int( m.group( 2 ) )
|
|
else:
|
|
error( "Can\'t parse port " + port )
|
|
|
|
if m.group( 1 ) != "!":
|
|
return( 0, min, max )
|
|
else:
|
|
return( 1, min, max )
|
|
|
|
######
|
|
|
|
# Convert the common head of a Snort rule
|
|
def convertHead( prot, srcip, srcport, dstip, dstport ):
|
|
|
|
rule = ""
|
|
|
|
# Convert IP protocol
|
|
if prot.lower() != "ip":
|
|
rule += " ip-proto == %s\n" % prot.lower()
|
|
|
|
# Convert IPs
|
|
for ( tag, ip ) in ( ( "src-ip", srcip ), ( "dst-ip", dstip ) ):
|
|
if ip != "any":
|
|
( negate, iplist ) = parseIP( ip )
|
|
if not negate:
|
|
cmp = "=="
|
|
else:
|
|
cmp = "!="
|
|
|
|
rule += " %s %s %s\n" % ( tag, cmp, ",".join( iplist ) )
|
|
|
|
# Convert Ports
|
|
for ( tag, port ) in ( ( "src-port", srcport ), ( "dst-port", dstport ) ):
|
|
if port != "any":
|
|
( negate, min, max ) = parsePort( port )
|
|
if min == max:
|
|
if not negate:
|
|
cmp = "=="
|
|
else:
|
|
cmp = "!="
|
|
rule += " %s %s %s\n" % ( tag, cmp, min )
|
|
else:
|
|
if not negate:
|
|
cmp1 = ">="
|
|
cmp2 = "<="
|
|
else:
|
|
cmp1 = "<"
|
|
cmp2 = ">"
|
|
|
|
rule += " %s %s %s\n" % ( tag, cmp1, min )
|
|
rule += " %s %s %s\n" % ( tag, cmp2, max )
|
|
|
|
return rule
|
|
|
|
# Converts one of Snort's bit strings (like "A+" for "flags")
|
|
def convertBitSet( str, bitspecs, hdrfield, fieldmask ):
|
|
|
|
# Split into flags and mask
|
|
parts = str.split(",")
|
|
if len(parts) > 1:
|
|
( str, snortmask ) = parts
|
|
# FIXME: We ignore the mask for now. This would again need some
|
|
# digging in Snort's source as I don't really understand what they
|
|
# are doing...
|
|
|
|
# This is not strictly Snort conforming but works as long as the
|
|
# flag string is not ambigious.
|
|
|
|
type = ""
|
|
mask = 0
|
|
for c in str:
|
|
if "+!*".find( c ) >= 0:
|
|
type = c
|
|
continue
|
|
try:
|
|
mask |= bitspecs[c]
|
|
except LookupError:
|
|
error( "Unknown bit in \"%s\"" % str )
|
|
|
|
if type == "":
|
|
# Snort's default is check for equality (undocumented)
|
|
rule = " %s & %d == %d\n" % ( hdrfield, fieldmask, mask )
|
|
if type == "+":
|
|
rule = " %s & %d == %d\n" % ( hdrfield, ( mask | fieldmask ), mask )
|
|
if type == "*":
|
|
rule = " %s & %d != 0\n" % ( hdrfield, ( mask | fieldmask ) )
|
|
if type == "!":
|
|
rule = " %s & %d == 0\n" % ( hdrfield, ( mask | fieldmask ) )
|
|
|
|
return rule
|
|
|
|
# Converts a test for a value which may include some range (e.g. "<5", "21-42")
|
|
def convertVal( str, hdrfield ):
|
|
|
|
m = snortvalrange.match( str )
|
|
if m:
|
|
try:
|
|
min = int( m.group( 1 ) )
|
|
except TypeError:
|
|
min = 0
|
|
try:
|
|
max = int( m.group( 2 ) )
|
|
except TypeError:
|
|
max = -1
|
|
|
|
rule = ""
|
|
if min > 0:
|
|
rule += " %s >= %d\n" % ( hdrfield, min )
|
|
if min >= 0:
|
|
rule += " %s <= %d\n" % ( hdrfield, max )
|
|
return rule
|
|
|
|
m = snortval.match( str )
|
|
if m:
|
|
val = int( m.group( 2 ) )
|
|
cmp = m.group( 1 )
|
|
|
|
if cmp == "<" or cmp == ">":
|
|
return " %s %s %d\n" % ( hdrfield, cmp, val )
|
|
if cmp == "!":
|
|
return " %s != %d\n" % ( hdrfield, val )
|
|
if cmp == "" or cmp == "=":
|
|
return " %s == %d\n" % ( hdrfield, val )
|
|
|
|
error( "Can\'t parse value \"%s\"" % str )
|
|
|
|
# Convert one value of the RPC triple into a RE
|
|
def convertRPCVal( str ):
|
|
if str == "*":
|
|
return "."
|
|
else:
|
|
# Convert value to hex pattern in network bye order
|
|
val = struct.pack( "!I", int( str ) )
|
|
return ( "\\x%02x" * 4 ) % struct.unpack( "BBBB", val )
|
|
|
|
# Sanity check to see if the two protocols match
|
|
def checkProt( testprot, option, ruleprot ):
|
|
if testprot != ruleprot:
|
|
error( "Option \"%s\" only valid for %s rules" % ( option, testprot.upper() ) )
|
|
|
|
def convertOptions( options, prot ):
|
|
|
|
global IsPayloadRule
|
|
|
|
rule = ""
|
|
payloads = []
|
|
|
|
for ( key, vallist ) in options.items():
|
|
|
|
if key == "ack":
|
|
checkProt( "tcp", key, prot )
|
|
rule += " header tcp[8:4] == %d\n" % int( vallist[0] )
|
|
|
|
elif key == "classtype":
|
|
pass
|
|
|
|
elif key == "content":
|
|
|
|
IsPayloadRule = 1
|
|
|
|
for i in range( len( vallist ) ):
|
|
|
|
( content, depth, distance, negate, nocase, offset, regex, within ) = vallist[i]
|
|
|
|
# Special case: If have we have a payload size which equals the size of the pattern,
|
|
# the payload has to match exactly
|
|
|
|
prefix = ".*"
|
|
|
|
if "dsize" in options.keys():
|
|
try:
|
|
if patternLength( content ) == int( options["dsize"][0] ):
|
|
prefix = ""
|
|
except ValueError:
|
|
# dsize is something which contains a range; do nothing as
|
|
# it's just an optimization after all
|
|
pass
|
|
|
|
realdepth = depth;
|
|
realoffset = offset;
|
|
|
|
maxdist = 1
|
|
|
|
if depth >= 0:
|
|
realdepth -= patternLength( content )
|
|
|
|
if offset >= 0:
|
|
realoffset -= 1
|
|
maxdist = 0
|
|
|
|
if offset >= 0 and depth >= 0:
|
|
realdepth -= offset
|
|
maxdist = 1
|
|
|
|
if within >= 0:
|
|
realdepth += within - patternLength( content ) + 1
|
|
|
|
if distance >= 0:
|
|
realoffset += distance + 1
|
|
maxdist = 0
|
|
|
|
if within >= 0 and distance >= 0:
|
|
maxdist = 1
|
|
|
|
if realdepth < 0 and realoffset <= 0:
|
|
re = prefix
|
|
else:
|
|
re = ""
|
|
|
|
if realdepth < 0:
|
|
realdepth = -1
|
|
|
|
if realoffset > 0:
|
|
if maxdist:
|
|
re += ".{%d}" % realoffset
|
|
else:
|
|
re += ".{%d}.*" % realoffset
|
|
elif realoffset == 0 and not maxdist:
|
|
re += ".*"
|
|
|
|
if realdepth > 0 and negate < 0:
|
|
re += ".{0,%d}" % realdepth
|
|
maxdist = 1
|
|
|
|
re += patternToRE( content, nocase >= 0, 0, regex >= 0, negate >= 0, realdepth + 1 )
|
|
|
|
if distance >= 0 or within >= 0:
|
|
try:
|
|
payloads[-1] += re
|
|
except LookupError:
|
|
payloads = [ re ]
|
|
else:
|
|
payloads += [ re ]
|
|
|
|
if REFile:
|
|
print >>REFile, re
|
|
if PatternFile:
|
|
print >>PatternFile, val
|
|
|
|
elif key == "depth":
|
|
# Ignore it here; we test for it when handling content
|
|
pass
|
|
|
|
elif key == "distance":
|
|
# Ignore it here; we test for it when handling content
|
|
pass
|
|
|
|
elif key == "dsize":
|
|
rule += convertVal( vallist[0], "payload-size" )
|
|
pass
|
|
|
|
elif key == "flags":
|
|
checkProt( "tcp", key, prot )
|
|
|
|
TCPFlags = { "F": 0x01, "S": 0x02, "R": 0x04, "P": 0x08,
|
|
"A": 0x10, "U": 0x20, "2": 0x40, "1": 0x80, "0": 0x00 }
|
|
rule += convertBitSet( "".join( vallist ), TCPFlags, "header tcp[13:1]", 0xff )
|
|
|
|
elif key == "flow":
|
|
checkProt( "tcp", key, prot )
|
|
|
|
Flows = { "established" : "established",
|
|
"to_server" : "originator",
|
|
"to_client" : "responder",
|
|
"from_server" : "responder",
|
|
"from_client" : "originator",
|
|
"stateless" : "stateless"
|
|
}
|
|
|
|
tcpstate = []
|
|
for val in vallist:
|
|
for ( snort, bro ) in Flows.items():
|
|
if val.find( snort.lower() ) >= 0:
|
|
tcpstate += ( bro, )
|
|
if tcpstate:
|
|
rule += " tcp-state %s\n" % ",".join( tcpstate )
|
|
|
|
elif key == "fragbits":
|
|
FragFlags = { "M": 0x20, "D": 0x40, "R": 0x80 }
|
|
rule += convertBitSet( "".join( vallist ), FragFlags, "header ip[6:1]", 0xe0 )
|
|
|
|
elif key == "icmp_seq":
|
|
checkProt( "icmp", key, prot )
|
|
# Check if it's an ECHO or ECHO_REPLY packet
|
|
# Note Snort's undocumented behaviour: It does check REPLYs
|
|
# as well, and it does not check the ICMP code
|
|
rule += " header icmp[0:1] == 0,8\n"
|
|
rule += " header icmp[6:2] == %d\n" % int( vallist[0] )
|
|
|
|
elif key == "icmp_id":
|
|
checkProt( "icmp", key, prot )
|
|
# Check if it's an ECHO or ECHO_REPLY packet
|
|
# Note Snort's undocumented behaviour: It does check REPLYs
|
|
# as well, and it does not check the ICMP code
|
|
rule += " header icmp[0:1] == 0,8\n"
|
|
rule += " header icmp[4:2] == %d\n" % int( vallist[0] )
|
|
|
|
elif key == "icode":
|
|
checkProt( "icmp", key, prot )
|
|
rule += " header icmp[1:1] == %d\n" % int( vallist[0] )
|
|
|
|
elif key == "id":
|
|
rule += " header ip[4:2] == %d\n" % int( vallist[0] )
|
|
|
|
elif key == "ip_proto":
|
|
rule += convertVal( vallist[0], "header ip[9:1]" )
|
|
|
|
elif key == "ipopts":
|
|
rule += " ip-options %s\n" % ",".join( vallist ).lower()
|
|
|
|
elif key == "itype":
|
|
checkProt( "icmp", key, prot )
|
|
rule += " header icmp[0:1] == %d\n" % int( vallist[0] )
|
|
|
|
elif key == "msg":
|
|
rule += " event \"%s\"\n" % quoteStr( removeQuotes( " ".join( vallist ) ) )
|
|
|
|
elif key == "nocase":
|
|
# Ignore it here; we test for it when handling content
|
|
pass
|
|
|
|
elif key == "offset":
|
|
# Ignore it here; we test for it when handling content
|
|
pass
|
|
|
|
elif key == "rawbytes":
|
|
# We can ignore this as we're always matching raw bytes.
|
|
pass
|
|
|
|
elif key == "ref":
|
|
pass
|
|
|
|
elif key == "reference":
|
|
pass
|
|
|
|
elif key == "regex":
|
|
pass
|
|
|
|
elif key == "rev":
|
|
pass
|
|
|
|
elif key == "rpc":
|
|
m = snortrpc.match( vallist[0] )
|
|
if not m:
|
|
error( "Can\'t parse RPC values" )
|
|
( app, proc, version ) = ( m.group( 1 ), m.group( 3 ), m.group( 5 ) )
|
|
|
|
# The Snort rule set has only explicit UDP/TCP rules containg
|
|
# "rpc" but no general IP rules. So we use the rule type
|
|
# to decide whether we check for UDP- or TCP-style RPC
|
|
#
|
|
# Snort only looks at calls, but not on replies (undocumented)
|
|
# Snort validates the RPC_MSG_VERSION (undocumented)
|
|
if prot == "udp":
|
|
off = 7
|
|
else:
|
|
checkProt( "tcp", key, prot )
|
|
off = 11
|
|
|
|
# RPC version == 2, Call == 1 \
|
|
rule += " payload /" + ( "." * off ) \
|
|
+ "\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01" \
|
|
+ convertRPCVal( app ) + convertRPCVal( version ) + convertRPCVal( proc ) \
|
|
+ "/\n"
|
|
|
|
elif key == "sameip":
|
|
rule += " same-ip\n"
|
|
|
|
elif key == "seq":
|
|
checkProt( "tcp", key, prot )
|
|
rule += " header tcp[4:4] == %d\n" % int( vallist[0] )
|
|
|
|
elif key == "sid":
|
|
pass
|
|
|
|
elif key == "rev":
|
|
pass
|
|
|
|
elif key == "tag":
|
|
# In Snort, this capture packets of session/host for later analysis.
|
|
# Unsupported for now, but we could come up with something similar
|
|
# by passing it on to set_record_contents(). Doesn't affect the
|
|
# matching, though.
|
|
pass
|
|
|
|
elif key == "ttl":
|
|
rule += convertVal( vallist[0], "header ip[8:1]" )
|
|
pass
|
|
|
|
elif key == "uricontent":
|
|
|
|
IsPayloadRule = 1
|
|
|
|
for val in vallist:
|
|
# if ALTERNATIVE_PATTERNS:
|
|
# re = "(|.*\\r\\n\\r\\n)(GET|HEAD|POST) *[^\\n]*%s" % patternToRE( val, "nocase" in options.keys(), 1, 0 )
|
|
# else:
|
|
# re = "(GET|HEAD|POST) *[^\\n]*%s" % patternToRE( val, "nocase" in options.keys(), 1, 0 )
|
|
#
|
|
# rule += " payload /%s/\n" % re
|
|
|
|
if ALTERNATIVE_PATTERNS:
|
|
re = "(|.*\\r\\n\\r\\n)(GET|HEAD|POST) *[^\\n]*%s" % patternToRE( val, "nocase" in options.keys(), 1, 0, 0, 0 )
|
|
rule += " payload /%s/\n" % re
|
|
else:
|
|
re = ".*%s" % patternToRE( val, "nocase" in options.keys(), 1, 0, 0, 0 )
|
|
rule += " http /%s/\n" % re
|
|
|
|
|
|
|
|
if REFile:
|
|
print >>REFile, re
|
|
if PatternFile:
|
|
print >>PatternFile, val
|
|
|
|
elif key == "within":
|
|
# Ignore it here; we test for it when handling content
|
|
pass
|
|
|
|
else:
|
|
warning( "Option '%s' not supported currently; ignored" % key )
|
|
rule += " # Not supported: %s: %s\n" % ( key, ",".join( vallist ) )
|
|
|
|
for p in payloads:
|
|
rule += " payload /%s/\n" % p
|
|
|
|
return rule
|
|
|
|
def parseAlert( rule ):
|
|
|
|
global UnknownCount
|
|
global IsPayloadRule
|
|
|
|
IsPayloadRule = 0
|
|
|
|
m = snortrule.match( rule )
|
|
if not m:
|
|
error( "Can\'t parse alert rule" )
|
|
|
|
fields = m.groups()
|
|
|
|
( prot, srcip, srcport, dir, dstip, dstport ) = map( lambda s: s.lower(), fields[0:6] )
|
|
|
|
options = parseRuleOptions( fields[6] )
|
|
|
|
try:
|
|
sid = options["sid"][0]
|
|
rev = options["rev"][0]
|
|
try:
|
|
if int( sid ) in IgnoreSIDs:
|
|
return
|
|
# id = "sid-" + sid
|
|
# 2004-06-21, rwinslow, Added rev level for sid
|
|
id = sid + "-" + rev
|
|
except ValueError:
|
|
id = sid
|
|
|
|
except LookupError:
|
|
id = "sid-unknown-%d" % UnknownCount
|
|
UnknownCount += 1
|
|
|
|
try:
|
|
# Create rules depending on the direction; for "<>" we simply create
|
|
# two rules
|
|
|
|
#if dir == "<>":
|
|
# id1 = id + "-a"
|
|
# id2 = id + "-b"
|
|
#else:
|
|
# id1 = id2 = id
|
|
|
|
id1 = id2 = id
|
|
|
|
if dir == "<>":
|
|
rule = "signature %s {\n" % id1
|
|
rule += convertHead( prot, "any", dstport, "any", srcport )
|
|
rule += convertOptions( options, prot )
|
|
|
|
if not PAYLOAD_ONLY or IsPayloadRule or id in AlwaysIncludeSIDs:
|
|
print rule + " }\n"
|
|
else:
|
|
if dir != "->":
|
|
rule = "signature %s {\n" % id1
|
|
rule += convertHead( prot, dstip, dstport, srcip, srcport )
|
|
rule += convertOptions( options, prot )
|
|
|
|
if not PAYLOAD_ONLY or IsPayloadRule or id in AlwaysIncludeSIDs:
|
|
print rule + " }\n"
|
|
|
|
if dir != "<-":
|
|
rule = "signature %s {\n" % id2
|
|
rule += convertHead( prot, srcip, srcport, dstip, dstport )
|
|
rule += convertOptions( options, prot )
|
|
|
|
if not PAYLOAD_ONLY or IsPayloadRule or id in AlwaysIncludeSIDs:
|
|
print rule + " }\n"
|
|
|
|
|
|
except LookupError, e:
|
|
error( "Can\'t convert rule: " + str( e ) )
|
|
|
|
#### Main
|
|
|
|
# Use some alternative patterns
|
|
ALTERNATIVE_PATTERNS = 0
|
|
# If PatternFile is a file, all content/uricontent patterns are written to it
|
|
PatternFile = 0
|
|
# If REFile is a file, all REs generated from content/uricontent patterns are written to it
|
|
REFile = 0
|
|
# Directories where to look for include's
|
|
INCPATH = [ "./" ]
|
|
# Only include signatures which match contain payload/uricontent
|
|
# (Exceptions can be defined in AlwaysIncludeSIDs)
|
|
PAYLOAD_ONLY = 0
|
|
|
|
# Number of Snort rules to output (no conversion to Bro format)
|
|
SnortRules = -1
|
|
|
|
# Total number of rules
|
|
TotalRules = 0
|
|
|
|
def openInputFile( filename ):
|
|
|
|
for i in INCPATH:
|
|
try:
|
|
fullpath = os.path.join( i, filename )
|
|
file = open( fullpath )
|
|
print >>sys.stderr, "Reading", fullpath
|
|
return file
|
|
except IOError:
|
|
pass
|
|
|
|
error( "Can\'t find file %s" % filename )
|
|
|
|
def ReadConfig( file ):
|
|
|
|
try:
|
|
cfg = open( arg )
|
|
for line in cfg:
|
|
|
|
line = line.strip()
|
|
if len( line ) == 0 or line.startswith("#"):
|
|
continue
|
|
|
|
try:
|
|
( action, sid ) = line.split()
|
|
sid = int( sid )
|
|
except ValueError:
|
|
print >>sys.stderr, "Warning: illegal format '%s'" % line
|
|
continue
|
|
|
|
if action == "ignoresid":
|
|
IgnoreSIDs[sid] = 1
|
|
|
|
except IOError:
|
|
print >>sys.stderr, "Warning: Can't read config file %s" % arg
|
|
|
|
def usage():
|
|
print >>sys.stderr
|
|
print >>sys.stderr, "Usage: snort2bro [<options>] [<snort-files>] "
|
|
print >>sys.stderr
|
|
print >>sys.stderr, " Options:"
|
|
print >>sys.stderr
|
|
print >>sys.stderr, " -c <file>: File containing signature in-/excludes"
|
|
print >>sys.stderr, " -p : Only include signatures which match on payload"
|
|
print >>sys.stderr, " -I <dir> : Add dir to search path for Snort\'s include statement"
|
|
print >>sys.stderr
|
|
print >>sys.stderr, " -P : Write all patterns to patterns.txt and "
|
|
print >>sys.stderr, " all generared REs to res.txt"
|
|
print >>sys.stderr, " -S <n> : No conversion; just print out one big Snort config file"
|
|
print >>sys.stderr, " containing the first <n> rules"
|
|
print >>sys.stderr, " -X : Produce some alternate REs"
|
|
|
|
print >>sys.stderr
|
|
sys.exit( 1 )
|
|
|
|
try:
|
|
options, rest = getopt.getopt( sys.argv[1:], "XPI:S:pc:" )
|
|
except:
|
|
usage()
|
|
|
|
for( opt, arg ) in options:
|
|
if opt == "-X":
|
|
ALTERNATIVE_PATTERNS = 1
|
|
|
|
if opt == "-P":
|
|
PatternFile = open( "patterns.txt", "w" );
|
|
REFile = open( "res.txt", "w" );
|
|
|
|
if opt == "-I":
|
|
INCPATH += [ arg, ]
|
|
|
|
if opt == "-S":
|
|
SnortRules = int( arg )
|
|
|
|
if opt == "-p":
|
|
PAYLOAD_ONLY = 1
|
|
|
|
if opt == "-c":
|
|
ReadConfig( arg )
|
|
|
|
if len( rest ):
|
|
Inputs = [ ( openInputFile( file ), 0 ) for file in rest ]
|
|
else:
|
|
Inputs = [ ( sys.stdin, 0 ) ]
|
|
|
|
continued = False
|
|
|
|
while Inputs:
|
|
|
|
if not continued:
|
|
line = ""
|
|
|
|
RawInputLine = Inputs[0][0].readline()
|
|
if not RawInputLine:
|
|
Inputs = Inputs[1:]
|
|
continue
|
|
|
|
single_line = RawInputLine.strip()
|
|
|
|
# Increase line count
|
|
Inputs[0] = ( Inputs[0][0], Inputs[0][1] + 1 )
|
|
|
|
# Continuation of lines
|
|
if single_line.endswith("\\"):
|
|
line += single_line[:-1]
|
|
continued = True
|
|
continue
|
|
|
|
line += single_line
|
|
continued = False
|
|
|
|
# Empty or comments
|
|
if not line or line.startswith( '#' ):
|
|
continue
|
|
|
|
line = expandVars( line )
|
|
|
|
m = snortcmd.match( line )
|
|
if not m:
|
|
error( "Can\'t parse line " + line )
|
|
|
|
cmd = m.group( 1 )
|
|
args = m.group( 2 )
|
|
|
|
if cmd == "include":
|
|
Inputs = [ ( openInputFile( args ), 0 ) ] + Inputs
|
|
continue
|
|
|
|
if cmd == "var":
|
|
parseVar( args )
|
|
|
|
if cmd == "alert":
|
|
TotalRules += 1
|
|
|
|
if SnortRules >= 0:
|
|
|
|
noprint = 0
|
|
|
|
if cmd == "alert":
|
|
|
|
for i in IgnoreSIDs:
|
|
m = snortsid.search( args )
|
|
if m and int( m.group( 1 ) ) == i:
|
|
noprint = 1
|
|
|
|
if SnortRules == 0:
|
|
noprint = 1
|
|
|
|
if not noprint:
|
|
SnortRules -= 1
|
|
|
|
if not noprint:
|
|
print RawInputLine,
|
|
|
|
continue
|
|
|
|
if cmd == "var":
|
|
continue
|
|
|
|
if cmd == "alert":
|
|
parseAlert( args )
|
|
continue
|
|
|
|
if cmd == "output":
|
|
continue
|
|
|
|
if cmd == "preprocessor":
|
|
continue
|
|
|
|
if cmd == "config":
|
|
# FIXME: Should we convert this to Bro?
|
|
continue
|
|
|
|
warning( "'%s' is not supported yet; line ignored." % cmd )
|
|
|
|
|
|
# Options -S returns the number of rules as exit code
|
|
if SnortRules >= 0:
|
|
print >>sys.stderr, TotalRules
|