NanoCore: Process Hollowing and Resource-Packed Payload — Detection

NanoCore Image

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 VirtualAllocExWriteProcessMemory (headers, then each section)
Hijack GetThreadContextSetThreadContextResumeThread
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

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