Latest Posts

Wrap around the New-MpPerformanceRecording

Why Defender Boys decided to build Invoke-DBDefenderPerformance powershell tool

When you run into performance issues on a Windows endpoint, Microsoft Defender is sometimes part of the conversation.

So why does this happen?

It usually falls back to the fact that modern environments are messy:

  • legacy applications
  • developer tooling
  • self-updating binaries
  • temporary execution paths
  • unsigned components

The Reality of Modern Endpoint Protection

Microsoft Defender today is far more than traditional antivirus.

It includes:

  • real-time protection
  • behavioral monitoring
  • Endpoint Detection & Response (EDR, and if you are looking at all workloads such as identity, data etc we have XDR)
  • script and memory inspection
  • integration with cloud intelligence

All of this is designed to handle both:

  • modern threats
  • and older, less predictable software

That combination is important. Because in many environments, the biggest performance impact doesn’t come from malware – it comes from:

  • complex applications
  • heavy file churn
  • development workloads
  • installers and updaters
  • or software that doesn’t follow modern best practices

Where Things Get Difficult

From a troubleshooting perspective, this creates a challenge.

You might see:

  • CPU spikes in MsMpEng.exe
  • activity from MsSense.exe
  • delays during file operations

But that doesn’t tell you:

  • which files are being scanned
  • why they are being scanned
  • which process triggered the activity
  • or how expensive the scans actually are

Without that visibility, it’s easy to fall into guesswork.

What Microsoft Provides

Microsoft provides built-in tooling for this:

  • New-MpPerformanceRecording
  • Get-MpPerformanceReport

Which allow you to capture:

  • detailed scan activity
  • durations
  • top files and processes
  • scan reasons (like EDRSensor)

The data is there but the format is not very easy to interpret for everyone

  • timestamps are not human-readable
  • durations are in ticks
  • output is deeply nested
  • there’s no prioritization

So while everything you need is technically there, it still takes effort to interpret.

Bridging the Gap

That’s where these functions come in:

  • Invoke-DBDefenderPerformance
  • Convert-DBDefenderPerformanceReport

The idea is simple, take Defender’s detailed trace data and make it usable for real-world analysis. Instead of manually parsing JSON, you get:

  • normalized timings (milliseconds instead of ticks)
  • clear overview of scan activity
  • grouped insights by:
  • scan reason
  • file type
  • path
  • visibility into what’s actually consuming time

This makes it easier to answer questions like:

  • Is this driven by AV scanning or EDR activity?
  • Are specific file types more expensive?
  • Are certain paths generating excessive activity?
  • Is this expected behavior for the workload?
  • Why This Matters

In many cases, Defender is working exactly as intended but it has to operate in environments where:

  • software may be outdated
  • files change frequently
  • large numbers of small files are accessed
  • or applications behave in ways that trigger deeper inspection

Without proper analysis, it’s easy to jump straight to exclusions. That approach can work – but could introduce risks.

A better approach is:

  • Understand what’s happening
  • Identify the actual cost drivers
  • Decide whether tuning is necessary

A typical trace might show:

  • OnDemandScan activity
  • Reason: EDRSensor
  • Files under:
  • AppData\Local\Programs\Microsoft VS Code
  • node_modules
  • Process-based scans (pid:xxxxx)

Defender is simply doing its job – inspecting what could be risky.

The Goal

The purpose of these functions isn’t to reduce protection. It’s to provide clarity. Because once you can see:

  • what is being scanned
  • why it’s being scanned
  • and how much it costs
The tool is published in the following Github repo:
https://github.com/mattiasborg82/PublicTools/blob/main/Defender%20Boys/Defender/Performance/Invoke-DBDefenderPerformance.ps1

Example usage to Create a recording:
Invoke-DBDefenderPerformance -Seconds 900 -Top 50 -Raw -ExportJson
Example usage to Read the report:
$report = Get-MpPerformanceReport -Path "C:\Temp\DefenderPerformance-20260428-095235.etl" -Raw

$summary = Convert-DBDefenderPerformanceReport -Report $report -Top 10 -SlowScanMs 200 -IncludeRaw
$summary | Format-Table -AutoSize

$summary.SlowScans | Format-Table DurationMs, ScanType, Reason, SkipReason, Extension, Path -AutoSize
    $summary.ByReason | Format-Table -AutoSize
    $summary.ByExtension | Format-Table -AutoSize
    $summary.ByParentPath | Format-Table -AutoSize
    $summary.SlowScans |
        Where-Object Reason -eq "EDRSensor" |
        Select-Object DurationMs, ScanType, Extension, Path |
        Format-Table -AutoSize

# Additional analysis example: Check authenticode signatures of slow scans involving executables
    $summary.SlowScans |
        Where-Object { $_.Extension -in ".exe", ".dll" } |
        Select-Object -ExpandProperty Path -Unique |
        ForEach-Object {
            if (Test-Path -LiteralPath $_) {
                Get-AuthenticodeSignature -FilePath $_ |
                    Select-Object Path, Status, StatusMessage, SignerCertificate
            }
        }

Basically, you can now make well-informed decisions in a easy way

#Happy Hunting

DisruptionAndResponseEvents Table Preview

What can be more exciting than new Advance Hunting tables. Since it’s Preview it could change

The DisruptionAndResponseEvents table in Microsoft Defender XDR is your visibility layer into automatic attack disruption – Microsoft’s built-in capability to actively stop attacks in progress.

It records what Defender did, not just what the attacker did.

Telemetry

Most telemetry tells you what happened while this table tells you how an attack was disrupted in real time and it helps you understand attack scope, attack impact and Defender’s automated response decisions.

What kind of events you’ll see?

The table focuses on defensive actions

  • Containment actions
  • Examples:
  • User logon blocked
  • SMB access blocked
  • RPC access denied
  • RDP sessions disconnected

Hardening & protection policies

  • Safe Boot blocked
  • Safe Boot Guard applied
  • Group Policy hardening applied
  • SYSVOL access blocked

These indicate environment hardening triggered automatically

Session / activity termination

  • SMB session killed
  • Remote session logged off

These show active attacker sessions being cut off

Important nuance (most people miss this) the table does NOT log the action itself (like “Contain User”) instead, it logs the resulting effects, such as:

  • Blocked logons
  • Denied access
  • Policy enforcement

Data sources & dependencies

Populated from multiple Defender services (Endpoint, Identity, etc.) which requires proper onboarding – otherwise you’ll see little or no data

No onboarding = no visibility.

This table shines when you want to answer:

  • What did Defender automatically stop?
  • Why did this attack fail?
  • Which users/devices were actively contained?
  • What hardening actions were triggered during an incident?

If you want to have the table extended with the description you can use the following User-defined function (which runs in query time in Defender)

For further information about Attack Disruption, please visit: Automatic attack disruption in Microsoft Defender – Microsoft Defender XDR | Microsoft Learn

#Happy Hunting

Unified detections in Defender XDR

Something detection engineers (working with Defender XDR) has been waiting for is being able to use other static data to enrich queries. In Sentinel we have been able to use watchlists for a long time but on the Defender side this has been missing.

Some short updates (this give value):

_GetWatchlist('ExcludedIPs')

So what happens when you configure a new detection?

The detection wizard is showing and one of the new things here is the Frequenzy “Custom” which is familiar from a Sentinel perspective.

When it comes to lookback time, there are a few things to know:

https://learn.microsoft.com/en-us/defender-xdr/custom-detection-rules

In the next step, the Alert settings we have the opportunity to dynamically set the Provider name and other custom details (key value pairs)

We can also do detailed entity mapping as well as “Related Evidence” mapping. This means Entities that are related to the alert, but are not impacted assets.


Depending on data source we can also configure automated actions (as before).

We recommend you to AVOID doing isolate device as an automated action, things can go wrong fast.

Comparison between Analytics rules and custom detections: Compare Microsoft Sentinel analytics rules and Microsoft Defender custom detections – Microsoft Security | Microsoft Learn

#Happy Hunting


Unquoted – Combining vuln data with processes

The background is not new in anyway, this is more on how to combine information about existing vulnerability and find exploitation of the vulnerability.

Let’s do a refresh on unquoted service paths and why it’s dangerous

If you’ve done any amount of Windows privilege escalation, you’ve probably come across unquoted service paths. It’s one of those vulnerabilities that feels almost too simple-but it still shows up in real environments far more often than it should.

//Query to list all services vulnerable to unquoted service path
DeviceTvmSecureConfigurationAssessment
| where ConfigurationId == "scid-3001"
| where IsApplicable == 1 and IsCompliant == 0
| extend ParsedContext = parse_json(Context)
| mv-expand ServiceEntry = ParsedContext
| extend ServiceName = tostring(ServiceEntry[0]), ServicePathRaw = tostring(ServiceEntry[1])
| extend ServicePathRawClean = tolower(replace_string(ServicePathRaw, "/", "\\"))
| where ServicePathRawClean contains " "
| where not(ServicePathRawClean startswith "\"")
| extend ServicePath = trim(@" """, ServicePathRawClean)
| where ServicePath matches regex @"^[a-zA-Z]:\\.*\.exe$"
| summarize count() by ServicePath

So how does this work?

Let’s break down how it works, why it’s exploitable, and how to actually abuse it in a real scenario.

The Root Cause
In Windows, services are defined with a binary path (Image Path) that tells the Service Control Manager (SCM) what executable to run.

A typical service path might look like this:
C:\Program Files\Example App\service.exe

If this path is not enclosed in quotes, Windows does something really dangerous.
Instead of treating the full string as a the single path, Windows parses it by splitting on spaces and tries to execute each possibility in order.
So this:
C:\Program Files\Example App\service.exe

Gets interpreted like this:
C:\Program.exe
C:\Program Files\Example.exe
C:\Program Files\Example App\service.exe

Windows will execute the first match it finds.
That behavior is the entire vulnerability.

Why does this become a Privilege Escalation?
Many services run as LocalSystem or another highly privileged account. If you can influence what binary gets executed, you effectively get code execution at that privilege level.

So the attack becomes very straightforward:

  • Find a service with an unquoted path
  • Identify writable directories in the path chain
  • Drop a malicious executable with the right name
  • Restart the service (or wait for reboot)
  • If Windows hits your binary first, it executes it as SYSTEM.
    Game over.

You might think that no user has write access to a subfolder in Program Files… Normally no, but some apps sets ACLs that allow any user to write there (this is in you application management process to check if this happens)

Real Example

Let’s say a service is configured like this:
PathName: C:\Program Files\Vendor Corp\Service App\appservice.exe
– No quotes.
Windows will try:

  • C:\Program.exe
  • C:\Program Files\Vendor.exe
  • C:\Program Files\Vendor Corp\Service.exe
  • C:\Program Files\Vendor Corp\Service App\appservice.exe

Now imagine you have write permissions to:
C:\Program Files\Vendor Corp\
You can drop:
Service.exe
And when the service starts, Windows executes your payload instead of the legitimate binary.

Finding Vulnerable Services

You don’t need fancy tools to identify these. A simple command works:

Get-WmiObject Win32_Service | Where-Object { $_.PathName -match ' ' -and $_.PathName -notmatch '"'} | Select Name, PathName, StartName

( There are other powershell variants as well)

You’re looking for:

  • Paths with spaces
  • No surrounding quotes (“)
  • Services running as SYSTEM (or at least higher privileges, or as a user you want to impersonate)
  • Services that are restartable

Exploitation in Practice
Once you’ve found a candidate, the next step is checking permissions.
You need write access to one of the directories in the path chain. Tools like icacls or accesschk (from Sysinternals) are useful here.
Example:

icacls "C:\Program Files\Vendor Corp"

If you can write there, you’re good to go

  • Create a payload (e.g., reverse shell, add admin user, etc.)
  • Name it according to the parsed path (Service.exe, Vendor.exe, etc.)
  • Drop it in the directory
  • Restart the service:
sc stop Vendorervice
sc start AcmeService

If you don’t have permission to restart it, you wait for System reboot, Scheduled restart etc

Detection and Defense

From a defensive perspective, this is one of the easiest issues to fix-and one of the easiest to miss.
The fix is trivial:
“C:\Program Files\Venfdor Corp\Service App\appservice.exe”
Just add quotes.

Using Defender to find the vulnerabilities

In Defender portal and Vulnerability management there are recommendations related to configurations that should be fixed

https://security.microsoft.com/exposure-recommendations?tabId=Devices

The configuration ID for unquoted service paths is: scid-3001 (This ID can be used in advanced hunting

But only finding the vulnerable devices is not enough. It would be even better to also find exploitation attempts which can be done by enumerating all possibilities for each service path and then check for file writes using DeviceFileEvents, Process executions from DeviceProcessEvents.

Below is a query to detect if someone tries to exploit unquoted service paths, but don’t forget, you need to adapt your process to identify these issues when introducing a new software in your environment

// Detect Unquoted Service Path Abuse
// Identifies vulnerable unquoted service paths(scid-3001) and correlates them with executions from possible abuse paths.
// Generates all executable candidates from space paths and detects when services executes those.
// Added enrichment with file activity when available.
// 
// DefenderBoys
let Lookback = 30d;
let UnquotedServicePathService =
DeviceTvmSecureConfigurationAssessment
| where ConfigurationId == "scid-3001"
| where Timestamp >= ago(Lookback)
| where IsApplicable == 1 and IsCompliant == 0
| extend ParsedContext = parse_json(Context)
| mv-expand ServiceEntry = ParsedContext
| extend ServiceName = tostring(ServiceEntry[0]), ServicePathRaw = tostring(ServiceEntry[1])
| extend ServicePathRawClean = tolower(replace_string(ServicePathRaw, "/", "\\"))
| where ServicePathRawClean contains " "
| where not(ServicePathRawClean startswith "\"")
| extend ServicePath = trim(@" """, ServicePathRawClean)
| where ServicePath matches regex @"^[a-zA-Z]:\\.*\.exe$"
| extend CandidateIndexes = range(0, strlen(ServicePath) - 1, 1)
| mv-apply CandidateIndex = CandidateIndexes on (
    extend SpaceIndex = toint(CandidateIndex)
    | where substring(ServicePath, SpaceIndex, 1) == " "
    | extend CandidatePrefix = substring(ServicePath, 0, SpaceIndex)
    | extend HijackFolderPath = strcat(CandidatePrefix, ".exe")
    | where HijackFolderPath matches regex @"^[a-zA-Z]:\\.*\.exe$"
    | where HijackFolderPath != ServicePath
    | project DeviceId, DeviceName, ServiceName, ServicePath, HijackFolderPath
)
| summarize by DeviceId, DeviceName, ServiceName, ServicePath, HijackFolderPath;
let ServiceHijackExecutions =
DeviceProcessEvents
| where Timestamp >= ago(Lookback)
| where InitiatingProcessFileName =~ "services.exe"
| extend ExecutedPath = tolower(FolderPath)
| project ExecutionTime = Timestamp, DeviceId, DeviceName, ExecutedPath, FileName, FolderPath, ProcessCommandLine, SHA1, SHA256, MD5, FileSize, ProcessVersionInfoCompanyName, ProcessVersionInfoProductName, ProcessVersionInfoOriginalFileName, AccountDomain, AccountName, AccountUpn, ProcessIntegrityLevel, ProcessTokenElevation, InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessIntegrityLevel, InitiatingProcessTokenElevation, ReportId;
let FileActivityOnHijackFolderPath =
// Adding File activities to provide more data into an eventual incident
DeviceFileEvents
| where Timestamp >= ago(Lookback)
| where FolderPath matches regex @"^[a-zA-Z]:\\.*\.exe$"
| extend CurrentFolderPath = tolower(FolderPath)
| extend PreviousPath = tolower(iff(ActionType =~ "FileRenamed" and isnotempty(PreviousFolderPath) and isnotempty(PreviousFileName), strcat(PreviousFolderPath, "\\", PreviousFileName), ""))
| project Timestamp, DeviceId, DeviceName, FileActionType=ActionType, CurrentFolderPath, PreviousPath, FileName, SHA1, SHA256, MD5, FileSize, FileOriginUrl, FileOriginReferrerUrl, FileOriginIP, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessAccountUpn, InitiatingProcessFolderPath, InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessIntegrityLevel, InitiatingProcessTokenElevation, InitiatingProcessParentFileName, RequestProtocol, RequestSourceIP, RequestSourcePort, RequestAccountName, RequestAccountDomain, ShareName, ReportId;
UnquotedServicePathService
| join kind=inner ServiceHijackExecutions on DeviceId
| where ExecutedPath == HijackFolderPath
| join kind=leftouter FileActivityOnHijackFolderPath on DeviceId // File events is just extra not mandatory
| where CurrentFolderPath == HijackFolderPath or isempty(CurrentFolderPath)
| extend FileActivityBeforeExecution = iff(isnotempty(Timestamp) and Timestamp <= ExecutionTime, true, false)
| extend FileActivityType = case(
    CurrentFolderPath == HijackFolderPath and FileActionType =~ "FileCreated", "Hijack file created",
    CurrentFolderPath == HijackFolderPath and FileActionType =~ "FileModified", "Hijack file modified",
    CurrentFolderPath == HijackFolderPath and FileActionType =~ "FileDeleted", "Hijack file deleted",
    CurrentFolderPath == HijackFolderPath and FileActionType =~ "FileRenamed", "Hijack file renamed into place",
    isempty(FileActionType), "No matching file event",
    "N/A"
)
| project ExecutionTime, DeviceName, ServiceName, VulnerableServicePath = ServicePath, ExpectedHijackFolderPath = HijackFolderPath, ExecutedPath, FileActivityTimestamp = Timestamp, FileActivityBeforeExecution, FileActivityType, CurrentFolderPath, PreviousPath, FileName, FolderPath, ProcessCommandLine, SHA1, SHA256, MD5, FileSize, ProcessVersionInfoCompanyName, ProcessVersionInfoProductName, ProcessVersionInfoOriginalFileName, AccountDomain, AccountName, AccountUpn, ProcessIntegrityLevel, ProcessTokenElevation, InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessIntegrityLevel, InitiatingProcessTokenElevation, InitiatingProcessParentFileName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessAccountUpn, InitiatingProcessFolderPath, FileOriginUrl, FileOriginReferrerUrl, FileOriginIP, RequestProtocol, RequestSourceIP, RequestSourcePort, RequestAccountName, RequestAccountDomain, ShareName, ReportId
| order by ExecutionTime desc

Over and out – Defender Boys

Update – Some queries are uploaded to this github repository:

mattiasborg82/Talks: Demo code and other information from my talks

Happy Hunting

New security KQL functions focused around graph

Microsoft has published a set of new security functions (user-defined) in KQL. These are focused on the graph mindset, meaning relationships between nodes (objects). A previous blogpost explains graph semantics a bit more:

Graph semantics in Kusto – SEC-LABS R&D

The new functions released has many goodies, such as calculating blast radius, exposure perimeter and much more

User-defined functions can be saved as functions in Defender XDR or executed within the query itself.

Function NameDescription
detect_anomalous_new_entity_fl()Detect the appearance of anomalous new entities in timestamped data.
detect_anomalous_spike_fl()Detect the appearance of anomalous spikes in numeric variables in timestamped data.
graph_blast_radius_fl()Calculate the Blast Radius (list and score) of source nodes over path or edge data.
graph_exposure_perimeter_fl()Calculate the Exposure Perimeter (list and score) of target nodes over path or edge data.
graph_node_centrality_fl()Calculate various metrics of node centrality (such as degree and betweenness) over graph data (edge and nodes).
graph_path_discovery_fl()Discover valid paths between relevant endpoints (sources and targets) over graph data (edge and nodes).

https://learn.microsoft.com/en-us/kusto/functions-library/functions-library?view=azure-data-explorer#cybersecurity-functions

Example query (with defined table as input) from learn

let edges = datatable (SourceNodeName:string, EdgeName:string, EdgeType:string, TargetNodeName:string, Region:string)[						
    'vm-work-1',            'e1',           'can use',	            'webapp-prd', 	          'US',
    'vm-custom',        	'e2',           'can use',	            'webapp-prd', 	          'US',
    'webapp-prd',           'e3',           'can access',	        'vm-custom', 	          'US',
    'webapp-prd',       	'e4',           'can access',	        'test-machine', 	      'US',
    'vm-custom',        	'e5',           'can access',	        'server-0126', 	          'US',
    'vm-custom',        	'e6',	        'can access',	        'hub_router', 	          'US',
    'webapp-prd',       	'e7',	        'can access',	        'hub_router', 	          'US',
    'test-machine',       	'e8',	        'can access',	        'vm-custom',              'US',
    'test-machine',        	'e9',	        'can access',	        'hub_router', 	          'US',
    'hub_router',           'e10',	        'routes traffic to',	'remote_DT', 	          'US',
    'vm-work-1',            'e11',	        'can access',	        'storage_main_backup', 	  'US',
    'hub_router',           'e12',	        'routes traffic to',	'vm-work-2', 	          'US',
    'vm-work-2',        	'e13',          'can access',	        'backup_prc', 	          'US',
    'remote_DT',            'e14',	        'can access',	        'backup_prc', 	          'US',
    'backup_prc',           'e15',	        'moves data to',        'storage_main_backup', 	  'US',
    'backup_prc',           'e16',	        'moves data to',        'storage_DevBox', 	      'US',
    'device_A1',            'e17',	        'is connected to',      'sevice_B2', 	          'EU',
    'sevice_B2',            'e18',	        'is connected to',      'device_A1', 	          'EU'
];
let nodes = datatable (NodeName:string, NodeType:string, NodeEnvironment:string, Region:string) [
        'vm-work-1',                'Virtual Machine',      'Production',       'US',
        'vm-custom',                'Virtual Machine',      'Production',       'US',
        'webapp-prd',               'Application',          'None',             'US',
        'test-machine',             'Virtual Machine',      'Test',             'US',
        'hub_router',               'Traffic Router',       'None',             'US',
        'vm-work-2',                'Virtual Machine',      'Production',       'US',
        'remote_DT',                'Virtual Machine',      'Production',       'US',
        'backup_prc',               'Service',              'Production',       'US',
        'server-0126',              'Server',               'Production',       'US',
        'storage_main_backup',      'Cloud Storage',        'Production',       'US',
        'storage_DevBox',           'Cloud Storage',        'Test',             'US',
        'device_A1',                'Device',               'Backend',          'EU',
        'device_B2',                'Device',               'Backend',          'EU'
];
let nodesEnriched = (
    nodes
    | extend IsValidStart = (NodeType == 'Virtual Machine'),             IsValidEnd = (NodeType == 'Cloud Storage')              // option 1
    //| extend IsValidStart = (NodeName in('vm-work-1', 'vm-work-2')),     IsValidEnd = (NodeName in('storage_main_backup'))       // option 2
    //| extend IsValidStart = (NodeEnvironment == 'Test'),                 IsValidEnd = (NodeEnvironment == 'Production')          // option 3
);
let graph_path_discovery_fl = (   edgesTableName:string, nodesTableName:string, scopeColumnName:string
								, isValidPathStartColumnName:string, isValidPathEndColumnName:string
								, nodeIdColumnName:string, edgeIdColumnName:string, sourceIdColumnName:string, targetIdColumnName:string
								, minPathLength:long = 1, maxPathLength:long = 8, resultCountLimit:long = 100000) 
{
let edges = (
    table(edgesTableName)
    | extend sourceId           = column_ifexists(sourceIdColumnName, '')
    | extend targetId           = column_ifexists(targetIdColumnName, '')
    | extend edgeId             = column_ifexists(edgeIdColumnName, '')
    | extend scope              = column_ifexists(scopeColumnName, '')
    );
let nodes = (
    table(nodesTableName)
    | extend nodeId             = column_ifexists(nodeIdColumnName, '')
    | extend isValidPathStart   = column_ifexists(isValidPathStartColumnName, '')
    | extend isValidPathEnd     = column_ifexists(isValidPathEndColumnName, '')
    | extend scope              = column_ifexists(scopeColumnName, '')
);
let paths = (
    edges
    // Build graph object partitioned by scope, so that no connections are allowed between scopes.
    // In case no scopes are relevant, partitioning should be removed for better performance.
    | make-graph sourceId --> targetId with nodes on nodeId partitioned-by scope (
    // Look for existing paths between source nodes and target nodes with less than predefined number of hops
    // Current configurations looks for directed paths without any cycles; this can be changed if needed
      graph-match cycles = none (s)-[e*minPathLength..maxPathLength]->(t)
        // Filter only by paths with that connect valid endpoints
        where ((s.isValidPathStart) and (t.isValidPathEnd))
        project   sourceId                  = s.nodeId
                , isSourceValidPathStart    = s.isValidPathStart
                , targetId                  = t.nodeId
                , isTargetValidPathEnd      = t.isValidPathEnd
                , scope                     = s.scope
                , edgeIds                   = e.edgeId
                , edgeAllTargetIds          = e.targetId
    | limit resultCountLimit
    )
    | extend  pathLength                    = array_length(edgeIds)
            , pathId                        = hash_md5(strcat(sourceId, strcat(edgeIds), targetId))
            , pathAllNodeIds                = array_concat(pack_array(sourceId), edgeAllTargetIds)
    | project-away edgeAllTargetIds
    | mv-apply with_itemindex = SortIndex nodesInPath = pathAllNodeIds to typeof(string), edgesInPath = edgeIds to typeof(string) on (
        extend step = strcat(
              iff(isnotempty(nodesInPath), strcat('(', nodesInPath, ')'), '')
            , iff(isnotempty(edgesInPath), strcat('-[',  edgesInPath, ']->'), ''))
       | summarize fullPath = array_strcat(make_list(step), '')
    )
);
paths
};
graph_path_discovery_fl(edgesTableName          = 'edges'
                , nodesTableName                = 'nodesEnriched'
                , scopeColumnName               = 'Region'
                , nodeIdColumnName              = 'NodeName'
                , edgeIdColumnName              = 'EdgeName'
                , sourceIdColumnName            = 'SourceNodeName'
                , targetIdColumnName            = 'TargetNodeName'
                , isValidPathStartColumnName    = 'IsValidStart'
                , isValidPathEndColumnName      = 'IsValidEnd'
)

Happy Hunting!

Query ADX Data from Defender Advanced Hunting

Azure Data Explorer is amazing storage for temporary data, for instance forensic artefacts and other structured data

To have access to this data from Defender Advanced Hunting is amazing capability

use the function adx() to access your data cluster and join the data together with the hunting data available in Defender XDR

Use Microsoft Sentinel custom functions in advanced hunting in Microsoft Defender – Microsoft Defender XDR | Microsoft Learn

Happy Hunting!

Using Exposure Graph Data to get Domain Admins and Domain Controllers dynamically

When working with both Threat Hunting and Detection Engineering it has been a challenge to dynamically get domain administrators and domain controllers. Sometimes, a valid guess was the best approach.

With the Exposure Graph data, we can dynamically retrieve these entities.

Another thing which will simplify your queries is to save this as a function

Example code for Domain Admins

//Get Domain Admin accounts (member of "Domain Admins")
ExposureGraphNodes
| extend json = (parse_json(NodeProperties)).rawData
| where json.nestedAdGroupNames has "Domain Admins"
| where NodeLabel == "user"
| mv-apply EntityIds on (
                            summarize Identifiers = make_bag(
                                                            pack(
                                                                    tostring(EntityIds.type), 
                                                                    EntityIds.id
                                                                )
                                                        )
                        )
| project accountName = json.accountName,
                        Identifiers,
                        accountEnabled=json.accountEnabled,
                        distinguishedName=json.distinguishedName,
                        adminCount=json.adminCount,
                        AccountControl=json.userAccountControl,
                        passwordUpdateTime=json.passwordUpdateTime,
                        createdDateTime = json.createdDateTime,
                        sidHistory=json.sidHistory,
                        accountDomain=json.accountDomain,
                        nestedAdGroupNames = json.nestedAdGroupNames

Example code of usage

//One usage of DAs
let DAs = 
ExposureGraphNodes
| extend json = (parse_json(NodeProperties)).rawData
| where json.nestedAdGroupNames has "Domain Admins"
| where NodeLabel == "user"
| distinct tostring(json.accountName);
IdentityLogonEvents
| where AccountName has_any (DAs)

//Another example
let DAs = 
ExposureGraphNodes
| extend json = (parse_json(NodeProperties)).rawData
| where json.nestedAdGroupNames has "Domain Admins"
| where NodeLabel == "user"
| distinct tostring(json.accountName);
DeviceLogonEvents
| where AccountName has_any (DAs)
| summarize DistinctDevices = dcount(DeviceName), 
            NetworkSuccessLogons = countif(ActionType == "LogonSuccess" and LogonType == "Network"), 
            RemoteInteractiveSuccessLogons = countif(ActionType=="LogonSuccess" and LogonType  =="RemoteInteractive"),
            TotalRemoteDeviceNames = dcount(RemoteDeviceName),
            RemoteIPs = dcount(RemoteIP),
            FailedLogins =countif(ActionType == "LogonFailed"),
            AttemptedLogins = countif(ActionType=="LogonAttempted"), 
            ProtocolsUsed = dcount(Protocol)
         by AccountName

Domain controllers

ExposureGraphNodes
| extend _jsonBlob = (parse_json(NodeProperties)).rawData
| extend DeviceName = tolower((_jsonBlob.deviceName)),
         Roles = _jsonBlob.deviceRole,
         OSVersionBuild = strcat(tostring(_jsonBlob.osVersionFriendlyName),"-",tostring(_jsonBlob.osReleaseId),"-",tostring(_jsonBlob.osBuild)),
         OSVersion = tostring(_jsonBlob.osVersionFriendlyName),
         ExposureScore = tostring(_jsonBlob.exposureScore),
         SenseClientVersion = tostring(_jsonBlob.senseClientVersion),
         isHybridAzureADJoined = _jsonBlob.isHybridAzureADJoined,
         isAzureADJoined = _jsonBlob.isAzureADJoined,
         SensorHealthState = tostring(_jsonBlob.sensorHealthState),
         DeviceRiskScore = tostring(_jsonBlob.riskScore),
         isInternetFacing = _jsonBlob.isInternetFacing,
         RDPStatus = _jsonBlob.rdpStatus,
         RemoteServices = _jsonBlob.remoteServicesInfo,
         TPMInfo = _jsonBlob.tpmData,
         VulnHasHighOrCritical = _jsonBlob.highRiskVulnerabilityInsights.hasHighOrCritical,
         VulnMaxCvssScore = _jsonBlob.highRiskVulnerabilityInsights.maxCvssScore
| extend RDPServiceStatus = RDPStatus.serviceRunning,
         RDPAllowConnections = RDPStatus.allowConnections
| where Roles has "DomainController"          
| mv-apply EntityIds on (
                            summarize Identifiers = make_bag(
                                                            pack(
                                                                    tostring(EntityIds.type), 
                                                                    EntityIds.id
                                                                )
                                                        )
                        )
| project-away NodeId,NodeLabel,NodeName,NodeProperties,_jsonBlob, Categories, Roles,RDPStatus

//Another example

let DCs = ExposureGraphNodes
| extend _jsonBlob = (parse_json(NodeProperties)).rawData
| extend DeviceName = tolower((_jsonBlob.deviceName)),
         Roles = _jsonBlob.deviceRole,
         OSVersionBuild = strcat(tostring(_jsonBlob.osVersionFriendlyName),"-",tostring(_jsonBlob.osReleaseId),"-",tostring(_jsonBlob.osBuild)),
         OSVersion = tostring(_jsonBlob.osVersionFriendlyName),
         ExposureScore = tostring(_jsonBlob.exposureScore),
         SenseClientVersion = tostring(_jsonBlob.senseClientVersion),
         isHybridAzureADJoined = _jsonBlob.isHybridAzureADJoined,
         isAzureADJoined = _jsonBlob.isAzureADJoined,
         SensorHealthState = tostring(_jsonBlob.sensorHealthState),
         DeviceRiskScore = tostring(_jsonBlob.riskScore),
         isInternetFacing = _jsonBlob.isInternetFacing,
         RDPStatus = _jsonBlob.rdpStatus,
         RemoteServices = _jsonBlob.remoteServicesInfo,
         TPMInfo = _jsonBlob.tpmData,
         VulnHasHighOrCritical = _jsonBlob.highRiskVulnerabilityInsights.hasHighOrCritical,
         VulnMaxCvssScore = _jsonBlob.highRiskVulnerabilityInsights.maxCvssScore
| extend RDPServiceStatus = RDPStatus.serviceRunning,
         RDPAllowConnections = RDPStatus.allowConnections
| where Roles has "DomainController"          
| mv-apply EntityIds on (
                            summarize Identifiers = make_bag(
                                                            pack(
                                                                    tostring(EntityIds.type), 
                                                                    EntityIds.id
                                                                )
                                                        )
                        )
| project-away NodeId,NodeLabel,NodeName,NodeProperties,_jsonBlob, Categories, Roles,RDPStatus
| distinct _DeviceName;
DeviceProcessEvents
| where DeviceName in(DCs)

Happy Hunting!

Another reason to make sure Defender is configured correctly

Attack Disruption

  • It uses Defender XDR’s ability to correlate signals from many different sources into a single, high-confidence incident through insights from endpoints, identities, email and collaboration tools, and SaaS apps.
  • It identifies assets controlled by the attacker and used to spread the attack.
  • It automatically takes response actions across relevant Microsoft Defender products to contain the attack in real-time by containing and disabling affected assets.

Previous blog post on Attack Disruption Automatic Attack Disruption Explained – SEC-LABS R&D

With all the capabilities provided within Attack Disruption, it’s important that Defender XDR is configured correctly. For instance, if you have configured a gMSA account for the MDI actions, and it does not work properly, it will affect disruption negatively.

MDE Prereqs

  • Minimum Sense version for contain user:v10.8470
  • Auto IR:  Full – remediate threats automatically (recommended for full coverage, no automated response turns off automatic disruption for device group)
  • Endpoint discovery set to Standard (not basic)

In MDE the most common configuration improvements seen are:

  • Sample submission is not configured to send files automatically
  • Cloud block out level is not set to high
  • Cloud block timeout period is not set to 50s

more on MDE in this previous post 3 common Defender for Endpoint configuration errors

MDI Prereqs

  • Validate DC auditing
  • Validate permissions for Action Account or use LocalSystem
  • Validate no other systems interfere with attack disruption, such as systems for auto activating users

MDO Prereqs

  • Mailboxes are required to be hosted in Exchange Online.
  • The following mailbox events need to be audited by minimum:
    • MailItemsAccessed
    • UpdateInboxRules
    • MoveToDeletedItems
    • SoftDelete
    • HardDelete
    • Safelinks policy needs to be present.

MDA Prereqs

  • Microsoft Defender for Cloud Apps must be connected to Microsoft Office 365 through the connector
  • App Governance must be turned on

Happy Hunting!

Automatic Attack Disruption Explained

Some time ago, Microsoft announced and released Automatic Attack Disruption which is a feature within the Microsoft Defender XDR suite. Since then it has growth and from working for a service provider of Managed Detection and Response and running 100% Microsoft XDR, this is an amazing feature.

This blogpost will explain different aspects of disruption and how certain things works and the requirements in form of configurations to make it work.

Please note: All screenshots from Defender is from a Demo and Lab tenants and not customer tenants

Incident view attack disruption

The feature has to many parts possible to write a blog post about on its own, but let’s start somewhere:

Automatic Attack Disruption is not a new Auto IR (MDE) that acts on a single detection and tries to remediate according to defined playbook. Attack Disruption is chain of events, lower signals, such as RDP login to next event to next etc. until a certain level of confidence is reached. When the confidence reaches 99%+ SNR (Signal to Noise Ratio) then it kicks in. But it doesn’t simply stops processes or uses other classical Antimalware techniques, it goes back to the initial event and blocks everything used on the way.

Example scenario:

Internet to exposed RDP > Ask for service tickets for all SPNs (Kerberoast) > [Offline crack] > Login interactively to a server from the compromised RDP using one of the credentials from the Kerberoast attack > Enumerate ADCS templates > enroll certificate with new SAN > Domain admin

What Attack disruption will do is take a note of the initial RDP (similar to a human like: “Ok, someone used RDP, it’s not malicious but I will keep an eye on it”).

Next step is the Kerberoast

Still not malicious activities depending on how the threat actor executes that it could be an SPN enumeration alert. However, asking DC for a ticket is how Active Directory works and let’s play this as no incidents are generated in this case.

Threat Actor’s next step, after the tickets where cracked and the SPN account password is now in clear text (using hashcat, john to crack the password). Now it’s time for Threat Actor to login using these credentials and they user RDP to login interactively on the remote server.

The server they end up on has access as system to enroll a vulnerable certificate template (ESC1) and becomes domain admin using a sensitive account as the SAN name.

When Attack Disruption is over 99% certain that this is malicious activities, it kicks in. Remember that just RDP is very common still by both users and admins and Attack Disruption does not kick in until it is certain.

What is awesome with the feature is that when it kicks in, it takes care of entities involved in the chain, to completely block the Threat Actor. Such as containing device (If not onboarded to MDE, it will tell all onboarded endpoints that they are not allowed to communicate with the mentioned device). The involved users will be contained as well, blocking them from logons and disabling accounts.

Another example I would like to explain is the Business Email Compromise, BEC

A user clicks on a phishing link (even if we, as a community pushes security awareness, it still happens frequently)

The user who clicked ends up on the link lands on a phishing website, which act as a proxy and therefor can handle MFA requests as well since the proxy will try authenticate to, in this case, Office 365

MFA

From the Threat Actor side and the Evilginx, the authentication is captured

The cookies from the authentication can then be injected to the browser and the threat actors are logged in.

The Threat Actor then takes further steps, such as configure rules to exfiltrate all incoming emails using a forward rule to an external email address, or send a email to finance department to pay an invoice, and configure a rule to move all replies to such email to another folder to avoid the compromised user to notice this.

Back to Attack Disruption…

So what happens in the last scenario is that the service detects the events which by themselves might not be that bad, but when chained it’s certain that it’s something bad going on and disruption kicks in to stop the attack.

Another example

Attack Disruption is definitely a game-changer. When it detects something bad it will take actions on all assets used by the Threat actor automatically which will happen faster than a human can react and click. The humans are still needed, but this is a great feature the back the human SecOps.

Building Disruption Scenarios

When Microsoft develop new scenarios, they start, as in proactive Threat Hunting, with a hypothesis.

  • Research creates hypothesis
  • Research validates hypothesis in telemetry
  • Detector(s) development for events worth noticing
  • Detectors trigger as non-customer facing detections
    (establish 99% confidence)
  • Non customer facing alerts are manually investigated
    (establishing 99%  confidence)
  • Release alerts to customers
    (not taking action)
  • Alerts are released but throttled – Disrupt engages
    (Triggered alerts are manually investigated)
  • Alerts are released through release rings
    (Customer grading is monitored)
  • Customers with Disrupt incidents are being contacted and interviewed
    (Validating response efficacy)
  • Scenario gets released to all customers
  • Detector are being reviewed and updated
    (based on new TTPs and customer feedback/ researcher grading)

From correlated signals to disruption

Scenarios and requirements

EndpointsOn-Prem IdentityOffice 365Cloud AppsSentinelCloud Identity
Human Operated Ransomware****
Hands on Keyboard Attack**
Business Email Compromise****
Adversary-in-the-Middle**********
Compomised Credentials by
Known Threat Actor
****
Credential Stuffing****
Compromised IaaS Cloud Resource**
SAP Financial Process Manipulation
(3rd Party Signals required)
****