My First 2 Zeek Scripts


I wrote my first two Zeek scripts to solve a simple problem of logging every UDP packet.

Published on April 09, 2021 by Tony E.

zeek scripts pcap

4 min READ

tl;dr This blog post was for documentation purposes. Nothing to see here.

Problem Statement:

I need to have Zeek log every UDP packet instead of only per UDP session/conversation

Because of Zeek’s session tracking it will only log one connection “uid” at the start of a session and for all subsequent packets which are part of the same session or conversation.

For example, using the popular smallFlows.pcap you can see there are 501 UDP packets:

# tcpdump

tcpdump -nr smallFlows.pcap udp | wc -l
501

# Wireshark Display Filter:
udp && !icmp
^^^ This count is 501 packets

# NOTE: You have to exclude 'icmp' because the Type-11's and Type-3's re-encapsulate the original UDP packet.
# You can identify these packets using the filter 'udp && icmp'(<-- this count is 22 packets).
# Using just 'udp' filter will result in also counting the icmp ecapsulated udp packets (<-- this count is 523)

Using Zeek on the same PCAP and filtering the logs showing only protocol UDP yields a much lower count:

zeek -C -r smallFlows.pcap
grep udp conn.log | wc -l
173

So, where did all the other packets go? The packets are there but Zeek only creates a new log entry for every unique session. Packets that are part of the same flow or conversation are counted together. In fact if you use the below command and add up all the numbers in the 6th and 7th columns you’ll get 501:

cat conn.log | /opt/zeek/bin/zeek-cut id.orig_h id.orig_p id.resp_h id.resp_p proto orig_pkts resp_pkts uid | grep udp

NOTE: The 6th and 7th columns are the number of packets seen with the Originator as the source or the Responder as the source (on a per-packet analysis)

How do we get Zeek to log each UDP packet instead of each session or conversation?

I’m glad you asked, I wrote a script for that!

This first script simply logs to STDOUT for every UDP request or UDP reply and logs them as individual unique connections.

Script #1: udp_script.zeek

event udp_request(u: connection){
# Choose one format to un-comment
#	print fmt("UDP Request: %s", u$id);
#	print fmt("%s UDP Request: %s %s --> %s %s", u$uid, u$id$orig_h, u$id$orig_p, u$id$resp_h, u$id$resp_p);
	print fmt("UDP Request: %s %s --> %s %s", u$id$orig_h, u$id$orig_p, u$id$resp_h, u$id$resp_p);
}

event udp_reply(u: connection){
# Choose the matching format from above to un-comment
#	print fmt("UDP Reply  : %s", u$id);
#	print fmt("%s UDP Reply  : %s %s <-- %s %s", u$uid, u$id$orig_h, u$id$orig_p, u$id$resp_h, u$id$resp_p);
	print fmt("UDP Reply  : %s %s <-- %s %s", u$id$orig_h, u$id$orig_p, u$id$resp_h, u$id$resp_p);
}

Lets run this script against our PCAP file:

# NOTE: I'm in the same directory as my pcap and my script file. The Zeek binary is in its default location.

sudo /opt/zeek/bin/zeek -C -r smallFlows.pcap udp_script.zeek

UDP Request: 192.168.3.131 57757/udp --> 239.255.255.250 1900/udp
UDP Request: 192.168.3.131 57757/udp --> 239.255.255.250 1900/udp
UDP Request: 192.168.3.131 57757/udp --> 239.255.255.250 1900/udp
UDP Request: 192.168.3.131 57757/udp --> 239.255.255.250 1900/udp
UDP Request: 192.168.3.131 57757/udp --> 239.255.255.250 1900/udp
UDP Request: 192.168.3.131 57757/udp --> 239.255.255.250 1900/udp
UDP Request: 192.168.3.131 68/udp --> 255.255.255.255 67/udp
UDP Request: 192.168.3.131 54600/udp --> 224.0.0.252 5355/udp
UDP Request: 192.168.3.131 54600/udp --> 224.0.0.252 5355/udp
UDP Request: 172.16.255.1 50983/udp --> 71.224.25.112 33695/udp
[ ... OMITTED FOR BREVITY ... ]

Sweet, now lets make sure we have the same number of log lines as we do UDP packets in the PCAP:

sudo /opt/zeek/bin/zeek -C -r smallFlows.pcap udp_script.zeek | wc -l
501

Awsome! While this is great and proof that we are generating logs the way we intended. It isn’t actaully logging anywhere and the results aren’t being picked up by other tools (Filebeat–>Logstash–>Elastic<–Kibana).

Leverage the Zeek Logging Framework

This script will generate a Zeek log file “udp_packets.log” with the columns: Timestamp, UID, Source IP, SRC Port, Destination IP, Dest Port

Script #2: udp_log.zeek

module Udplog;

export {
	redef enum Log::ID += { LOG };

	type Info: record {
		ts: time	&log;
		uid: string	&log;
		id: conn_id	&log;
	};
}

event zeek_init(){
	Log::create_stream(Udplog::LOG, [$columns=Info, $path="udp_packets"]);
}

event udp_request(u: connection){
	local rec: Udplog::Info = [$ts=network_time(), $uid=u$uid, $id=u$id];
	Log::write(Udplog::LOG, rec);
}

event udp_reply(u: connection){
	local rec: Udplog::Info = [$ts=network_time(), $uid=u$uid, $id=u$id];
	Log::write(Udplog::LOG, rec);
}

Running this against the same PCAP, Zeek will create a new log file: udp_packets.log. Zeeks standard log file format includes 8 lines of metadata pre-pended and 1 line appended.

wc -l udp_packets.log
510

# NOTE: Subtracting the 9 lines of metadata leave 501 lines of logs.

Using zeek-cut can clean-up the log for you:

cat udp_packets.log | /opt/zeek/bin/zeek-cut | wc -l
501

^^^ BOOM !

You do not need to run both of these scripts together at the same time. The first script was a proof of concept and prints to STDOUT while the second is a more permanent script to be used during live capture and when ingesting your logs into other tools. You can also send output as JSON if needed.

Thanks for reading.