The Premise:
While working with Security Onion, I wanted to do more with Bro and custom scripts. I worked on the exercises at try.bro.org and watched a few videos on YouTube. I had a pretty good understanding of how the language worked, but it wasn't until I bought a new router and started sending the syslog output to the onion that I found a "problem" I could work on solving with a custom Bro script.Basically, the router's syslog would have entries for accepted and dropped traffic, DHCP IP assignments, and other router specific logs. I wanted a cleaner Bro log for just the firewall accept and drop logs. The default syslog.log had the info, and I parsed it out with bro-cut, grep, and awk, but that didn't give me any practice with bro scripting.
So I wrote a new module called RouterFW that would be a new namespace for my router firewall log parsing script. This script would in essence react to the syslog_message event in bro, parse out the data I wanted, and then log it to the RouterFW.log log.
Bro scripting tips:
Mostly, I wrote this script based on the exercises at try.bro.org and the Bro documentation.I wrote the script as just one file called RouterFW.bro, but should probably go back and break it out into the recommended convention of having a directory called RouterFW named after the module, and in that having a __load__.bro file that just loads the script (renamed main.bro) and any other script files in the directory.
With Bro, you can test a script by running it at the command line with just
bro -C -i eth0 /pathto/script.bro
But remember that when running it this way it writes the log files to the directory you are in when running the command.
Once you have the script working and want it in production, you need to put the script in the /opt/bro/share/bro/site/ directory. Here is where you can create a directory named the same as your module, put your script in it and name it main.bro (to follow convention). Create a file called __load__.bro that has load statements for each of the files in the directory, including main.bro.
You'll also need to add a load statement to /opt/bro/share/bro/site/local.bro that loads your new module directory.
In order for Bro to use the script in normal production, you'll need to run the following commands:
- Check for errors with: broctl check
- Tell Bro to use the script with: broctl install
- Restart Bro with: broctl restart
You can verify your script loaded by grepping for your script's module name in the loaded scripts log: /nsm/bro/logs/current/loaded_scripts.log
The Script:
So, here's a breakdown of the script:The first few lines load any necessary base functionality that will be used in my module script. Since I'm using the syslog_message event, I'll need to load syslog. Since I'll be using items from the connection event, I've loaded conn also. These will most likely already be loaded by Bro running in production but it is recommended to add it to the script in case it is run by itself.
@load base/protocols/syslog @load base/protocols/conn | |
module RouterFW; |
Next, I identify any record, variable, or function that needs to be accessed from other scripts. Basically I'm just creating the ID for the new stream and defining the record type for the log file.
export { | |
#Create an ID for the new stream. | |
redef enum Log::ID += { LOG }; | |
#Define the record type that will contain the data to log. | |
type Info: record { | |
syslog_ts: time &log; | |
syslog_uid: string &log; | |
fw_ts: string &log; | |
packet_src: addr &log; | |
packet_dest: addr &log; | |
packet_dport: string &log; | |
packet_proto: string &log; | |
action: string &log; | |
}; | |
} | |
Next is tying it to the bro_init() so the stream is created when bro starts up.
event bro_init() &priority=5
{
{
#Create the stream. this adds a default filter automatically
Log::create_stream(RouterFW::LOG, [$columns=Info, $path="RouterFW"]);
}
This next part is cool. It will add the RouterRW record to the connection record so it can be accessed by other scripts in Bro using the $ notation. For example c$routerfw$action would return the action drop or accept.
#add a new field to the connection record so that data is accessible in variety of event handlers
redef record connection += {
routerfw: Info &optional;
};
The last part is the logic that parses out the data from the syslog message field of the syslog record. This field is a string and holds whatever the router put in the log message. This is specific to my router and took a bit of playing around to be sure I was getting the right fields.
#use syslog_message event as defined in Bro_Syslog.events.bif.bro
event syslog_message(c:connection; facility:count; severity:count; msg: string)
{
#split message field to get data we want
local messagedata = split_string(c$syslog$message, / /);
#concatenate back together the time and date from the log message itself
#concatenate back together the time and date from the log message itself
local fw_time = cat_sep(" ", "-", messagedata[0], messagedata[1], messagedata[2]);
#log any ACCEPT or DROP message from the firewall
if (( "ACCEPT" in msg ) || ("DROP" in msg))
{
local action = messagedata[4];
for (i in messagedata)
{
if ("SRC=" in messagedata[i])
{
local src_ip = to_addr((split_string(messagedata[i], /=/ ))[1]);
};
if ("DST=" in messagedata[i])
{
local dst_ip = to_addr((split_string(messagedata[i], /=/))[1]);
};
if ("DPT=" in messagedata[i])
{
local dst_p = (split_string(messagedata[i], /=/))[1];
};
if ("PROTO=" in messagedata[i])
{
local proto = (split_string(messagedata[i], /=/))[1];
dst_p = "No Port";
};
};
#Log format
local rec: RouterFW::Info = [$syslog_ts=c$syslog$ts, $syslog_uid=c$uid, $fw_ts=fw_time,
$packet_src=src_ip, $packet_dest=dst_ip, $packet_dport=dst_p, $packet_proto=proto,
$action=action];
c$routerfw = rec;
Log::write(RouterFW::LOG, rec);
};
}
The Log Files:
I was confused at first because the order of the fields in the log didn't match the order I used in the statement
starting with "local rec: RouterFW::Info =". I found the order is determined by the export statement at the top
of the script.
The log file looks like this.
So my first script worked out well enough that I duplicated it and reworked the logic to be a RouterDHCP
module that creates logs of DHCP ACKs.
That log looks like this:
All in all, not the most necessary scripts, but I learned a bunch figuring out how to make them work.
starting with "local rec: RouterFW::Info =". I found the order is determined by the export statement at the top
of the script.
The log file looks like this.
So my first script worked out well enough that I duplicated it and reworked the logic to be a RouterDHCP
module that creates logs of DHCP ACKs.
That log looks like this:
All in all, not the most necessary scripts, but I learned a bunch figuring out how to make them work.
can you help me to add custom scripts to connect to a firewall and block an IP
ReplyDelete