Query Details

Infostealer Browser Kill And Verify Sweep From Single Process

Query

// one root process terminates several browsers with taskkill (often through
// short-lived cmd.exe children) and checks with tasklist that they are gone, to release
// the locked credential and cookie databases before reading them.
let Lookback = 4h;
let ParentContext = 10m;            // cmd.exe may start shortly before the lookback window
let SweepWindow = 2m;               // maximum duration of one sweep
let MinBrowserBrands = 2;           // distinct targets (killed or verified)
let MinKilledBrands = 1;            // distinct targets actually killed
let ExcludeLowConfidence = false;   // true: only Medium and High results
let AllowedRootSHA1 = dynamic([]);  // known good updaters / uninstallers
let BrowserTerms = dynamic(["chrome", "chromium", "brave", "msedge", "opera", "vivaldi",
    "firefox", "browser", "dragon", "iridium", "waterfox", "librewolf", "thunderbird"]);
let RawOps = materialize(
    DeviceProcessEvents
    | where Timestamp >= ago(Lookback)
    | where FileName in~ ("taskkill.exe", "tasklist.exe")
        or ProcessVersionInfoOriginalFileName startswith "taskkill.exe"
        or ProcessVersionInfoOriginalFileName startswith "tasklist.exe"
    | where ProcessCommandLine has_any (BrowserTerms)
    | extend ImageName = tolower(iff(FileName in~ ("taskkill.exe", "tasklist.exe"),
        FileName, ProcessVersionInfoOriginalFileName))
    | extend Operation = iff(ImageName startswith "taskkill", "Kill", "Verify")
    | extend BrowserNames = iff(Operation == "Kill",
        extract_all(@"(?i)[/-]im\s+[""']?(chrome|chromium|brave|msedge|opera|vivaldi|firefox|browser|dragon|iridium|waterfox|librewolf|thunderbird)(?:\.exe|\*)?[""']?(?:\s|$|[&|)])", ProcessCommandLine),
        extract_all(@"(?i)[/-]fi\s+[""']?\s*imagename\s+eq\s+(chrome|chromium|brave|msedge|opera|vivaldi|firefox|browser|dragon|iridium|waterfox|librewolf|thunderbird)(?:\.exe|\*)?[""']?(?:\s|$|[&|)])", ProcessCommandLine))
    | where array_length(BrowserNames) > 0
    | mv-expand Browser = BrowserNames to typeof(string)
    | extend Browser = tolower(Browser),
        ViaCmd = InitiatingProcessFileName =~ "cmd.exe"
            or InitiatingProcessVersionInfoOriginalFileName startswith "cmd.exe",
        CmdKey = tostring(InitiatingProcessUniqueId)
);
// Resolve only the cmd.exe instances that are actually needed (semi-join)
let NeededCmdKeys =
    RawOps
    | where ViaCmd and isnotempty(CmdKey)
    | distinct DeviceId, CmdKey;
let CmdParents =
    DeviceProcessEvents
    | where Timestamp >= ago(Lookback + ParentContext)
    | where FileName =~ "cmd.exe" or ProcessVersionInfoOriginalFileName startswith "cmd.exe"
    | extend CmdKey = tostring(ProcessUniqueId)
    | join kind=inner NeededCmdKeys on DeviceId, CmdKey
    | project DeviceId, CmdKey,
        CmdRootKey = tostring(InitiatingProcessUniqueId),
        CmdRootFileName = InitiatingProcessFileName,
        CmdRootFolderPath = InitiatingProcessFolderPath,
        CmdRootCommandLine = InitiatingProcessCommandLine,
        CmdRootSHA1 = InitiatingProcessSHA1,
        CmdRootProcessId = InitiatingProcessId,
        CmdRootCreationTime = InitiatingProcessCreationTime,
        CmdRootSignatureStatus = InitiatingProcessSignatureStatus
    | summarize take_any(*) by DeviceId, CmdKey;
let Operations = materialize(
    RawOps
    | join kind=leftouter CmdParents on DeviceId, CmdKey
    | extend ResolvedRoot = iff(ViaCmd, CmdRootKey, tostring(InitiatingProcessUniqueId))
    | extend ParentResolved = isnotempty(ResolvedRoot)
    | extend FallbackPid = iff(ViaCmd, InitiatingProcessParentId, InitiatingProcessId),
        FallbackTime = iff(ViaCmd, InitiatingProcessParentCreationTime, InitiatingProcessCreationTime)
    // Do not merge unrelated processes when no attribution is possible.
    | extend RootKey = case(
        ParentResolved, ResolvedRoot,
        FallbackPid > 0 and isnotnull(FallbackTime), strcat("pp:", FallbackPid, "|", FallbackTime),
        "")
    | where isnotempty(RootKey)
    | extend
        RootFileName = case(not(ViaCmd), InitiatingProcessFileName,
            ParentResolved, CmdRootFileName, InitiatingProcessParentFileName),
        RootFolderPath = case(not(ViaCmd), InitiatingProcessFolderPath,
            ParentResolved, CmdRootFolderPath, ""),
        RootCommandLine = case(not(ViaCmd), InitiatingProcessCommandLine,
            ParentResolved, CmdRootCommandLine, ""),
        RootSHA1 = case(not(ViaCmd), InitiatingProcessSHA1,
            ParentResolved, CmdRootSHA1, ""),
        RootProcessId = case(not(ViaCmd), InitiatingProcessId,
            ParentResolved, CmdRootProcessId, InitiatingProcessParentId),
        RootCreationTime = case(not(ViaCmd), InitiatingProcessCreationTime,
            ParentResolved, CmdRootCreationTime, InitiatingProcessParentCreationTime),
        RootSignatureStatus = case(not(ViaCmd), InitiatingProcessSignatureStatus,
            ParentResolved, CmdRootSignatureStatus, "")
    | where isempty(RootSHA1) or RootSHA1 !in (AllowedRootSHA1)
    | extend UserWritableLocation = RootFolderPath matches regex
        @"(?i)^(?:[a-z]:\\(?:users|programdata|windows\\temp|windows\\tasks|windows\\tracing|perflogs|\$recycle\.bin)\\|\\\\)"
    | extend SystemNameOutsideSystemDirectory =
        RootFileName in~ (
            "svchost.exe", "lsass.exe", "services.exe", "winlogon.exe",
            "csrss.exe", "smss.exe", "wininit.exe", "dwm.exe", "spoolsv.exe",
            "rundll32.exe", "regsvr32.exe", "conhost.exe", "dllhost.exe",
            "taskhostw.exe", "cmd.exe", "powershell.exe", "explorer.exe")
        and isnotempty(RootFolderPath)
        and not(RootFolderPath matches regex @"(?i)^[a-z]:\\windows\\(?:system32|syswow64)\\")
        and not(RootFileName =~ "explorer.exe" and RootFolderPath matches regex @"(?i)^[a-z]:\\windows\\explorer\.exe$")
    // Common injection / proxy hosts used by commodity stealers
    | extend KnownStealerHost = RootFileName in~ (
            "regasm.exe", "regsvcs.exe", "msbuild.exe", "applaunch.exe", "installutil.exe",
            "aspnet_compiler.exe", "rundll32.exe", "regsvr32.exe", "mshta.exe",
            "wscript.exe", "cscript.exe", "powershell.exe", "pwsh.exe")
    | extend UnsignedRoot = isnotempty(RootSignatureStatus)
        and RootSignatureStatus !in~ ("Valid", "Unknown")
    | project Timestamp, DeviceId, DeviceName, ReportId, Operation, Browser, ImageName,
        ProcessCommandLine, RootKey, ParentResolved, RootFileName, RootFolderPath,
        RootCommandLine, RootSHA1, RootProcessId, RootCreationTime, RootSignatureStatus,
        UserWritableLocation, SystemNameOutsideSystemDirectory, KnownStealerHost, UnsignedRoot,
        AccountDomain, AccountName, AccountSid
);
let Anchors =
    Operations
    | project DeviceId, RootKey, AnchorTime = Timestamp
    | distinct DeviceId, RootKey, AnchorTime;
Operations
| join kind=inner Anchors on DeviceId, RootKey
| where Timestamp between (AnchorTime .. (AnchorTime + SweepWindow))
| summarize
    Killed = make_set_if(Browser, Operation == "Kill", 16),
    Verified = make_set_if(Browser, Operation == "Verify", 16),
    SweepStart = min(Timestamp),
    SweepEnd = max(Timestamp),
    OperationCount = count(),
    Accounts = make_set(strcat(AccountDomain, @"\", AccountName), 8),
    Evidence = make_set(bag_pack(
        "op", Operation, "browser", Browser, "time", Timestamp,
        "image", ImageName, "cmd", ProcessCommandLine), 40),
    arg_max(Timestamp, ReportId, DeviceName, ParentResolved,
        RootFileName, RootFolderPath, RootCommandLine, RootSHA1,
        RootProcessId, RootCreationTime, RootSignatureStatus,
        UserWritableLocation, SystemNameOutsideSystemDirectory, KnownStealerHost, UnsignedRoot,
        AccountDomain, AccountName, AccountSid)
    by DeviceId, RootKey, AnchorTime
| extend Brands = set_union(Killed, Verified), Paired = set_intersect(Killed, Verified)
| extend BrandCount = array_length(Brands),
    KilledCount = array_length(Killed),
    PairedCount = array_length(Paired)
| where BrandCount >= MinBrowserBrands and KilledCount >= MinKilledBrands
| extend RootScore = iff(UserWritableLocation, 1, 0)
    + iff(SystemNameOutsideSystemDirectory, 1, 0)
    + iff(KnownStealerHost, 1, 0)
    + iff(UnsignedRoot, 1, 0)
| extend Confidence = case(
    PairedCount >= MinBrowserBrands and RootScore > 0, "High",
    PairedCount >= MinBrowserBrands or RootScore > 0, "Medium",
    "Low")
| where not(ExcludeLowConfidence and Confidence == "Low")
// One result per root process: keep the widest sweep
| summarize arg_max(BrandCount, *) by DeviceId, RootKey
| project Timestamp, DeviceId, DeviceName, ReportId,
    Confidence, RootScore, BrandCount, KilledCount, PairedCount,
    Brands, Killed, Verified, Paired, SweepStart, SweepEnd, OperationCount,
    RootKey, ParentResolved, RootProcessId, RootCreationTime,
    RootFileName, RootFolderPath, RootCommandLine, RootSHA1, RootSignatureStatus,
    UserWritableLocation, SystemNameOutsideSystemDirectory, KnownStealerHost, UnsignedRoot,
    AccountDomain, AccountName, AccountSid, Accounts, Evidence

About this query

Explanation

This query is designed to detect suspicious activity related to infostealers targeting web browsers. Infostealers are malicious programs that aim to steal sensitive information, such as credentials and cookies, from web browsers. Here's a simplified explanation of what the query does:

  1. Objective: The query aims to identify processes that terminate multiple web browsers and verify their termination to access locked credential and cookie databases. This is a common tactic used by infostealers to extract sensitive data.

  2. Detection Method:

    • The query looks for the use of taskkill.exe to terminate browser processes and tasklist.exe to verify their termination.
    • It focuses on specific browser names like Chrome, Firefox, Edge, etc., and checks if these processes are being killed and verified.
    • It attributes these operations to a root process, which is the initial process that triggered the termination and verification actions.
  3. Process Attribution:

    • The query resolves the root process responsible for the actions, even if the operations are executed through intermediate processes like cmd.exe.
    • It uses unique process IDs and timestamps to accurately track the chain of events leading back to the root process.
  4. Criteria for Detection:

    • The query requires that at least three different browsers are targeted, with at least two being successfully terminated.
    • It scores the root process based on factors like whether it resides in a user-writable location, uses a system binary name outside the System32 directory, is a known injection host, or lacks a valid signature.
  5. Confidence Levels:

    • The query assigns a confidence level (Low, Medium, High) to each detection based on the completeness of the kill and verify operations and the score of the root process.
    • Users can adjust parameters like the minimum number of browser brands targeted, the sweep window duration, and whether to exclude low-confidence results.
  6. Output:

    • The query outputs details about the detected activity, including the root process information, the browsers targeted, the confidence level, and evidence of the operations.
    • It helps security analysts identify potential infostealer activity and take appropriate actions to secure affected accounts.

Overall, this query is a sophisticated tool for detecting and analyzing malicious activities related to infostealers targeting web browsers, helping organizations protect sensitive information from being compromised.