Detection rules › Kusto
PRT Credential Stealing
This query detects when BrowserCore.exe is accessed by a suspicious process. The BrowserCore.exe binary is responsible for allowing browser add-ons to use Single Sign On via Azure AD. This rule detects when an uncommon process interacts with the BrowserCore.exe process.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | T1134.001 Access Token Manipulation: Token Impersonation/Theft |
| Credential Access | T1003 OS Credential Dumping, T1555 Credentials from Password Stores |
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*1h;
let default_global_prevalence = 0;
let lolbins = dynamic(["at.exe", "atbroker.exe", "bash.exe", "bitsadmin.exe", "certreq.exe", "certutil.exe", "cmd.exe", "cmdkey.exe", "cmstp.exe", "control.exe", "csc.exe", "cscript.exe", "desktopimgdownldr.exe", "dfsvc.exe", "diantz.exe", "diskshadow.exe", "dnscmd.exe", "esentutl.exe", "eventvwr.exe", "expand.exe", "extexport.exe", "extrac32.exe", "findstr.exe", "forfiles.exe", "ftp.exe", "gfxdownloadwrapper.exe", "gpscript.exe", "hh.exe", "ie4uinit.exe", "ieexec.exe", "ilasm.exe", "infdefaultinstall.exe", "installutil.exe", "jsc.exe", "makecab.exe", "mavinject.exe", "microsoft.workflow.compiler.exe", "mmc.exe", "mpcmdrun.exe", "msbuild.exe", "msconfig.exe", "msdt.exe", "mshta.exe", "msiexec.exe", "netsh.exe", "odbcconf.exe", "pcalua.exe", "pcwrun.exe", "pktmon.exe", "presentationhost.exe", "print.exe", "psr.exe", "rasautou.exe", "reg.exe", "regasm.exe", "regedit.exe", "regini.exe", "register-cimprovider.exe", "regsvcs.exe", "regsvr32.exe", "replace.exe", "rpcping.exe", "rundll32.exe", "runonce.exe", "runscripthelper.exe", "sc.exe", "schtasks.exe", "scriptrunner.exe", "syncappvpublishingserver.exe", "ttdinject.exe", "tttracer.exe", "vbc.exe", "verclsid.exe", "wab.exe", "wmic.exe", "wscript.exe", "wsreset.exe", "xwizard.exe", "agentexecutor.exe", "appvlp.exe", "bginfo.exe", "cdb.exe", "csi.exe", "devtoolslauncher.exe", "dnx.exe", "dotnet.exe", "dxcap.exe", "excel.exe", "mftrace.exe", "msdeploy.exe", "msxsl.exe", "ntdsutil.exe", "powerpnt.exe", "rcsi.exe", "sqldumper.exe", "sqlps.exe", "sqltoolsps.exe", "squirrel.exe", "te.exe", "tracker.exe", "vsjitdebugger.exe", "winword.exe", "wsl.exe", "powershell.exe", "pwsh.exe"]);
// Adding services.exe and svchost.exe to the list of LOLBINs since these can be abused to start cmd.exe and talk to browsercore.
let extendedLolbins = array_concat(lolbins, dynamic(["services.exe", "svchost.exe"]));
let trustedProcess = dynamic(["chrome.exe", "teams.exe"]);
let trustedProcessHashes = materialize(DeviceProcessEvents | where ActionType =~ "ProcessCreated" | where FileName in~ (trustedProcess) | distinct SHA1);
let suspiciousHashes = materialize(trustedProcessHashes | extend SHA1=tolower(SHA1) | invoke FileProfile(SHA1, 1000) | where not(ProfileAvailability =~ "Error") | where coalesce(GlobalPrevalence,default_global_prevalence) < 200 or isempty(GlobalPrevalence));
let allBrowserCores = DeviceProcessEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType =~ "ProcessCreated"
| where ProcessVersionInfoOriginalFileName =~ "BrowserCore.exe";
allBrowserCores
// FileProfile is case-sensitive and works on lower-case hashes.
| extend SHA1=tolower(SHA1)
// We need the grandparent SHA1, since the normal execution hierarchy is chrome->cmd->browsercore.exe (or similar with Teams).
| join kind = leftouter (DeviceProcessEvents | where ActionType =~ "ProcessCreated" | where FileName in~ ((allBrowserCores | project InitiatingProcessParentFileName))) on $left.InitiatingProcessParentId == $right.ProcessId, DeviceId, $left.InitiatingProcessParentFileName == $right.FileName
// Filter grandparent.
| extend GrandParentFileSHA1 = SHA11 // Renaming SHA1 to GrandParentFileSHA1 for future readability.
| where GrandParentFileSHA1 in~ (suspiciousHashes) or InitiatingProcessParentFileName !in~ (trustedProcess)
// Filter parent.
| where InitiatingProcessSHA1 in~ (suspiciousHashes) or InitiatingProcessFileName !in~ (trustedProcess)
// Removing empty GrandParentFileSHA1 hashes since sometimes the process start event is not logged and hence GrandParentFileSHA1 is empty.
// This contaminates the results. A future improvement would be to check if the trusted process we're looking
// for is logged as parent (instead of as child).
| where not(isempty(GrandParentFileSHA1))
| invoke FileProfile(GrandParentFileSHA1, 1000)
| where not(ProfileAvailability =~ "Error")
| where coalesce(GlobalPrevalence,default_global_prevalence) < 200 or isempty(GlobalPrevalence) or InitiatingProcessParentFileName in~ (extendedLolbins)
// Begin environment-specific filter.
// End environment-specific filter.
Stages and Predicates
Parameters
let timeframe = 2*1h;
let default_global_prevalence = 0;
let extendedLolbins = array_concat(lolbins, dynamic(["services.exe", "svchost.exe"]));
let trustedProcess = dynamic(["chrome.exe", "teams.exe"]);
Let binding: lolbins
let lolbins = dynamic(["at.exe", "atbroker.exe", "bash.exe", "bitsadmin.exe", "certreq.exe", "certutil.exe", "cmd.exe", "cmdkey.exe", "cmstp.exe", "control.exe", "csc.exe", "cscript.exe", "desktopimgdownldr.exe", "dfsvc.exe", "diantz.exe", "diskshadow.exe", "dnscmd.exe", "esentutl.exe", "eventvwr.exe", "expand.exe", "extexport.exe", "extrac32.exe", "findstr.exe", "forfiles.exe", "ftp.exe", "gfxdownloadwrapper.exe", "gpscript.exe", "hh.exe", "ie4uinit.exe", "ieexec.exe", "ilasm.exe", "infdefaultinstall.exe", "installutil.exe", "jsc.exe", "makecab.exe", "mavinject.exe", "microsoft.workflow.compiler.exe", "mmc.exe", "mpcmdrun.exe", "msbuild.exe", "msconfig.exe", "msdt.exe", "mshta.exe", "msiexec.exe", "netsh.exe", "odbcconf.exe", "pcalua.exe", "pcwrun.exe", "pktmon.exe", "presentationhost.exe", "print.exe", "psr.exe", "rasautou.exe", "reg.exe", "regasm.exe", "regedit.exe", "regini.exe", "register-cimprovider.exe", "regsvcs.exe", "regsvr32.exe", "replace.exe", "rpcping.exe", "rundll32.exe", "runonce.exe", "runscripthelper.exe", "sc.exe", "schtasks.exe", "scriptrunner.exe", "syncappvpublishingserver.exe", "ttdinject.exe", "tttracer.exe", "vbc.exe", "verclsid.exe", "wab.exe", "wmic.exe", "wscript.exe", "wsreset.exe", "xwizard.exe", "agentexecutor.exe", "appvlp.exe", "bginfo.exe", "cdb.exe", "csi.exe", "devtoolslauncher.exe", "dnx.exe", "dotnet.exe", "dxcap.exe", "excel.exe", "mftrace.exe", "msdeploy.exe", "msxsl.exe", "ntdsutil.exe", "powerpnt.exe", "rcsi.exe", "sqldumper.exe", "sqlps.exe", "sqltoolsps.exe", "squirrel.exe", "te.exe", "tracker.exe", "vsjitdebugger.exe", "winword.exe", "wsl.exe", "powershell.exe", "pwsh.exe"]);
Let binding: trustedProcessHashes
let trustedProcessHashes = materialize(DeviceProcessEvents | where ActionType =~ "ProcessCreated" | where FileName in~ (trustedProcess) | distinct SHA1);
Derived from trustedProcess.
Let binding: suspiciousHashes
let suspiciousHashes = materialize(trustedProcessHashes | extend SHA1=tolower(SHA1) | invoke FileProfile(SHA1, 1000) | where not(ProfileAvailability =~ "Error") | where coalesce(GlobalPrevalence,default_global_prevalence) < 200 or isempty(GlobalPrevalence));
Derived from default_global_prevalence, trustedProcessHashes.
The stages below define let allBrowserCores (the rule's main pipeline source).
Stage 1: source
DeviceProcessEvents
Stage 2: where
| where ingestion_time() >= ago(timeframe)
Stage 3: where
| where ActionType =~ "ProcessCreated"
Stage 4: where
| where ProcessVersionInfoOriginalFileName =~ "BrowserCore.exe"
The stages below run on allBrowserCores (the outer pipeline).
Stage 5: extend
allBrowserCores
| extend SHA1=tolower(SHA1)
Stage 6: join
| join kind = leftouter (DeviceProcessEvents | where ActionType =~ "ProcessCreated" | where FileName in~ ((allBrowserCores | project InitiatingProcessParentFileName))) on $left.InitiatingProcessParentId == $right.ProcessId, DeviceId, $left.InitiatingProcessParentFileName == $right.FileName
Stage 7: extend
| extend GrandParentFileSHA1 = SHA11
Stage 8: where
| where GrandParentFileSHA1 in~ (suspiciousHashes) or InitiatingProcessParentFileName !in~ (trustedProcess)
References suspiciousHashes (defined above).
Stage 9: where
| where InitiatingProcessSHA1 in~ (suspiciousHashes) or InitiatingProcessFileName !in~ (trustedProcess)
References suspiciousHashes (defined above).
Stage 10: where
| where not(isempty(GrandParentFileSHA1))
Stage 11: invoke
| invoke FileProfile(GrandParentFileSHA1, 1000)
Stage 12: where
| where not(ProfileAvailability =~ "Error")
Stage 13: where
| where coalesce(GlobalPrevalence,default_global_prevalence) < 200 or isempty(GlobalPrevalence) or InitiatingProcessParentFileName in~ (extendedLolbins)
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
GrandParentFileSHA1 | is_null | |
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 | is_null | |
GlobalPrevalence | lt |
|
GrandParentFileSHA1 | in |
|
InitiatingProcessParentFileName | in |
|
InitiatingProcessSHA1 | in |
|
ProcessVersionInfoOriginalFileName | eq |
|
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 |
|---|---|
SHA1 | extend |
GrandParentFileSHA1 | extend |