fix(launcher): harden LA11 gate evidence
This commit is contained in:
parent
accd01a008
commit
9f9c116792
7 changed files with 514 additions and 89 deletions
|
|
@ -1,31 +1,127 @@
|
|||
Set-StrictMode -Version Latest
|
||||
|
||||
function Get-CampaignLaSha256([string]$Text) {
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Text)
|
||||
return [Convert]::ToHexString(
|
||||
[Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Get-CampaignLaCommandLineFingerprint(
|
||||
[string]$ExecutablePath,
|
||||
[string]$ConfigArgument,
|
||||
[string]$SessionConfigPath) {
|
||||
if (-not [IO.Path]::IsPathFullyQualified($ExecutablePath) -or
|
||||
-not [IO.Path]::IsPathFullyQualified($SessionConfigPath) -or
|
||||
$ConfigArgument -cnotin @('--config', '--session-config')) {
|
||||
throw 'Cannot fingerprint an incomplete launcher-child command line.'
|
||||
}
|
||||
$executable = [IO.Path]::GetFullPath($ExecutablePath)
|
||||
$config = [IO.Path]::GetFullPath($SessionConfigPath)
|
||||
# Only the executable and the recognized config argument are retained in
|
||||
# this projection. Launcher credentials use stdin; unrelated argv is
|
||||
# deliberately excluded so an accidental secret can never enter evidence.
|
||||
return Get-CampaignLaSha256(
|
||||
"campaign-la-child-command-v1`n$executable`n$ConfigArgument`n$config")
|
||||
}
|
||||
|
||||
function Get-CampaignLaLinuxProcessIdentity(
|
||||
[string]$ProcessDirectory,
|
||||
[string]$BootId) {
|
||||
$stat = [IO.File]::ReadAllText((Join-Path $ProcessDirectory 'stat'))
|
||||
$commandEnd = $stat.LastIndexOf(')')
|
||||
if ($commandEnd -lt 2 -or $commandEnd + 2 -ge $stat.Length) {
|
||||
throw 'Linux process stat record is malformed.'
|
||||
}
|
||||
# The tail begins at field 3 (state); field 22 (starttime) is index 19.
|
||||
$tail = @($stat.Substring($commandEnd + 2).Split(
|
||||
' ',
|
||||
[StringSplitOptions]::RemoveEmptyEntries))
|
||||
if ($tail.Count -le 19) { throw 'Linux process stat record has no starttime.' }
|
||||
$startTicks = [uint64]::Parse(
|
||||
$tail[19],
|
||||
[Globalization.NumberStyles]::None,
|
||||
[Globalization.CultureInfo]::InvariantCulture)
|
||||
return "linux-proc-start-v1:$BootId`:$startTicks"
|
||||
}
|
||||
|
||||
function Get-CampaignLaProcessInstanceIdentity {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateRange(1, 2147483647)][int]$ProcessId)
|
||||
|
||||
if ($IsWindows) {
|
||||
$candidate = Get-CimInstance Win32_Process `
|
||||
-Filter "ProcessId=$ProcessId" -ErrorAction Stop
|
||||
if ($null -eq $candidate) { return $null }
|
||||
if ($null -eq $candidate.CreationDate) {
|
||||
throw "Windows process $ProcessId has no creation time."
|
||||
}
|
||||
return "windows-creation-v1:$($candidate.CreationDate.ToUniversalTime().Ticks)"
|
||||
}
|
||||
if ($IsLinux) {
|
||||
$directory = "/proc/$ProcessId"
|
||||
if (-not [IO.Directory]::Exists($directory)) { return $null }
|
||||
try {
|
||||
$bootId = [IO.File]::ReadAllText(
|
||||
'/proc/sys/kernel/random/boot_id').Trim().ToLowerInvariant()
|
||||
if ($bootId -notmatch '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') {
|
||||
throw 'Linux boot id is malformed.'
|
||||
}
|
||||
return Get-CampaignLaLinuxProcessIdentity $directory $bootId
|
||||
}
|
||||
catch [IO.FileNotFoundException] { return $null }
|
||||
catch [IO.DirectoryNotFoundException] { return $null }
|
||||
catch [IO.IOException] {
|
||||
if (-not [IO.Directory]::Exists($directory)) { return $null }
|
||||
throw
|
||||
}
|
||||
}
|
||||
throw 'Campaign LA process identity supports Windows and Linux only.'
|
||||
}
|
||||
|
||||
function Get-CampaignLaSessionProcessCorrelations {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$matches = [Collections.Generic.List[object]]::new()
|
||||
$correlations = [Collections.Generic.List[object]]::new()
|
||||
if ($IsWindows) {
|
||||
$pattern = '(?i)(?:^|\s)(?:--config|--session-config)\s+(?:"([^"]+)"|(\S+))'
|
||||
$pattern = '(?i)(?:^|\s)(--config|--session-config)\s+(?:"([^"]+)"|(\S+))'
|
||||
foreach ($candidate in @(Get-CimInstance Win32_Process -ErrorAction Stop)) {
|
||||
$commandLine = [string]$candidate.CommandLine
|
||||
if ([string]::IsNullOrWhiteSpace($commandLine)) { continue }
|
||||
$executablePath = [string]$candidate.ExecutablePath
|
||||
if ([string]::IsNullOrWhiteSpace($commandLine) -or
|
||||
-not [IO.Path]::IsPathFullyQualified($executablePath) -or
|
||||
$null -eq $candidate.CreationDate) {
|
||||
continue
|
||||
}
|
||||
$identity = "windows-creation-v1:$($candidate.CreationDate.ToUniversalTime().Ticks)"
|
||||
foreach ($match in [Text.RegularExpressions.Regex]::Matches(
|
||||
$commandLine,
|
||||
$pattern)) {
|
||||
$value = if ($match.Groups[1].Success) {
|
||||
$match.Groups[1].Value
|
||||
} else { $match.Groups[2].Value }
|
||||
$argument = $match.Groups[1].Value.ToLowerInvariant()
|
||||
$value = if ($match.Groups[2].Success) {
|
||||
$match.Groups[2].Value
|
||||
} else { $match.Groups[3].Value }
|
||||
if ([IO.Path]::IsPathFullyQualified($value)) {
|
||||
$matches.Add([pscustomobject]@{
|
||||
$configPath = [IO.Path]::GetFullPath($value)
|
||||
$correlations.Add([pscustomobject]@{
|
||||
ProcessId = [int]$candidate.ProcessId
|
||||
SessionConfigPath = [IO.Path]::GetFullPath($value)
|
||||
ProcessInstanceIdentity = $identity
|
||||
SessionConfigPath = $configPath
|
||||
CommandLineFingerprintSha256 =
|
||||
Get-CampaignLaCommandLineFingerprint `
|
||||
$executablePath $argument $configPath
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($IsLinux) {
|
||||
$bootId = [IO.File]::ReadAllText('/proc/sys/kernel/random/boot_id').Trim().ToLowerInvariant()
|
||||
if ($bootId -notmatch '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') {
|
||||
throw 'Linux boot id is malformed.'
|
||||
}
|
||||
foreach ($directory in [IO.Directory]::EnumerateDirectories('/proc')) {
|
||||
$leaf = [IO.Path]::GetFileName($directory)
|
||||
$processId = 0
|
||||
|
|
@ -37,24 +133,34 @@ function Get-CampaignLaSessionProcessCorrelations {
|
|||
continue
|
||||
}
|
||||
try {
|
||||
$identityBefore = Get-CampaignLaLinuxProcessIdentity $directory $bootId
|
||||
$bytes = [IO.File]::ReadAllBytes((Join-Path $directory 'cmdline'))
|
||||
if ($bytes.Length -eq 0) { continue }
|
||||
$arguments = @([Text.Encoding]::UTF8.GetString($bytes).Split(
|
||||
[char]0,
|
||||
[StringSplitOptions]::RemoveEmptyEntries))
|
||||
$identityAfter = Get-CampaignLaLinuxProcessIdentity $directory $bootId
|
||||
if ($identityBefore -cne $identityAfter -or $arguments.Count -eq 0 -or
|
||||
-not [IO.Path]::IsPathFullyQualified($arguments[0])) {
|
||||
continue
|
||||
}
|
||||
for ($index = 0; $index + 1 -lt $arguments.Count; $index++) {
|
||||
if ($arguments[$index] -cin @('--config', '--session-config') -and
|
||||
[IO.Path]::IsPathFullyQualified($arguments[$index + 1])) {
|
||||
$matches.Add([pscustomobject]@{
|
||||
$configPath = [IO.Path]::GetFullPath($arguments[$index + 1])
|
||||
$correlations.Add([pscustomobject]@{
|
||||
ProcessId = $processId
|
||||
SessionConfigPath = [IO.Path]::GetFullPath(
|
||||
$arguments[$index + 1])
|
||||
ProcessInstanceIdentity = $identityBefore
|
||||
SessionConfigPath = $configPath
|
||||
CommandLineFingerprintSha256 =
|
||||
Get-CampaignLaCommandLineFingerprint `
|
||||
$arguments[0] $arguments[$index] $configPath
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
catch [IO.IOException] {
|
||||
# A process may exit between /proc enumeration and cmdline read.
|
||||
# A process may exit between /proc enumeration and either read.
|
||||
}
|
||||
catch [UnauthorizedAccessException] {
|
||||
# Other-user processes cannot be the owner-readable gate child.
|
||||
|
|
@ -65,7 +171,7 @@ function Get-CampaignLaSessionProcessCorrelations {
|
|||
throw 'Campaign LA process correlation supports Windows and Linux only.'
|
||||
}
|
||||
|
||||
return @($matches)
|
||||
return @($correlations)
|
||||
}
|
||||
|
||||
function Get-CampaignLaCorrelatedProcessIds {
|
||||
|
|
@ -79,15 +185,48 @@ function Get-CampaignLaCorrelatedProcessIds {
|
|||
$comparison = if ($IsWindows) {
|
||||
[StringComparison]::OrdinalIgnoreCase
|
||||
} else { [StringComparison]::Ordinal }
|
||||
$matches = [Collections.Generic.HashSet[int]]::new()
|
||||
$processIds = [Collections.Generic.HashSet[int]]::new()
|
||||
foreach ($candidate in @(Get-CampaignLaSessionProcessCorrelations)) {
|
||||
if ([string]::Equals(
|
||||
$candidate.SessionConfigPath,
|
||||
$SessionConfigPath,
|
||||
$comparison)) {
|
||||
$null = $matches.Add([int]$candidate.ProcessId)
|
||||
$null = $processIds.Add([int]$candidate.ProcessId)
|
||||
}
|
||||
}
|
||||
|
||||
return @($matches | Sort-Object)
|
||||
return @($processIds | Sort-Object)
|
||||
}
|
||||
|
||||
function Test-CampaignLaCapturedProcessState {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][int]$ProcessId,
|
||||
[Parameter(Mandatory = $true)][string]$ProcessInstanceIdentity,
|
||||
[Parameter(Mandatory = $true)][string]$SessionConfigPath,
|
||||
[AllowNull()][string]$CurrentProcessInstanceIdentity,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyCollection()][object[]]$Correlations)
|
||||
|
||||
$comparison = if ($IsWindows) {
|
||||
[StringComparison]::OrdinalIgnoreCase
|
||||
} else { [StringComparison]::Ordinal }
|
||||
$sameInstanceAlive = -not [string]::IsNullOrEmpty($CurrentProcessInstanceIdentity) -and
|
||||
$CurrentProcessInstanceIdentity -ceq $ProcessInstanceIdentity
|
||||
$exactConfigPathAlive = $false
|
||||
$pidReused = -not [string]::IsNullOrEmpty($CurrentProcessInstanceIdentity) -and
|
||||
$CurrentProcessInstanceIdentity -cne $ProcessInstanceIdentity
|
||||
foreach ($candidate in $Correlations) {
|
||||
if ([string]::Equals(
|
||||
[string]$candidate.SessionConfigPath,
|
||||
$SessionConfigPath,
|
||||
$comparison)) {
|
||||
$exactConfigPathAlive = $true
|
||||
}
|
||||
}
|
||||
return [pscustomobject]@{
|
||||
SameInstanceAlive = $sameInstanceAlive
|
||||
ExactConfigPathAlive = $exactConfigPathAlive
|
||||
PidReused = $pidReused
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue