Detection rules › Kusto

LSASS Dumping using Debug Privileges

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

This query searches for a process that requests the SeDebugPrivilege privilege and opens LSASS memory using specific permission 0x1fffff which represents PROCESS_ALL_ACCESS.

MITRE ATT&CK coverage

TacticTechniques
ExecutionT1106 Native API
Credential AccessT1003.001 OS Credential Dumping: LSASS Memory

References

Event coverage

Rule body kusto

let timeframe = 2*1h;
let SeDebugPrivilege = binary_shift_left(1, 20); // Value for SeDebugPrivilege is 2**20 = 0x100000.
let LSASSOpen=materialize (
    DeviceEvents
    | where ingestion_time() >= ago(timeframe)
    | where ActionType == "OpenProcessApiCall"
    | where FileName =~ "lsass.exe"
    | extend AccessRights=parse_json(AdditionalFields).DesiredAccess
    | where AccessRights == 0x1fffff // PROCESS_ALL_ACCESS.
    | summarize by DeviceId, InitiatingProcessId, InitiatingProcessSHA1
);
DeviceEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType == "ProcessPrimaryTokenModified"
| where isnotempty(InitiatingProcessSHA1)
// Look for processes that request debug privilege that also opened LSASS
| where InitiatingProcessSHA1 in ((LSASSOpen | project InitiatingProcessSHA1)) // Speeds up the query.
| lookup kind=inner LSASSOpen on DeviceId, InitiatingProcessSHA1, InitiatingProcessId
// Check that debug privilege is enabled.
| extend AdditionalFields=parse_json(AdditionalFields)
| extend CurrentTokenPrivEnabled = toint(AdditionalFields.CurrentTokenPrivEnabled)
| extend OriginalTokenPrivEnabled = toint(AdditionalFields.OriginalTokenPrivEnabled)
// Value for SeDebugPrivilege is 2**20 = 0x100000.
// Refer to https://downloads.volatilityfoundation.org//omfw/2012/OMFW2012_Gurkok.pdf for numeric values for privileges.
| extend DebugPrivCurrent = binary_and(CurrentTokenPrivEnabled,SeDebugPrivilege) == SeDebugPrivilege
| extend DebugPrivOrig = binary_and(OriginalTokenPrivEnabled,SeDebugPrivilege) == SeDebugPrivilege
// Check for processes that have debug privilege after the event, but did not have it before.
| where not(DebugPrivOrig) and DebugPrivCurrent
| extend CleanCmdLine = parse_command_line(InitiatingProcessCommandLine, "windows")
| where not(InitiatingProcessFileName =~ "tasklist.exe" and CleanCmdLine has_any ("/m", "-m"))
| extend HostName=tostring(split(DeviceName,".")[0]),DnsDomain=iif(DeviceName contains ".", substring(DeviceName, indexof(DeviceName, ".") + 1, strlen(DeviceName)),"")
| project-reorder Timestamp, DeviceId, InitiatingProcessFileName
// Begin environment-specific filter.
// End environment-specific filter.

Stages and Predicates

Parameters

let timeframe = 2*1h;
let SeDebugPrivilege = binary_shift_left(1, 20);

Let binding: LSASSOpen

let LSASSOpen = materialize (
    DeviceEvents
    | where ingestion_time() >= ago(timeframe)
    | where ActionType == "OpenProcessApiCall"
    | where FileName =~ "lsass.exe"
    | extend AccessRights=parse_json(AdditionalFields).DesiredAccess
    | where AccessRights == 0x1fffff
    | summarize by DeviceId, InitiatingProcessId, InitiatingProcessSHA1
);

Derived from timeframe.

Stage 1: source

DeviceEvents

Stage 2: where

| where ingestion_time() >= ago(timeframe)

Stage 3: where

| where ActionType == "ProcessPrimaryTokenModified"

Stage 4: where

| where isnotempty(InitiatingProcessSHA1)

Stage 5: where

| where InitiatingProcessSHA1 in ((LSASSOpen | project InitiatingProcessSHA1))

Stage 6: kusto:lookup

| lookup kind=inner LSASSOpen on DeviceId, InitiatingProcessSHA1, InitiatingProcessId

Stage 7: extend (5 consecutive steps)

| extend AdditionalFields=parse_json(AdditionalFields)
| extend CurrentTokenPrivEnabled = toint(AdditionalFields.CurrentTokenPrivEnabled)
| extend OriginalTokenPrivEnabled = toint(AdditionalFields.OriginalTokenPrivEnabled)
| extend DebugPrivCurrent = binary_and(CurrentTokenPrivEnabled,SeDebugPrivilege) == SeDebugPrivilege
| extend DebugPrivOrig = binary_and(OriginalTokenPrivEnabled,SeDebugPrivilege) == SeDebugPrivilege

Stage 8: where

| where not(DebugPrivOrig) and DebugPrivCurrent

Stage 9: extend

| extend CleanCmdLine = parse_command_line(InitiatingProcessCommandLine, "windows")

Stage 10: where

| where not(InitiatingProcessFileName =~ "tasklist.exe" and CleanCmdLine has_any ("/m", "-m"))

Stage 11: extend

| extend HostName=tostring(split(DeviceName,".")[0]),DnsDomain=iif(DeviceName contains ".", substring(DeviceName, indexof(DeviceName, ".") + 1, strlen(DeviceName)),"")

Stage 12: project-reorder

| project-reorder Timestamp, DeviceId, InitiatingProcessFileName

Stage 13: summarize

summarize by DeviceId, InitiatingProcessId, InitiatingProcessSHA1

Exclusions

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

FieldKindExcluded values
CleanCmdLinematch/m, -m
InitiatingProcessFileNameeqtasklist.exe

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
  • ProcessPrimaryTokenModified transforms: cased
InitiatingProcessSHA1is_not_null
  • (no value, null check)

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
InitiatingProcessIdsummarize
InitiatingProcessSHA1summarize