Detection rules › Kusto

Detect Unsigned executable launch from scheduled task

Group by
SHA1
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

Persistence via Scheduled Tasks is a well-known technique used by adversaries to make sure their malware programs keep running an the compromised device. With this detection rule, you can search for unknown executables being launched from scheduled tasks. > [!WARNING] > This detection rule is the base for the detection. You will need to add environment specific finetuning in order to limit the BP detections on legitimate processes

MITRE ATT&CK coverage

References

Event coverage

Rule body yaml

let scheduled_binaries = (
    DeviceProcessEvents
    | where ActionType !contains "aggregated"
    | where Timestamp > ago(1h)
    | where InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
    | distinct SHA1
);
let untrusted_binaries = (
    scheduled_binaries
    | join kind=leftanti (
        DeviceFileCertificateInfo 
        | where Timestamp > ago(1h) 
        | summarize max_trusted=max(IsTrusted) by SHA1 
        | where max_trusted==1
    ) on SHA1
);
untrusted_binaries
| invoke FileProfile(SHA1,1000)
| where IsCertificateValid != 1 // Exclude signed binaries
| where (isnotempty(GlobalPrevalence) and GlobalPrevalence < 1000)
| join (
    DeviceProcessEvents 
    | where ActionType !contains "aggregated"
    | where InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
) on SHA1

Stages and Predicates

Stage 0: let

let scheduled_binaries = ( <inlined as stages below>;
let untrusted_binaries = ( <inlined as stages below>;

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

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where ActionType !contains "aggregated"

Stage 3: where

| where Timestamp > ago(1h)

Stage 4: where

| where InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"

Stage 5: distinct

| distinct SHA1

Stage 6: join (negated)

| join kind=leftanti (
        DeviceFileCertificateInfo 
        | where Timestamp > ago(1h) 
        | summarize max_trusted=max(IsTrusted) by SHA1 
        | where max_trusted==1
    ) on SHA1

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

Stage 7: invoke

untrusted_binaries
| invoke FileProfile(SHA1,1000)

Stage 8: where

| where IsCertificateValid != 1

Stage 9: where

| where (isnotempty(GlobalPrevalence) and GlobalPrevalence < 1000)

Stage 10: join

| join (
    DeviceProcessEvents 
    | where ActionType !contains "aggregated"
    | where InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
) on SHA1

Stage 11: summarize

summarize by SHA1

Exclusions

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

FieldKindExcluded values
ActionTypecontainsaggregated
max_trustedeq1
ActionTypecontainsaggregated

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
GlobalPrevalenceis_not_null
  • (no value, null check)
GlobalPrevalencelt
  • 1000 transforms: cased corpus 4 (kusto 4)
InitiatingProcessCommandLineeq
  • svchost.exe -k netsvcs -p -s Schedule transforms: cased corpus 2 (kusto 2)
IsCertificateValidne
  • 1 transforms: cased corpus 2 (kusto 2)

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
SHA1summarize