Extending Defender XDR with Velociraptor

James Fox, Cyber Security Operations Consultant | 4 December 2024

Extending Defender XDR with Velociraptor

In this blog post, we'll go through how we at Fortian Managed Security Services use Velociraptor with Defender XDR to extend security operations investigations beyond the boundaries of SIEM telemetry.

In Fortian, we often are alerted by EDR systems of "suspicious" activity on an endpoint, for instance, a malicious network connection, suspected process injection, or discovery actions. Generally, security operations analysts have access to enough telemetry from EDR and other systems within a centralised SIEM in order to answer all the investigative questions and triage the alert.

However, this isn't always the case. Sometimes there just isn't any telemetry available to answer all the questions need to figure out the nature of an alert or potential impact. In these cases, there are a few strategies that SOCs employ to fill in the gaps:

  • Use threat intelligence and external databases to help strengthen the case for benign or malicious activity. For instance, an anomalous sign-in from a residential IP address would be less suspicious than from an AWS IP range.
  • In the case of EDR alerts, collecting host-based or stateful information directly from the impacted system for analysis. This is a pretty typical step when triaging suspected malware - it's going to be easier to tell if the file is or is not malicious by pulling it apart in a sandbox environment rather than fighting through EDR telemetry.

Using threat intelligence during triage is second nature for security operations analysts, but collecting and analysing host-based forensic artefacts isn't a super commonplace activity. This is not to say that it isn't immensely helpful, if not required in certain types of investigations, but there is a tendency to try and struggle through an analysis using log data only. We find this isn't necessarily because log data is *easier* to understand, but it is more accessible and requires less of a context shift for an analyst who might be triaging many alerts in a day.

Instead of ignoring host-based forensics for SOC triage and relegating it to an incident response-only activity, we should seek to reduce the effort it takes for SOC analysts to retrieve and start analysing forensic artefacts.

Enter Velociraptor

Velociraptor is a free and open-source DFIR tool which allows incident responders and forensic analysts to monitor, hunt and collect forensic artefacts from endpoints. It's typically deployed in a client-server model, where DFIR teams will stand up a Velociraptor server that keeps in constant communication with clients (the target endpoints).

Then, DFIR teams execute "hunts" on these endpoints in the form of Velociraptor artefacts and review the results. This is great if your an internal SOC where you have full control over the environment, but if you're a managed service provider it can cross some boundaries pretty quickly considering it's effectively RCE over the entire environment, and depending on where the Velociraptor server is deployed, outside of the control of the client.

Velociraptor can also execute in "offline collector" mode. In this mode, a standalone Velociraptor executable is generated ahead of time with a hardcoded list of artefacts to hunt for, and executed on a client endpoint. Then, results of the hunt are written to a ZIP file on disk (or pushed to some cloud storage account). For instance, if we're looking to collect browsing history from a device during a suspected malvertising incident, we can define an offline collector ahead of time to hunt for browser artefacts, execute it on the machine involved in the malvertising alert, and then collect the resultant ZIP file for analysis.

Extending this even further, ahead of time a SOC can build up their own library of offline collectors to use when triaging common endpoint alerts. Browsing activity is a classic example, but things like the content of a user's downloads directory, or reconstructed files from Defender's quarantine would be a huge win for a SOC to be able to gather quickly during an investigation.

Despite Velociraptor taking the effort out of collecting the artefacts on the endpoint, it's still a pain for triage to use because we have to deliver the offline collector somehow. We could ask the user or client to run this directly, and then to send the results back to us, but time is of the essence in security operations! Ideally, an analyst hits a single button and is done with it.

Integrating with Defender XDR

The best-case scenario to deliver the offline collector would be to exploit some existing method for execution that's accessible to security operations. Thankfully, most EDR tools including Defender for Endpoint come with native capabilities to execute code on monitored endpoints.

This can be done in Defender XDR through the Live Response feature, where arbitrary PowerShell scripts can be executed in a Live Response session on a host-by-host basis. There are a few challenges with this however:

  • The scripts need to be signed to avoid potential security ramifications (*cough cough* a bad guy getting access to Defender and using it to ransomware a server or two).
  • We need to get the collector itself to the host to execute it, which is dynamic depending on what types of artefacts we want to collect.
  • Velociraptor can do some pretty suspicious actions depending on what artefacts it's hunting for, so we need to make sure that Defender doesn't nuke it whilst it's executing. The easiest way to ensure this is to have Velociraptor execute as a signed binary (from the signed PowerShell script). This also gets around the single most annoying ASR rule under the sun "Block executable files from running unless they meet a prevalence, age, or trusted list criterion".
  • After execution, the resultant ZIP file needs to be written to disk then collected automatically by an analyst. Since this might contain sensitive information, it should be written to a non-world readable directory and cleaned up after collection.

On the topic of signing, Velociraptor offline collectors don't run within a signed context since they are built based on the artefacts they are hunting. But, with the addition of embedded collectors in Velociraptor 0.7.0, we can instead build embedded configuration files which define our collector and reference them from the original signed Velociraptor binary.

In this way we can collect whatever we want from a host and remain within a signed context. Additionally, we also need to sign our PowerShell wrapper around Velociraptor. The easiest way we've found is through Azure Trusted Signing.

In Fortian MSS, we have a number of these embedded offline collectors defined for usage by security operations analysts during investigations or targeted threat hunts. For example, the following configuration for collecting browsing history in Velociraptor is pretty simple (and just uses out-of-the-box artefacts).

Figure 1: Target Velociraptor artefacts in collector


Figure 2: Generic/embedded offline collector configuration



Once we have the collector sorted, we need a way to get the Velociraptor binary and embedded configuration file to the target endpoint, execute it, then collect the ZIP. All of these actions can be performed via Defender XDR Live Response through the API. The default executing directory for Live Response is protected and is cleaned up by Defender automatically. All together, we need two scripts to run through the whole process:

  • A PowerShell wrapper around Velociraptor which takes the offline collector embedded configuration file as an argument, retrieves the Velociraptor binary from GitHub and executes it. We're going to call this `Invoke-LocalVRCollector.ps1` (see below for detail). This is the script that needs to be signed (since it's executing on the target endpoint via Defender) and remains unchanged inside of the Defender Live Response library.
  • A PowerShell script executed by an analyst on their machine which takes an embedded configuration file and device identifier as parameters, calls the Defender Live Response API to upload the configuration file to the library, downloads the configuration file from the library to the endpoint, execute `Invoke-LocalVRCollector.ps1`, and then finally retrieves the result ZIP file. We're going to call this `Invoke-RemoteVRCollector.ps1` (see below for detail). After the file has been collected, this script will present the download URL to the analyst. It shouldn't automatically download it considering that it may contain malicious content.

Figure 3: System sequence diagram between the analyst machine, Defender XDR and the target/remote endpoint


Using the above scripts, collecting host-based forensic artefacts from an endpoint is now as simple as an analyst executing `Invoke-RemoteVRCollector.ps1` (after authenticating, e.g. `az login`). For example, to collect browsing history from an endpoint an analyst can simply run the following single command. From start to finish, this usually takes around 3 minutes to run (if Live Response isn't being temperamental).

Figure 4: Example of collecting browsing history from a remote endpoint, given the Defender Device ID and an embedded configuration file


Conclusion

By using Defender XDR Live Response to automatically deliver and execute Velociraptor embedded offline collectors, we can rapidly acquire host-based forensic artefacts as required. Since collection and execution is all automated, it can be easily integrated into SOC triage workflows to triage tricky EDR alerts with as little pain as possible!

Additional information

Invoke-LocalVRCollector.ps1

param (
    [Parameter(Mandatory = $true)]
    [string]$CollectorConfig,
    [Parameter(Mandatory = $false)]
    [string]$WorkingDirectory
)
$VelociraptorExecutable = "https://github.com/Velocidex/velociraptor/releases/download/v0.73/velociraptor-v0.73.1-windows-386.exe"

$MDELiveResponseDir = "C:\ProgramData\Microsoft\Windows Defender Advanced Threat Protection\Downloads"

$VRBinaryName = "velociraptor.exe"

# Set default value for WorkingDirectory if not provided
if (-not $WorkingDirectory) {
    $WorkingDirectory = $MDELiveResponseDir
}
# Ensure the WorkingDirectory exists, create it if it doesn't
if (-not (Test-Path -Path $WorkingDirectory -PathType Container)) {
    New-Item -Path $WorkingDirectory -ItemType Directory -Force
}
# Change working directory
Set-Location -Path $WorkingDirectory

# Download VR from the remote URL to the current directory
Invoke-WebRequest -Uri $VelociraptorExecutable -OutFile $VRBinaryName

if(-not (Test-Path -Path (Join-Path -Path $MDELiveResponseDir -ChildPath $CollectorConfig))){
    Write-Error "Unable to find specified collector in MDE Live Response download/upload directory"
    exit 1
}
# Assuming that the collector configuration file was written to the default location through live response, move it to the current directory
Move-Item -Path (Join-Path -Path $MDELiveResponseDir -ChildPath $CollectorConfig) -Destination $CollectorConfig

# Run the generic offline collector
$args = @("--", "--embedded_config", "$CollectorConfig")
& ".\$VRBinaryName" $args 2>&1

Invoke-RemoteVRCollector.ps1

param (
    [Parameter(Mandatory = $true)]
    [string]$DeviceId,

    [Parameter(Mandatory = $true)]
    [string]$CollectorConfig,

    [Parameter(Mandatory = $false)]
    [string]$WorkingDirectory
)
$DefaultWorkingDirectory = "C:\ProgramData\Microsoft\Windows Defender Advanced Threat Protection\Downloads"
$tokenEndpoint = "https://api.securitycenter.microsoft.com"
$DefenderXDRAPI = "https://au.api.security.microsoft.com" # Use the Australian Live Response API endpoint to reduce latency
$LocalScriptName = "Invoke-LocalVRCollector.ps1"

# Set default value for WorkingDirectory if not provided
if (-not $WorkingDirectory) {
    $WorkingDirectory = $DefaultWorkingDirectory
}
# Check if CollectorConfig points to a valid file
if (-not (Test-Path -Path $CollectorConfig -PathType Leaf)) {
    Write-Error "Error: The file path specified in -CollectorConfig does not exist."
    exit 1
}
# Verify if valid Azure credentials are available
Write-Host "Getting Azure authentication material from session context..."
$azContext = Get-AzContext
if (-not $azContext) {
    Write-Error "Error: No valid Azure credentials found. Please run 'Connect-AzAccount' to login."
    exit 1
}
Write-Host "Getting Defender XDR bearer token..."
# Get a bearer token for the Defender XDR API
$token = (Get-AzAccessToken -ResourceUrl $tokenEndpoint).Token

# Extract the name of the job from the collector filename
$Name = (Get-Item $CollectorConfig).BaseName
$CollectorFileName = (Get-Item $CollectorConfig).Name
$zipPath = Join-Path -Path $WorkingDirectory -ChildPath "$Name.zip"

# Setup API headers
$defenderHeaders = @{
    "Authorization"="Bearer $token"
    "Content-Type"="application/json"
}
# Make sure the collector exists in the Live Response library
$libraryUri = "$DefenderXDRAPI/api/libraryfiles"
Write-Host "Uploading collector file to Live Response library..."
$resp = (curl.exe -X POST "$libraryUri" -H "Authorization: Bearer $token" -F "file=@$CollectorConfig" -F "OverrideIfExists=true") | ConvertFrom-Json
if ($resp.error) {
    Write-Error "Error: Unable to upload collector to $resp"
} else {
    Write-Host "Collector '$CollectorFileName' uploaded to Live Response library."
}
# Format the Live Response RUN API call
$liveResponseUri = "$DefenderXDRAPI/API/machines/$DeviceId/runliveresponse"
$LRCommands = @{
    "Commands" = @(
        @{
            type = "PutFile"
            "params" = @(
                @{
                    "key"   = "FileName"
                    "value" = "$CollectorFileName"
                }
            )
        },
        @{
            "type" = "RunScript"
            "params" = @(
                @{
                    "key"   = "ScriptName"
                    "value" = "$LocalScriptName"
                },
                @{
                    "key"   = "Args"
                    "value" = "-CollectorConfig $CollectorFileName"
                }
            )
        },
        @{
            "type" = "GetFile"
            "params" = @(
                @{
                    "key"   = "Path"
                    "value" = "$zipPath"
                }
            )
        }
    )
    "Comment" = "Fortian Secops Triage (Velociraptor Generic/Embedded Offline Collector): $Name"
} | ConvertTo-Json -Depth 15
  
# Send the Live Response command
Write-Host "Running Live Response commands to execute offline collector..."
try {
    $LRCommandCreation = Invoke-RestMethod -Method "POST" -Uri $liveResponseUri -Headers $defenderHeaders -Body $LRCommands
} catch [System.Net.WebException] {
     $errorResponse = $_.Exception.Response
     if ($errorResponse) {
         # Read the error response body
         $reader = New-Object System.IO.StreamReader($errorResponse.GetResponseStream())
         $responseBody = $reader.ReadToEnd()
         Write-Output "Status Code: $($errorResponse.StatusCode)"
         Write-Output "Status Description: $($errorResponse.StatusDescription)"
         Write-Output "Response Body: $responseBody"
     } else {
         Write-Output "No response received."
     }
     Write-Error "Unable to create Live Response session, exiting."
     exit 1
}
$id = $LRCommandCreation.id
Write-Host "Live Response session $id created, uploading files to endpoint..."

$status = "Pending"
$uploaded = $false
$executed = $false
$downloaded = $false

while ($status -eq "Pending") {
    try {
        $response = Invoke-RestMethod -Method "GET" -Uri "$DefenderXDRAPI/api/machineactions/$id" -Headers $defenderHeaders
        $status = $response.status
    } catch [System.Net.WebException] {
        $errorResponse = $_.Exception.Response
        $reader = New-Object System.IO.StreamReader($errorResponse.GetResponseStream())
        $responseBody = $reader.ReadToEnd()
        Write-Host $responseBody
        Write-Error "Failed to get information for Live Response action $id, cancelling job to cleanup..."
        Invoke-RestMethod -Method "POST" -Uri "https://api.securitycenter.microsoft.com/api/machineactions/$id/cancel/" -Headers $defenderHeaders -Body (@{"Comment"="Failed execution"} | ConvertTo-Json)
        exit 1
    }
    if(-not $uploaded -and ($response.commands[0].commandStatus -eq "Completed")){
        Write-Host "Files uploaded, executing script and waiting for completition..."
        $uploaded = $true
    }
    if(-not $executed -and ($response.commands[1].commandStatus -eq "Completed")){
        Write-Host "Script executed, retreiving results..."
        $executed = $true
    }
    if(-not $downloaded -and ($response.commands[2].commandStatus -eq "Completed"){
        Write-Host "File retreived by Live Response."
        $downloaded = $true
    }
    # Keep polling until completion
    if ($status -eq "Pending") {
        Start-Sleep -Seconds 10
    }
}
if ($status -ne "Succeeded"){
    # Print error
    Write-Error "Failed to run Live Response commands"
    Write-Host $resp
    exit 1
}
# Get the final download link, and display to user
Write-Host "Getting the collection download link..."

$downloadAPICall = Invoke-RestMethod -Method "GET" -Uri "$DefenderXDRAPI/api/machineactions/$id/GetLiveResponseResultDownloadLink(index=2)" -Headers $defenderHeaders
$downloadLink = $downloadAPICall.value

Write-Host "Download link (active for 30min): $downloadLink"
CONTACT US

Speak with a Fortian Security Specialist

Request a consultation with one of our security specialists today.

Get in touch