Detection rules › Kusto

Process Injection From Untrusted Process

Group by
DeviceId, InitiatingProcessFolderPath
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query searches for processes performing remote process injection via multiple API calls related to process injection. It filters out programs that inject into their own process or into a process from the same directory. It then finds suspicious processes based on the global prevalence.

MITRE ATT&CK coverage

References

Event coverage

Rule body kusto

let timeframe = 2*1h;
let default_global_prevalence = 0;
let AllProcessInjectionEvents = materialize(
    DeviceEvents
    | where ingestion_time() >= ago(timeframe)
    | where ActionType in~ ("QueueUserApcRemoteApiCall","NtAllocateVirtualMemoryRemoteApiCall", "CreateRemoteThreadApiCall", "SetThreadContextRemoteApiCall", "NtMapViewOfSectionRemoteApiCall") and ProcessId != InitiatingProcessId
    | extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)
    | where not(InitiatingProcessFolderPath startswith FolderPath) // Exclude injection into processes in the same directory.
);
let SuspiciousProcessInjectionEvents = (
    AllProcessInjectionEvents
    | where not(isempty(InitiatingProcessSHA1)) // Only with a valid SHA1.
    | summarize MachineCount=dcount(DeviceId) by InitiatingProcessSHA1
    // Take 1000 of the most unique hashes, as files with high prevalence are very likely to be legitimately signed.
    | top 1000 by MachineCount asc
    | invoke FileProfile(InitiatingProcessSHA1, 1000)
    | where not(ProfileAvailability =~ "Error")
    | where coalesce(GlobalPrevalence,default_global_prevalence) < 200 or ((isempty(Signer) or not(IsCertificateValid)) and coalesce(GlobalPrevalence,default_global_prevalence) < 500)
);
AllProcessInjectionEvents
| lookup kind=inner SuspiciousProcessInjectionEvents on InitiatingProcessSHA1
// Work around the Defender limitation where FolderPath for CreateRemoteThreadApiCall does not contain FileName where it does for other events.
| extend InjectionTarget=strcat(FolderPath,@"\",FileName)
// Begin environment-specific filter.
// End environment-specific filter.
| summarize arg_min(Timestamp, *), InjectionTargets=make_set(InjectionTarget) by DeviceId, InitiatingProcessFolderPath // Show only the first invocation per device.
| extend InjectionSource=InitiatingProcessFolderPath, InjectionCommandLine=InitiatingProcessCommandLine
| project-reorder Timestamp, InjectionSource, InjectionCommandLine, InjectionTargets

Stages and Predicates

Parameters

let timeframe = 2*1h;
let default_global_prevalence = 0;

Let binding: SuspiciousProcessInjectionEvents

let SuspiciousProcessInjectionEvents = (
    AllProcessInjectionEvents
    | where not(isempty(InitiatingProcessSHA1))
    | summarize MachineCount=dcount(DeviceId) by InitiatingProcessSHA1
    | top 1000 by MachineCount asc
    | invoke FileProfile(InitiatingProcessSHA1, 1000)
    | where not(ProfileAvailability =~ "Error")
    | where coalesce(GlobalPrevalence,default_global_prevalence) < 200 or ((isempty(Signer) or not(IsCertificateValid)) and coalesce(GlobalPrevalence,default_global_prevalence) < 500)
);

Derived from default_global_prevalence, AllProcessInjectionEvents.

The stages below define let AllProcessInjectionEvents (the rule's main pipeline source).

Stage 1: source

DeviceEvents

Stage 2: where

| where ingestion_time() >= ago(timeframe)

Stage 3: where

| where ActionType in~ ("QueueUserApcRemoteApiCall","NtAllocateVirtualMemoryRemoteApiCall", "CreateRemoteThreadApiCall", "SetThreadContextRemoteApiCall", "NtMapViewOfSectionRemoteApiCall") and ProcessId != InitiatingProcessId

Stage 4: extend

| extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)

Stage 5: where

| where not(InitiatingProcessFolderPath startswith FolderPath)

The stages below run on AllProcessInjectionEvents (the outer pipeline).

Stage 6: kusto:lookup

AllProcessInjectionEvents
| lookup kind=inner SuspiciousProcessInjectionEvents on InitiatingProcessSHA1

Stage 7: extend

| extend InjectionTarget=strcat(FolderPath,@"\",FileName)

Stage 8: summarize

| summarize arg_min(Timestamp, *), InjectionTargets=make_set(InjectionTarget) by DeviceId, InitiatingProcessFolderPath

Stage 9: extend

| extend InjectionSource=InitiatingProcessFolderPath, InjectionCommandLine=InitiatingProcessCommandLine

Stage 10: project-reorder

| project-reorder Timestamp, InjectionSource, InjectionCommandLine, InjectionTargets

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
InitiatingProcessFolderPathstarts_withFolderPath

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
ActionTypein
  • CreateRemoteThreadApiCall
  • NtAllocateVirtualMemoryRemoteApiCall
  • NtMapViewOfSectionRemoteApiCall
  • QueueUserApcRemoteApiCall
  • SetThreadContextRemoteApiCall
ProcessIdne
  • InitiatingProcessId transforms: cased

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
DeviceIdsummarize
InitiatingProcessFolderPathsummarize
InjectionTargetssummarize
InjectionCommandLineextend
InjectionSourceextend