Detection rules › Kusto

Persistence Via Scheduled Tasks

Group by
SHA1
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query identifies binaries that run as a scheduled task, by looking at the parent process command line. Of the identified binaries running as scheduled tasks it finds suspicious binaries by looking at the file signature and global prevalence.

MITRE ATT&CK coverage

References

Event coverage

Rule body kusto

let timeframe = 2*1d;
let default_global_prevalence = 0;
// Time to look back for same scheduled binary.
let lookback= 7d;
let ScheduledBinaries = (
    DeviceProcessEvents
    | where Timestamp >= ago(lookback)
    | where ActionType =~ "ProcessCreated"
    | where InitiatingProcessCommandLine startswith "svchost.exe -k netsvcs -p" and InitiatingProcessCommandLine contains "Schedule" // First argument after -p is censored with ** so can't look for the actual command line
);
let NewScheduledBinaries=(
    ScheduledBinaries
    | where Timestamp >= ago(lookback)
    | summarize FirstSeen=min(Timestamp),LastSeen=max(Timestamp) by DeviceId, SHA1
    | where LastSeen >= ago(timeframe)
    | where FirstSeen >= ago(timeframe)
);
let NewScheduledBinaryExecution=(
    ScheduledBinaries
    | where ingestion_time() >= ago(timeframe)
    | lookup kind=inner NewScheduledBinaries on DeviceId, SHA1
);
NewScheduledBinaryExecution
| summarize MachineCount=dcount(DeviceId) by SHA1
// Find the max 1000 least used binaries.
| top 1000 by MachineCount asc
// FileProfile is case-sensitive and works on lower-case hashes.
| extend SHA1=tolower(SHA1)
| invoke FileProfile(SHA1,1000)
| where not(ProfileAvailability =~ "Error")
| where coalesce(GlobalPrevalence,default_global_prevalence) < 100
| join NewScheduledBinaryExecution on SHA1
| summarize arg_max(Timestamp, *), Devices=make_set(DeviceName), MachineCount=dcount(DeviceName) by SHA1 // Gives the last execution with all details per SHA1.
// Begin environment-specific filter.
// End environment-specific filter.

Stages and Predicates

Parameters

let timeframe = 2*1d;
let default_global_prevalence = 0;
let lookback = 7d;

Let binding: NewScheduledBinaries

let NewScheduledBinaries = (
    ScheduledBinaries
    | where Timestamp >= ago(lookback)
    | summarize FirstSeen=min(Timestamp),LastSeen=max(Timestamp) by DeviceId, SHA1
    | where LastSeen >= ago(timeframe)
    | where FirstSeen >= ago(timeframe)
);

Derived from timeframe, lookback, ScheduledBinaries.

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

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where Timestamp >= ago(lookback)

Stage 3: where

| where ActionType =~ "ProcessCreated"

Stage 4: where

| where InitiatingProcessCommandLine startswith "svchost.exe -k netsvcs -p" and InitiatingProcessCommandLine contains "Schedule"

Stage 5: where

| where ingestion_time() >= ago(timeframe)

Stage 6: kusto:lookup

| lookup kind=inner NewScheduledBinaries on DeviceId, SHA1

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

Stage 7: summarize

NewScheduledBinaryExecution
| summarize MachineCount=dcount(DeviceId) by SHA1

Stage 8: top

| top 1000 by MachineCount asc

Stage 9: extend

| extend SHA1=tolower(SHA1)

Stage 10: invoke

| invoke FileProfile(SHA1,1000)

Stage 11: where

| where not(ProfileAvailability =~ "Error")

Stage 12: where

| where coalesce(GlobalPrevalence,default_global_prevalence) < 100

Stage 13: join

| join NewScheduledBinaryExecution on SHA1

Stage 14: summarize

| summarize arg_max(Timestamp, *), Devices=make_set(DeviceName), MachineCount=dcount(DeviceName) by SHA1

Exclusions

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

FieldKindExcluded values
ProfileAvailabilityeqError

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
ActionTypeeq
  • ProcessCreated corpus 10 (kusto 10)
GlobalPrevalencelt
  • 100 transforms: coalesce_default:0 corpus 4 (kusto 4)
InitiatingProcessCommandLinecontains
  • Schedule corpus 2 (splunk 1, kusto 1)
InitiatingProcessCommandLinestarts_with
  • svchost.exe -k netsvcs -p

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
Devicessummarize
MachineCountsummarize
SHA1summarize