Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
194 lines (172 sloc) 7.71 KB
##! This script will generate a notice if a host exceeds threshold
##! configurable velocities, length, or counts of DNS queries.
##!
##! Future massive improvement opportunities:
##! "Practical Comprehensive Bounds on Surreptitious Communication Over DNS"
##! http://www.icir.org/vern/papers/covert-dns-usec13.pdf
@load base/frameworks/sumstats
@load base/protocols/dns
@load evernote/human
module Exfiltration;
export {
# Notice DNS strangeness
redef enum Notice::Type += {
DNS_Excessive_Query_Velocity,
DNS_Excessive_Query_Length,
DNS_too_many_TXT_Answers,
DNS_too_many_NULL_Answers,
};
# configurable options to be added to local.bro
const frequent_queriers: set[subnet] &redef;
const query_interval = 2min &redef;
const queries_per_query_interval = 800.0 &redef;
const query_length_sum_per_interval = 500.0 &redef;
const txt_answer_types_per_interval = 20.0 &redef;
const null_answer_types_per_interval = 20.0 &redef;
}
event DNS::connection_state_remove(c: connection) &priority=5
{
# Ignore frequent queriers
if (c$id$orig_h in frequent_queriers)
return;
# Ignore non-standard ports
if (c$id$resp_p == 53/udp || c$id$resp_p == 53/tcp)
return;
# Ignore PTR lookups
if (c$dns?$qtype_name && c$dns$qtype_name == "PTR")
return;
# Ignore failed lookups
if (c$dns?$rcode_name && c$dns$rcode_name != "NOERROR")
return;
# Ignore local bare-word lookups like WPAD that are typically local
if ("." !in c$dns$query)
return;
# Ignore Service Discovery
if ("_dns-sd_" in c$dns$query)
return;
# Ignore responses without answers
if (!c$dns?$answers)
return;
# Ignore PTR lookups
if (c$dns?$qtype_name && c$dns$qtype_name == "PTR")
return;
# Ignore our own zones
if (whitelisted_zones_regex in c$dns$query)
{
return;
}
# Sumstat all queries per origin
SumStats::observe("Queries",
SumStats::Key($host=c$id$orig_h),
SumStats::Observation($str=c$dns$query));
# Sumstat all query lengths per origin
SumStats::observe("Query Length",
SumStats::Key($host=c$id$orig_h),
SumStats::Observation($num=|c$dns$query|));
# Sumstat TXT query answers by origin
if (c$dns$qtype_name == "TXT")
{
SumStats::observe("TXT Answers",
SumStats::Key($host=c$id$orig_h),
SumStats::Observation($str=join_string_vec(c$dns$answers, "\t\t")));
}
# Sumstat NULL query answers by origin
if (c$dns$qtype_name == "NULL")
{
SumStats::observe("NULL Answers",
SumStats::Key($host=c$id$orig_h),
SumStats::Observation($str=join_string_vec(c$dns$answers, "\t\t")));
}
}
event bro_init()
{
# For all queries, only keep unique ones
local queries_reducer = SumStats::Reducer($stream="Queries", $apply=set(SumStats::UNIQUE));
local txt_reducer = SumStats::Reducer($stream="TXT Answers", $apply=set(SumStats::UNIQUE));
local null_reducer = SumStats::Reducer($stream="NULL Answers", $apply=set(SumStats::UNIQUE));
# Count the amount of data that could be in a stream of queries and answers by length
local query_length_reducer = SumStats::Reducer($stream="Query Length", $apply=set(SumStats::SUM));
# Notice too many unique queries
SumStats::create([$name = "queries",
$epoch = query_interval,
$reducers = set(queries_reducer),
$threshold = queries_per_query_interval,
$threshold_val(key: SumStats::Key, result: SumStats::Result) =
{
return result["Queries"]$sum;
},
$threshold_crossed(key: SumStats::Key, result: SumStats::Result) =
{
local dur = Human::interval_to_human_string(query_interval);
Reporter::info(fmt("%s sent %d DNS queries in %s", key$host, result["Queries"]$sum, dur));
NOTICE([$note=DNS_Excessive_Query_Velocity,
$src=key$host,
$msg=fmt("%s sent %d DNS queries in %s", key$host, result["Queries"]$sum, dur),
$suppress_for=1mins,
$identifier=cat(key$host)]);
}
]);
# Notice too much data stuffed into queries
SumStats::create([$name = "query_length",
$epoch = query_interval,
$reducers = set(query_length_reducer),
$threshold = query_length_sum_per_interval,
$threshold_val(key: SumStats::Key, result: SumStats::Result) =
{
return result["Query Length"]$sum;
},
$threshold_crossed(key: SumStats::Key, result: SumStats::Result) =
{
local dur = Human::interval_to_human_string(query_interval);
Reporter::info(fmt("%s sent %f characters of DNS queries in %s", key$host, result["Query Length"]$sum, dur));
NOTICE([$note=DNS_Excessive_Query_Length,
$src=key$host,
$msg=fmt("%s sent %f characters of DNS queries in %s", key$host, result["Query Length"]$sum, dur),
$suppress_for=1mins,
$identifier=cat(key$host)]);
}
]);
# Notice too many TXT queries
SumStats::create([$name = "txt",
$epoch = query_interval,
$reducers = set(txt_reducer),
$threshold = txt_answer_types_per_interval,
$threshold_val(key: SumStats::Key, result: SumStats::Result) =
{
return result["TXT Answers"]$unique+0.0;
},
$threshold_crossed(key: SumStats::Key, result: SumStats::Result) =
{
local dur = Human::interval_to_human_string(query_interval);
Reporter::info(fmt("%s received %d unique TXT answers to DNS queries against zones we don't whitelist in %s",
key$host,result["TXT Answers"]$unique, dur));
NOTICE([$note=DNS_too_many_TXT_Answers,
$src=key$host,
$msg=fmt("%s received %d unique TXT answers to DNS queries against zones we don't whitelist in %s",
key$host, result["TXT Answers"]$unique, dur),
$suppress_for=1mins,
$identifier=cat(key$host)]);
}
]);
# Notice too many NULL queries
SumStats::create([$name = "null",
$epoch = query_interval,
$reducers = set(null_reducer),
$threshold = null_answer_types_per_interval,
$threshold_val(key: SumStats::Key, result: SumStats::Result) =
{
return result["NULL Answers"]$unique+0.0;
},
$threshold_crossed(key: SumStats::Key, result: SumStats::Result) =
{
local dur = Human::interval_to_human_string(query_interval);
Reporter::info(fmt("%s received %d unique NULL answers to DNS queries against zones we don't whitelist in %s",
key$host, result["NULL Answers"]$unique, dur));
NOTICE([$note=DNS_too_many_NULL_Answers,
$src=key$host,
$msg=fmt("%s received %d unique NULL answers to DNS queries against zones we don't whitelist in %s",
key$host, result["NULL Answers"]$unique, dur),
$suppress_for=1mins,
$identifier=cat(key$host)]);
}
]);
You can’t perform that action at this time.