Skip to main content

Command Palette

Search for a command to run...

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

Updated
12 min readView as Markdown
N

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:

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

For reference, all rules mentioned in this article are available at the official GitHub repo RFiles.

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

C
commSync18d ago

Hi, I'm the author of ICMP-Ghost.

Good analysis overall; you correctly identified the RDTSC high-bits constraint and the asymmetric ID+SEQ authentication. These are real structural fingerprints.

However, I'd push back on the conclusion that these rules defeat the evasion claims:

All rules target the default public build constants. Changing three values; the auth sum (45000/55000), the mimicry padding sequence, and the DNS fake domain breaks every rule you've written. That's a 5-minute recompile for any operator.

More importantly, the VTable protocol pivot (!D/!I) is completely invisible here. An operator who pivots to DNS mid-session and changes the default chunk size produces traffic none of these rules match.

The XOR key (0x42, +0x07) is also hardcoded and public — but your rules don't decrypt the payload to inspect plaintext commands. That would be the harder, more robust detection approach.

These rules detect the toy/demo build, not a hardened operational variant. The evasion claims hold against production deployments.

  • JM00NJ
N

Hi commSync,

Thank you for taking the time to read the article and for your input.

While I understand your pushback on the conclusion, the generalization and the ability to detect such tunnelling traffic using the PacketSmith Yara-X detection module without writing any script, and with relative ease, is a feat in itself.

Your concerns about changing those constants in the code to bypass the detection logic are valid for much malware C&C traffic, except that in your case those constants are embedded in the code itself and are not provided in a separate configuration file. I reckon that a lot of threat actors, read-teamers, script kiddies, and general users of the tool would compile the code as is.

For the ICMP rules, I could also generalize them so they do not account for asymmetric authentication logic, albeit at the expense of some false positives.

With respect to the possibility of detecting the traffic by XOR-decrypting it (key-independent) on the fly, it is not "possible" since the data is VESQER-compressed before encryption.

In the section “ICMP Detection Module”, we demonstrate how the PacketSmith ICMP detection module detects suspicious/malicious ICMP Echo traffic using behavioural and protocol-semantic analysis, independent of any hardcoded rule.

Once again, thank you for your contribution to this article.

Best regards,

M.

C
commSync18d ago

Hi M.,

Thank you for the detailed response.

One correction regarding your Type 7 detection: "istream starts with a reply packet > malicious". Looking at the source directly, the agent's very first action is to send an Echo Request (not a Reply):

asm ; --- First Beacon hello packet --- mov r12, 0 ; Empty payload call [rbp + 0x3010] ; VTable → _icmp_send (Echo REQUEST)

_sniff: call [rbp + 0x3008] ; Now enters recv loop

The istream begins with a Request from the agent. Type 7 would not trigger on a legitimate ICMP-Ghost session; it either produces a false negative here, or your test PCAP was replayed in a way that altered the packet ordering.

Regarding VESQER: this is not a known third-party library. It is a custom DPCM+RLE hybrid compression algorithm I wrote from scratch in pure x86-64 Assembly, with zero external dependencies.

The compression format has no public specification or magic bytes. Even if an analyst XOR-decrypts the payload (the key is public — 0x42, +0x07 per byte), they get compressed data in an undocumented custom format with no standard tooling to decompress it. Two layers of obscurity, not one.

Finally, on scalability: PacketSmith operates on offline PCAPs. In enterprise environments processing tens of thousands of ICMP packets per second, per-stream behavioural correlation becomes computationally impractical for live traffic. The detection works well for forensic post-mortem analysis, but not as a real-time prevention mechanism, which is where the original evasion claim stands.

Best regards, JM00NJ

C
commSync18d ago

That said, I acknowledge that the rules are effective against the default public build, and realistically most users of this tool would compile it as-is without modifying the constants. For that threat model; script kiddies and unsophisticated actors, the detection logic is valid and useful. The original evasion claims hold specifically against a hardened, operationally configured deployment, not the out-of-the-box version.

N

No, it is not the agent sending the first ICMP Echo Reply packet; it is the system's ICMP network stack. This is what I said in the article:

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

You can see it in the attached pcap traffic screenshot for packets 5, 6 and 7. Try it on your own, and let me know the results.

I have no issue with your points on scalability.

N

Fair enough!