Rework to the DHCP analyzer.

Highlights:
 - Reduced all DHCP events into a single dhcp_message event. (removed legacy events since they weren't widely used anyway)
 - Support many more DHCP options.
 - DHCP log is completely reworked and now represents DHCP sessions
   based on the transaction ID (and works on clusters).
 - Removed the known-devices-and-hostnames script since it's generally
   less relevant now with the updated log.
This commit is contained in:
Seth Hall 2018-03-01 08:36:32 -08:00
parent c2f35920fd
commit e76b56ce53
28 changed files with 1234 additions and 732 deletions

View file

@ -4,27 +4,186 @@
module DHCP;
export {
## Types of DHCP messages. See :rfc:`1533`.
## Types of DHCP messages. See :rfc:`1533`, :rfc:`3203`,
## :rfc:`4388`, :rfc:`6926`, and :rfc:`7724`.
const message_types = {
[1] = "DHCP_DISCOVER",
[2] = "DHCP_OFFER",
[3] = "DHCP_REQUEST",
[4] = "DHCP_DECLINE",
[5] = "DHCP_ACK",
[6] = "DHCP_NAK",
[7] = "DHCP_RELEASE",
[8] = "DHCP_INFORM",
[9] = "DHCP_FORCERENEW",
[10] = "DHCP_LEASEQUERY",
[11] = "DHCP_LEASEUNASSIGNED",
[12] = "DHCP_DHCPLEASEUNKNOWN",
[13] = "DHCP_LEASEACTIVE",
[14] = "DHCP_BULKLEASEQUERY",
[15] = "DHCP_LEASEQUERYDONE",
[16] = "DHCP_ACTIVELEASEQUERY",
[17] = "DHCP_LEASEQUERYSTATUS",
[18] = "DHCP_TLS",
[1] = "DISCOVER",
[2] = "OFFER",
[3] = "REQUEST",
[4] = "DECLINE",
[5] = "ACK",
[6] = "NAK",
[7] = "RELEASE",
[8] = "INFORM",
[9] = "FORCERENEW", # RFC3203
[10] = "LEASEQUERY", # RFC4388
[11] = "LEASEUNASSIGNED", # RFC4388
[12] = "LEASEUNKNOWN", # RFC4388
[13] = "LEASEACTIVE", # RFC4388
[14] = "BULKLEASEQUERY", # RFC6926
[15] = "LEASEQUERYDONE", # RFC6926
[16] = "ACTIVELEASEQUERY", # RFC7724
[17] = "LEASEQUERYSTATUS", # RFC7724
[18] = "TLS", # RFC7724
} &default = function(n: count): string { return fmt("unknown-message-type-%d", n); };
## Option types mapped to their names.
const option_types: table[int] of string = {
[0] = "Pad",
[1] = "Subnet Mask",
[2] = "Time Offset",
[3] = "Router",
[4] = "Time Server",
[5] = "Name Server",
[6] = "Domain Server",
[7] = "Log Server",
[8] = "Quotes Server",
[9] = "LPR Server",
[10] = "Impress Server",
[11] = "RLP Server",
[12] = "Hostname",
[13] = "Boot File Size",
[14] = "Merit Dump File",
[15] = "Domain Name",
[16] = "Swap Server",
[17] = "Root Path",
[18] = "Extension File",
[19] = "Forward On/Off",
[20] = "SrcRte On/Off",
[21] = "Policy Filter",
[22] = "Max DG Assembly",
[23] = "Default IP TTL",
[24] = "MTU Timeout",
[25] = "MTU Plateau",
[26] = "MTU Interface",
[27] = "MTU Subnet",
[28] = "Broadcast Address",
[29] = "Mask Discovery",
[30] = "Mask Supplier",
[31] = "Router Discovery",
[32] = "Router Request",
[33] = "Static Route",
[34] = "Trailers",
[35] = "ARP Timeout",
[36] = "Ethernet",
[37] = "Default TCP TTL",
[38] = "Keepalive Time",
[39] = "Keepalive Data",
[40] = "NIS Domain",
[41] = "NIS Servers",
[42] = "NTP Servers",
[43] = "Vendor Specific",
[44] = "NETBIOS Name Srv",
[45] = "NETBIOS Dist Srv",
[46] = "NETBIOS Node Type",
[47] = "NETBIOS Scope",
[48] = "X Window Font",
[49] = "X Window Manager",
[50] = "Address Request",
[51] = "Address Time",
[52] = "Overload",
[53] = "DHCP Msg Type",
[54] = "DHCP Server Id",
[55] = "Parameter List",
[56] = "DHCP Message",
[57] = "DHCP Max Msg Size",
[58] = "Renewal Time",
[59] = "Rebinding Time",
[60] = "Class Id",
[61] = "Client Id",
[62] = "NetWare/IP Domain",
[63] = "NetWare/IP Option",
[64] = "NIS-Domain-Name",
[65] = "NIS-Server-Addr",
[66] = "Server-Name",
[67] = "Bootfile-Name",
[68] = "Home-Agent-Addrs",
[69] = "SMTP-Server",
[70] = "POP3-Server",
[71] = "NNTP-Server",
[72] = "WWW-Server",
[73] = "Finger-Server",
[74] = "IRC-Server",
[75] = "StreetTalk-Server",
[76] = "STDA-Server",
[77] = "User-Class",
[78] = "Directory Agent",
[79] = "Service Scope",
[80] = "Rapid Commit",
[81] = "Client FQDN",
[82] = "Relay Agent Information",
[83] = "iSNS",
[85] = "NDS Servers",
[86] = "NDS Tree Name",
[87] = "NDS Context",
[88] = "BCMCS Controller Domain Name list",
[89] = "BCMCS Controller IPv4 address option",
[90] = "Authentication",
[91] = "client-last-transaction-time option",
[92] = "associated-ip option",
[93] = "Client System",
[94] = "Client NDI",
[95] = "LDAP",
[97] = "UUID/GUID",
[98] = "User-Auth",
[99] = "GEOCONF_CIVIC",
[100] = "PCode",
[101] = "TCode",
[112] = "Netinfo Address",
[113] = "Netinfo Tag",
[114] = "URL",
[116] = "Auto-Config",
[117] = "Name Service Search",
[118] = "Subnet Selection Option",
[119] = "Domain Search",
[120] = "SIP Servers DHCP Option",
[121] = "Classless Static Route Option",
[122] = "CCC",
[123] = "GeoConf Option",
[124] = "V-I Vendor Class",
[125] = "V-I Vendor-Specific Information",
[128] = "PXE - undefined (vendor specific)",
[129] = "PXE - undefined (vendor specific)",
[130] = "PXE - undefined (vendor specific)",
[131] = "PXE - undefined (vendor specific)",
[132] = "IEEE 802.1Q VLAN ID",
[133] = "IEEE 802.1D/p Layer 2 Priority",
[134] = "Diffserv Code Point (DSCP) for VoIP signalling and media streams",
[135] = "HTTP Proxy for phone-specific applications",
[136] = "OPTION_PANA_AGENT",
[137] = "OPTION_V4_LOST",
[138] = "OPTION_CAPWAP_AC_V4",
[139] = "OPTION-IPv4_Address-MoS",
[140] = "OPTION-IPv4_FQDN-MoS",
[141] = "SIP UA Configuration Service Domains",
[142] = "OPTION-IPv4_Address-ANDSF",
[144] = "GeoLoc",
[145] = "FORCERENEW_NONCE_CAPABLE",
[146] = "RDNSS Selection",
[150] = "TFTP server address",
[151] = "status-code",
[152] = "base-time",
[153] = "start-time-of-state",
[154] = "query-start-time",
[155] = "query-end-time",
[156] = "dhcp-state",
[157] = "data-source",
[158] = "OPTION_V4_PCP_SERVER",
[159] = "OPTION_V4_PORTPARAMS",
[160] = "DHCP Captive-Portal",
[161] = "OPTION_MUD_URL_V4 (TEMPORARY - registered 2016-11-17)",
[175] = "Etherboot (Tentatively Assigned - 2005-06-23)",
[176] = "IP Telephone (Tentatively Assigned - 2005-06-23)",
[177] = "PacketCable and CableHome (replaced by 122)",
[208] = "PXELINUX Magic",
[209] = "Configuration File",
[210] = "Path Prefix",
[211] = "Reboot Time",
[212] = "OPTION_6RD",
[213] = "OPTION_V4_ACCESS_DOMAIN",
[220] = "Subnet Allocation Option",
[221] = "Virtual Subnet Selection (VSS) Option",
[252] = "auto-proxy-config",
[255] = "End",
} &default = function(n: int): string { return fmt("unknown-option-type-%d", n); };
}

View file

@ -1,5 +1,5 @@
signature dhcp_cookie {
ip-proto == udp
payload /^.*\x63\x82\x53\x63/
payload /^.{236}\x63\x82\x53\x63/
enable "dhcp"
}
}

View file

@ -1,12 +1,11 @@
##! Analyzes DHCP traffic in order to log DHCP leases given to clients.
##! This script ignores large swaths of the protocol, since it is rather
##! noisy on most networks, and focuses on the end-result: assigned leases.
##!
##! If you'd like to track known DHCP devices and to log the hostname
##! supplied by the client, see
##! :doc:`/scripts/policy/protocols/dhcp/known-devices-and-hostnames.bro`.
##! Analyze DHCP traffic and provide a log that is organized around
##! the idea of a DHCP "conversation" defined by messaes exchanged within
##! a relatively short period of time using the same transaction ID.
##! The log will have information from clients and servers to give a more
##! complete picture of what happened.
@load ./utils.bro
@load base/frameworks/cluster
@load ./consts
module DHCP;
@ -17,34 +16,81 @@ export {
type Info: record {
## The earliest time at which a DHCP message over the
## associated connection is observed.
ts: time &log;
## A unique identifier of the connection over which DHCP is
## occurring.
uid: string &log;
## The connection's 4-tuple of endpoint addresses/ports.
id: conn_id &log;
ts: time &log;
## A series of unique identifiers of the connections over which
## DHCP is occurring. This behavior with multiple connections is
## unique to DHCP because of the way it uses broadcast packets
## on local networks.
uids: set[string] &log;
## IP address of the client. If a transaction
## is only a client sending INFORM messages then
## there is no lease information exchanged so this
## is helpful to know who sent the messages.
## Getting an address in this field does require
## that the client sources at least one DHCP message
## using a non-broadcast address.
client_addr: addr &log &optional;
## IP address of the server involved in actually
## handing out the lease. There could be other
## servers replying with OFFER messages which won't
## be represented here. Getting an address in this
## field also requires that the server handing out
## the lease also sources packets from a non-broadcast
## IP address.
server_addr: addr &log &optional;
## Client's hardware address.
mac: string &log &optional;
## Client's actual assigned IP address.
assigned_ip: addr &log &optional;
mac: string &log &optional;
## Name given by client in Hostname option 12.
host_name: string &log &optional;
## FQDN given by client in Client FQDN option 81.
client_fqdn: string &log &optional;
## Domain given by the server in option 15.
domain: string &log &optional;
## IP address requested by the client.
requested_addr: addr &log &optional;
## IP address assigned by the server.
assigned_addr: addr &log &optional;
## IP address lease interval.
lease_time: interval &log &optional;
## A random number chosen by the client for this transaction.
trans_id: count &log;
## the message type
msg_type: string &log &optional;
## client ID
client_id: string &log &optional;
## the server ID
server_id: addr &log &optional;
## the host name
host_name: string &log &optional;
## the subscriber id (if present)
subscriber_id: string &log &optional;
## the agent remote id (if present)
agent_remote_id: string &log &optional;
lease_time: interval &log &optional;
## Message typically accompanied with a DHCP_DECLINE
## so the client can tell the server why it rejected
## an address.
client_message: string &log &optional;
## Message typically accompanied with a DHCP_NAK to let
## the client know why it rejected the request.
server_message: string &log &optional;
## The DHCP message types seen by this DHCP transaction
msg_types: vector of string &log &default=string_vec();
## Duration of the DHCP "session" representing the
## time from the first message to the last.
duration: interval &log &default=0secs;
};
## The maximum amount of time that a transation ID will be watched
## for to try and tie messages together into a single DHCP
## transaction narrative.
const DHCP::max_txid_watch_time = 30secs &redef;
## This event is used internally to distribute data around clusters
## since DHCP doesn't follow the normal "connection" model used by
## most protocols. It can also be handled to extend the DHCP log.
## bro:see::`DHCP::log_info`.
global DHCP::aggregate_msgs: event(ts: time, id: conn_id, uid: string, is_orig: bool, msg: DHCP::Msg, options: DHCP::Options);
## This is a global variable that is only to be used in the
## :bro::see::`DHCP::aggregate_msgs` event. It can be used to avoid
## looking up the info record for a transaction ID in every event handler
## for :bro:see::`DHCP::aggregate_msgs`.
global DHCP::log_info: Info;
## Event that can be handled to access the DHCP
## record as it is sent on to the logging framework.
global log_dhcp: event(rec: Info);
@ -55,8 +101,13 @@ redef record connection += {
dhcp: Info &optional;
};
redef record Info += {
last_message_ts: time &optional;
};
# 67/udp is the server's port, 68/udp the client.
const ports = { 67/udp, 68/udp };
# 4011/udp seems to be some proxyDHCP thing.
const ports = { 67/udp, 68/udp, 4011/udp };
redef likely_server_ports += { 67/udp };
event bro_init() &priority=5
@ -65,54 +116,144 @@ event bro_init() &priority=5
Analyzer::register_for_ports(Analyzer::ANALYZER_DHCP, ports);
}
event dhcp_message(c: connection, is_orig: bool, msg: DHCP::Msg, options: DHCP::Options) &priority=-5
# Setup the clusterized config that is needed to tie messages together on a cluster.
redef Cluster::worker2manager_events += /DHCP::aggregate_msgs/;
function join_data_expiration(t: table[count] of Info, idx: count): interval
{
if ( msg$m_type == 5 ) # DHCP_ACK
local info = t[idx];
local now = network_time();
# If a message hasn't been seen in the past 5 seconds or the
# total time watching has been more than the maximum time
# allowed by the configuration then log this data and expire it.
# Also, if Bro is shutting down.
if ( (now - info$last_message_ts) > 5sec ||
(now - info$ts) > max_txid_watch_time ||
bro_is_terminating() )
{
local info = Info($ts = network_time(),
$id = c$id,
$uid = c$uid,
$trans_id = msg$xid);
Log::write(LOG, info);
if ( msg$h_addr != "" )
info$mac = msg$h_addr;
# Go ahead and expire the data now that the log
# entry has been written.
return 0secs;
}
else
{
return 5secs;
}
}
if ( reverse_ip(msg$yiaddr) != 0.0.0.0 )
info$assigned_ip = reverse_ip(msg$yiaddr);
# This is where the data is stored as it's centralized. All data for a log must
# arrive within the expiration interval if it's to be logged fully. On a cluster,
# this data is only maintained on the manager.
global join_data: table[count] of Info = table()
&create_expire=10secs &expire_func=join_data_expiration;
@if ( ! Cluster::is_enabled() || Cluster::local_node_type() == Cluster::MANAGER )
# We are handling this event at priority 1000 because we really want
# the DHCP::log_info global to be set correctly before a user might try
# to access it.
event DHCP::aggregate_msgs(ts: time, id: conn_id, uid: string, is_orig: bool, msg: DHCP::Msg, options: DHCP::Options) &priority=1000
{
if ( msg$xid !in join_data )
{
join_data[msg$xid] = Info($ts=ts,
$uids=set(uid));
}
log_info = join_data[msg$xid];
}
event DHCP::aggregate_msgs(ts: time, id: conn_id, uid: string, is_orig: bool, msg: DHCP::Msg, options: DHCP::Options) &priority=5
{
log_info$duration = ts - log_info$ts;
if ( uid !in log_info$uids )
add log_info$uids[uid];
log_info$msg_types[|log_info$msg_types|] = DHCP::message_types[msg$m_type];
# Let's watch for messages in any DHCP message type
# and split them out based on client and server.
if ( options?$message )
{
if ( is_orig )
log_info$client_message = options$message;
else
info$assigned_ip = c$id$orig_h;
log_info$server_message = options$message;
}
if ( options?$lease )
info$lease_time = options$lease;
# Update the last message time so that we can do some data
# expiration handling.
log_info$last_message_ts = ts;
if ( options?$sub_opt )
if ( is_orig ) # client requests
{
# Assign the client addr in case this is a session
# of only INFORM messages (no lease handed out).
# This also works if a normal lease handout uses
# unicast.
if ( id$orig_h != 0.0.0.0 && id$orig_h != 255.255.255.255 )
log_info$client_addr = id$orig_h;
if ( options?$host_name )
log_info$host_name = options$host_name;
if ( options?$client_fqdn )
log_info$client_fqdn = options$client_fqdn$domain_name;
if ( options?$client_id &&
options$client_id$hwtype == 1 ) # ETHERNET
log_info$mac = options$client_id$hwaddr;
if ( options?$addr_request )
log_info$requested_addr = options$addr_request;
}
else # server reply messages
{
# Only log the address of the server if it handed out
# an IP address.
if ( msg$yiaddr != 0.0.0.0 &&
id$resp_h != 255.255.255.255 )
{
for ( param in options$sub_opt )
{
local sub_opt = options$sub_opt[param];
#if ( sub_opt$code == 1 )
# {
# print fmt("Relay Agent Information:");
# print fmt( "sub option: code=%d circuit id=%s",sub_opt$code,sub_opt$value );
# }
if ( sub_opt$code == 2 )
info$agent_remote_id = bytestring_to_hexstr(sub_opt$value);
if ( sub_opt$code == 6 )
info$subscriber_id = (sub_opt$value);
}
log_info$server_addr = id$resp_h;
}
c$dhcp = info;
# Only use the client hardware address from the server
# if we didn't already pick one up from the client.
if ( msg$chaddr != "" && !log_info?$mac )
log_info$mac = msg$chaddr;
if ( msg$yiaddr != 0.0.0.0 )
log_info$assigned_addr = msg$yiaddr;
# If no client address has been seen yet, let's use the assigned addr.
if ( ! log_info?$client_addr && log_info?$assigned_addr )
log_info$client_addr = log_info$assigned_addr;
if ( options?$domain_name )
log_info$domain = options$domain_name;
if ( options?$lease )
log_info$lease_time = options$lease;
}
}
@endif
# Aggregate DHCP messages to the manager.
event dhcp_message(c: connection, is_orig: bool, msg: DHCP::Msg, options: DHCP::Options) &priority=-5
{
if ( msg$m_type == 5 ) # DHCP_ACK
{
Log::write(DHCP::LOG, c$dhcp);
}
event DHCP::aggregate_msgs(network_time(), c$id, c$uid, is_orig, msg, options);
}
event bro_done() &priority=-5
{
# Log any remaining data that hasn't already been logged!
for ( i in DHCP::join_data )
join_data_expiration(DHCP::join_data, i);
}

View file

@ -1,19 +0,0 @@
##! Utilities specific for DHCP processing.
module DHCP;
export {
## Reverse the octets of an IPv4 address.
##
## ip: An IPv4 address.
##
## Returns: A reversed IPv4 address.
global reverse_ip: function(ip: addr): addr;
}
function reverse_ip(ip: addr): addr
{
local octets = split_string(cat(ip), /\./);
return to_addr(cat(octets[3], ".", octets[2], ".", octets[1], ".", octets[0]));
}