# $Id: dns.bro,v 1.16 2003/08/31 00:40:21 vern Exp $

@load alert
@load udp

redef enum Alert += {
	SensitiveDNS_Lookup,	# DNS lookup of a sensitive hostname/address
	DNS_PTR_Scan,		# summary of a set of PTR lookups
};

redef capture_filters += { ["dns"] = "port 53" };

type dns_session_info: record {
	id: count;
	is_zone_transfer: bool;
	last_active: time;	# when we last saw activity

	# Indexed by query id, returns string annotation corresponding to
	# queries for which no answer seen yet.
	pending_queries: table[count] of string;
};

global dns_log = open_log_file("dns") &redef;

# Indexed by client and server.
global dns_sessions: table[addr, addr] of dns_session_info;
global num_dns_sessions = 0;

const PTR_pattern = /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\.in-addr\.arpa/;
const PTR = 12;
const EDNS0 = 41;
const ANY = 255;

# Annotations that if returned for a PTR lookup actually indicate
# a rejected query; for example, "illegal-address.lbl.gov".
const actually_rejected_PTR_anno: set[string] &redef;

const query_types = {
	[1] = "A", [2] = "NS", [5] = "CNAME", [6] = "SOA",
	[11] = "WKS", [PTR] = "PTR", [13] = "HINFO", [15] = "MX",
	[16] = "TXT", [33] = "SRV", [EDNS0] = "EDNS0",
	[252] = "AXFR",
	[ANY] = "*",
} &default = function(n: count): string { return fmt("query-%d", n); };

const DNS_code_types = {
	[0] = "X0",
	[1] = "Xfmt",
	[2] = "Xsrv",
	[3] = "Xnam",
	[4] = "Ximp",
	[5] = "X[",
} &default = function(n: count): string { return "?"; };

const sensitive_lookup_hosts: set[addr] &redef;
const okay_to_lookup_sensitive_hosts: set[addr] &redef;

global distinct_PTR_requests: table[addr, string] of count &default = 0;
global distinct_rejected_PTR_requests: table[addr] of count &default = 0;
global distinct_answered_PTR_requests: table[addr] of count &default = 0;

# Start considering whether we're seeing PTR scanning if we've seen
# at least this many rejected PTR queries.
global report_rejected_PTR_thresh = 100 &redef;

# Generate a PTR_scan event if at any point (once we're above
# report_rejected_PTR_thresh) we see this many more distinct
# rejected PTR requests than distinct answered PTR requests.
global report_rejected_PTR_factor = 2.0 &redef;

global allow_PTR_scans: set[addr] &redef;

# Keeps track of for which addresses we processed a PTR_scan event.
global did_PTR_scan_event: table[addr] of count &default = 0;

event expire_DNS_session(orig: addr, resp: addr)
	{
	if ( [orig, resp] in dns_sessions )
		{
		local session = dns_sessions[orig, resp];
		local last_active = session$last_active;
		if ( network_time() > last_active + dns_session_timeout ||
		     done_with_network )
			{
			# Flush out any pending requests.
			for ( query in session$pending_queries )
				print dns_log, fmt("#%d %s", session$id,
						session$pending_queries[query]);

			print dns_log, fmt("#%d finish", session$id);

			delete dns_sessions[orig, resp];
			}

		else
			schedule dns_session_timeout {
				expire_DNS_session(orig, resp)
			};
		}
	}

function lookup_DNS_session(c: connection): dns_session_info
	{
	local id = c$id;
	local orig = id$orig_h;
	local resp = id$resp_h;

	if ( [orig, resp] !in dns_sessions )
		{
		local session: dns_session_info;
		session$id = ++num_dns_sessions;
		session$last_active = network_time();
		session$is_zone_transfer = F;

		# A hack to initialize the table in the record; this
		# should instead happen automatically! :-(
		local query_table: table[count] of string;
		session$pending_queries = query_table;

		print dns_log, fmt("%.6f #%d %s start",
			c$start_time, session$id, id_string(id));

		dns_sessions[orig, resp] = session;

		schedule 15 sec { expire_DNS_session(orig, resp) };

		append_addl(c, fmt("#%d", session$id));

		return session;
		}

	else
		return dns_sessions[orig, resp];
	}

event sensitive_addr_lookup(c: connection, a: addr, is_query: bool)
	{
	local orig = c$id$orig_h;
	local resp = c$id$resp_h;

	if ( orig in okay_to_lookup_sensitive_hosts )
		return;

	local session_id: string;
	if ( [orig, resp] in dns_sessions )
		session_id = fmt("#%d", dns_sessions[orig, resp]$id);
	else
		session_id = "#?";

	local id = fmt("%s > %s (%s)", orig, resp, session_id);

	if ( is_query )
		ALERT([$alert=SensitiveDNS_Lookup, $conn=c,
			$msg=fmt("%s PTR lookup of %s", id, a),
			$sub="PTR lookup"]);
	else
		ALERT([$alert=SensitiveDNS_Lookup, $conn=c,
			$msg=fmt("%s name lookup of %s", id, a),
			$sub="name lookup"]);
	}

function DNS_query_annotation(c: connection, msg: dns_msg, query: string,
				qtype: count, is_zone_xfer: bool): string
	{
	local anno: string;

	if ( (qtype == PTR || qtype == ANY) && query == PTR_pattern )
		{
		# convert PTR text to more readable form.
		local a = ptr_name_to_addr(query);
		if ( a in sensitive_lookup_hosts && ! is_zone_xfer )
			event sensitive_addr_lookup(c, a, T);

		anno = fmt("?%s %As", query_types[qtype], a);
		}
	else
		anno = fmt("?%s %s", query_types[qtype], query);

	if ( ! is_zone_xfer &&
	     (msg$num_answers > 0 || msg$num_auth > 0 || msg$num_addl > 0) )
		anno = fmt("%s <query addl = %d/%d/%d>", anno,
			msg$num_answers, msg$num_auth, msg$num_addl);

	return anno;
	}


event dns_zone_transfer_request(c: connection, session: dns_session_info,
				msg: dns_msg, query: string)
	{
	session$is_zone_transfer = T;

	if ( ! is_tcp_port(c$id$orig_p) )
		event conn_weird("UDP_zone_transfer", c);
	}

event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count)
	{
	local id = c$id;
	local orig = id$orig_h;
	local resp = id$resp_h;
	local session = lookup_DNS_session(c);
	local anno = DNS_query_annotation(c, msg, query, qtype, F);

	session$pending_queries[msg$id] = anno;

	if ( query_types[qtype] == "AXFR" )
		event dns_zone_transfer_request(c, session, msg, query);

	session$last_active = network_time();
	}

event dns_rejected(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count)
	{
	local session = lookup_DNS_session(c);
	local code = DNS_code_types[msg$rcode];
	local id = msg$id;

	if ( id in session$pending_queries )
		{
		print dns_log, fmt("#%d %s %s",
				session$id, session$pending_queries[id], code);
		delete session$pending_queries[id];
		}

	else if ( c$start_time == network_time() )
		print dns_log, fmt("#%d [?%s] %s", session$id, query, code);

	else
		print dns_log, fmt("#%d %s", session$id, code);
	}

event PTR_scan_summary(src: addr)
	{
	ALERT([$alert=DNS_PTR_Scan, $src=src,
		$msg=fmt("%s totaled %d/%d un/successful PTR lookups", src,
			distinct_rejected_PTR_requests[src],
			distinct_answered_PTR_requests[src]),
		$sub="final summary"]);
	}

event PTR_scan(src: addr)
	{
	++did_PTR_scan_event[src];

	if ( src !in allow_PTR_scans && src !in okay_to_lookup_sensitive_hosts )
		{
		ALERT([$alert=DNS_PTR_Scan, $src=src,
			$msg=fmt("%s has made %d/%d un/successful PTR lookups",
				src, distinct_rejected_PTR_requests[src],
				distinct_answered_PTR_requests[src]),
			$sub="scan detected"]);

		schedule 1 day { PTR_scan_summary(src) };
		}
	}

function check_PTR_scan(src: addr)
	{
	if ( src !in did_PTR_scan_event &&
	     distinct_rejected_PTR_requests[src] >=
	     distinct_answered_PTR_requests[src] * report_rejected_PTR_factor )
		event PTR_scan(src);
	}

function DNS_answer(c: connection, msg: dns_msg, ans: dns_answer, annotation: string)
	{
	local is_answer = ans$answer_type == DNS_ANS;
	local session = lookup_DNS_session(c);
	local report = fmt("#%d %s", session$id, c$id$orig_h);
	local id = msg$id;
	local query: string;

	if ( id in session$pending_queries )
		{
		query = session$pending_queries[id];
		delete session$pending_queries[id];
		report = fmt("%s %s", report, query);
		}

	else if ( session$is_zone_transfer )
		{
		# Need to provide the query directly.
		query = DNS_query_annotation(c, msg, ans$query, ans$qtype, T);
		report = fmt("%s ?%s", report, query);
		}

	else
		{
		# No corresponding query.  This can happen if it's
		# already been deleted because we've already processed
		# an answer to it; or if the session itself was timed
		# out prior to this answer being generated.  In the
		# first case, we don't want to provide the query again;
		# in the second, we do.  We can determine that we're
		# likely in the second case if either (1) this session
		# was just now created, or (2) we're now processing the
		# sole answer to the original query.
		if ( c$start_time == network_time() ||
		     (is_answer && msg$num_answers == 1) )
			{
			query = DNS_query_annotation(c, msg, ans$query, ans$qtype, F);
			report = fmt("%s [?%s]", report, query);
			}
		else
			query = "";
		}

	if ( is_answer )
		report = fmt("%s %s", report, annotation);

	local src = c$id$orig_h;

	if ( msg$rcode != 0 )
		{
		if ( /\?(PTR|\*.*in-addr).*/ in query )
			##### check for private address
			{
			if ( ++distinct_PTR_requests[src, query] == 1 &&
			     ++distinct_rejected_PTR_requests[src] >=
			       report_rejected_PTR_thresh )
				check_PTR_scan(src);
			}

		report = fmt("%s %s", report, DNS_code_types[msg$rcode]);
		}

	else if ( is_answer )
		{
		if ( /\?(PTR|\*.*in-addr).*/ in query )
			{
			if ( annotation in actually_rejected_PTR_anno )
				{
				if ( ++distinct_PTR_requests[src, query] == 1 &&
				     ++distinct_rejected_PTR_requests[src] >=
				       report_rejected_PTR_thresh )
					check_PTR_scan(src);
				}
			else
				{
				if ( ++distinct_PTR_requests[src, query] == 1 )
					++distinct_answered_PTR_requests[src];
				}
			}
		}

	else if ( ans$answer_type == DNS_AUTH )
		report = fmt("%s <auth %s>", report, query_types[ans$qtype]);

	else if ( ans$answer_type == DNS_ADDL )
		{
		local flag_addl =
			(! msg$QR && ans$qtype != EDNS0) ||
			ans$answer_type != DNS_ADDL;

		report = fmt("%s <addl%s %s>", report, flag_addl ? "*" : "",
				query_types[ans$qtype]);
		}

	if ( ! is_answer )
		# This needs to come after the <auth>/<addl> annotation.
		report = fmt("%s %s", report, annotation);

	print dns_log, fmt("%s <TTL = %f>", report, ans$TTL);

	session$last_active = network_time();
	}

event dns_A_reply(c: connection, msg: dns_msg, ans: dns_answer, a: addr)
	{
	if ( a in sensitive_lookup_hosts )
		event sensitive_addr_lookup(c, a, F);

	DNS_answer(c, msg, ans, fmt("= %As", a));
	}

event dns_NS_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string)
	{
	DNS_answer(c, msg, ans, fmt("= %s", name));
	}

event dns_CNAME_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string)
	{
	DNS_answer(c, msg, ans, fmt("= %s", name));
	}

event dns_PTR_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string)
	{
	DNS_answer(c, msg, ans, fmt("= %s", name));
	}

event dns_SOA_reply(c: connection, msg: dns_msg, ans: dns_answer, soa: dns_soa)
	{
	DNS_answer(c, msg, ans, fmt("= %s", soa$mname));
	}

event dns_MX_reply(c: connection, msg: dns_msg, ans: dns_answer, name: string,
			preference: count)
	{
	DNS_answer(c, msg, ans, fmt("= %s/%d", name, preference));
	}

event dns_EDNS0(c: connection, msg: dns_msg, ans: dns_answer)
	{
	DNS_answer(c, msg, ans, "<?>");
	}
