diff --git a/scripts/base/init-default.zeek b/scripts/base/init-default.zeek index 90ccaf3445..81daf697e2 100644 --- a/scripts/base/init-default.zeek +++ b/scripts/base/init-default.zeek @@ -53,6 +53,7 @@ @load base/protocols/conn @load base/protocols/dce-rpc @load base/protocols/dhcp +@load base/protocols/dhcpv6 @load base/protocols/dnp3 @load base/protocols/dns @load base/protocols/finger diff --git a/scripts/base/protocols/dhcpv6/__load__.zeek b/scripts/base/protocols/dhcpv6/__load__.zeek new file mode 100644 index 0000000000..c04423a855 --- /dev/null +++ b/scripts/base/protocols/dhcpv6/__load__.zeek @@ -0,0 +1,4 @@ +@load ./consts +@load ./main + +@load-sigs ./dpd.sig diff --git a/scripts/base/protocols/dhcpv6/consts.zeek b/scripts/base/protocols/dhcpv6/consts.zeek new file mode 100644 index 0000000000..cc639271fd --- /dev/null +++ b/scripts/base/protocols/dhcpv6/consts.zeek @@ -0,0 +1,12 @@ +module DHCPv6; + +export { + const message_types = { + [1] = "SOLICIT", + } &default = function(n: count): string { return fmt("unknown-message-type-%d", n); }; + + ## Option types mapped to their names. + const option_types = { + [1] = "???", + } &default = function(n: count): string { return fmt("unknown-option-type-%d", n); }; +} diff --git a/scripts/base/protocols/dhcpv6/main.zeek b/scripts/base/protocols/dhcpv6/main.zeek new file mode 100644 index 0000000000..83d3281817 --- /dev/null +++ b/scripts/base/protocols/dhcpv6/main.zeek @@ -0,0 +1,54 @@ +@load base/frameworks/cluster +@load ./consts + +module DHCPv6; + +export { + redef enum Log::ID += { LOG }; + + global log_policy: Log::PolicyHook; + + ## The record type which contains the column fields of the DHCP log. + type Info: record { + ## The earliest time at which a DHCP message over the + ## associated connection is observed. + ts: time &log; + }; + + ## Event that can be handled to access the DHCP + ## record as it is sent on to the logging framework. + global log_dhcpv6: event(rec: Info); +} + +# Add the dhcp info to the connection record. +redef record connection += { + dhcpv6: Info &optional; +}; + +const ports = { 546/udp, 547/udp }; +redef likely_server_ports += { 547/udp }; + +event zeek_init() &priority=5 + { + Log::create_stream(DHCP::LOG, [$columns=Info, $ev=log_dhcpv6, $path="dhcpv6", $policy=log_policy]); + Analyzer::register_for_ports(Analyzer::ANALYZER_DHCPV6, ports); + } + + +# Aggregate DHCP messages to the manager. +event dhcpv6_message(c: connection, is_orig: bool) + { + print c$uid, c$id, is_orig; +# if ( Cluster::is_enabled() && Cluster::local_node_type() != Cluster::MANAGER ) +# Broker::publish(Cluster::manager_topic, DHCP::aggregate_msgs, +# network_time(), c$id, c$uid, is_orig, msg, options); +# else +# event DHCP::aggregate_msgs(network_time(), c$id, c$uid, is_orig, msg, options); + } + +event zeek_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); + } diff --git a/src/analyzer/protocol/CMakeLists.txt b/src/analyzer/protocol/CMakeLists.txt index 82cfb58b47..b2f66dbc52 100644 --- a/src/analyzer/protocol/CMakeLists.txt +++ b/src/analyzer/protocol/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(bittorrent) add_subdirectory(conn-size) add_subdirectory(dce-rpc) add_subdirectory(dhcp) +add_subdirectory(dhcpv6) add_subdirectory(dnp3) add_subdirectory(dns) add_subdirectory(file) diff --git a/src/analyzer/protocol/dhcpv6/CMakeLists.txt b/src/analyzer/protocol/dhcpv6/CMakeLists.txt new file mode 100644 index 0000000000..97a3671c08 --- /dev/null +++ b/src/analyzer/protocol/dhcpv6/CMakeLists.txt @@ -0,0 +1 @@ +spicy_add_analyzer(NAME DHCPv6 SOURCES dhcpv6.spicy dhcpv6.evt) diff --git a/src/analyzer/protocol/dhcpv6/dhcpv6.evt b/src/analyzer/protocol/dhcpv6/dhcpv6.evt new file mode 100644 index 0000000000..609753c952 --- /dev/null +++ b/src/analyzer/protocol/dhcpv6/dhcpv6.evt @@ -0,0 +1,11 @@ +# Copyright (c) 2024 by the Zeek Project. See LICENSE for details. + +%doc-id= Zeek::DHCPv6; +%doc-description = "DHCPv6 analyzer"; + +protocol analyzer DHCPv6 over UDP: + parse with DHCPv6::Message; + +import DHCPv6; + +on DHCPv6::Message -> event dhcpv6_message($conn, $is_orig); diff --git a/src/analyzer/protocol/dhcpv6/dhcpv6.spicy b/src/analyzer/protocol/dhcpv6/dhcpv6.spicy new file mode 100644 index 0000000000..a97952f305 --- /dev/null +++ b/src/analyzer/protocol/dhcpv6/dhcpv6.spicy @@ -0,0 +1,143 @@ +# Copyright (c) 2024 by the Zeek Project. See LICENSE for details. + +# https://datatracker.ietf.org/doc/html/rfc8415 +# +# IANA has updated the All_DHCP_Relay_Agents_and_Servers (ff02::1:2) +# and All_DHCP_Servers (ff05::1:3) table entries in the "IPv6 Multicast +# Address Space Registry" at to reference this document instead of +# [RFC3315]. + +module DHCPv6; + +import spicy; + +type MessageType = enum { + SOLICIT = 1, + ADVERTISE = 2, + REQUEST = 3, + CONFIRM = 4, + RENEW = 5, + REBIND = 6, + REPLY = 7, + RELEASE = 8, + DECLINE = 9, + RECONFIGURE = 10, + INFORMATION_REQUEST = 11, +}; + +type DDUIDType = enum { + LLT = 1, # Link-Layer Address Plus Time https://datatracker.ietf.org/doc/html/rfc8415#section-11.2 + EN = 2, # Enterprise Number https://datatracker.ietf.org/doc/html/rfc8415#section-11.3 + LL = 3, # Link-Layer Address https://datatracker.ietf.org/doc/html/rfc8415#section-11.4 + UUID = 4, # Universally Unique Identifier https://datatracker.ietf.org/doc/html/rfc8415#section-11.5 +}; + +type DUIDOption_LLT = unit { + hw_type: uint16; + time_: uint32; + ll_addr: bytes &eod &convert=spicy::bytes_to_mac($$); +}; + +type DUIDOption_EN = unit { + enterprise_number: uint32; # https://www.iana.org/assignments/enterprise-numbers/, https://www.iana.org/assignments/enterprise-numbers.txt + identifier: bytes &eod; +}; + +type DUIDOption_LL = unit { + hw_type: uint16; + ll_addr: bytes &eod &convert=spicy::bytes_to_mac($$); +}; + +type DUIDOption_UUID = unit { + uuid: bytes &size=16; # 128 bit UUID +}; + +type DUIDOption = unit { + duid_type: uint16 &convert=DDUIDType($$); + switch (self.duid_type) { + DDUIDType::LLT -> llt: DUIDOption_LLT; + DDUIDType::EN -> en: DUIDOption_EN; + DDUIDType::LL -> ll: DUIDOption_LL; + DDUIDType::UUID -> uuid: DUIDOption_UUID; + }; +}; + +type StatusCode = enum { + Success = 0, + UnspecFail = 1, + NoAddrsAvail = 2, + NoBinding = 3, + NotOnLink = 4, + UseMulticast = 5, + NoPrefixAvail = 6, +}; + +type StatusCodeOption = unit { + code: uint16 &convert=StatusCode($$); + message: bytes &eod; +}; + +type IA_NAOption = unit { + iaid: uint32; + t1: uint32; # seconds + t2: uint32; # seconds + options: Option[] &eod; +}; + +type IAADDROption = unit { + addr_: addr &ipv6; + preferred_lifetime: uint32; + valid_lifetime: uint32; + options: Option[] &eod; +}; + +type OptionCode = enum { + CLIENTID = 1, + SERVERID = 2, + IA_NA = 3, + IA_TA = 4, + IAADDR = 5, + STATUS_CODE = 13, +}; + +type Option = unit { + code: uint16 &convert=OptionCode($$); + len: uint16; + + switch (self.code) { + OptionCode::CLIENTID -> client_id: DUIDOption; + OptionCode::SERVERID -> server_id: DUIDOption; + OptionCode::IA_NA -> ia_na: IA_NAOption; + # IA_TA + OptionCode::IAADDR -> iaaddr: IAADDROption; + OptionCode::STATUS_CODE -> status_code: StatusCodeOption; + * -> unknown: bytes &eod; + } &size=self.len; +}; + +# XXX: 10. Representation and Use of Domain Names +# AS in DHCP, but uncompressed! + +public type Message = unit { + msg_type: uint8 &convert=MessageType($$); + + # TODO: Handle Relay messages!? + # https://datatracker.ietf.org/doc/html/rfc8415#section-9 + + transaction_id: uint8[3] &convert=uint32((uint32($$[0]) << 16) + (uint32($$[1]) << 8) + uint32($$[2])); + + options: Option[] &eod; + + # Once the options are parsed, what do we actually want to + # send to script land? Maybe a vector with options that have + # a lot of optionals? + + on %done { + print self.msg_type, self.transaction_id; + for (o in self.options) { + print o; + } + spicy::accept_input(); + } +};