A new fileless C2 agent called ICMP-Ghost surfaced on GitHub in mid-August, and it’s a useful case study in how far a Linux implant can go without touching disk. The agent is written in pure x64 assembly with zero libc dependencies, injects itself into a running process via ptrace, and communicates using dynamic protocol pivoting — an operator can flip the implant’s channel between raw ICMP echo packets and DNS UDP/53 queries mid-session. Netomize published a network detection write-up on the ICMPv4 and DNS channels on August 14, and the agent reportedly evaded a default Suricata v8.0.3 ruleset in testing, so it’s worth walking through what’s actually detectable and where.
The interesting part for defenders isn’t the payload — it’s the plumbing. No ELF headers, no .text segment on disk, command output routed through anonymous memfd_create files instead of pipes, and a packet-authentication scheme that filters out legitimate ICMP and DNS traffic before the operator’s real commands ever get parsed. That combination means signature matching on the payload bytes is close to useless; detection has to target the syscall chain and the protocol math instead.
MITRE ATT&CK Mapping
| Technique | ID | Description |
|---|---|---|
| Ptrace System Calls | T1055.008 | Remote process injection via ptrace(ATTACH) + mmap/mprotect |
| Reflective Code Loading | T1620 | Position-independent shellcode with no on-disk ELF artifact |
| Application Layer Protocol: DNS | T1071.004 | DNS UDP/53 used as a covert C2 channel |
| Non-Application Layer Protocol | T1095 | Raw ICMP sockets used as a covert C2 channel |
| Obfuscated Files or Information | T1027 | Rolling XOR cipher tuned to mimic benign traffic entropy |
How the Injection Chain Works
The loader (dubbed “Phantom Loader” in the public source) doesn’t drop a binary. It walks /proc for a target process by command name, ptrace(ATTACH)s to it, remotely mmap()s an RW region into the target’s address space, writes position-independent shellcode into that region, then mprotect()s it from RW to RX before redirecting the instruction pointer and detaching. That RW→RX transition on a freshly mapped, unbacked memory region combined with a ptrace(ATTACH)/ptrace(DETACH) pair on an unrelated process is the single highest-fidelity host indicator this implant leaves behind — it’s a small set of syscalls, but the sequence is unusual outside of debuggers.
Once running, the agent writes command output to memfd_create-backed anonymous files rather than disk or a named pipe, which shows up under /proc/<pid>/fd/ as [shm]-style entries with no backing path.
Sigma Rule — Ptrace Injection Chain (auditd)
title: Suspicious Ptrace Attach with RW-to-RX Memory Transition
id: 6a1e9d34-8b7f-4c2a-9e15-3d7f0a2c6e4b
status: experimental
description: Detects a ptrace ATTACH to an unrelated process followed by an
mprotect call transitioning memory from writable to executable — the
injection pattern used by the ICMP-Ghost fileless C2 loader ("Phantom Loader").
author: SOC Analyst Hub
date: 2026-08-26
tags:
- attack.defense_evasion
- attack.t1055.008
- attack.t1620
logsource:
product: linux
service: auditd
detection:
selection_ptrace:
type: SYSCALL
syscall: 'ptrace'
a0:
- '16' # PTRACE_ATTACH
- '17' # PTRACE_DETACH
success: 'yes'
selection_mprotect:
type: SYSCALL
syscall: 'mprotect'
success: 'yes'
filter_debuggers:
comm:
- 'gdb'
- 'strace'
- 'ltrace'
- 'perf'
timeframe: 5s
condition: selection_ptrace and selection_mprotect and not filter_debuggers
falsepositives:
- Legitimate debuggers, profilers, or crash-handling tools attaching to processes
- JIT runtimes performing self-modifying code transitions (baseline your JVM/V8 hosts)
level: high
Sigma Rule — memfd_create Anonymous Output Buffer
title: Anonymous memfd_create File Following Ptrace Attach
id: 1f4c8e02-5a93-4d76-b2e1-8c9f6d4a0173
status: experimental
description: Detects creation of an anonymous memfd-backed file shortly after
a ptrace attach to the same process, consistent with fileless command output
handling used by ICMP-Ghost.
author: SOC Analyst Hub
date: 2026-08-26
tags:
- attack.defense_evasion
- attack.t1620
logsource:
product: linux
service: auditd
detection:
selection_ptrace:
type: SYSCALL
syscall: 'ptrace'
success: 'yes'
selection_memfd:
type: SYSCALL
syscall: 'memfd_create'
success: 'yes'
timeframe: 30s
condition: selection_ptrace and selection_memfd
falsepositives:
- Container runtimes and sandboxing tools that legitimately use memfd_create
- Chromium/Electron processes (they use memfd_create heavily — scope this rule away from browser hosts)
level: medium
Detecting the ICMP Channel
The ICMP-Ghost protocol uses an asymmetric authentication check on the ICMP Echo identifier and sequence fields specifically so the implant ignores normal ping traffic: a valid inbound command packet requires identifier + sequence == 45000, and a valid reply requires identifier + sequence == 55000. It also copies real iputils ping padding bytes (0x10–0x1F) and uses RDTSC-derived timestamps to look like a legitimate ping payload structurally. That authentication math is exactly what makes it detectable on the wire — it’s a fixed constant that real ping traffic will only satisfy by coincidence.
If you’re forwarding Zeek icmp.log or equivalent flow records with ICMP identifier/sequence fields into your SIEM, you can hunt directly on the sum:
// Requires ICMP identifier/sequence fields — adjust table/column names to your Zeek/Corelight ingestion pipeline
CommonSecurityLog
| where DeviceEventClassID == "icmp" or Activity has "ICMP"
| extend IcmpId = toint(extract(@"id=(\d+)", 1, AdditionalExtensions)),
IcmpSeq = toint(extract(@"seq=(\d+)", 1, AdditionalExtensions))
| where isnotnull(IcmpId) and isnotnull(IcmpSeq)
| extend IdSeqSum = IcmpId + IcmpSeq
| where IdSeqSum in (45000, 55000)
| summarize PacketCount = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated)
by SourceIP, DestinationIP
| where PacketCount > 3
| order by PacketCount desc
Even without the identifier/sequence fields, a simpler and very durable baseline is volume: sustained bidirectional ICMP Echo Request/Reply pairs between an internal host and a single external IP, outside of normal monitoring/uptime-check traffic, over a period of minutes rather than a single ping burst.
// Generic ICMP volume anomaly — works against most flow/firewall log sources
CommonSecurityLog
| where TimeGenerated > ago(1h)
| where Protocol == "ICMP"
| summarize PacketCount = count(), Bytes = sum(SentBytes + ReceivedBytes),
FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated)
by SourceIP, DestinationIP
| where PacketCount > 100 // tune against your environment's monitoring-tool baseline
| extend DurationMin = datetime_diff('minute', LastSeen, FirstSeen)
| where DurationMin > 2
| order by PacketCount desc
Detecting the DNS Channel
When pivoted to DNS, the implant encodes data with Base32 (RFC 4648) rather than raw hex, rotates queries across a pool of five domains it treats as a CDN-style front, and simulates RFC 1035-compliant responses (QR=1, RCODE=0, ANCOUNT=1) with synthetic A records. The Base32 encoding and the fixed five-domain rotation are the two most usable signals — Base32-encoded subdomains are longer and use a smaller character set than typical hex-encoded DNS tunnels, and a small fixed set of apex domains receiving high query volume from a single host is a classic tunneling tell regardless of the encoding.
Sigma Rule — Base32-Pattern DNS Query Volume to Rotating Domain Set
title: High-Volume Base32-Like DNS Subdomain Queries to Small Domain Set
id: 9e2d5c71-3f84-4b19-a6d0-7c1e4f9b2a58
status: experimental
description: Detects a client issuing a high volume of DNS A-record queries
with long, Base32-charset subdomains rotating across a small pool of apex
domains — consistent with ICMP-Ghost's DNS tunneling channel.
author: SOC Analyst Hub
date: 2026-08-26
tags:
- attack.command_and_control
- attack.t1071.004
- attack.exfiltration
logsource:
category: dns
detection:
selection:
query_type: 'A'
query|re: '^[A-Z2-7]{20,63}\.'
condition: selection
falsepositives:
- Legitimate CDN or load-balancer subdomains using long alphanumeric identifiers
- Some anti-malware and MDM agents use similarly structured check-in subdomains
level: medium
KQL — DNS Beacon: Long Base32 Labels Concentrated on Few Apex Domains
DnsEvents
| where TimeGenerated > ago(24h)
| extend Label = tostring(split(Name, ".")[0])
| where strlen(Label) >= 20
| where Label matches regex @"^[A-Z2-7]+$"
| extend ApexDomain = strcat(tostring(split(Name, ".")[-2]), ".", tostring(split(Name, ".")[-1]))
| summarize QueryCount = count(), DistinctLabels = dcount(Label), Clients = make_set(ClientIP, 5)
by ApexDomain
| where DistinctLabels > 20 and QueryCount > 50
| order by QueryCount desc
If your DNS logging captures response codes, cross-check for the synthetic-response pattern: a high ratio of NOERROR responses each carrying exactly one A record, for queries where the subdomain length and entropy are inconsistent with any real CDN naming convention your org uses.
Why Signature Matching Fails Here
The implant’s rolling XOR cipher shifts its key every message and deliberately avoids AES-style S-boxes and constant tables, producing entropy around 5.0 Shannon instead of the ~8.0 you’d expect from real encryption — close enough to plausible application data that DPI entropy thresholds tuned for “detect encryption” won’t fire, and there are no static byte sequences for YARA to anchor on. Combined with the DPCM-RLE compression hybrid that fragments payloads unpredictably, payload-content detection is a dead end for this family. Everything in this guide targets syscall sequences and protocol-field math instead, both of which are structural to how the implant has to operate and much harder for an operator to vary without breaking the tool.
Baseline and Tuning Notes
The ptrace/mprotect Sigma rule is high value but needs a debugger/profiler allowlist tuned to your fleet — expect noise from APM agents and JIT runtimes if you don’t scope filter_debuggers correctly. The memfd_create rule is noisy on browser and container hosts by design; restrict its logsource to servers where Chromium/Electron and container runtimes aren’t expected. For the ICMP identifier+sequence rule, false positives are essentially zero once you have the field extraction working — the 45000/55000 constants are specific enough that legitimate ping traffic will rarely coincide, but confirm your Zeek/Corelight parser is actually populating the identifier and sequence fields before relying on it. For the DNS rules, baseline your own CDN and MDM subdomain conventions first; a week of tuning against known-good long-subdomain traffic will cut false positives substantially before you move either rule to a blocking or high-priority alerting posture.