Detection rules › Kusto
Persistence Via Scheduled Tasks
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
| Tactic | Techniques |
|---|---|
| Execution | T1053.005 Scheduled Task/Job: Scheduled Task |
| Persistence | T1053.005 Scheduled Task/Job: Scheduled Task |
| Privilege Escalation | T1053.005 Scheduled Task/Job: Scheduled Task |
References
Event coverage
| Provider | Event/ActionType | Title |
|---|---|---|
| Sysmon | Event ID 1 | Process creation |
| Security-Auditing | Event ID 4688 | A new process has been created. |
| Defender-DeviceProcessEvents | ProcessCreated | Process created |
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.
| Field | Kind | Excluded values |
|---|---|---|
ProfileAvailability | eq | Error |
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.
| Field | Kind | Values |
|---|---|---|
ActionType | eq |
|
GlobalPrevalence | lt |
|
InitiatingProcessCommandLine | contains |
|
InitiatingProcessCommandLine | starts_with |
|
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.
| Field | Source |
|---|---|
Devices | summarize |
MachineCount | summarize |
SHA1 | summarize |