NanoCore: Non-Application Layer Protocol — Detection

NanoCore Image

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

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