A Network Threat Hunter’s Guide to C2 over QUIC

Section A – Introduction
Every new technology comes with both promise and risk for the security landscape. While established protocols have weathered years of investigation, new ones are largely uncharted territory. For threat actors, the unknown can often be exploited to serve their goals.
The QUIC protocol (RFC 9000) exemplifies this pattern. Shortly after its introduction, the open-source command-and-control framework Merlin, developed by Ne0nd0g, added support for QUIC as a communication channel in July 2018. In this guide, we’ll examine C2 over QUIC through a network threat hunter’s lens with the goal of deriving concrete detection patterns.
In Section B we’ll kick off with a brief overview of the key QUIC concepts essential to our threat hunting discussion. Specifically, we’ll focus on three critical elements: the QUIC handshake, main packet types, and the Connection ID. Understanding these components is fundamental to developing effective detection strategies.
In Section C, we’ll dive into our practical analysis. Using a combination of tools including RITA, Wireshark, Zeek, and custom Python applications, we’ll examine real C2 over QUIC traffic to identify detection opportunities. Our approach will be hands-on and evidence-based.
Data sets used in this guide are available for download at the end of the article, where I’ve also included a link to a GitHub repository containing our custom analysis tool. I encourage you to download these resources and follow along – there’s no better way to learn than through direct experimentation.
Section B – Theory
Important Context
Before diving into QUIC’s technical details, we should establish some important context. Even though understanding the protocol specification can help us know what to expect when analyzing traffic, we need to keep in mind that there is often a difference between the “ideal” and the “real”.
RFCs are specifications, not laws. Despite having the terms “task force” in their title, the IETF is in fact not a special ops team of elite nerds infiltrating the domiciles of developers to arrest them for ignoring RFC guidelines. Developers can, and indeed often do, deviate from the suggestions to suit their own needs. Especially if said developers are creating software that is intended to be inherently deceptive.
As Chris Brenton aptly notes, “Malware does not break the rules, it bends them.” So while RFC 9000 defines how QUIC should behave, tools like Merlin implement the protocol in unexpected ways to achieve their objectives. So think of the RFC as a baseline and don’t expect “real” communication to map to it 1:1 at all times. Indeed, as we’ll see more often than not these exact deviations provide the greatest source of insight to us.
The QUIC Handshake
We can only analyze what we can see. When it comes to QUIC, unless we have the ability to decrypt the communication, almost all of what we can see occurs during the handshake.
To understand QUIC’s handshake, I find it useful to depart from something familiar – the TCP three-way handshake and TLS handshake. Though not the entire picture, the first major change you can think of is that QUIC took these two separate handshakes and merged them together. The intended goal was to, in most circumstances, reduce the required round trips from three to just one – Image 1 below.
The Client’s Initial Packets
As we can see on the right, the QUIC handshake begins with the Initial (INIT) packet from the client. This packet contains a number of important connection initialization parameters – see Image 2 below.

Image 2. The Client’s INIT packet long header contains a number of important pieces of information, including Packet Type (00 – Initial), as well as the Client and Destination IDs.
This initial packet also contains a CRYPTO section, which includes the Client Hello message traditionally found in TLS handshakes – see Image 3 below.
It’s worth mentioning that QUIC initial packets were designed to prevent amplification attacks. Not only must every packet be padded to at least 1200 bytes, but they are also required to include a token field that can help verify the client’s address. This ensures that the server’s responses don’t inadvertently amplify spoofed requests from malicious actors trying to conduct DDoS attacks.
The Server’s Initial Packets
After receiving the client’s initial packets, the server then responds with a series of packets that progress through different encryption levels. It first sends its own Initial packet, which similarly contains connection initialization parameters, as well as the CRYPTO frames containing the TLS ServerHello needed to complete the TLS handshake. This packet also includes an ACK frame acknowledging the client’s Initial packet – see Image 4 below.
Immediately following this, the server sends one or more Handshake packets, which represent a crucial intermediate encryption stage. These Handshake packets contain additional CRYPTO frames carrying the server’s TLS handshake messages (certificates, finished messages, etc.) and are protected with Handshake-level encryption keys derived from the Initial exchange – see Image 5 below.

Image 5. The server’s second packet is termed the handshake (Packet Type 10) and, since it is now encrypted, no longer presents any information in the clear.
Conclusion of Handshake
At this point both the client and server have proposed their sets of connection parameters and cryptographic information. Values that align will be used, and those that differ are determined according to rules specified in RFC 9000. For instance, if the client and server propose different max idle timeout values, the protocol automatically selects the shorter duration (RFC 9000, Section 10.1).
The handshake reaches completion when both endpoints have received and verified all necessary cryptographic material. The server signals this by sending a HANDSHAKE_DONE frame to the client, whereafter the client considers the handshake to be complete. Note however that unless we provide the session keys we won’t be able to see this frame since it’s encrypted and sent as a 1-RTT Packet (RFC 9000 Section 19.20).
Free Exchange of Data
At this point, both parties can freely exchange fully-encrypted 1-RTT packets containing application data. Note that these packets are also known as “protected payload” packets – you can think of them as the “normal” payload packets in QUIC. They are used for the majority of data transmission after the handshake completes.
The “1-RTT” name comes from the fact that a complete round-trip (client to server and back) is required during the handshake before these packets can be sent. Note that 1-RTT packets use a simplified “short header” format that omits the explicit packet type field, as shown in Image 6 below.

Image 6. Since an explicit packet type field is omitted the packet type is implicitly 1-RTT – the presence of a short header itself indicates it’s a 1-RTT packet used for data transfer.
Connection ID
In addition to being able to freely exchange data in both directions, upon successful completion of the handshake, several important capabilities become available including connection migration. This feature allows endpoints to change their network parameters (like IP address or port) while maintaining the connection, and is possible due to one of QUIC’s core innovations – the Connection ID.
Connection IDs serve as the primary connection identifier in QUIC, replacing the traditional 4-tuple of source IP, source port, destination IP, and destination port. Each endpoint generates its own Connection ID, and these IDs are exchanged during the handshake. When network parameters change, the connection persists because packets are tracked by their Connection IDs rather than their network addresses.
Before concluding our theoretical section, it is worth noting a few other packets and frames you might encounter when analyzing QUIC.
0-RTT Packets
0-RTT packets are special – they allow a client to send data to a server before the handshake completes, essentially starting communication immediately. However, there’s a catch: this is only possible if the client has previously connected to the server and saved some information from that connection. Note that some servers might reject 0-RTT packets, in which case they would need to be resent as 1-RTT packets.
RETRY Packets
Retry packets are sent by servers in special circumstances when they want additional verification of a client’s address before proceeding with the connection. When a client receives a Retry packet, they must create a new initial packet including the token from the Retry packet. These packets are unique because they don’t contain any payload data or packet numbers – they just carry connection IDs and the token.
Connection Close Frame
QUIC’s CONNECTION_CLOSE frame exists in two variants: type 0x1c for transport-level errors and type 0x1d for application-level errors. When closing a connection, an endpoint sends this frame within an appropriate packet type (Initial, Handshake, or 1-RTT) and enters a closing state where it stops processing new frames but briefly continues responding to incoming packets with the same CONNECTION_CLOSE frame to ensure reliable delivery.
Conclusion
That covers the core theory we’ll require to effectively understand C2 over QUIC behaviour, and thus detection opportunities. Readers that would like a broader introduction to QUIC in general are encouraged to consult the References section, where I’ve included a number of excellent external resources.
Section C – Practical Analysis
Setup
Our test environment used two systems: a Windows 11 machine (10.0.0.4 or 192.168.2.115) running the Merlin agent, and an Ubuntu 22.04 VM (24.199.110.233) hosting both the Merlin server and CLI components.
We captured three separate 24-hour datasets, varying only the delay and jitter (skew) settings between each. We’ll refer to these datasets by the following labels throughout this report:
- Short (delay 30s, skew 3000ms),
- Medium (delay 90s, skew 3000ms),
- Long (delay 180s, skew 1000ms)
Let’s begin by looking at our results in RITA.
RITA Overview
Below we can see results for our Short (Image 7), Medium (Image 8), and Long (Image 9) datasets.
The most striking observation is that all three connections, regardless of their delay settings, are flagged as long connections and receive a ‘High’ severity score. This occurs because the same QUIC connection persists throughout the entire session. Unlike other C2 frameworks like Cobalt Strike’s HTTPS beacon that terminate and reestablish connections between check-ins, Merlin maintains a single, continuous QUIC connection across all delays.
As we discussed in our previous section, this single QUIC connection is identified by a consistent Connection ID. Though these IDs are crucial for connection tracking, they’re often omitted from payload packet headers when the standard 4-tuple (source IP, source port, destination IP, destination port) is sufficient for packet routing. The IDs are exchanged during the handshake and maintained in connection state, but are not required in every packet.
One way in which we could identify continuous QUIC connections is by monitoring for specific termination events: idle timeouts, explicit connection closes, fatal errors, or resource exhaustion. However, there’s a simpler method – tracking the source port. Since new connections between the same hosts typically use different source ports, if the first and last packets in a trace share the same source port, it’s likely a single, persistent connection.
The exception to this is the case of connection migration taking place on the client’s end. But, since most systems targeted are typically stationary workstations, this should not be an issue. If however we are analyzing a potential compromise of a portable host known to switch connection types this no longer remains true.
WireShark – Max Idle Timeout
One puzzling aspect was why these connections never timed out, even with delays up to 3 minutes. While QUIC connections can indeed timeout – the client and server agree on an idle timeout value during the handshake – something was presumably preventing this.
Let’s examine our Wireshark capture to understand the timeout behavior. We’ll start by checking the negotiated max idle timeout values in our Medium dataset. Opening the initial packet (#3041, client to server Initial packet), we’ll find these values under QUIC IETF > CRYPTO > Client Hello > Extension: quic_transport_parameters, as shown in Image 10 below.
Looking at max_idle_timeout in the client packet above, we see a proposed value of 30000 ms (30 seconds). As mentioned before, the client and server must agree on a timeout value, and if there is a discrepancy the shortest proposed duration becomes the effective timeout.
While the server’s proposed value is already encrypted (in the handshake packet), we can infer the value by looking at the open-source code of Merlin here. The relevant server configuration is shown in Image 11 below.
With the server proposing 42 months and the client proposing 30 seconds, the latter becomes the effective timeout for this connection.
WireShark – Payload Packet Delta
Let’s examine our payload packets, focusing on the time intervals (delta) between consecutive packets, as shown in Image 12 below. This should help us understand why the connection persists despite having delays longer than the timeout period.
Looking at Image 12, we can see a series of 6 packets from client to server, each separated by roughly 15-second intervals (highlighted by red rectangles). This suggests that Merlin breaks down a 90-second delay into six 15-second intervals. Note that this same pattern is found throughout the entire trace file.
We can verify this behavior by examining our other datasets – the Short dataset with its 30-second delay (Image 13) and the Long dataset with its 180-second delay (Image 14).
Looking at Images 13 and 14 confirms our pattern: the Short dataset splits its 30-second delay into 2 intervals of 15 seconds, while the Long dataset breaks its 180-second delay into 12 intervals of 15 seconds. Regardless of the delay settings, Merlin’s QUIC agent appears to chunk all communication into approximately 15-second intervals between client-to-server packets. This consistent behavior explains how Merlin maintains its connection despite delays longer than the timeout period.
While this consistent pattern – packets repeating at precise 15-second intervals throughout the connection – could serve as a detection signature, implementation would require pcap analysis since Zeek doesn’t preserve these timing deltas. Let’s therefore focus instead on detection strategies using Zeek, which offers more practical value for network threat hunting.
Zeek – quic.log > uid > conn.log
Starting with Zeek 6.1.0 (October 2023), QUIC connections automatically generate entries in quic.log. The log file from our Medium dataset can be seen in Image 15 below. For detailed information about the protocol parameters and example outputs, see the provided links here and here.
The quic.log shows 40 total QUIC connections in our dataset, with our target connection appearing at the top. The connection’s unique identifier (uid: C2g6dk3BeKeu4IrGhk) – coincidentally starting with “C2” – can be used to locate additional details in conn.log, as shown in Image 16.
Zeek – conn.log Duration
In this case, while conn.log provides more granular connection details, it doesn’t reveal anything significant beyond what RITA already showed us. However, when we expand our search in conn.log to examine all QUIC connections, something notable emerges – as shown in Image 17 below.
It’s clear that the connection duration produced by Merlin was vastly more than any others recorded on this host during that same time period. To better visualize this pattern, we can examine a bar graph with connection labels on the y-axis and a logarithmic scale of duration on the x-axis, as shown in Image 18 below.
Our bar graph emphasizes how Merlin’s connection duration stands apart. While typical QUIC connections in our dataset ranged from 2.58 ms to 30.50 s, the Merlin connection persisted for almost 24 hours. Most striking is that the majority of legitimate connections lasted less than one second – note the logarithmic scale.
This extreme persistence in QUIC connections appears to be a strong indicator for C2 traffic. However, we should note that our test environment, with its unused target system, may not fully represent enterprise scenarios where legitimate long-duration QUIC traffic might be more common. To gather more representative data, we analyzed QUIC connections over a 24-hour period on a workstation with an active user and multiple UDP-based services – Image 19 below.

Image 19. An overview of QUIC connections (6423) seen in a “more representative” 24-hour trace file.
The distribution reveals that the vast majority of QUIC connections are brief: 4,364 connections lasted less than 0.1 seconds, and 1,800 connections fell between 1 second and 1 minute. Only 9 connections (0.14% of total) persisted beyond an hour, with the longest connection in the 24-hour period being 4 hours and 11 minutes.
So even in an environment with active users, a QUIC connection persisting for nearly 24 hours likely remains unusual. That makes intuitive sense – typical workstation behavior generates QUIC connections that naturally terminate due to application timeouts, user activity patterns, and browser resource management.
So while long-duration connections should always be evaluated in context, a QUIC connection spanning almost 24 hours merits careful investigation, as it suggests intentional evasion of standard timeout behaviors. I believe that this specific angle – looking at the most persistent QUIC connections in a dataset – can serve as a very effective way to tease out potential C2 over QUIC connections – at least for the time being 😉
Let’s pivot back to quic.log to discuss one final potential detection vector.
Zeek – quic.log History
Looking back at our Medium dataset’s quic.log in Image 15, Zeek provides several other data columns for QUIC traffic. Of particular interest is the final column, History, which uses a sequence of letters to record the chronological order of key network events – shown in Image 20 below.

Image 20. The History column’s letters represent connection events, with uppercase letters indicating client-originated events and lowercase for server-originated ones.
While technically showing the entire connection history, these values primarily capture handshake events, as most recorded events (except perhaps C/c) occur during connection establishment.
Examining our datasets, Merlin produces distinctive history patterns – either ISishIH or ISisIH. These stand out for two reasons: they’re notably shorter than typical QUIC histories and uniquely begin with a single I rather than the double I seen in all other connections across all 3 datasets.
After initially suspecting this abbreviated handshake pattern might be due to connection resumption, I tested with new host pairs and recompiled binaries – yet the unusual pattern persisted.
Next, I investigated whether this behavior stemmed from the default configuration of Merlin’s QUIC module (quic-go 0.47). However, a test implementation of basic gRPC over QUIC using the same module produced a different history pattern: IIiishZZZH.
After finding no explicit handshake configurations in the codebase, I investigated whether Merlin’s unusual QuicConfig settings might produce this behavior as an unintended emergent behaviour. However, even after adjusting these settings to match more conventional implementations, the distinctive Merlin handshake pattern remained unchanged.
As is advised by the Buddhist “Parable of the Poisonous Arrow”, if struck by a poisonous arrow, the victim does not need to know who shot the arrow, or for what reason, in order to allow a healer to save you from its effects. Or in this case, we don’t need to know what’s causing this unusual handshake, or why, but merely that it is in fact unusual and can therefore potentially be leveraged as a detection vector.
Custom Python Analysis
To validate this potential detection method, I wrote a Python script to analyze the History column patterns across our three Merlin datasets and four control datasets (without Merlin present). This allowed us to assess the fingerprint’s detection accuracy.
The script examined 433 QUIC connections across all seven datasets, with three datasets containing Merlin connections to 24.199.110.233. It identified potential Merlin servers by searching for the unique history patterns (ISishIH or ISisIH). The results of this analysis are shown in Image 21 below.

Image 21. Results of our Python script looking for the presence of the Merlin Handshake fingerprint.
The results are perfectly accurate, successfully identifying the single Merlin connection in each experimental dataset while generating no false positives in the control sets. While our sample size of 433 connections across seven datasets isn’t exhaustive, this preliminary analysis suggests that Merlin’s unique QUIC handshake pattern could serve as an effective detection signature.
Section D: Conclusion
Our investigation into Merlin’s C2 over QUIC implementation revealed three distinct detection strategies.
RITA
Using RITA, we consistently detected Merlin’s C2 over QUIC activity owing to its persistent connection pattern. This demonstrates RITA’s protocol-agnostic strength – whether traffic flows over HTTP/1.1, HTTP/2, or HTTP/3, RITA focuses on the universal characteristic that matters most: connection duration.
A perpetual connection will always rise to the top, and as we are keen to advise – investigate all long-running connections and invest the time upfront to safelist those with a known business need. Any company doing this will create an extremely effective, protocol-agnostic detection to keep their infrastructure safe.
Duration, QUIC-specific (using Zeek’s conn.log)
While RITA examines duration across all protocols, we discovered additional value in analyzing QUIC connection durations specifically through Zeek’s conn.log. Our analysis showed that typical QUIC connections last only seconds, with multi-hour connections being extremely rare.
This makes unusually persistent QUIC connections stand out even more prominently. Thus by focussing on duration specifically within a dataset containing only the QUIC connections on your network one could possibly identify C2 over QUIC connections.
This is because Merlin’s agent was specifically designed to ensure never timing out, based on it’s agent’s QuicConfig settings – see Image 22 below.

Image 22. Even in the case where the delay may exceed the idle timeout period, Merlin’s agent will send a Keep-Alive PING over HTTP/2 to prevent a timeout from occurring.
This careful approach to maintain persistence, while intended to be stealthy, actually makes these connections more conspicuous. Sometimes something can be so inconspicuous, that it in fact becomes conspicuous – “conspicuously inconspicuous”, if you like. Networks and applications naturally experience timeouts and connection resets, meaning perfect persistence is itself an anomaly.
This being the case, an extremely persistent QUIC connection is in my opinion the most sure-fire way to identify C2 over QUIC activity mediated by Merlin. At least as of this writing. The bad news of course is that it won’t take much for Merlin to alter this behaviour to recreate this connection every couple of hours. In that case they would appear as new connections in Zeek as their Connection ID and 4-tuples will change.
The good news however is that, when it comes to RITA, that matters not. RITA identifies these attempts to disguise persistent connections and will tally the cumulative time between the two hosts over the analyzed period. As RITA also indicates service (quic in this case) we advise always being on the lookout for persistent connections over QUIC.
Merlin Handshake Fingerprint
Our third discovery was Merlin’s distinctive abbreviated handshake pattern. This unique signature, visible in Zeek’s quic.log, can be detected through simple pattern matching, providing an additional method for identifying Merlin’s C2 traffic.
Final Thoughts
The emergence of new protocols like QUIC continues the familiar cycle: attackers seek novel exploitation opportunities, while defenders develop new detection strategies. Our analysis demonstrates that effective detection can emerge from protocol-agnostic patterns (like RITA’s duration analysis), protocol-specific behaviors (like QUIC handshake fingerprinting), and a combination of the two (like QUIC-specific duration analysis).
This research reinforces a crucial insight about command and control traffic: true stealth isn’t necessarily about minimizing noisy behaviour, but about blending in with legitimate traffic. A person walking into a bank wearing a ski-mask might have succeeded in concealing their identity, but they’ve almost certainly also attracted unwanted attention.
RITA’s enduring value lies in its ability to cut through protocol details and identify these fundamental C2 patterns, providing a stable foundation for threat detection. While we’ll most certainly continue to develop protocol-specific detection techniques in an attempt to keep up, these invariably become obsolete. RITA on the other hand will continue to provide foundational network protection as the various transport trends inevitably come and go.
I think in the end some form of a multi-layered approach serves best to aid us in detecting the presence of C2 malware – RITA’s broad pattern analysis as the foundation, combined with protocol-specific detections to look for specific current threats. Attackers will continue to explore new protocols and techniques – that’s what they do. So let’s continue to deepen our understanding of both the universal patterns of C2 traffic and the specific quirks of individual implementations in order to best keep our networks safe.
References
General Introduction to QUIC
Personal favourite introductory talk on QUIC (Peter Door)
Another excellent introductory lecture + WireShark analysis (Chris Greer)
Technical Guides to QUIC
Excellent technical overview of QUIC specification (Author Unknown)
RFC 9000: Core QUIC Protocol
RFC 9001: QUIC TLS Security
RFC 9002: QUIC Loss Detection and Congestion Control
QUIC Security + Detection
An informative, though perhaps dated, talk on QUIC fingerprinting (Caleb Yu & John Althouse)
Overview of Zeek’s QUIC protocol
Overview of Zeek’s quic.log
“Revisiting QUIC attacks” (Chatzoglou et al.) – review article discussing broad security challenges related to QUIC
Concise + informative lecture discussing vulnerabilities of QUIC (Diego Esquivel)
Data
Merlin C2 over QUIC PCAP – Short Dataset
Name: merlin_quic_d30_j30_24hr.pcapng
File Size: 90.8 MB
SHA-256 Hash: 77EBEF78B108D0F3C950B3B1926BDCBA6FFE232D48CDA15B7EBF3DB15899477A
Merlin C2 over QUIC PCAP – Medium Dataset
Name: merlin_quic_d90_j30_24hr.pcapng
File Size: 1.8 GB
SHA-256 Hash: DECBE98B91DE0D1EDD108ACDBD6CE7ADB531A02D0C5BC6EB9200247198C9F5E5
Merlin C2 over QUIC PCAP – Long Dataset
Name: merlin_quic_d180_j10_24hr.pcapng
File Size: 1.8 GB
SHA-256 Hash: 303DFB0C3A9F52298B16354C09F23EB7A2788426AC4088B96A0E13A43D646366
Merlin C2 over QUIC Zeek logs – Short Dataset
Name: zeek_quic_d30_j30_24hr.zip
File Size: 5.5 MB
SHA-256 Hash: F0888E0736E6B05595FE11D18C53E00A10B830F808EB8A054E6426BB8B6447BF
Merlin C2 over QUIC Zeek logs – Medium Dataset
Name: zeek_quic_d90_j30_24hr.zip
File Size: 6.3 MB
SHA-256 Hash: 19739D6BD00129BAB64A2179ED610445F1E7B4ABC9CEAE78E7B097743E048D50
Merlin C2 over QUIC Zeek logs – Long Dataset
Name: zeek_quic_d180_j10_24hr.zip
File Size: 6.4 MB
SHA-256 Hash: 8C0573D9D1D74731B622158C52793D04EC044AED2DA49A767C7695350C9A1FA0
Source Code
The “Merlin QUIC Handshake Fingerprinting” script can be found here. Instructions are available in the README.
Special Thanks
Thanks to Chris Brenton, Bill Stearns, and Keith Chew for their support, encouragement and guidance. And a special shoutout to Ne0nd0g, the author of Merlin C2, who was kind enough to answer my barrage of questions.

Faan has a profound love for the natural world, technology, design, and retro aesthetics. He is incredibly grateful to have discovered cybersecurity as a path relatively late in his life, and his main interests are threat hunting and post-exploitation custom tooling, in particular C2 frameworks and RATs.