Let Me Teach You: Bro
Bro is an event-driven scripting engine and an intrusion detection system. It sniffs packets, decodes them from hexadecimal, and parses their protocol fields. If it notices anything specific or weird, it generates logs for the analyst to review. Bro’s namesake is a reference to the surveillance activities of “Big Brother,” a fictional character from the book 1984. In 2018, it was renamed to “Zeek” in honor of the hunting dog from the Far Side comics. In this blog post, I will demonstrate how to use the Bro, or Zeek, scripting language to help automate traffic analysis and threat hunting.
Table of Contents
- Lab Setup
- How to Craft a Packet with Scapy
- How to Write a Zeek Signature
- How to Write a Zeek Script
- How to Parse Zeek Logs
- How to Sniff Packets with Zeek
- How to Correlate Network Events with Zeek
- References
Lab Setup
First, download and deploy the Security Onion platform as a Virtual Machine (VM) to follow along. It ships with almost everything you will need to get started. For example, it already has Zeek, Scapy, tcpreplay, and Vim installed (I’ll explain what each of these tools do later). When invoked manually, Zeek will dump various logs to your current directory. Therefore, open a terminal, make a “working” or “temporary” directory, and change your location on the filesystem to it. This will help keep things clean and organized.
mkdir workbench
cd workbench
How to Craft a Packet with Scapy
Scapy is a Python library used for manually crafting and manipulating packets. In this instance, a “defender” may wish to use it to emulate a known threat and test their intrusion detection capabilities. To fire-up a Python interpreter and have the library imported automatically, type scapy
at the console.
scapy
In the example below, I created a Packet Capture, or PCAP, file containing a DNS query for the domain name, “www.malicious.life.” Although this website is for a cybersecurity podcast, it’s TLD (Top Level Domain) extension of “.life” is often associated with malware distribution points. To make the PCAP more realistic, I also specified “recursion is desired” (or set the Recursion bit to 1) in order to force the Google DNS server at 8.8.8.8
to recursively look for answer if it doesn’t have one itself.
ip = IP()
ip.src = "192.168.1.69"
ip.dst = "8.8.8.8"
udp = UDP()
udp.sport = 4444
udp.dport = 53
dns = DNS()
dns.rd = 1
dns.qd = DNSQR()
dns.qd.qname = "www.malicious.life"
packet = ip/udp/dns
packet.show()
wrpcap("dns-traffic.pcap", packet)
How to Write a Zeek Signature
We now need to write a signature file so Zeek triggers on blacklisted TLDs like “.life.” First, open a text-editor. I prefer to use Vim.
vim dns-intel.sig
Next, identify the protocol, destination, and payload string we’re interested in. In the example below, I’m choosing to first label this signature “dns-intel” since blacklisted TLDs are often shared as “threat intelligence.” Ultimately, you can label it whatever you want, but understand, it will be key in the next step. I also used a Regex, or Regular Expression, pattern to identify multiple indicators of concern. In other words, I want Zeek to be on the look out for these extensions inside of any UDP traffic destined for port 53.
signature dns-intel {
ip-proto == udp
dst-port == 53
payload /.*ooo|.*gdn|.*bar|.*work|.*life/
event "[Suspicious DNS query] "
}
How to Write a Zeek Script
As mentioned before, the real power behind Zeek is it’s protocol analyzers. They take stimulus and response functions found in most “network protocols” and present them to the script developer as individual “events.” For instance, a DNS query and a DNS response are both unique events you can leverage in a script. Another example is when a signature is matched. For this particular exercise, we’re going to have Zeek alert us (print something to the console) if it detects anything matching what we specified in our signature file.
vim dns-alert.bro
In the scripting context of Zeek, “network events” are objects with properties. We must ensure it understands every argument provided as input in order for each event to be processed correctly. For instance, the “signature_match” event includes three arguments: state
, msg
, and data
. state
is of the signature_state
data type while msg
and data
are string data types. If we confused the two data types or supplied a bogus one, Zeek will fail at handling the event as desired.
Lastly, to access or manipulate the properties of an object (network event), we must use the $ symbol. In my script below, I compare the state$sig_id
property of the signature_match
event to the string dns-intel
. Any event matching this criteria will cause Zeek to trigger and then, execute my code. To keep things easy to digest, my code shown here prints a dynamically formatted output using the state$conn$dns$query
property (a.k.a the “domain name” in question).
event signature_match (state: signature_state, msg: string, data: string) {
if (state$sig_id == "dns-intel") {
print fmt ("[Suspicious DNS query] %s", state$conn$dns$query)
}
}
How to Parse Zeek Logs
Let’s daisy-chain all of the concepts covered thus far. The syntax below asks Zeek (identified as bro
) to read our handmade PCAP and execute our script using the dns-intel.sig
signature file as support.
bro -r dns-traffic.pcap -s dns-intel.sig dns-alert.bro
If you typed everything correctly, you should get output similar to what is shown below.
[ALERT] Suspicious DNS query: www.malicious.life
Now, list the contents of your current working directory. You should see a handful of .log
files.
ls *.log
These logs are handy for network forensics and threat hunting. Although, beware, you must use a special Zeek tool called bro-cut
to effectively extract and correlate interesting data points. As example, look at the dns.log
using only the cat
utility.
cat dns.log
Your output should be something similar to what is shown below. As you’ll see, it looks important, but it’s hard to read.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path dns
#open 2019-11-19-18-01-41
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto trans_id rtt query qclass qclass_name qtype qtype_name rcode rcode_name AA TC RD RA Z answers TTLs rejected
#types time string addr port addr port enum count interval string count string count string count stringbool bool bool bool count vector[string] vector[interval] bool
1573934403.323682 Cs9G761UH0Nh67wo0g 0.0.0.0 4444 8.8.8.8 53 udp 0 - www.malicious.life 1 C_INTERNET 1 A - - F F T F 0 - - F
#close 2019-11-19-18-01-41
Execute the command sentence below to only look at the #fields
row.
cat dns.log | head -n7 | tail -n1
The result will include the unique fields, or column headers, that exist in your log of interest.
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto trans_id rtt query qclass qclass_name qtype qtype_name rcode rcode_name AA TC RD RA Z answers TTLs rejected
To cut-out and only focus on certain columns, you must use the bro-cut command. Let’s try it with the ts
, id.orig_h
, id.orig_p
, and query
columns. Include -u
with ts
to view timestamps in UTC.
cat dns.log | bro-cut ts id.orig_h id.orig_p query
Great. We know how to create a packet, a signature, and a script. We also see how bro-cut
can help us parse a Zeek logs. Now, let’s try to use Zeek for sniffing packets off the network.
2019-11-16T20:00:03+0000 0.0.0.0 4444 www.malicious.life