Detect ICMP-Ghost Implant ICMP and DNS Tunnelling C2 Traffic Using PacketSmith Yara-X & ICMP Detection Modules
We empower businesses with cutting-edge software and expert services to navigate the complexities of today's cyber landscape. Secure your network with cutting-edge software and services that ensure your safety and peace of mind!
Introduction
ICMP-Ghost is an open-source tunnelling framework written in pure x64 assembly. What stands out about this framework, compared to other closed- and open-source ones, is the author's claims about its EDR evasion and Suricata IDS/IPS evasion capabilities. The framework supports a dual-channel C2 architecture (despite the exclusivity of the "ICMP" in the framework title), including ICMPv4 and DNS, with the ability to switch between them on the fly. The author makes some grandiose claims about its architecture and design with respect to performance, efficiency, endpoint and network evasion, and memory footprint.
Despite the author's claims, in this article, we detail the structures of each of the C2 protocols, along with PacketSmith Yara-X detection module rules for detecting the traffic of both C2 channels.
ICMPv4 Tunnelling
ICMP-Ghost ICMP tunnelling channel uses the Echo Request (type: 8, code: 0) and Reply (type: 0, code: 0) packets.
ICMP Echo Request Packet
The structure of the ICMP Echo Request Packet is as follows:
| Offset | Length | Description |
|---|---|---|
| 0x00 | 0x08 | RDTSC value |
| 0x08 | 0x0F | Padding |
| 0x18 | 0x38 | Rolling-XOR encrypted data (chunk size 56 bytes) |
The ICMP Identifier and Sequence numbers are mathematically linked and used for asymmetric authentication to ensure that the packet received belongs to ICMP-Ghost, and in case the packet is not successfully validated, the packet is dropped. This relationship is established as follows:
The sum of the Identifier and the Sequence numbers has to be equal to 45000.
$$(Identifier + Sequence) == 45000 \ | \ Identifier \in [10000, 29999)$$
So, with the established conditions, the next question should be: what's the valid range of the Sequence number with respect to the rest of the constraints? This could be derived as follows:
$$Sequence = Identifier - 45000$$
$$\left|\begin{array}{l} \ if\ id = 10,000 \to seq = 35000 \ \ if\ id = 29,999 \to seq = 15001 \end{array}\right.$$
$$\therefore \ \ 15001 <= Sequence <= 35000$$
We'll leverage these constraints and relationships to enforce the values on the specified ICMP headers to harden the detection logic and detect ICMP-Ghost traffic.
The RDTSC value is dynamically generated per packet. There's a caveat about how the RDTSC value is computed in the code:
cld
lea rdi, [rbp + 0x100 + 32] ; Write data after 8 (Header) + 24 (Mimicry)
xor al, al
mov rcx, 56
rep stosb
; Update dynamic timestamp for stealth
rdtsc
mov [rbp + 0x100 + 8], rax
Only the low 32 bits are accounted for (rax), and effectively the high 32-bits are always zeros. We enforce this condition in the rule as follows:
uint32be(idata + 4) == 0x00000000 // rdtsc high 32-bits
The padding is hardcoded in the code, mimicking the Linux ICMP 16 bytes [0x01 - 0x1f].
The control command data is rolling-XOR encrypted with the fixed key 0x42 as the seed, adding 0x07 to it for every subsequent byte.
The following is a screenshot of the actual packet sent by the client to the server:
The following PacketSmith Yara-X detection rule detects the ICMPv4 Echo request packet, checking for all the aforementioned indicators irrespective of any fixed indicators, except for the padding characters. This rule uses the Pattern Identifier icmp4.
rule icmp_ghost_implant_icmp_echo_request_c2_detection
{
meta:
description = "Detect ICMP-Ghost ICMP Echo Request C2 traffic"
reference = """https://github.com/JM00NJ/ICMP-Ghost-A-Fileless-x64-Assembly-C2-Agent
https://netacoding.com/posts/icmp-ghost/
"""
filter = "Frames (frames:)"
author = "Netomize"
date = "08/11/2026"
strings:
$padding = { 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f }
condition:
ip4.is_set and not ip4.in_ip6
and
icmp4.is_set and icmp4.type == 0x08 and icmp4.code == 0x00
and
with ident_num = uint16be(icmp4.data.offset), seq_num = uint16be(icmp4.data.offset+2),
idata = icmp4.data.offset + 4, isize = icmp4.data.size :
(
// data is rolling XOR encrypted
isize >= 28 + 56 // (28 -> ident, seq, rdtsc value and the padding bytes) + (56 -> chunk size)
and
math.in_range(ident_num, 10000, 29999)
and
math.in_range(seq_num, 15001, 35000)
and
(ident_num + seq_num == 45000) // asymmetric authentication
and
uint32be(idata + 4) == 0x00000000 // rdtsc high 32-bits
and
$padding at (idata + 8) // + 8 -> skipping over the rdtsc value
)
}
ICMP Echo Reply Packet
For the ICMP echo reply packet, ICMP-Ghost uses the same structure to construct the packet, except for the following differences:
The relationship between the Identifier number and the Sequence numbers is such that the sum of both has to be equal to 55000.
The data after the padding characters is VESQER compressed and rolling-XOR encrypted with the same algorithm and constants as that of the Echo request packet.
The structure of the ICMP Echo Reply Packet is as follows:
| Offset | Length | Description |
|---|---|---|
| 0x00 | 0x08 | RDTSC value |
| 0x08 | 0x0F | Padding |
| 0x18 | Variable | Rolling-XOR encrypted and VESQER compressed data |
The sum of the Identifier and the Sequence numbers has to be equal to 55000.
$$(Identifier + Sequence) == 55000 \ | \ Identifier \in [10000, 29999)$$
So, with the established conditions, the next question should be: what's the valid range of the Sequence number with respect to the rest of the constraints? This could be derived as follows:
$$Sequence = 55000 - Identifier$$
$$\left|\begin{array}{l} \ if\ id = 10,000 \to seq = 45000 \ \ if\ id = 29,999 \to seq = 25001 \end{array}\right.$$
$$\therefore \ \ 25001 <= Sequence <= 45000$$
We'll leverage these constraints and relationships to enforce the values on the specified ICMP headers to harden the detection logic and detect ICMP-Ghost traffic.
The following is a screenshot of the actual packet sent by the server to the client:
The following PacketSmith Yara-X detection rule detects the ICMPv4 Echo reply packet, checking for all the aforementioned indicators irrespective of any fixed indicators, except for the padding characters. This rule uses the Pattern Identifier icmp4.
rule icmp_ghost_implant_icmp_echo_reply_ctrl_cmd_c2_detection
{
meta:
description = "Detect ICMP-Ghost ICMP Echo Reply C2 control command traffic"
reference = """https://github.com/JM00NJ/ICMP-Ghost-A-Fileless-x64-Assembly-C2-Agent
https://netacoding.com/posts/icmp-ghost/
"""
filter = "Frames (frames:)"
author = "Netomize"
date = "08/11/2026"
strings:
$padding = { 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f }
condition:
ip4.is_set and not ip4.in_ip6
and
icmp4.is_set and icmp4.type == 0x00 and icmp4.code == 0x00
and
with ident_num = uint16be(icmp4.data.offset), seq_num = uint16be(icmp4.data.offset+2),
idata = icmp4.data.offset + 4, isize = icmp4.data.size :
(
isize > 28 + 10 // data is rolling XOR encrypted and VESQER compressed
and
math.in_range(ident_num, 10000, 29999)
and
math.in_range(seq_num, 25001, 45000)
and
ident_num + seq_num == 55000 // asymmetric authentication
and
uint32be(idata + 4) == 0x00000000 // rdtsc high 32-bits
and
$padding at (idata + 8) // + 8 -> skipping over the rdtsc value
)
}
DNS Tunnelling
In addition to the ICMP Echo tunnelling channel, ICMP-Ghost supports DNS tunnelling, but not in the traditional sense of how DNS tunnelling works. The whole DNS Query packet is constructed byte-by-byte over UDP. There's no DNS Response packet from server-to-client; it is a Query packet in both directions. It's attempting to mimic the structure of a DNS query packet.
This is an example of a DNS query packet sent by ICMP-Ghost:
Similar to the ICMP asymmetric authentication, the DNS channel employs a similar one, based on the Transaction ID (TransID), with the constraint:
$$TransID(HighByte) + TransID(LowByte) = 0xff$$
For example, the TransID of 0xe01f shown in the screenshot above:
$$0xe0 + 0x1f == 0xff$$
The TransID is dynamically generated per packet.
The Flags, Questions, Answer RRs, Authority RRs and Additional RRs values are hardcoded in both directions. Moreover, the query is fixed to type A, class IN, with the author claiming that this is meant to blend with normal DNS traffic.
The tunnelled data is passed in the query name, and in the first segment/label/subdomain only, up to a max of 56 characters, base32 encoded (DNS chunk size is 35 bytes before encoding), consisting of the character class [a-z2-7]. The data is VESQER compressed and rolling-XOR encrypted with the same algorithm and constants as that of the Echo request packet, barring these differences between the client-to-server query packet and server-to-client query packet:
From client-to-server, the data is rolling-XOR encrypted and base32 encoded.
From server-to-client, the data is VESQER compressed, rolling-XOR encrypted and finally base32 encoded.
The second-level-domain and top-level domain are dynamically indexed per request, from a list of hardcoded benign domain names: github.com, microsoft.com, cloudflare.com, google.com and windows.com.
A potential PacketSmith Yara-X rule would account for all the aforementioned indicators. Moreover, with respect to the subdomain length, although some query packets might contain less than 56 characters, we instead fix it to 56 characters to avoid potential false positives at the expense of some false negatives.
This rule uses PacketSmith Yara-X Pattern Identifier dns.
rule icmp_ghost_implant_dns_query_c2_detection
{
meta:
description = "Detect ICMP-Ghost DNS Query C2 control command traffic"
reference = """https://github.com/JM00NJ/ICMP-Ghost-A-Fileless-x64-Assembly-C2-Agent
https://netacoding.com/posts/icmp-ghost/
"""
filter = "Frames (frames:)"
author = "Netomize"
date = "08/11/2026"
condition:
udp.is_set and dns.is_set
and
((dns.id >> 8) + (dns.id & 0xff)) == 0xff // asymmetric authentication
and
not dns.flag.response
and
dns.flag.opcode == 0
and
not dns.flag.truncated
and
dns.flag.recdesired
and
not dns.flag.z
and
not dns.flag.authenticated
and
dns.count.queries == 1
and
dns.count.ansr_rr == 0
and
dns.count.auth_rr == 0
and
dns.count.addi_rr == 0
and
// Type: A (1) (Host Address); Class: IN (0x0001)
dns.qry[0].type == 1 and dns.qry[0].class == 1
and
// ex., mjffavtomskxfamakshkfhfevk33qb6cz3ko7yxl6ak74fim3amsekae.github.com
with qry_name = dns.qry[0].name, nlabels = qry_name.labels.total :
(
// minimum query name length (worst-case scenario)
// and number of labels/segments > 2
string.length(qry_name.qname) > 60 and nlabels > 2
and
with first_seg = qry_name.labels.segments[0]:
(
string.length(first_seg) == 56
and
first_seg matches /^[a-z2-7]{56}$/
)
)
}
The above rule uses the dns PaID with the assumption that the DNS traffic is over port 53, but since the packet is a mimic of a DNS Query packet, the user is at liberty to use any other port, thereby rendering the DNS dissector unable to parse it. ICMP-Ghost's default DNS port for testing is set to 5300, and recommended to switch to 53 for production.
Although it is possible to add another port to the DNS dissector, for PacketSmith to recognize it as a DNS packet in the configuration file under the [dns] section, key udp_ports, it is not possible to know beforehand what port this packet will use, therefore, it is prudent to write another rule that attempts to check for the same indicators using the raw udp PaID, port independent, as follows:
rule icmp_ghost_implant_dns_udp_query_c2_detection
{
meta:
description = "Detect ICMP-Ghost DNS UDP Query C2 control command traffic"
reference = """https://github.com/JM00NJ/ICMP-Ghost-A-Fileless-x64-Assembly-C2-Agent
https://netacoding.com/posts/icmp-ghost/
"""
filter = "Frames (frames:)"
author = "Netomize"
date = "08/11/2026"
strings:
// flags, questions, answer rrs, authority rrs, additional rrs
$dheader = { 01 00 00 01 00 00 00 00 00 00 }
$qname_rgx = /[a-z2-7]/
$type_class = { 00 00 01 00 01 }
condition:
udp.is_set
and
with udata = udp.data.offset, qname = udp.data.offset + 12, usize = udp.data.size :
(
// asymmetric authentication
((uint16be(udata) >> 8) + (uint16be(udata) & 0xff)) == 0xff
and
$dheader at (udata + 2)
and
// get qname length
uint8(qname) == 56
and
// check subdomain character set
$qname_rgx in (qname + 1 .. qname + 1 + uint8(qname))
and
// check end of label, type and class in reverse
$type_class at (udata + usize - 5)
)
}
This is possible because most of the UDP packet structure is fixed, except for the query name.
ICMP Detection Module
As detailed in a dedicated article on the official website of PacketSmith, "Detect Suspicious/Malicious ICMP Echo Traffic", PacketSmith employs its own ICMPv4/v6 detection engine that attempts to detect suspicious/malicious ICMP Echo traffic using behavioural and protocol semantic analysis. And this framework's ICMP traffic is the perfect candidate to test it once again, absent any specific detection rule.
Running the ICMP-Ghost pcap against the PacketSmith ICMP detection module, we get the following output:
| ID | Type | Verdict |
|---|---|---|
| 1 | Echo requests with more than one reply | not found |
| 2 | ICMP sequence number not incrementing by 1 | not found |
| 3 | Echo request/reply packets data mismatch | not found |
| 4 | Time series analysis (> 2 pkts per second) | suspicious |
| 5 | Number of echo requests less than 3 | suspicious |
| 6 | Echo requests with no replies | not found |
| 7 | Istream starts with a reply packet | malicious |
| 8 | Payload size is greater than 64 bytes | suspicious |
The details of each of the types are omitted for brevity. Note that considering how ICMP-Ghost ICMP traffic is replayed, certain anomalies are bound to show up. For example, when tunnelling an Echo Request packet, first the system's ICMP network stack responds with the same packet's data payload as an Echo Reply packet using the same sequence and identifier numbers, and the second Echo Reply packet is the tunnelling packet carrying the results of a given command, but using different identifier and sequence numbers.
As shown in the above screenshot, packet 7, the Reply packet to packet 5, starts a new istream since it uses different sequence and identifier numbers.
Conclusion
In this article, we dissected the command and control structure of the open-source framework ICMP-Ghost for both the ICMP and DNS tunnelling channels. Contrary to the claims made by the author about its IDS/IPS evasion capabilities, we managed to write powerful and efficient PacketSmith Yara-X detection module rules to detect the C&C traffic of both channels.
Mohamad Mokbel
August 14, 2026

