├── .gitignore ├── CHANGES ├── CMakeLists.txt ├── COPYING ├── LICENSE ├── Makefile ├── README ├── README.rst ├── VERSION ├── configure ├── configure.plugin ├── scripts ├── Corelight │ └── GQUIC │ │ ├── __load__.zeek │ │ ├── dpd.sig │ │ └── main.zeek ├── __load__.zeek ├── __preload__.zeek ├── init.zeek └── types.zeek ├── src ├── GQUIC.cc ├── GQUIC.h ├── Plugin.cc ├── Plugin.h ├── consts.bif ├── events.bif ├── gquic-analyzer.pac ├── gquic-protocol.pac ├── gquic.pac └── types.bif ├── tests ├── Baseline │ ├── gquic.q039 │ │ └── output │ ├── gquic.q043 │ │ └── output │ ├── gquic.show-plugin │ │ └── output │ └── gquic.version-negotiation │ │ └── output ├── Makefile ├── Scripts │ ├── diff-remove-timestamps │ ├── get-zeek-env │ └── gquic_events.zeek ├── Traces │ ├── gquic-negotiation.pcap │ ├── gquic-q039.pcap │ └── gquic-q043.pcap ├── btest.cfg ├── gquic │ ├── q039.zeek │ ├── q043.zeek │ ├── show-plugin.zeek │ └── version-negotiation.zeek └── random.seed └── zkg.meta /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .btest.failed.dat 3 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 2 | 0.2 Wed Nov 14 16:10:17 CST 2018 3 | 4 | - Release 0.2. 5 | 6 | - Update show-plugin test to remove version number from baseline. (Michael Dopheide) 7 | 8 | - Update configure script to support building against bro installation. (Jon Siwek, Corelight) 9 | 10 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | cmake_minimum_required(VERSION 2.8) 3 | 4 | project(ZeekPluginGQUIC) 5 | 6 | include(TestBigEndian) 7 | test_big_endian(WORDS_BIGENDIAN) 8 | 9 | if ( WORDS_BIGENDIAN ) 10 | add_definitions(-DWORDS_BIGENDIAN) 11 | endif () 12 | 13 | include(ZeekPlugin) 14 | 15 | zeek_plugin_begin(Corelight GQUIC) 16 | zeek_plugin_cc(src/GQUIC.cc src/Plugin.cc) 17 | zeek_plugin_bif(src/events.bif src/consts.bif src/types.bif) 18 | zeek_plugin_pac(src/gquic.pac src/gquic-protocol.pac src/gquic-analyzer.pac) 19 | zeek_plugin_dist_files(README CHANGES COPYING VERSION) 20 | zeek_plugin_end() 21 | 22 | file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" VERSION LIMIT_COUNT 1) 23 | 24 | if ("${PROJECT_SOURCE_DIR}" STREQUAL "${CMAKE_SOURCE_DIR}") 25 | # Allows building rpm/deb packages via "make package" in build dir. 26 | include(ConfigurePackaging) 27 | ConfigurePackaging(${VERSION}) 28 | endif () 29 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2018 by Corelight, Inc 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | (1) Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 9 | (2) Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in 11 | the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | (3) Neither the name of Corelight, Inc, nor the names of contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | COPYING -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Convenience Makefile providing a few common top-level targets. 3 | # 4 | 5 | cmake_build_dir=build 6 | arch=`uname -s | tr A-Z a-z`-`uname -m` 7 | 8 | all: build-it 9 | 10 | build-it: 11 | @test -e $(cmake_build_dir)/config.status || ./configure 12 | -@test -e $(cmake_build_dir)/CMakeCache.txt && \ 13 | test $(cmake_build_dir)/CMakeCache.txt -ot `cat $(cmake_build_dir)/CMakeCache.txt | grep BRO_DIST | cut -d '=' -f 2`/build/CMakeCache.txt && \ 14 | echo Updating stale CMake cache && \ 15 | touch $(cmake_build_dir)/CMakeCache.txt 16 | 17 | ( cd $(cmake_build_dir) && make ) 18 | 19 | install: 20 | ( cd $(cmake_build_dir) && make install ) 21 | 22 | clean: 23 | ( cd $(cmake_build_dir) && make clean ) 24 | 25 | distclean: 26 | rm -rf $(cmake_build_dir) 27 | 28 | test: 29 | make -C tests 30 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | Google QUIC analyzer/detector for Zeek 3 | ====================================== 4 | 5 | This analyzer can parse and detect the Google implementation of QUIC 6 | (called GQUIC here for simplicity), using the wire format described at: 7 | 8 | https://www.chromium.org/quic. 9 | 10 | The version of GQUIC used by Chrome at the time of writing this analyzer 11 | was Q039 with some Google servers (and possibly Chrome canary builds) 12 | also being able to use Q043. This analyzer was able to detect both 13 | those versions during testing. 14 | 15 | The wire format described in GQUIC documents at that time (May 4-5 2018) 16 | also appeared out of sync with the actual implementations used in the 17 | wild. 18 | 19 | Additionally, there seems to be regular effort to migrate GQUIC to track 20 | the IETF draft standard for QUIC described at: 21 | 22 | https://github.com/quicwg/base-drafts 23 | https://datatracker.ietf.org/wg/quic/documents/ 24 | 25 | With that draft differing in significant ways from the GQUIC 26 | implementations this analyzer was tested against and also having changed 27 | its wire format recently. 28 | 29 | To summarize, this GQUIC analyzer, if not updated in the future, isn't 30 | expected to continue working with later evolutions of the QUIC protocol. 31 | 32 | Installation 33 | ------------ 34 | 35 | Via `zkg `_:: 36 | 37 | zkg install corelight/zeek-quic 38 | 39 | Else you will have to build and install it yourself (assuming `zeek-config` 40 | in in your `PATH`):: 41 | 42 | ./configure 43 | make 44 | make install 45 | 46 | Usage 47 | ----- 48 | 49 | By default this simply detects whether a UDP connection looks like the 50 | QUIC protocol, adds the "guic" string to conn.log's "service" field and 51 | then stops parsing. 52 | 53 | If you want to write a custom script that handles other events provided 54 | by the analyzer, you might want to have the analyzer continue to parse 55 | the connection even after the protocol has been confirmed:: 56 | 57 | redef GQUIC::skip_after_confirm = F; 58 | 59 | The events provided can give coarse information regarding packet type 60 | (Regular Packet vs. Version Negotiation vs Public Reset) as well as the 61 | content of the Public Header (like Packet Number, QUIC Version, 62 | Connection ID, or Diversification Nonce). 63 | 64 | References / Tools 65 | ------------------ 66 | 67 | Tools: https://github.com/quicwg/base-drafts/wiki/Tools 68 | 69 | Implementations: https://github.com/quicwg/base-drafts/wiki/Implementations 70 | 71 | Chromium: https://www.chromium.org/quic/playing-with-quic 72 | 73 | Acknowledgements 74 | ---------------- 75 | 76 | Prior QUIC analyzer work by Mike Dopheide: https://github.com/dopheide-esnet/bro-quic 77 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.1 2 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Wrapper for viewing/setting options that the plugin's CMake 4 | # scripts will recognize. 5 | # 6 | # Don't edit this. Edit configure.plugin to add plugin-specific options. 7 | # 8 | 9 | set -e 10 | command="$0 $*" 11 | 12 | if [ -e `dirname $0`/configure.plugin ]; then 13 | # Include custom additions. 14 | . `dirname $0`/configure.plugin 15 | fi 16 | 17 | usage() { 18 | 19 | cat 1>&2 </dev/null 2>&1; then 34 | plugin_usage 1>&2 35 | fi 36 | 37 | echo 38 | 39 | exit 1 40 | } 41 | 42 | # Function to append a CMake cache entry definition to the 43 | # CMakeCacheEntries variable 44 | # $1 is the cache entry variable name 45 | # $2 is the cache entry variable type 46 | # $3 is the cache entry variable value 47 | append_cache_entry () { 48 | CMakeCacheEntries="$CMakeCacheEntries -D $1:$2=$3" 49 | } 50 | 51 | # set defaults 52 | builddir=build 53 | zeekdist="" 54 | installroot="default" 55 | CMakeCacheEntries="" 56 | 57 | while [ $# -ne 0 ]; do 58 | case "$1" in 59 | -*=*) optarg=`echo "$1" | sed 's/[-_a-zA-Z0-9]*=//'` ;; 60 | *) optarg= ;; 61 | esac 62 | 63 | case "$1" in 64 | --help|-h) 65 | usage 66 | ;; 67 | 68 | --cmake=*) 69 | CMakeCommand=$optarg 70 | ;; 71 | 72 | --zeek-dist=*) 73 | zeekdist=`cd $optarg && pwd` 74 | ;; 75 | 76 | --install-root=*) 77 | installroot=$optarg 78 | ;; 79 | 80 | --with-binpac=*) 81 | append_cache_entry BinPAC_ROOT_DIR PATH $optarg 82 | binpac_root=$optarg 83 | ;; 84 | 85 | --with-broker=*) 86 | append_cache_entry BROKER_ROOT_DIR PATH $optarg 87 | broker_root=$optarg 88 | ;; 89 | 90 | --with-caf=*) 91 | append_cache_entry CAF_ROOT_DIR PATH $optarg 92 | caf_root=$optarg 93 | ;; 94 | 95 | --with-bifcl=*) 96 | append_cache_entry BifCl_EXE PATH $optarg 97 | ;; 98 | 99 | --enable-debug) 100 | append_cache_entry BRO_PLUGIN_ENABLE_DEBUG BOOL true 101 | ;; 102 | 103 | *) 104 | if type plugin_option >/dev/null 2>&1; then 105 | plugin_option $1 && shift && continue; 106 | fi 107 | 108 | echo "Invalid option '$1'. Try $0 --help to see available options." 109 | exit 1 110 | ;; 111 | esac 112 | shift 113 | done 114 | 115 | if [ -z "$CMakeCommand" ]; then 116 | # prefer cmake3 over "regular" cmake (cmake == cmake2 on RHEL) 117 | if command -v cmake3 >/dev/null 2>&1 ; then 118 | CMakeCommand="cmake3" 119 | elif command -v cmake >/dev/null 2>&1 ; then 120 | CMakeCommand="cmake" 121 | else 122 | echo "This package requires CMake, please install it first." 123 | echo "Then you may use this script to configure the CMake build." 124 | echo "Note: pass --cmake=PATH to use cmake in non-standard locations." 125 | exit 1; 126 | fi 127 | fi 128 | 129 | if [ -z "$zeekdist" ]; then 130 | if type zeek-config >/dev/null 2>&1; then 131 | zeek_config="zeek-config" 132 | else 133 | echo "Either 'zeek-config' must be in PATH or '--zeek-dist=' used" 134 | exit 1 135 | fi 136 | 137 | append_cache_entry BRO_CONFIG_PREFIX PATH `${zeek_config} --prefix` 138 | append_cache_entry BRO_CONFIG_INCLUDE_DIR PATH `${zeek_config} --include_dir` 139 | append_cache_entry BRO_CONFIG_PLUGIN_DIR PATH `${zeek_config} --plugin_dir` 140 | append_cache_entry BRO_CONFIG_CMAKE_DIR PATH `${zeek_config} --cmake_dir` 141 | append_cache_entry CMAKE_MODULE_PATH PATH `${zeek_config} --cmake_dir` 142 | 143 | build_type=`${zeek_config} --build_type` 144 | 145 | if [ "$build_type" = "debug" ]; then 146 | append_cache_entry BRO_PLUGIN_ENABLE_DEBUG BOOL true 147 | fi 148 | 149 | if [ -z "$binpac_root" ]; then 150 | append_cache_entry BinPAC_ROOT_DIR PATH `${zeek_config} --binpac_root` 151 | fi 152 | 153 | if [ -z "$broker_root" ]; then 154 | append_cache_entry BROKER_ROOT_DIR PATH `${zeek_config} --broker_root` 155 | fi 156 | 157 | if [ -z "$caf_root" ]; then 158 | append_cache_entry CAF_ROOT_DIR PATH `${zeek_config} --caf_root` 159 | fi 160 | else 161 | if [ ! -e "$zeekdist/zeek-path-dev.in" ]; then 162 | echo "$zeekdist does not appear to be a valid Zeek source tree." 163 | exit 1 164 | fi 165 | 166 | # BRO_DIST is the canonical/historical name used by plugin CMake scripts 167 | # ZEEK_DIST doesn't serve a function at the moment, but set/provided anyway 168 | append_cache_entry BRO_DIST PATH $zeekdist 169 | append_cache_entry ZEEK_DIST PATH $zeekdist 170 | append_cache_entry CMAKE_MODULE_PATH PATH $zeekdist/cmake 171 | fi 172 | 173 | if [ "$installroot" != "default" ]; then 174 | mkdir -p $installroot 175 | append_cache_entry BRO_PLUGIN_INSTALL_ROOT PATH $installroot 176 | fi 177 | 178 | echo "Build Directory : $builddir" 179 | echo "Zeek Source Directory : $zeekdist" 180 | 181 | mkdir -p $builddir 182 | cd $builddir 183 | 184 | "$CMakeCommand" $CMakeCacheEntries .. 185 | 186 | echo "# This is the command used to configure this build" > config.status 187 | echo $command >> config.status 188 | chmod u+x config.status 189 | -------------------------------------------------------------------------------- /configure.plugin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Hooks to add custom options to the configure script. 4 | # 5 | 6 | plugin_usage() 7 | { 8 | : # Do nothing 9 | # cat <FlowEOF(true); 28 | interp->FlowEOF(false); 29 | } 30 | 31 | void GQUIC_Analyzer::DeliverPacket(int len, const u_char* data, bool orig, 32 | uint64_t seq, const zeek::IP_Hdr* ip, 33 | int caplen) 34 | { 35 | Analyzer::DeliverPacket(len, data, orig, seq, ip, caplen); 36 | 37 | try 38 | { 39 | interp->NewData(orig, data, data + len); 40 | } 41 | catch ( const binpac::Exception& e ) 42 | { 43 | ProtocolViolation(zeek::util::fmt("Binpac exception: %s", e.c_msg())); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/GQUIC.h: -------------------------------------------------------------------------------- 1 | #ifndef ANALYZER_PROTOCOL_GQUIC_GQUIC_H 2 | #define ANALYZER_PROTOCOL_GQUIC_GQUIC_H 3 | 4 | #include "zeek/zeek-config.h" 5 | 6 | #include "zeek/analyzer/protocol/udp/UDP.h" 7 | #include "zeek/analyzer/protocol/pia/PIA.h" 8 | 9 | namespace binpac { 10 | namespace GQUIC { 11 | class GQUIC_Conn; 12 | } 13 | } 14 | 15 | namespace analyzer { namespace gquic { 16 | 17 | class GQUIC_Analyzer : public zeek::analyzer::Analyzer { 18 | public: 19 | GQUIC_Analyzer(zeek::Connection* conn); 20 | virtual ~GQUIC_Analyzer(); 21 | 22 | virtual void Done(); 23 | virtual void DeliverPacket(int len, const u_char* data, bool orig, 24 | uint64_t seq, const zeek::IP_Hdr* ip, 25 | int caplen); 26 | 27 | static zeek::analyzer::Analyzer* Instantiate(zeek::Connection* conn) 28 | { return new GQUIC_Analyzer(conn); } 29 | 30 | protected: 31 | int did_session_done; 32 | 33 | bool orig_done; 34 | bool resp_done; 35 | 36 | zeek::analyzer::pia::PIA_UDP* pia; 37 | binpac::GQUIC::GQUIC_Conn* interp; 38 | }; 39 | 40 | } } // namespace analyzer::* 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /src/Plugin.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "Plugin.h" 3 | #include "GQUIC.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace plugin { namespace Corelight_GQUIC { Plugin plugin; } } 9 | 10 | using namespace plugin::Corelight_GQUIC; 11 | 12 | zeek::plugin::Configuration Plugin::Configure() 13 | { 14 | auto c = new zeek::analyzer::Component("GQUIC", 15 | ::analyzer::gquic::GQUIC_Analyzer::Instantiate); 16 | AddComponent(c); 17 | zeek::plugin::Configuration config; 18 | config.name = "Corelight::GQUIC"; 19 | config.description = "Google QUIC (QGUIC) protocol analyzer"; 20 | config.version.major = 0; 21 | config.version.minor = 4; 22 | return config; 23 | } 24 | -------------------------------------------------------------------------------- /src/Plugin.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | 6 | namespace plugin { 7 | namespace Corelight_GQUIC { 8 | 9 | class Plugin : public zeek::plugin::Plugin 10 | { 11 | protected: 12 | // Overridden from plugin::Plugin. 13 | zeek::plugin::Configuration Configure() override; 14 | }; 15 | 16 | extern Plugin plugin; 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/consts.bif: -------------------------------------------------------------------------------- 1 | module GQUIC; 2 | 3 | const skip_after_confirm: bool; 4 | -------------------------------------------------------------------------------- /src/events.bif: -------------------------------------------------------------------------------- 1 | 2 | ## Raised for every GQUIC Regular Packet. 3 | event gquic_packet%(c: connection, is_orig: bool, hdr: GQUIC::PublicHeader%); 4 | 5 | ## Raised whenever a GQUIC client sends a Regular Packet with a novel GQUIC 6 | ## version number. E.g. you might see this twice for a single connection if 7 | ## Version Negotiation happened. 8 | event gquic_client_version%(c: connection, version: count%); 9 | 10 | ## Raised whenever a GQUIC Version Negotiation Packet is seen. 11 | event gquic_version_negotiation%(c: connection, is_orig: bool, 12 | versions: index_vec%); 13 | 14 | ## Raised whenever a GQUIC Public Reset Packet is seen. 15 | event gquic_reset%(c: connection, is_orig: bool%); 16 | -------------------------------------------------------------------------------- /src/gquic-analyzer.pac: -------------------------------------------------------------------------------- 1 | %extern{ 2 | #include 3 | #include 4 | #include "consts.bif.h" 5 | #include "types.bif.h" 6 | %} 7 | 8 | %header{ 9 | %} 10 | 11 | %code{ 12 | %} 13 | 14 | refine connection GQUIC_Conn += { 15 | 16 | %member{ 17 | bool saw_server_pkt1; 18 | uint16 last_known_client_version; 19 | std::unordered_set potential_client_versions; 20 | 21 | void confirm() 22 | { 23 | zeek_analyzer()->ProtocolConfirmation(); 24 | 25 | if ( zeek::BifConst::GQUIC::skip_after_confirm ) 26 | zeek_analyzer()->SetSkip(true); 27 | } 28 | 29 | uint16 extract_gquic_version(const uint8* version_bytes) 30 | { 31 | if ( version_bytes[0] != 'Q' ) 32 | { 33 | zeek_analyzer()->ProtocolViolation("invalid GQUIC Version", 34 | reinterpret_cast(version_bytes), 4); 35 | return 0; 36 | } 37 | 38 | for ( auto i = 1u; i < 4; ++i ) 39 | { 40 | if ( ! isdigit(version_bytes[i]) ) 41 | { 42 | zeek_analyzer()->ProtocolViolation( 43 | "invalid GQUIC Version", 44 | reinterpret_cast(version_bytes), 4); 45 | return 0; 46 | } 47 | } 48 | 49 | uint16 rval = 0; 50 | rval += (version_bytes[1] - 0x30) * 100; 51 | rval += (version_bytes[2] - 0x30) * 10; 52 | rval += (version_bytes[3] - 0x30); 53 | return rval; 54 | } 55 | %} 56 | 57 | %init{ 58 | saw_server_pkt1 = false; 59 | last_known_client_version = 0; 60 | %} 61 | 62 | %cleanup{ 63 | %} 64 | 65 | function process_packet(pkt: GQUIC_Packet, is_orig: bool): bool 66 | %{ 67 | switch ( ${pkt.flags.packet_type} ) { 68 | case NEGOTIATION: 69 | { 70 | auto vlist = ${pkt.neg_pkt.version_list}; 71 | 72 | if ( vlist.length() % 4 != 0 ) 73 | { 74 | zeek_analyzer()->ProtocolViolation( 75 | "invalid GQUIC Version Negotation list", 76 | reinterpret_cast(vlist.data()), 77 | vlist.length()); 78 | return true; 79 | } 80 | 81 | std::vector parsed_version_list; 82 | parsed_version_list.reserve(vlist.length() / 4); 83 | 84 | for ( auto i = 0; i < vlist.length(); i += 4 ) 85 | { 86 | auto ptr = reinterpret_cast(vlist.begin() + i); 87 | auto parsed_version = extract_gquic_version(ptr); 88 | 89 | if ( parsed_version == 0 ) 90 | return true; 91 | 92 | parsed_version_list.emplace_back(parsed_version); 93 | } 94 | 95 | if ( last_known_client_version ) 96 | confirm(); 97 | 98 | if ( gquic_version_negotiation ) 99 | { 100 | static auto vt = zeek::id::find_type("index_vec"); 101 | auto vv = zeek::make_intrusive(vt); 102 | 103 | for ( auto i = 0u; i < parsed_version_list.size(); ++i ) 104 | vv->Assign(vv->Size(), zeek::val_mgr->Count(parsed_version_list[i])); 105 | 106 | zeek::BifEvent::enqueue_gquic_version_negotiation( 107 | zeek_analyzer(), zeek_analyzer()->Conn(), is_orig, std::move(vv)); 108 | } 109 | } 110 | break; 111 | case RESET: 112 | { 113 | if ( gquic_reset ) 114 | zeek::BifEvent::enqueue_gquic_reset(zeek_analyzer(), 115 | zeek_analyzer()->Conn(), 116 | is_orig); 117 | } 118 | break; 119 | case REGULAR: 120 | { 121 | auto pkt_num = 0u; 122 | auto pkt_version = get_gquic_version(${pkt.reg_pkt}); 123 | 124 | if ( is_orig ) 125 | { 126 | if ( pkt_version ) 127 | { 128 | last_known_client_version = pkt_version; 129 | auto p = potential_client_versions.emplace(pkt_version); 130 | 131 | if ( gquic_client_version && p.second ) 132 | zeek::BifEvent::enqueue_gquic_client_version( 133 | zeek_analyzer(), 134 | zeek_analyzer()->Conn(), 135 | pkt_version); 136 | } 137 | 138 | pkt_num = get_packet_number(${pkt.reg_pkt}, 139 | last_known_client_version); 140 | 141 | if ( last_known_client_version && saw_server_pkt1 ) 142 | confirm(); 143 | } 144 | else 145 | { 146 | pkt_num = get_packet_number(${pkt.reg_pkt}, 147 | last_known_client_version); 148 | 149 | if ( pkt_num == 1 ) 150 | saw_server_pkt1 = true; 151 | 152 | if ( last_known_client_version && saw_server_pkt1 ) 153 | confirm(); 154 | } 155 | 156 | if ( gquic_packet ) 157 | { 158 | auto rv = zeek::make_intrusive(zeek::BifType::Record::GQUIC::PublicHeader); 159 | rv->Assign(0, zeek::val_mgr->Count(pkt_num)); 160 | rv->Assign(1, zeek::val_mgr->Bool(${pkt.flags.is_multipath})); 161 | rv->Assign(2, zeek::val_mgr->Bool(${pkt.flags.reserved_bit})); 162 | 163 | if ( ${pkt.cid}->present_case_index() ) 164 | { 165 | auto bytes = ${pkt.cid.bytes}; 166 | auto ptr = reinterpret_cast(bytes->data()); 167 | rv->Assign(3, zeek::make_intrusive(bytes->size(), ptr)); 168 | } 169 | 170 | if ( ${pkt.reg_pkt}->version_case_index() ) 171 | rv->Assign(4, zeek::val_mgr->Count(pkt_version)); 172 | 173 | if ( ${pkt.reg_pkt}->nonce_case_index() ) 174 | { 175 | auto bytes = ${pkt.reg_pkt.nonce_val}; 176 | auto ptr = reinterpret_cast(bytes->data()); 177 | rv->Assign(5, zeek::make_intrusive(bytes->size(), ptr)); 178 | } 179 | 180 | zeek::BifEvent::enqueue_gquic_packet(zeek_analyzer(), 181 | zeek_analyzer()->Conn(), 182 | is_orig, std::move(rv)); 183 | } 184 | } 185 | break; 186 | default: 187 | break; 188 | } 189 | 190 | return true; 191 | %} 192 | 193 | function get_gquic_version(pkt: RegularPacket): uint16 194 | %{ 195 | if ( ! pkt->version_case_index() ) 196 | return 0; 197 | 198 | return extract_gquic_version(${pkt.version_val}->data()); 199 | %} 200 | 201 | function get_packet_number(pkt: RegularPacket, version: uint16): uint64 202 | %{ 203 | return convert_packet_bytes(get_packet_number_bytes(pkt), version); 204 | %} 205 | 206 | function get_packet_number_bytes(pkt: RegularPacket): uint8[] 207 | %{ 208 | switch ( pkt->pkt_num_bytes_case_index() ) { 209 | case 0x00: 210 | return ${pkt.pkt_num_bytes1}; 211 | case 0x10: 212 | return ${pkt.pkt_num_bytes2}; 213 | case 0x20: 214 | return ${pkt.pkt_num_bytes4}; 215 | case 0x30: 216 | return ${pkt.pkt_num_bytes6}; 217 | default: 218 | assert(false); 219 | } 220 | return nullptr; 221 | %} 222 | 223 | function convert_packet_bytes(bytes: uint8[], version: uint16): uint64 224 | %{ 225 | uint64 rval = 0; 226 | uint8* byte_ptr = reinterpret_cast(&rval); 227 | byte_ptr += sizeof(rval) - bytes->size(); 228 | 229 | for ( auto i = 0u; i < bytes->size(); ++i ) 230 | { 231 | auto byte = (*bytes)[i]; 232 | *byte_ptr = byte; 233 | ++byte_ptr; 234 | } 235 | 236 | // Version 0 essentially means we haven't seen a version yet, so 237 | // assume a recent version of GQUIC. 238 | auto gquic_is_big_endian = version == 0 || version >= 39; 239 | 240 | if ( gquic_is_big_endian ) 241 | rval = zeek::ntohll(rval); 242 | else 243 | { 244 | #ifdef WORDS_BIGENDIAN 245 | uint64 tmp; 246 | uint8* src = reinterpret_cast(&rval); 247 | uint8* dst = reinterpret_cast(&tmp); 248 | dst[0] = src[7]; 249 | dst[1] = src[6]; 250 | dst[2] = src[5]; 251 | dst[3] = src[4]; 252 | dst[4] = src[3]; 253 | dst[5] = src[2]; 254 | dst[6] = src[1]; 255 | dst[7] = src[0]; 256 | rval = tmp; 257 | #endif 258 | } 259 | 260 | return rval; 261 | %} 262 | }; 263 | 264 | refine typeattr GQUIC_Packet += &let { 265 | proc = $context.connection.process_packet(this, is_orig); 266 | }; 267 | -------------------------------------------------------------------------------- /src/gquic-protocol.pac: -------------------------------------------------------------------------------- 1 | # Google QUIC docs: https://www.chromium.org/quic 2 | # Wire format spec: https://docs.google.com/document/d/1WJvyZflAO2pq77yOLbp9NsGjC1CHetAXV8I0fQe-B_U/edit 3 | # 4 | # At time of writing, the IETF draft of QUIC had already significantly 5 | # diverged in terms of wire format from the Google version of QUIC and it 6 | # is likely the above links will eventually change to reflect the actual 7 | # QUIC standard. i.e. Google is slowly working to migrate GQUIC to 8 | # track the changes made by the IETF. 9 | 10 | # For reference, the following message format was written on May 4-7, 2018 11 | # and tested on GQUIC Version Q039 and Q049. The wire format specification 12 | # document was also not completely up to date at that time, with it only 13 | # acknowleding the changes through version Q036. e.g. the doc still claimed 14 | # a little-endian byte order even though Q039 made the switch to big-endian. 15 | 16 | type GQUIC_Packet(is_orig: bool) = record { 17 | flags: PublicFlags(is_orig); 18 | cid: ConnectionID(flags); 19 | content: case flags.packet_type of { 20 | NEGOTIATION -> neg_pkt: VersionNegotiationPacket(flags, cid, is_orig); 21 | RESET -> res_pkt: PublicResetPacket(flags, cid, is_orig); 22 | REGULAR -> reg_pkt: RegularPacket(flags, cid, is_orig); 23 | default -> nil_pkt: empty; 24 | }; 25 | } &byteorder = bigendian; # NOTE: versions before Q039 are little-endian 26 | 27 | type PublicFlags(is_orig: bool) = record { 28 | byte: uint8; 29 | } 30 | &let { 31 | # Clients may set the nonce bit in the header even though there is no 32 | # nonce in the message. See: 33 | # https://groups.google.com/a/chromium.org/forum/#!topic/proto-quic/kQVDZal_iwo 34 | 35 | have_version: bool = (byte & 0x01) == 0x01; 36 | is_negotiate: bool = (byte & 0x01) == 0x01 && ! is_orig; 37 | is_reset: bool = (byte & 0x02) == 0x02; 38 | have_nonce: bool = (byte & 0x04) == 0x04 && ! is_orig; 39 | have_conn_id: bool = (byte & 0x08) == 0x08; 40 | pkt_num_size: uint8 = (byte & 0x30); 41 | is_multipath: bool = (byte & 0x40) == 0x40; 42 | reserved_bit: bool = (byte & 0x80) == 0x80; 43 | packet_type: int = (is_reset ? RESET : 44 | (is_negotiate ? NEGOTIATION : 45 | REGULAR)); 46 | }; 47 | 48 | type ConnectionID(flags: PublicFlags) = record { 49 | present: case flags.have_conn_id of { 50 | true -> bytes: uint8[8]; 51 | false -> nil: empty; 52 | }; 53 | }; 54 | 55 | type RegularPacket(flags: PublicFlags, 56 | cid: ConnectionID, is_orig: bool) = record { 57 | version: case flags.have_version of { 58 | true -> version_val: uint8[4]; 59 | false -> version_nil: empty; 60 | }; 61 | 62 | nonce: case flags.have_nonce of { 63 | true -> nonce_val: uint8[32]; 64 | false -> nonce_nil: empty; 65 | }; 66 | 67 | pkt_num_bytes: case flags.pkt_num_size of { 68 | 0x00 -> pkt_num_bytes1: uint8[1]; 69 | 0x10 -> pkt_num_bytes2: uint8[2]; 70 | 0x20 -> pkt_num_bytes4: uint8[4]; 71 | 0x30 -> pkt_num_bytes6: uint8[6]; 72 | default -> pkt_num_bytesx: empty; # not possible, all 4 cases handled 73 | }; 74 | 75 | # A sequence of Frame Packets follows. 76 | # TODO: it may be possible to extract info for certain frame types 77 | # though most appear to be encrypted. 78 | frames: bytestring &restofdata &transient; 79 | }; 80 | 81 | type VersionNegotiationPacket(flags: PublicFlags, 82 | cid: ConnectionID, is_orig: bool) = record { 83 | version_list: bytestring &restofdata; 84 | }; 85 | 86 | type PublicResetPacket(flags: PublicFlags, 87 | cid: ConnectionID, is_orig: bool) = record { 88 | tag: uint8[4]; 89 | # Remaining bytes are a variable length tag value map. 90 | data: bytestring &restofdata &transient; 91 | }; 92 | 93 | enum PacketType { 94 | NEGOTIATION, 95 | RESET, 96 | REGULAR, 97 | }; 98 | -------------------------------------------------------------------------------- /src/gquic.pac: -------------------------------------------------------------------------------- 1 | %include zeek/binpac.pac 2 | %include zeek/zeek.pac 3 | 4 | %extern{ 5 | #include "GQUIC.h" 6 | #include "events.bif.h" 7 | %} 8 | 9 | analyzer GQUIC withcontext { 10 | connection: GQUIC_Conn; 11 | flow: GQUIC_Flow; 12 | }; 13 | 14 | connection GQUIC_Conn(zeek_analyzer: ZeekAnalyzer) { 15 | upflow = GQUIC_Flow(true); 16 | downflow = GQUIC_Flow(false); 17 | }; 18 | 19 | %include gquic-protocol.pac 20 | 21 | flow GQUIC_Flow(is_orig: bool) { 22 | datagram = GQUIC_Packet(is_orig) withcontext(connection, this); 23 | }; 24 | 25 | %include gquic-analyzer.pac 26 | -------------------------------------------------------------------------------- /src/types.bif: -------------------------------------------------------------------------------- 1 | module GQUIC; 2 | 3 | type PublicHeader: record; 4 | -------------------------------------------------------------------------------- /tests/Baseline/gquic.q039/output: -------------------------------------------------------------------------------- 1 | gquic_client_version, [orig_h=192.168.1.102, orig_p=62238/udp, resp_h=52.55.120.73, resp_p=443/udp], 39 2 | gquic_packet, [orig_h=192.168.1.102, orig_p=62238/udp, resp_h=52.55.120.73, resp_p=443/udp], T, [pkt_num=1, multipath=F, reserved=F, cid=\xe9\xa4\xe4\xf0z\x06Zf, version=39, nonce=] 3 | gquic_packet, [orig_h=192.168.1.102, orig_p=62238/udp, resp_h=52.55.120.73, resp_p=443/udp], F, [pkt_num=1, multipath=F, reserved=F, cid=\xe9\xa4\xe4\xf0z\x06Zf, version=, nonce=] 4 | -------------------------------------------------------------------------------- /tests/Baseline/gquic.q043/output: -------------------------------------------------------------------------------- 1 | gquic_client_version, [orig_h=192.168.1.102, orig_p=55925/udp, resp_h=74.125.197.105, resp_p=443/udp], 43 2 | gquic_packet, [orig_h=192.168.1.102, orig_p=55925/udp, resp_h=74.125.197.105, resp_p=443/udp], T, [pkt_num=1, multipath=F, reserved=F, cid=\xa0\x89\xfa\x84_\x8b-\xd1, version=43, nonce=] 3 | gquic_packet, [orig_h=192.168.1.102, orig_p=55925/udp, resp_h=74.125.197.105, resp_p=443/udp], F, [pkt_num=1, multipath=F, reserved=F, cid=\xa0\x89\xfa\x84_\x8b-\xd1, version=, nonce=] 4 | -------------------------------------------------------------------------------- /tests/Baseline/gquic.show-plugin/output: -------------------------------------------------------------------------------- 1 | Corelight::GQUIC - Google QUIC (QGUIC) protocol analyzer (dynamic, version) 2 | [Analyzer] GQUIC (ANALYZER_GQUIC, enabled) 3 | [Event] gquic_packet 4 | [Event] gquic_client_version 5 | [Event] gquic_version_negotiation 6 | [Event] gquic_reset 7 | [Constant] GQUIC::skip_after_confirm 8 | [Type] GQUIC::PublicHeader 9 | 10 | -------------------------------------------------------------------------------- /tests/Baseline/gquic.version-negotiation/output: -------------------------------------------------------------------------------- 1 | gquic_version_negotiation, [orig_h=192.168.1.102, orig_p=53859/udp, resp_h=52.55.120.73, resp_p=443/udp], F, [35, 37, 38, 39] 2 | gquic_client_version, [orig_h=192.168.1.102, orig_p=53859/udp, resp_h=52.55.120.73, resp_p=443/udp], 43 3 | gquic_packet, [orig_h=192.168.1.102, orig_p=53859/udp, resp_h=52.55.120.73, resp_p=443/udp], T, [pkt_num=2, multipath=F, reserved=F, cid=A\xd5&\x8bmw\xf6r, version=43, nonce=] 4 | gquic_version_negotiation, [orig_h=192.168.1.102, orig_p=53859/udp, resp_h=52.55.120.73, resp_p=443/udp], F, [35, 37, 38, 39] 5 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @btest 4 | -------------------------------------------------------------------------------- /tests/Scripts/diff-remove-timestamps: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Replace anything which looks like timestamps with XXXs (including the #start/end markers in logs). 4 | 5 | # Get us "modern" regexps with sed. 6 | if [ `uname` == "Linux" ]; then 7 | sed="sed -r" 8 | else 9 | sed="sed -E" 10 | fi 11 | 12 | $sed 's/(0\.000000)|([0-9]{9,10}\.[0-9]{2,8})/XXXXXXXXXX.XXXXXX/g' | \ 13 | $sed 's/^ *#(open|close).(19|20)..-..-..-..-..-..$/#\1 XXXX-XX-XX-XX-XX-XX/g' 14 | -------------------------------------------------------------------------------- /tests/Scripts/get-zeek-env: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # 3 | # BTest helper for getting values for Zeek-related environment variables. 4 | 5 | base=`dirname $0` 6 | zeek_dist=`cat ${base}/../../build/CMakeCache.txt | grep ZEEK_DIST | cut -d = -f 2` 7 | 8 | if [ -n "${zeek_dist}" ]; then 9 | if [ "$1" = "zeekpath" ]; then 10 | ${zeek_dist}/build/zeek-path-dev 11 | elif [ "$1" = "zeek_plugin_path" ]; then 12 | ( cd ${base}/../.. && pwd ) 13 | elif [ "$1" = "path" ]; then 14 | echo ${zeek_dist}/build/src:${zeek_dist}/aux/btest:${zeek_dist}/auxil/btest:${base}/:${zeek_dist}/aux/zeek-cut:${zeek_dist}/auxil/zeek-cut:$PATH 15 | else 16 | echo "usage: `basename $0` " >&2 17 | exit 1 18 | fi 19 | else 20 | # Use Zeek installation for testing. In this case zeek-config must be in PATH. 21 | if ! which zeek-config >/dev/null; then 22 | echo "zeek-config not found" >&2 23 | exit 1 24 | fi 25 | 26 | if [ "$1" = "zeekpath" ]; then 27 | zeek-config --zeekpath 28 | elif [ "$1" = "zeek_plugin_path" ]; then 29 | ( cd ${base}/../.. && pwd ) 30 | elif [ "$1" = "path" ]; then 31 | echo ${PATH} 32 | else 33 | echo "usage: `basename $0` " >&2 34 | exit 1 35 | fi 36 | fi 37 | -------------------------------------------------------------------------------- /tests/Scripts/gquic_events.zeek: -------------------------------------------------------------------------------- 1 | 2 | @load Corelight/GQUIC 3 | 4 | event gquic_packet(c: connection, is_orig: bool, hdr: GQUIC::PublicHeader) 5 | { 6 | print "gquic_packet", c$id, is_orig, hdr; 7 | } 8 | 9 | event gquic_client_version(c: connection, version: count) 10 | { 11 | print "gquic_client_version", c$id, version; 12 | } 13 | 14 | event gquic_version_negotiation(c: connection, is_orig: bool, 15 | versions: index_vec) 16 | { 17 | print "gquic_version_negotiation", c$id, is_orig, versions; 18 | } 19 | 20 | event gquic_reset(c: connection, is_orig: bool) 21 | { 22 | print "gquic reset", c$id, is_orig; 23 | } 24 | -------------------------------------------------------------------------------- /tests/Traces/gquic-negotiation.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corelight/zeek-quic/9733ad355368bacef8de874f954947900c6b3b77/tests/Traces/gquic-negotiation.pcap -------------------------------------------------------------------------------- /tests/Traces/gquic-q039.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corelight/zeek-quic/9733ad355368bacef8de874f954947900c6b3b77/tests/Traces/gquic-q039.pcap -------------------------------------------------------------------------------- /tests/Traces/gquic-q043.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corelight/zeek-quic/9733ad355368bacef8de874f954947900c6b3b77/tests/Traces/gquic-q043.pcap -------------------------------------------------------------------------------- /tests/btest.cfg: -------------------------------------------------------------------------------- 1 | [btest] 2 | TestDirs = gquic 3 | TmpDir = %(testbase)s/.tmp 4 | BaselineDir = %(testbase)s/Baseline 5 | IgnoreDirs = .svn CVS .tmp 6 | IgnoreFiles = *.tmp *.swp #* *.trace .DS_Store 7 | 8 | [environment] 9 | ZEEKPATH=`%(testbase)s/Scripts/get-zeek-env zeekpath`:%(testbase)s/../scripts:%(testbase)s/Scripts 10 | ZEEK_PLUGIN_PATH=`%(testbase)s/Scripts/get-zeek-env zeek_plugin_path` 11 | ZEEK_SEED_FILE=%(testbase)s/random.seed 12 | PATH=`%(testbase)s/Scripts/get-zeek-env path` 13 | TZ=UTC 14 | LC_ALL=C 15 | TRACES=%(testbase)s/Traces 16 | TMPDIR=%(testbase)s/.tmp 17 | TEST_DIFF_CANONIFIER=%(testbase)s/Scripts/diff-remove-timestamps 18 | -------------------------------------------------------------------------------- /tests/gquic/q039.zeek: -------------------------------------------------------------------------------- 1 | # @TEST-EXEC: zeek -C -r $TRACES/gquic-q039.pcap gquic_events >output 2 | # @TEST-EXEC: btest-diff output 3 | # @TEST-EXEC: grep gquic conn.log 4 | -------------------------------------------------------------------------------- /tests/gquic/q043.zeek: -------------------------------------------------------------------------------- 1 | # @TEST-EXEC: zeek -C -r $TRACES/gquic-q043.pcap gquic_events >output 2 | # @TEST-EXEC: btest-diff output 3 | # @TEST-EXEC: grep gquic conn.log 4 | -------------------------------------------------------------------------------- /tests/gquic/show-plugin.zeek: -------------------------------------------------------------------------------- 1 | # @TEST-EXEC: zeek -NN Corelight::GQUIC |sed -e 's/version.*)/version)/g' >output 2 | # @TEST-EXEC: btest-diff output 3 | -------------------------------------------------------------------------------- /tests/gquic/version-negotiation.zeek: -------------------------------------------------------------------------------- 1 | # @TEST-EXEC: zeek -C -r $TRACES/gquic-negotiation.pcap gquic_events >output 2 | # @TEST-EXEC: btest-diff output 3 | # @TEST-EXEC: grep gquic conn.log 4 | -------------------------------------------------------------------------------- /tests/random.seed: -------------------------------------------------------------------------------- 1 | 2983378351 2 | 1299727368 3 | 0 4 | 310447 5 | 0 6 | 1409073626 7 | 3975311262 8 | 34130240 9 | 1450515018 10 | 1466150520 11 | 1342286698 12 | 1193956778 13 | 2188527278 14 | 3361989254 15 | 3912865238 16 | 3596260151 17 | 517973768 18 | 1462428821 19 | 0 20 | 2278350848 21 | 32767 22 | -------------------------------------------------------------------------------- /zkg.meta: -------------------------------------------------------------------------------- 1 | [package] 2 | description = Detects the Google QUIC (GQUIC) protocol and adds "gquic" 3 | to conn.log's "service" field. 4 | tags = plugin, analyzer, gquic, quic 5 | plugin_dir = build/Corelight_GQUIC.tgz 6 | script_dir = build/scripts/Corelight/GQUIC 7 | build_command = ./configure && make 8 | aliases = zeek-quic bro-quic 9 | 10 | depends = 11 | zeek >=4.0.0 12 | --------------------------------------------------------------------------------