NanoCore: Process Hollowing and Resource-Packed Payload — Detection
NanoCore hides its second stage in its own PE resource section, spawns a second copy of itself suspended, unmaps the original image, and writes the decrypted payload in its place. Self-hollowing is what makes it distinctive: parent and child are the same file on disk, so image-reputation checks see nothing new.
MITRE ATT&CK: T1055.012 · T1027.002
Technique write-up: NanoCore_ProcessHollowing
Detection Indicators
| Indicator | Value |
|---|---|
| Spawn | CreateProcessA(<own path>, ..., CREATE_SUSPENDED, ...) |
| Unmap | ZwUnmapViewOfSection on the child’s image base |
| Write | VirtualAllocEx → WriteProcessMemory (headers, then each section) |
| Hijack | GetThreadContext → SetThreadContext → ResumeThread |
| Payload source | FindResource / LoadResource / LockResource / SizeofResource |
| Encryption | the resource is decrypted at run time with the assembly’s own GUID |
Parent image path == child image path, with the child created suspended is the cheapest high-fidelity signal, and it does not depend on catching the memory writes.
Log Sources
- Microsoft Defender XDR (
DeviceProcessEvents) - Sysmon EID 1 (process create)
- Windows Security 4688 with command line inclusion
- Sysmon EID 8 (CreateRemoteThread), EID 10 (ProcessAccess)
- Microsoft Defender XDR
DeviceEvents—CreateRemoteThreadApiCall,WriteToProcessMemoryApiCall
Detection Engineering with Defender XDR
A process spawning its own image, then writing into it.
// 1. Self-spawn: parent and child are the same image
DeviceProcessEvents
| where isnotempty(InitiatingProcessFolderPath)
| where tolower(FolderPath) == tolower(InitiatingProcessFolderPath)
| where InitiatingProcessFolderPath !has @"\Windows\"
| project Timestamp, DeviceName, AccountName, FolderPath, ProcessCommandLine,
InitiatingProcessCommandLine, ProcessId, InitiatingProcessId
| order by Timestamp desc
// 2. Cross-process memory write into that child
DeviceEvents
| where ActionType in ("WriteToProcessMemoryApiCall", "SetThreadContextRemoteApiCall")
| project Timestamp, DeviceName, ActionType, FileName, FolderPath,
InitiatingProcessFileName, InitiatingProcessFolderPath, AdditionalFields
| order by Timestamp desc
Detection Engineering with Sysmon
The handle never comes from OpenProcess. Every memory operation in this technique uses
the handle CreateProcess already returned:
CreateProcessA(itself, ..., CREATE_SUSPENDED, ..., target_pi);
ZwUnmapViewOfSection(target_pi->hProcess, base);
VirtualAllocEx(target_pi->hProcess, ...);
WriteProcessMemory(target_pi->hProcess, ...);
Sysmon’s ProcessAccess event (EID 10) records a process opening a handle to another, so it
does not fire here at all. Any rule comparing SourceImage to TargetImage on EID 10 is
self-defeating: that comparison only matches in the self-hollowing case, which is exactly the
case that generates no event.
The dedicated signal is EID 25, ProcessTampering, added in Sysmon 13. It fires when a process image in memory stops matching the image on disk, which is the defining side effect of hollowing:
WindowsEvent //(sysmon)
| where EventID == 25 // ProcessTampering
| extend Image = tostring(EventData.Image), Type = tostring(EventData.Type)
| project TimeGenerated, Computer, Image, Type,
ProcessGuid = tostring(EventData.ProcessGuid)
| order by TimeGenerated desc
It needs <ProcessTampering onmatch="exclude"/> present in the Sysmon config; it is not
enabled by every published ruleset. Where Sysmon is older than 13, fall back to the self-spawn
query above, which is the reason that rule exists.
The Sigma rule for it uses the generic process_tampering logsource and matches on the event’s
Type field rather than a raw EventID, so it stays portable across backends:
logsource:
category: process_tampering
product: windows
detection:
selection:
Type|contains: 'Image is replaced'
condition: selection
Detection Engineering with SIGMA
Full rule: sigma_nanocore_process_hollowing.yml
detection:
selection_self:
Image|fieldref: ParentImage
selection_path:
Image|contains:
- '\AppData\'
- '\Temp\'
- '\Users\Public\'
condition: all of selection_*
Notes and tuning
Two rules ship here: the self-spawn above, and one on Sysmon EID 25. The fieldref modifier
in the first requires pySigma 0.10+ and a backend supporting field-to-field comparison; if
yours does not, drop selection_self and lean on the KQL self-spawn query, which expresses the
same idea. The EID 25 rule has no such requirement and is the better primary where Sysmon 13+
is deployed.
Software packing (T1027.002) is the other half and is best caught statically: the YARA rule flags the hollowing API set in one image. A binary carrying a large, high-entropy resource alongside those imports is the full picture.
Disclaimer
For educational purposes only!!!