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

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.


In the example below, I created a Packet Capture, or PCAP, file containing a DNS query for the domain name, “” 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 to recursively look for answer if it doesn’t have one itself.

ip = IP()
ip.src = ""
ip.dst = ""
udp = UDP() = 4444
udp.dport = 53
dns = DNS()
dns.rd = 1
dns.qd = DNSQR()
dns.qd.qname = ""
packet = ip/udp/dns
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:

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	4444	53	udp	0	-	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	4444

How to Sniff Packets with Zeek

First, specify your desired signature file (dns-intel.sig) within the local.bro script.

echo '@load-sigs $PREFIX/share/bro/site/dns-intel.sig' | sudo tee -a

Then, invoke BroControl (in your first terminal).

sudo broctl

Next, use the install command to load all of the scripts found under the default $PREFIX directory (i.e. our signature file) and start Zeek.

[BroControl] > install
[BroControl] > start

Finally, use tcpreplay (in your second window) to run the previously created PCAP against your “loopback” network interface. tcpreplay will convince the interface the traffic is real or live.

sudo tcpreplay -i lo dns-traffic.pcap

After running the PCAP, stop Zeek, and cut out the columns you’re interested in from the Zeek logs in your current directory.

[BroControl] > stop
[BroControl] > exit
zcat $PREFIX/logs/$DATE/signature$DATE.log.gz | bro-cut -u event_msg

How to Correlate Network Events with Zeek

The greatest feature that truly sets Zeek apart from other IDS solutions is it’s ability to correlate events between multiple traffic streams. Take a “watering hole” attack as an example. In the wild, if you poison the water, all the animals who drink from it get infected. In cyberspace, if you “poison” a file share with a malicious document all end-users who consume it get infected. Zeek can observe the evolution of this attack by way of the file_new and file_hash events. Before going further, let’s open our favorite text-editor, Vim.

vim detect-watering-hole-attack.bro

Copy and paste the entire code block below. Feel free to read the comments (followed by the # character) for more information. As a quick overview, the major components of my script below include:

  • Loading Zeek’s SMB framework of analzyers
  • Creating a few variables (they’ll get used like temporary scratch pads while it’s observing traffic)
  • Calling upon Zeek’s Files::ANALYZER_MD5 function to hash every new file seen in either HTTP or SMB traffic
  • Comparing & contrasting all file hashes to file hashes seen in both HTTP and SMB traffic
  • Triggering an alert if the previous condition is met
# load Zeek's SMB framework of analyzers
@load policy/protocols/smb

# create some global variables
global hashes_all: set[string];
global hashes_downloaded_via_HTTP: set[string];
global hashes_uploaded_via_SMB: set[string];

# when you see a file
event file_new (f: fa_file) {

	# if the file was seen in HTTP or SMB traffic, hash it
	if (f$source == "HTTP" || f$source == "SMB") {
		Files::add_analyzer(f, Files::ANALYZER_MD5);

# when you're asked to hash a file
event file_hash (f: fa_file, kind: string, hash: string) {

	# keep track of the hashed file
	add hashes_all[hash];
	# if the hashed file was seen in HTTP, track it as such
	if (f$source == "HTTP" && f$http$uri != "/") {
		add hashes_downloaded_via_HTTP[hash];
		print fmt ("[INFO] Connection: %s, Downloaded: %s, MD5: %s", f$http$uid, f$http$uri, hash);

	# if the hashed file was seen in SMB, track it as such
	if (f$source == "SMB") {
		add hashes_uploaded_via_SMB[hash];
		# extract the connection uid
		for (con in f$conns) { local cid = f$conns[con]$uid; }
		# extract the connection data
		for (con in f$conns) { local cdata = f$conns[con]; }
		print fmt ("[INFO] Connection: %s, Uploaded: %s, MD5: %s", cid, f$info$filename, hash);

		# if the hashed file was seen in BOTH HTTP & SMB, alert me
		if (hash in hashes_downloaded_via_HTTP && hash in hashes_uploaded_via_SMB) {
			print fmt ("[ALERT] Possible watering hole attack in progress.");
			print fmt ("   -->  %s, MD5: %s", f$info$filename, hash);

			# and send the following info to 'notice.log'
				$note = Weird::Activity, 
				$msg = "Possible watering hole attack in progress.",
				$conn = cdata

At this point, you have the option of either running Zeek from the console or modifying your local.bro configuration file. During testing, I liked the console method. Also, you can download the same PCAP I’m using from my Github.

bro -r traffic.pcap detect-watering-hole-attack.bro

If everything is where it is supposed to be, you should get output on your console similar to what is shown below.

[INFO] Connection: CWKAkk3Vcz0uMXDfda, Downloaded: /document.pdf, MD5: 4670a6f605d15fa3155462928e025fd6
[INFO] Connection: C0Np3i2v7fojT22c7b, Uploaded: Plan.txt, MD5: c695a16f2d128631fe8e92ff0420b3bb
[INFO] Connection: C0Np3i2v7fojT22c7b, Uploaded: Report.txt, MD5: 89d5739baabbbe65be35cbe61c88e06d
[INFO] Connection: C0Np3i2v7fojT22c7b, Uploaded: Presentation.txt, MD5: ed076287532e86365e841e92bfc50d8c
[INFO] Connection: C0Np3i2v7fojT22c7b, Uploaded: document.pdf:Zone.Identifier, MD5: fbccf14d504b7b2dbcb5a5bda75bd93b
[INFO] Connection: C0Np3i2v7fojT22c7b, Uploaded: document.pdf, MD5: 4670a6f605d15fa3155462928e025fd6
[ALERT] Possible watering hole attack in progress.
   -->  document.pdf, MD5: 4670a6f605d15fa3155462928e025fd6

You should also have a few logs in your current working directory like before.

# example command sentence
cat notice.log | bro-cut -u ts uid msg

# example output
2019-11-11T21:04:49+0000   C0Np3i2v7fojT22c7b   Possible watering hole attack in progress.

Hopefully you got something out of this blog post, it’s been on my mind and on my to-do list for the past nine months.