# Detect ICMP-Ghost Implant ICMP and DNS Tunnelling C2 Traffic Using PacketSmith Yara-X & ICMP Detection Modules

# Introduction

[ICMP-Ghost](https://github.com/JM00NJ/ICMP-Ghost-A-Fileless-x64-Assembly-C2-Agent) 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:

```c
    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:

![](https://cdn.hashnode.com/uploads/covers/698bbaaf2b3404faadd9aff8/ed5eeb93-c221-4cfe-8984-2dfc0e2950fa.png align="center")

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`.

```javascript
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:

1.  The relationship between the Identifier number and the Sequence numbers is such that the sum of both has to be equal to 55000.
    
2.  The data after the padding characters is [VESQER](https://github.com/JM00NJ/Vesqer-Baremetal-Compressor-DPCM-RLE-Hybrid-Engine) 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:

![](https://cdn.hashnode.com/uploads/covers/698bbaaf2b3404faadd9aff8/06c29f38-63c4-4b7f-beb6-dcbe55ca6c02.png align="center")

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`.

```javascript
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:

![](https://cdn.hashnode.com/uploads/covers/698bbaaf2b3404faadd9aff8/e0b357b3-8875-4f64-b52f-d7692e4be6d8.png align="center")

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](https://github.com/JM00NJ/Vesqer-Baremetal-Compressor-DPCM-RLE-Hybrid-Engine) 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](https://github.com/JM00NJ/Vesqer-Baremetal-Compressor-DPCM-RLE-Hybrid-Engine) 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`.

```javascript
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:

```javascript
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.

For reference, all rules mentioned in this article are available at the official GitHub repo [RFiles](https://github.com/Netomize/RFiles/blob/main/icmp-ghost.yar).

# ICMP Detection Module

As detailed in a dedicated article on the official website of PacketSmith, "[**Detect Suspicious/Malicious ICMP Echo Traffic**](https://packetsmith.ca/detect_icmp_echo_malicious_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) | <mark class="bg-yellow-200 dark:bg-yellow-500/30">suspicious</mark> |
| **5** | Number of echo requests less than 3 | <mark class="bg-yellow-200 dark:bg-yellow-500/30">suspicious</mark> |
| **6** | Echo requests with no replies | not found |
| **7** | Istream starts with a reply packet | <mark class="bg-yellow-200 dark:bg-yellow-500/30">malicious</mark> |
| **8** | Payload size is greater than 64 bytes | <mark class="bg-yellow-200 dark:bg-yellow-500/30">suspicious</mark> |

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.

![](https://cdn.hashnode.com/uploads/covers/698bbaaf2b3404faadd9aff8/cdd47e16-cb94-412c-96c1-08519a5cc1b3.png align="center")

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
