NanoCore: Non-Application Layer Protocol — Detection
Detection material for NanoCore’s raw TCP command-and-control channel. Technique write-up: NanoCore_NonAppLayerProtocol. Reference implementation of the socket mechanics: shaddy43/ReverseShell_NC.
MITRE ATT&CK: T1095
Detection Indicators
| Indicator | Value |
|---|---|
| Transport | outbound TCP to a non-standard port, no TLS handshake, no HTTP request line |
| Initiator | a binary under %APPDATA%, %TEMP% or C:\Users\Public\ — not a browser |
| Shape | one long-lived connection, or short reconnects at a regular interval |
| Destination | a bare IP, or a DNS name resolved once and reused for the session’s life |
| Companion | a shell or script host with redirected standard handles, for the interactive variant |
There is no payload signature to match. Every rule below keys on who is holding a socket open and what shape the traffic has, because that is all a raw channel exposes.
Log Sources
- Microsoft Defender XDR (
DeviceNetworkEvents) - Sysmon EID 3 (network connect) — requires network connection logging enabled
- Firewall / NetFlow / Zeek
conn.logfor the duration and volume view
Detection Engineering with Defender XDR
Outbound TCP from a process that has no business making one.
let userland = dynamic([@"\appdata\", @"\temp\", @"\users\public\", @"\downloads\"]);
DeviceNetworkEvents
| where ActionType == "ConnectionSuccess"
| where RemotePort !in (80, 443, 445, 139, 53, 88, 389, 636, 3268, 3269)
| where not(ipv4_is_private(RemoteIP))
| where tolower(InitiatingProcessFolderPath) has_any (userland)
| summarize Connections = count(),
FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
Ports = make_set(RemotePort, 10)
by DeviceName, InitiatingProcessFileName, InitiatingProcessFolderPath, RemoteIP
| extend WindowMinutes = datetime_diff('minute', LastSeen, FirstSeen)
| order by Connections desc
Beacon shape — a low-variance reconnect interval is the giveaway for the polling variant.
DeviceNetworkEvents
| where ActionType == "ConnectionSuccess"
| where not(ipv4_is_private(RemoteIP))
| order by DeviceId, InitiatingProcessId, Timestamp asc
| extend PrevTs = prev(Timestamp), PrevPid = prev(InitiatingProcessId)
| where InitiatingProcessId == PrevPid
| extend Gap = datetime_diff('second', Timestamp, PrevTs)
| summarize Beacons = count(), StdDev = stdev(Gap), AvgGap = avg(Gap)
by DeviceName, InitiatingProcessFileName, RemoteIP, RemotePort
| where Beacons >= 10 and StdDev < 5 and AvgGap > 10
| order by StdDev asc
Detection Engineering with Sysmon
WindowsEvent //(sysmon)
| where EventID == 3 // NetworkConnect
| extend Image = tostring(EventData.Image),
DestinationPort = toint(EventData.DestinationPort),
DestinationIp = tostring(EventData.DestinationIp),
Initiated = tostring(EventData.Initiated)
| where Initiated == "true"
| where Image has_any (@"\AppData\", @"\Temp\", @"\Users\Public\")
| where DestinationPort !in (80, 443, 53)
| project TimeGenerated, Computer, Image, DestinationIp, DestinationPort
Detection Engineering with SIGMA
Full rules: sigma_nanocore_raw_socket_c2.yml
detection:
selection:
Initiated: 'true'
selection_path:
Image|contains:
- '\AppData\'
- '\Temp\'
- '\Users\Public\'
filter_common_ports:
DestinationPort:
- 80
- 443
- 53
condition: all of selection* and not filter_common_ports
The second rule in that file covers the interactive variant — a shell or script host spawned by a process that is holding a network connection, which is what a reverse shell looks like from the process tree.
Notes and tuning
Port lists age badly. Attackers move to 443 precisely because everyone allow-lists it. A
raw socket on 443 with no TLS handshake is more suspicious than one on a random high port,
not less — if your sensor can tell you a connection on 443 carried no TLS ClientHello, that is
the better rule. Zeek’s conn.log with an empty service field on port 443 expresses it well.
Duration and volume beat ports. A single TCP session open for hours, with small
bidirectional exchanges, is the signature of an interactive channel regardless of port. Hunt
on conn.log duration percentiles rather than on port numbers.
Egress control is worth more than any rule here. If outbound TCP from user-writable directories is blocked at the firewall, this technique fails at the first connect.
Disclaimer
For educational purposes only!!!