Detection rules › Kusto

Persistence Via Scheduled Tasks

Group by
DeviceId, 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.

Known false positives

  • Some legitimate software also uses scheduled tasks, for example, for downloading periodic updates. If the software is unsigned or has a low global prevalence this might cause false positives.

MITRE ATT&CK coverage

References

Telemetry coverage

Rule body

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 ScheduledBinaries and let NewScheduledBinaryExecution are inlined into the numbered stages below.

Let binding: NewScheduledBinaries used in Stages 2, 9

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)
);

Stage 1: source

let ScheduledBinaries

Stage 2: source

let NewScheduledBinaries

Stage 3: source

let NewScheduledBinaryExecution

Stage 4: source

DeviceProcessEvents

Stage 5: where

where Timestamp >= ago(604800s)

Stage 6: where

where ActionType =~ "ProcessCreated"

Stage 7: where

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

Stage 8: where

where ...

Stage 9: kusto:lookup

lookup kind=inner (NewScheduledBinaries) on DeviceId, SHA1

Stage 10: summarize

summarize MachineCount by SHA1

Stage 11: top

top

Stage 12: extend

extend SHA1

Stage 13: invoke

invoke

Stage 14: where

where not (ProfileAvailability =~ "Error")

Stage 15: where

where GlobalPrevalence < 100

Stage 16: join

join (NewScheduledBinaryExecution) on SHA1

Stage 17: summarize

summarize Devices, MachineCount by SHA1

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ProfileAvailabilityeqErrorexcludes:ProfileAvailability field:"ProfileAvailability" value:"Error"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Devicessummarize
MachineCountsummarize
SHA1summarize