После того, как я очень четко (результативно и эффективно) разобрался у себя с botnet на Линуксе (в плане атак на bind, nginx, postfix, sshd и т.п. с помощью доработанного мною fail2ban), мне пришло в голову, что через эти шлюзы могут также идти атаки и на мои RDP серверы под Windows (2012 R2).
Введение
Для противодействия этой угрозе мне пришлось напрячь свою волю и освоить PowerShell — инструмент, который я обычно обхожу стороной, предпочитая bash. Скажу честно: процесс был непростым, и не все конструкции PowerShell кажутся мне интуитивно-понятными. Код, который вы увидите, наверняка покажется опытным Windows-администраторам сыроватым — я не скрываю, что здесь я новичок. Тем не менее, результат получился рабочим, и я считаю правильным им поделиться.
В итоге родилась система ES-RDP — набор скриптов, которые анализируют логи RDP, вычисляют злоумышленников и автоматически блокируют их через встроенный брандмауэр Windows. Ниже — как это устроено изнутри.
Конфигурация ES-RDP
Моя система состоит из двух частей: рабочей (которая запускается из Task Scheduler) и системы мониторинга. Первая называется rdp-run.ps1, а вторая — stat_all.ps1. Имеется также общий файл для глобальных настроек всей этой системы ES-RDP common.ps1 (начнем с него):
# Система защиты удаленного рабочего стола
# Extra Systems Remote Desktop Protection
# ES-RDP © Extra Systems, 2026
# --- УПРАВЛЯЮЩАЯ ТАБЛИЦА ПРАВИЛ (ID, Hours, Limit, Type) ---
# Здесь можно (но не нужно) менять только Hours и Limit.
$BanRules = @(
[PSCustomObject]@{ ID = 140; Hours = 24; Limit = 5; Type = "AUTH" },
[PSCustomObject]@{ ID = 131; Hours = 24; Limit = 10; Type = "SCAN" }
)
# Конфигурация системы
# Эти параметры менять нельзя
$LogName = "Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational"
$RulePrefix = "ES_RDP_Drop_"
# Эти параметры менять можно (но не нужно)
$BanTimeDays = 7
# Список исключений (Whitelist)
# Можно указывать конкретные IP или маски с wildcards
$WhiteList = @(
"192.168.0.*"
)
Таблица BanRules содержит перечень отслеживаемых в логе событий, глубину сканирования по времени (в часах) и предел количества ошибок, после которого клиент попадает в бан. Срок нахождения нарушителя в бане определяет переменная BanTimeDays.
Система охраны периметра
Теперь рассмотрим файл rdp-run.ps1, который периодически запускается через Task Scheduler. Сначала я вызывал его раз в 20 минут, но сейчас перешел на 10. Это уменьшает окно возможностей для злоумышленника и энергичный бот успевает при этом «наследить» в два раза меньше. И так, вот код скрипта rdp-run.ps1:
param (
[Parameter(Mandatory=$false)]
[switch]$Quiet
)
. "$PSScriptRoot\common.ps1"
# 1. ОЧИСТКА СТАРЬЯ
$ExpirationDate = (Get-Date).AddDays(-$BanTimeDays)
$AllRules = Get-NetFirewallRule -DisplayName "$RulePrefix*" -ErrorAction SilentlyContinue
$AmntOldBefore = $AllRules.Count
$OldRules = $AllRules | Where-Object {
if ($_.DisplayName -match "(\d{4}-\d{2}-\d{2})") {
[DateTime]$Matches[1] -lt $ExpirationDate
} else { $false }
}
$AmnistiedCount = 0
if ($OldRules) {
$AmnistiedCount = $OldRules.Count
$OldRules | Remove-NetFirewallRule
}
if (-not $Quiet) {
Write-Output "=== АУДИТ БАН-ЛИСТА ==="
Write-Output "Всего активных банов в Firewall: $AmntOldBefore"
Write-Output "АМНИСТИЯ: Удалено $AmnistiedCount правил.`n"
}
# 2. ПОЛУЧЕНИЕ НАРУШИТЕЛЕЙ (Через цикл по таблице)
$RawOffenders = @()
foreach ($Rule in $BanRules) {
$StartTime = (Get-Date).AddHours(-$Rule.Hours)
$Events = Get-WinEvent -FilterHashtable @{LogName=$LogName; ID=$Rule.ID; StartTime=$StartTime} -ErrorAction SilentlyContinue
if ($Events) {
$List = $Events | ForEach-Object {
if ($_.Message -match "(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})") {
$FoundIP = $Matches[1]
# ПРОВЕРКА ПО WHITELIST
$IsWhite = $false
foreach ($WhiteIP in $WhiteList) {
if ($FoundIP -like $WhiteIP) {
$IsWhite = $true
break
}
}
if (-not $IsWhite) {
[PSCustomObject]@{ IP = $FoundIP; Type = $Rule.Type }
}
}
} | Group-Object IP | Where-Object { $_.Count -ge $Rule.Limit }
if ($List) { $RawOffenders += $List }
}
}
# Объединяем и схлопываем дубликаты
$Offenders = $RawOffenders | Group-Object Name | ForEach-Object { $_.Group[0] }
# 3. БАН НОВЫХ ГАДОВ
$NewBansCount = 0
if ($Offenders) {
$CurDate = Get-Date -Format "yyyy-MM-dd"
foreach ($Offender in $Offenders) {
$IP = $Offender.Name
$Type = $Offender.Group[0].Type
$TechnicalName = $RulePrefix + $IP.Replace('.', '_')
if (-not (Get-NetFirewallRule -Name $TechnicalName -ErrorAction SilentlyContinue)) {
$Description = "Type: $Type. Attempts: $($Offender.Count). Created: $(Get-Date)"
New-NetFirewallRule -Name $TechnicalName -DisplayName "$RulePrefix$CurDate`_$IP" -Direction Inbound -Action Block -RemoteAddress $IP -Description $Description | Out-Null
if (-not $Quiet) { Write-Output "NEW BAN: $IP ($Type - $($Offender.Count) attempts)" }
$NewBansCount++
}
}
}
# 4. ФИНАЛЬНЫЙ СБОР ДАННЫХ
$TotalBannedAfter = (Get-NetFirewallRule -DisplayName "$RulePrefix*" -ErrorAction SilentlyContinue).Count
$Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Write-Output "$Timestamp $TotalBannedAfter $NewBansCount $AmnistiedCount"
# 5. СТАТИСТИКА
if (-not $Quiet -and $Offenders) {
Write-Output "`n=== СВОДНАЯ СТАТИСТИКА АТАК ==="
$Offenders | Select-Object @{N="IP Адрес";E={$_.Name}}, @{N="Ударов";E={$_.Count}}, @{N="Тип";E={$_.Group[0].Type}} | Sort-Object Ударов -Descending | Format-Table -AutoSize | Out-String
}
Из Task Scheduler у меня вызывается не сам rdp-run.ps1, а файл rdp-run.bat который имеет такой вид:
powershell.exe -File "C:\Scripts\rdp-run.ps1" -Quiet >> C:\Scripts\Logs\rdp-ban.log
Файл rdp-ban.log имеет примерно такое содержимое (ключ Quiet обеспечиват вывод в лог лишь минимума необходимой информации):
2026-04-03 01:30:09 104 0 0 2026-04-03 01:40:08 104 0 0 2026-04-03 01:50:07 104 0 0 2026-04-03 02:00:08 105 1 0 2026-04-03 02:10:08 105 0 0 2026-04-03 02:20:08 105 0 0 2026-04-03 02:30:07 105 0 0 2026-04-03 02:40:08 105 0 0 2026-04-03 02:50:08 105 0 0 2026-04-03 03:00:07 105 0 0 2026-04-03 03:10:08 105 0 0 2026-04-03 03:20:08 105 0 0 2026-04-03 03:30:08 105 0 0 2026-04-03 03:40:08 105 0 0 2026-04-03 03:50:08 105 0 0 2026-04-03 04:00:11 105 0 0 2026-04-03 04:10:07 105 0 0
Предпоследняя колонка содержит количество добавленных на этом проходе в бан клиентов, последняя — количество амнистированных, а 104 и 105 — количество заблокированных на данный момент вражеских хостов.
Статистика работы
Для анализа работы ES-RDP создан скрипт stat_all.ps1. Вот его полный код:
. "$PSScriptRoot\common.ps1"
Write-Host "`n=== [ ТЮРЬМА ES-RDP: СПИСОК ЗАКЛЮЧЕННЫХ ] ===" -ForegroundColor Cyan
Write-Host "Срок изоляции от общества: $BanTimeDays дн." -ForegroundColor Gray
# Получаем список правил
$Rules = Get-NetFirewallRule -DisplayName "$RulePrefix*" -ErrorAction SilentlyContinue
if (-not $Rules) {
Write-Host "В камерах пусто. Все чисты..." -ForegroundColor Green
} else {
$Results = foreach ($Rule in $Rules) {
$Address = (Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $Rule).RemoteAddress
$Attempts = 0
$Type = "AUTH"
# Парсим описание (SCAN/AUTH)
if ($Rule.Description -match "Type: (\w+). Attempts: (\d+)") {
$Type = $Matches[1]
$Attempts = [int]$Matches[2]
}
elseif ($Rule.Description -match "(\d+) attempts") {
$Attempts = [int]$Matches[1]
}
# Извлекаем дату и просто переставляем цифры (Europe Style)
$RawDate = $Rule.Description -replace ".*Created: ", ""
$FormattedDate = $RawDate -replace "(\d{2})/(\d{2})/(\d{4})", '$2.$1.$3'
[PSCustomObject]@{
"IP Адрес" = $Address
"Дата бана" = $FormattedDate
"Ударов" = $Attempts
"Тип" = $Type
"Правило" = $Rule.DisplayName
}
}
# Сортировка по дате
$Results | Sort-Object @{
Expression = { [DateTime]::ParseExact($_. "Дата бана", "dd.MM.yyyy HH:mm:ss", $null) }
} | Format-Table -AutoSize
Write-Host "-----------------------"
Write-Host "Всего в бане: $($Results.Count)" -ForegroundColor Yellow
}
# --- [ БЛОК: ОТЧЕТ СУДЕБНОЙ КАНЦЕЛЯРИИ ] ---
Write-Host "`n=== [ ОТЧЕТ СУДЕБНОЙ КАНЦЕЛЯРИИ ] ===" -ForegroundColor Cyan
Write-Host "Динамика колебаний уровня преступности" -ForegroundColor Gray
Write-Host "(за последние $BanTimeDays дн.)" -ForegroundColor Gray
if ($Results) {
# Группируем данные по дате (извлекаем только дату без времени, если оно там есть)
$Stats = $Results | Group-Object {
# Берем первые 10 символов из "Дата бана" (формат ДД.ММ.ГГГГ)
$_. "Дата бана".Substring(0, 10)
} | Select-Object @{Name="Дата"; Expression={$_.Name}},
@{Name="Преступлений"; Expression={ ($_.Group | Measure-Object "Ударов" -Sum).Sum }},
@{Name="Приговоров"; Expression={$_.Count}},
@{Name="Дата_Объект"; Expression={ [DateTime]::ParseExact($_.Name, "dd.MM.yyyy", $null) }}
# Выводим таблицу
# $Stats | Sort-Object "Дата" | Format-Table -AutoSize
$Stats | Sort-Object "Дата_Объект" | Select-Object "Дата", "Преступлений", "Приговоров" | Format-Table -AutoSize
} else {
Write-Host "Архивы пусты. Преступности не обнаружено." -ForegroundColor Green
}
# --- БЛОК: СТАТИСТИКА КАРЦЕРА ---
Write-Host "`n=== [ ИНСПЕКЦИЯ ЖУРНАЛА ОХРАНЫ ] ===" -ForegroundColor Cyan
Write-Host "СЕРВЕР: $env:COMPUTERNAME" -ForegroundColor Yellow
try {
$LogInfo = Get-WinEvent -ListLog $LogName
$OldestEvent = Get-WinEvent -LogName $LogName -MaxEvents 1 -Oldest -ErrorAction SilentlyContinue
$LifespanHours = 0
$FirstRecordDate = "Н/Д"
if ($OldestEvent) {
$FirstRecordDate = $OldestEvent.TimeCreated.ToString("dd.MM.yyyy HH:mm")
$Diff = New-TimeSpan -Start $OldestEvent.TimeCreated -End (Get-Date)
# Округляем до целого числа (вниз, до полных часов)
$LifespanHours = [Math]::Truncate($Diff.TotalHours)
}
$LogStats = [PSCustomObject]@{
"Макс. размер (МБ)" = [Math]::Round($LogInfo.MaximumSizeInBytes / 1MB, 2)
"Текущий вес (МБ)" = [Math]::Round($LogInfo.FileSize / 1MB, 2)
"Записей в логе" = $LogInfo.RecordCount
"Старейшая запись" = $FirstRecordDate
"Время жизни (ч)" = $LifespanHours
}
$LogStats | Format-Table -AutoSize
if ($LifespanHours -lt 24 -and $LifespanHours -gt 0) {
Write-Host "(!) ВНИМАНИЕ: Журнал живет меньше суток. Рекомендуется расширить лимит." -ForegroundColor Red
}
}
catch {
Write-Host "Ошибка доступа к журналу: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host "`n=== [ ОТЧЕТ ПО БЕЗОПАСНОСТИ ES-RDP ] ===" -ForegroundColor Cyan
Write-Host "Анализ логов по типам событий" -ForegroundColor Gray
# 1. Собираем активные баны из Firewall (один раз)
$BannedIPs = @()
$Rules = Get-NetFirewallRule -DisplayName "$RulePrefix*" -ErrorAction SilentlyContinue
if ($Rules) {
foreach ($Rule in $Rules) {
$Addr = (Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $Rule).RemoteAddress
if ($Addr) { $BannedIPs += $Addr }
}
}
# 2. Цикл генерации таблиц
foreach ($R in $BanRules) {
# Заголовок теперь включает и Limit, и Hours из правила, но поиск идет по $StartTime
Write-Host "`n>>> Выборка событий $($R.Type) (ID $($R.ID))" -ForegroundColor DarkYellow
Write-Host ">>> Глубина анализа $($R.Hours) час." -ForegroundColor DarkYellow
Write-Host ">>> Порог срабатывания $($R.Limit) ударов" -ForegroundColor DarkYellow
$StartTime = (Get-Date).AddHours(-$R.Hours)
$Events = Get-WinEvent -FilterHashtable @{LogName=$LogName; ID=$R.ID; StartTime=$StartTime} -ErrorAction SilentlyContinue
if (-not $Events) {
Write-Host "В логах активности не найдено." -ForegroundColor Green
continue
}
# Группируем ВСЕ найденные IP
$Report = $Events | ForEach-Object {
if ($_.Message -match "(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})") {
[PSCustomObject]@{ IP = $Matches[1]; Time = $_.TimeCreated }
}
} | Group-Object IP | Where-Object { $_.Count -gt 1 } | ForEach-Object {
$GroupSorted = $_.Group | Sort-Object Time
$CurrentIP = $_.Name
$IsBanned = $BannedIPs -contains $CurrentIP
[PSCustomObject]@{
"IP Адрес" = $CurrentIP
"Ударов" = $_.Count
"Статус" = if ($IsBanned) { "ОБЕЗВРЕЖЕН" } else { "СВОБОДЕН" }
"Первый удар" = $GroupSorted[0].Time.ToString("dd.MM HH:mm")
"Последний" = $GroupSorted[-1].Time.ToString("dd.MM HH:mm")
}
}
if ($Report) {
$Report | Sort-Object "Ударов" -Descending | Format-Table -AutoSize
$TotalBanned = ($Report | Where-Object { $_.Статус -eq "ОБЕЗВРЕЖЕН" }).Count
$FreeRadicals = ($Report | Where-Object { $_.Статус -eq "СВОБОДЕН" }).Count
$SummaryColor = "Green"
if ($FreeRadicals -gt 0) { $SummaryColor = "Yellow" }
Write-Host "ИТОГ: ОБЕЗВРЕЖЕНО: $TotalBanned | СВОБОДНО: $FreeRadicals" -ForegroundColor $SummaryColor
}
}
# --- БЛОК: СТАТИСТИКА КОДОВ СОБЫТИЙ (ТОЛЬКО С IP) ---
Write-Host "`n=== [ АНАЛИЗАТОР АКТИВНОСТИ ПО ID ] ===" -ForegroundColor Cyan
try {
# Вычисляем максимальный охват из таблицы правил
$MaxHours = ($BanRules | Measure-Object -Property Hours -Maximum).Maximum
if (-not $MaxHours) { $MaxHours = 24 } # Резервное значение
Write-Host "Статистика событий, содержащих IP-адреса" -ForegroundColor Gray
Write-Host "(за последние $MaxHours ч.)" -ForegroundColor Gray
# Извлекаем события только за нужный период (оптимизация)
$AnalysisPeriod = (Get-Date).AddHours(-$MaxHours)
$AllEvents = Get-WinEvent -FilterHashtable @{LogName=$LogName; StartTime=$AnalysisPeriod} -ErrorAction SilentlyContinue
if ($AllEvents) {
$EventStats = $AllEvents | Group-Object Id | ForEach-Object {
$CurrentID = $_.Name
# Извлекаем уникальные IP (проверка Message на наличие IP)
$UniqueIPs = $_.Group | ForEach-Object {
if ($_.Message -match "(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})") { $Matches[1] }
} | Select-Object -Unique
$IPCount = ($UniqueIPs | Measure-Object).Count
if ($IPCount -gt 0) {
[PSCustomObject]@{
"Код события" = $CurrentID
"Обращений" = $_.Count
"Уникальных IP" = $IPCount
}
}
}
# Выводим таблицу: сортировка по количеству обращений
if ($EventStats) {
$EventStats | Sort-Object "Обращений" -Descending | Format-Table -AutoSize
} else {
Write-Host "Событий с IP-адресами за этот период не обнаружено." -ForegroundColor Yellow
}
} else {
Write-Host "Лог пуст за последние $MaxHours ч." -ForegroundColor Yellow
}
} catch {
Write-Host "Ошибка при анализе кодов: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host "`n--------------------------------------------"
Этот скрипт формирует в окно консоли длинный отчет из нескольких разделов, которые мы сейчас последовательно рассмотрим. С самого начала идет полный список находящихся в бане хостов:
=== [ ТЮРЬМА ES-RDP: СПИСОК ЗАКЛЮЧЕННЫХ ] === Срок изоляции от общества: 7 дн. IP Адрес Дата бана Ударов Тип Правило -------- --------- ------ --- ------- 220.149.229.118 29.03.2026 00:00:06 6 AUTH ES_RDP_Drop_2026-03-29_220.149.229.118 185.218.138.19 29.03.2026 01:20:08 432 AUTH ES_RDP_Drop_2026-03-29_185.218.138.19 172.93.215.99 29.03.2026 01:40:08 5 AUTH ES_RDP_Drop_2026-03-29_172.93.215.99 157.254.223.79 29.03.2026 02:40:08 5 AUTH ES_RDP_Drop_2026-03-29_157.254.223.79 62.60.130.21 29.03.2026 05:20:06 5 AUTH ES_RDP_Drop_2026-03-29_62.60.130.21 185.218.138.20 29.03.2026 06:00:09 612 AUTH ES_RDP_Drop_2026-03-29_185.218.138.20 5.255.98.205 29.03.2026 06:40:09 12 AUTH ES_RDP_Drop_2026-03-29_5.255.98.205 149.50.107.12 29.03.2026 09:20:11 649 AUTH ES_RDP_Drop_2026-03-29_149.50.107.12 94.72.102.118 29.03.2026 09:40:09 5 AUTH ES_RDP_Drop_2026-03-29_94.72.102.118 94.26.88.29 29.03.2026 10:40:12 293 AUTH ES_RDP_Drop_2026-03-29_94.26.88.29 109.205.181.136 29.03.2026 11:00:11 5 AUTH ES_RDP_Drop_2026-03-29_109.205.181.136 182.160.117.73 29.03.2026 12:20:09 6 AUTH ES_RDP_Drop_2026-03-29_182.160.117.73 157.254.223.81 29.03.2026 13:00:09 5 AUTH ES_RDP_Drop_2026-03-29_157.254.223.81 46.250.232.3 29.03.2026 16:40:09 5 AUTH ES_RDP_Drop_2026-03-29_46.250.232.3 88.210.63.75 29.03.2026 18:40:16 98 AUTH ES_RDP_Drop_2026-03-29_88.210.63.75 167.148.195.143 29.03.2026 19:20:13 5 AUTH ES_RDP_Drop_2026-03-29_167.148.195.143 158.220.86.73 29.03.2026 19:20:14 5 AUTH ES_RDP_Drop_2026-03-29_158.220.86.73 45.251.7.130 29.03.2026 21:40:11 7 AUTH ES_RDP_Drop_2026-03-29_45.251.7.130 115.191.35.75 29.03.2026 22:00:09 5 AUTH ES_RDP_Drop_2026-03-29_115.191.35.75 156.245.239.2 29.03.2026 23:20:09 5 AUTH ES_RDP_Drop_2026-03-29_156.245.239.2 115.21.71.141 30.03.2026 00:00:10 10 AUTH ES_RDP_Drop_2026-03-30_115.21.71.141 65.75.200.95 30.03.2026 00:20:11 27 AUTH ES_RDP_Drop_2026-03-30_65.75.200.95 103.9.207.80 30.03.2026 00:20:11 5 AUTH ES_RDP_Drop_2026-03-30_103.9.207.80 194.163.159.77 30.03.2026 00:20:12 5 AUTH ES_RDP_Drop_2026-03-30_194.163.159.77 45.88.138.39 30.03.2026 01:00:07 8 AUTH ES_RDP_Drop_2026-03-30_45.88.138.39 103.116.196.5 30.03.2026 01:20:10 45 AUTH ES_RDP_Drop_2026-03-30_103.116.196.5 157.254.223.77 30.03.2026 02:00:10 5 AUTH ES_RDP_Drop_2026-03-30_157.254.223.77 157.254.223.75 30.03.2026 02:20:09 5 AUTH ES_RDP_Drop_2026-03-30_157.254.223.75 176.120.22.240 30.03.2026 03:00:11 196 AUTH ES_RDP_Drop_2026-03-30_176.120.22.240 80.66.83.74 30.03.2026 03:40:15 6 AUTH ES_RDP_Drop_2026-03-30_80.66.83.74 185.218.138.27 30.03.2026 03:40:15 568 AUTH ES_RDP_Drop_2026-03-30_185.218.138.27 185.218.138.26 30.03.2026 04:20:20 871 AUTH ES_RDP_Drop_2026-03-30_185.218.138.26 193.148.63.132 30.03.2026 06:40:17 5 AUTH ES_RDP_Drop_2026-03-30_193.148.63.132 109.205.180.231 30.03.2026 07:00:17 5 AUTH ES_RDP_Drop_2026-03-30_109.205.180.231 185.187.169.232 30.03.2026 07:20:17 5 AUTH ES_RDP_Drop_2026-03-30_185.187.169.232 15.204.128.147 30.03.2026 08:40:18 5 AUTH ES_RDP_Drop_2026-03-30_15.204.128.147 152.53.248.193 30.03.2026 11:00:15 5 AUTH ES_RDP_Drop_2026-03-30_152.53.248.193 151.240.151.71 30.03.2026 13:00:15 5 AUTH ES_RDP_Drop_2026-03-30_151.240.151.71 172.81.128.48 30.03.2026 13:00:15 5 AUTH ES_RDP_Drop_2026-03-30_172.81.128.48 152.53.135.48 30.03.2026 13:20:15 7 AUTH ES_RDP_Drop_2026-03-30_152.53.135.48 94.26.68.14 30.03.2026 14:00:15 22 AUTH ES_RDP_Drop_2026-03-30_94.26.68.14 151.240.151.70 30.03.2026 14:00:16 5 AUTH ES_RDP_Drop_2026-03-30_151.240.151.70 151.240.151.68 30.03.2026 14:00:16 5 AUTH ES_RDP_Drop_2026-03-30_151.240.151.68 191.101.51.162 30.03.2026 14:20:15 5 AUTH ES_RDP_Drop_2026-03-30_191.101.51.162 107.174.142.113 30.03.2026 14:40:15 6 AUTH ES_RDP_Drop_2026-03-30_107.174.142.113 185.218.138.3 30.03.2026 15:00:17 339 AUTH ES_RDP_Drop_2026-03-30_185.218.138.3 124.43.79.65 30.03.2026 16:20:18 5 AUTH ES_RDP_Drop_2026-03-30_124.43.79.65 205.210.31.167 30.03.2026 16:20:24 12 SCAN ES_RDP_Drop_2026-03-30_205.210.31.167 70.36.110.43 30.03.2026 17:00:16 5 AUTH ES_RDP_Drop_2026-03-30_70.36.110.43 194.186.168.106 30.03.2026 17:10:17 6 AUTH ES_RDP_Drop_2026-03-30_194.186.168.106 5.253.86.23 30.03.2026 17:40:19 190 AUTH ES_RDP_Drop_2026-03-30_5.253.86.23 195.158.8.162 30.03.2026 18:00:09 5 AUTH ES_RDP_Drop_2026-03-30_195.158.8.162 167.99.13.19 30.03.2026 18:00:13 11 SCAN ES_RDP_Drop_2026-03-30_167.99.13.19 194.165.16.6 30.03.2026 19:00:19 16 AUTH ES_RDP_Drop_2026-03-30_194.165.16.6 112.169.121.194 30.03.2026 20:00:17 8 AUTH ES_RDP_Drop_2026-03-30_112.169.121.194 185.218.138.21 30.03.2026 20:10:21 318 AUTH ES_RDP_Drop_2026-03-30_185.218.138.21 206.237.41.141 30.03.2026 21:10:19 5 AUTH ES_RDP_Drop_2026-03-30_206.237.41.141 161.178.132.46 30.03.2026 21:20:19 5 AUTH ES_RDP_Drop_2026-03-30_161.178.132.46 185.139.5.133 30.03.2026 22:20:19 6 AUTH ES_RDP_Drop_2026-03-30_185.139.5.133 210.223.56.219 30.03.2026 23:00:18 5 AUTH ES_RDP_Drop_2026-03-30_210.223.56.219 103.190.0.211 30.03.2026 23:30:19 5 AUTH ES_RDP_Drop_2026-03-30_103.190.0.211 103.68.95.158 31.03.2026 01:40:17 8 AUTH ES_RDP_Drop_2026-03-31_103.68.95.158 85.217.140.49 31.03.2026 06:10:14 11 SCAN ES_RDP_Drop_2026-03-31_85.217.140.49 91.103.143.74 31.03.2026 07:50:10 5 AUTH ES_RDP_Drop_2026-03-31_91.103.143.74 20.219.16.34 31.03.2026 09:10:15 10 SCAN ES_RDP_Drop_2026-03-31_20.219.16.34 157.254.223.78 31.03.2026 12:20:11 5 AUTH ES_RDP_Drop_2026-03-31_157.254.223.78 79.137.64.8 31.03.2026 14:20:11 14 AUTH ES_RDP_Drop_2026-03-31_79.137.64.8 43.134.177.187 31.03.2026 14:20:11 5 AUTH ES_RDP_Drop_2026-03-31_43.134.177.187 103.212.182.194 31.03.2026 16:20:09 37 AUTH ES_RDP_Drop_2026-03-31_103.212.182.194 104.152.52.140 31.03.2026 18:40:08 10 SCAN ES_RDP_Drop_2026-03-31_104.152.52.140 45.170.99.143 31.03.2026 20:30:04 5 AUTH ES_RDP_Drop_2026-03-31_45.170.99.143 194.180.176.29 31.03.2026 21:30:05 5 AUTH ES_RDP_Drop_2026-03-31_194.180.176.29 14.136.73.18 31.03.2026 21:40:05 5 AUTH ES_RDP_Drop_2026-03-31_14.136.73.18 5.181.86.179 01.04.2026 01:00:06 79 AUTH ES_RDP_Drop_2026-04-01_5.181.86.179 78.128.112.114 01.04.2026 03:00:06 30 AUTH ES_RDP_Drop_2026-04-01_78.128.112.114 5.181.86.60 01.04.2026 03:00:07 48 AUTH ES_RDP_Drop_2026-04-01_5.181.86.60 65.21.15.154 01.04.2026 03:10:05 5 AUTH ES_RDP_Drop_2026-04-01_65.21.15.154 194.5.237.223 01.04.2026 05:20:06 5 AUTH ES_RDP_Drop_2026-04-01_194.5.237.223 104.152.52.235 01.04.2026 07:30:07 11 SCAN ES_RDP_Drop_2026-04-01_104.152.52.235 157.254.223.80 01.04.2026 08:10:06 5 AUTH ES_RDP_Drop_2026-04-01_157.254.223.80 31.59.58.119 01.04.2026 17:20:06 6 AUTH ES_RDP_Drop_2026-04-01_31.59.58.119 212.64.199.69 01.04.2026 17:50:06 5 AUTH ES_RDP_Drop_2026-04-01_212.64.199.69 167.179.234.146 01.04.2026 18:30:05 8 AUTH ES_RDP_Drop_2026-04-01_167.179.234.146 160.191.55.81 01.04.2026 19:40:05 5 AUTH ES_RDP_Drop_2026-04-01_160.191.55.81 164.68.120.41 02.04.2026 00:10:06 5 AUTH ES_RDP_Drop_2026-04-02_164.68.120.41 165.154.174.27 02.04.2026 00:40:08 17 SCAN ES_RDP_Drop_2026-04-02_165.154.174.27 62.164.177.28 02.04.2026 01:50:06 26 AUTH ES_RDP_Drop_2026-04-02_62.164.177.28 160.187.146.213 02.04.2026 06:20:05 5 AUTH ES_RDP_Drop_2026-04-02_160.187.146.213 45.56.79.53 02.04.2026 06:50:06 14 SCAN ES_RDP_Drop_2026-04-02_45.56.79.53 45.33.12.122 02.04.2026 07:50:06 14 SCAN ES_RDP_Drop_2026-04-02_45.33.12.122 45.79.152.14 02.04.2026 08:00:06 146 SCAN ES_RDP_Drop_2026-04-02_45.79.152.14 212.102.40.218 02.04.2026 08:50:07 10 SCAN ES_RDP_Drop_2026-04-02_212.102.40.218 31.14.32.4 02.04.2026 09:10:07 11 SCAN ES_RDP_Drop_2026-04-02_31.14.32.4 151.240.151.73 02.04.2026 11:40:06 5 AUTH ES_RDP_Drop_2026-04-02_151.240.151.73 94.26.68.54 02.04.2026 12:30:06 16 AUTH ES_RDP_Drop_2026-04-02_94.26.68.54 202.88.241.169 02.04.2026 13:20:06 5 AUTH ES_RDP_Drop_2026-04-02_202.88.241.169 62.164.177.27 02.04.2026 15:40:07 87 AUTH ES_RDP_Drop_2026-04-02_62.164.177.27 66.132.224.226 02.04.2026 18:20:07 10 SCAN ES_RDP_Drop_2026-04-02_66.132.224.226 139.59.42.255 02.04.2026 19:20:07 11 SCAN ES_RDP_Drop_2026-04-02_139.59.42.255 94.72.110.157 02.04.2026 20:20:06 5 AUTH ES_RDP_Drop_2026-04-02_94.72.110.157 86.54.31.42 02.04.2026 21:00:07 28 SCAN ES_RDP_Drop_2026-04-02_86.54.31.42 80.66.83.75 02.04.2026 23:20:07 10 SCAN ES_RDP_Drop_2026-04-02_80.66.83.75 103.131.85.178 02.04.2026 23:30:06 5 AUTH ES_RDP_Drop_2026-04-02_103.131.85.178 80.94.95.221 02.04.2026 23:50:05 36 AUTH ES_RDP_Drop_2026-04-02_80.94.95.221 178.20.210.137 03.04.2026 02:00:05 18 AUTH ES_RDP_Drop_2026-04-03_178.20.210.137 37.53.82.5 03.04.2026 09:10:07 36 AUTH ES_RDP_Drop_2026-04-03_37.53.82.5 94.26.68.55 03.04.2026 14:10:07 9 AUTH ES_RDP_Drop_2026-04-03_94.26.68.55 165.154.120.77 03.04.2026 15:10:07 5 AUTH ES_RDP_Drop_2026-04-03_165.154.120.77 ----------------------- Всего в бане: 108
Затем идет статистика враждебной активности по дням:
=== [ ОТЧЕТ СУДЕБНОЙ КАНЦЕЛЯРИИ ] === Динамика колебаний уровня преступности (за последние 7 дн.) Дата Преступлений Приговоров ---- ------------ ---------- 29.03.2026 2170 20 30.03.2026 2777 41 31.03.2026 120 12 01.04.2026 207 11 02.04.2026 466 20 03.04.2026 68 4
По этому отчету хорошо видно, как спадает статистика «правонарушений», что четко показывает эффективность работы системы ES-RDP. Дальше в ленте идет статистика лог-файла Windows, в котором регистрируются все события протокола RDP:
=== [ ИНСПЕКЦИЯ ЖУРНАЛА ОХРАНЫ ] === СЕРВЕР: TITAN Макс. размер (МБ) Текущий вес (МБ) Записей в логе Старейшая запись Время жизни (ч) ----------------- ---------------- -------------- ---------------- --------------- 64 29,07 60522 28.03.2026 14:32 145
Обращаю ваше внимание, что размер этого лога установлен у меня в 64 Мбайта, а изначално он был около 1 Мбайта. На начальной стадии эксплуатации системы, когда большинство ботнета еще не было заблокировано, такой малый размер сохранял информацию только за какие-то пару часов, чего совершенно недостаточно для проведения глубокого анализа ситуации. Поэтому я рекомендую при использовании системы ES-RDP первым делом увеличить размер этого лога вот такой командой
@set /a "MBYTES=64" @set /a "BYTES=MBYTES * 1024 * 1024" @<nul set /p ="Old" & wevtutil gl "Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational" | findstr "maxSize" @wevtutil sl "Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational" /ms:%BYTES% @<nul set /p ="New" & wevtutil gl "Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational" | findstr "maxSize" @echo. @pause
Дальше в ленте идет отчет по событиям из лога за тот срок и по тем кодам, которые указаны в управляющей таблице файла конфигурации:
=== [ ОТЧЕТ ПО БЕЗОПАСНОСТИ ES-RDP ] === Анализ логов по типам событий >>> Выборка событий AUTH (ID 140) >>> Глубина анализа 24 час. >>> Порог срабатывания 5 ударов IP Адрес Ударов Статус Первый удар Последний -------- ------ ------ ----------- --------- 80.94.95.221 36 ОБЕЗВРЕЖЕН 02.04 23:41 02.04 23:41 37.53.82.5 36 ОБЕЗВРЕЖЕН 03.04 09:07 03.04 09:07 178.20.210.137 18 ОБЕЗВРЕЖЕН 03.04 01:50 03.04 01:51 94.26.68.55 10 ОБЕЗВРЕЖЕН 03.04 14:02 03.04 14:10 165.154.120.77 4 ОБЕЗВРЕЖЕН 02.04 17:06 03.04 15:05 157.254.223.76 3 СВОБОДЕН 03.04 04:12 03.04 07:35 115.190.244.156 2 СВОБОДЕН 02.04 18:16 02.04 22:33 103.131.85.178 2 ОБЕЗВРЕЖЕН 02.04 19:08 02.04 23:24 14.225.222.7 2 СВОБОДЕН 02.04 20:20 02.04 22:30 142.93.145.126 2 СВОБОДЕН 02.04 16:14 02.04 19:59 94.72.110.157 2 ОБЕЗВРЕЖЕН 02.04 17:20 02.04 20:11 84.247.165.192 2 СВОБОДЕН 02.04 18:37 03.04 14:47 192.168.0.67 2 СВОБОДЕН 03.04 08:25 03.04 08:25 103.49.131.118 2 СВОБОДЕН 03.04 07:38 03.04 10:55 165.154.120.211 2 СВОБОДЕН 03.04 11:44 03.04 12:12 209.126.82.146 2 СВОБОДЕН 02.04 21:09 03.04 14:01 207.189.16.214 2 СВОБОДЕН 03.04 03:32 03.04 05:43 ИТОГ: ОБЕЗВРЕЖЕНО: 7 | СВОБОДНО: 10 >>> Выборка событий SCAN (ID 131) >>> Глубина анализа 24 час. >>> Порог срабатывания 10 ударов IP Адрес Ударов Статус Первый удар Последний -------- ------ ------ ----------- --------- 37.53.82.5 38 ОБЕЗВРЕЖЕН 02.04 19:39 03.04 09:07 80.94.95.221 37 ОБЕЗВРЕЖЕН 02.04 18:41 02.04 23:41 86.54.31.42 28 ОБЕЗВРЕЖЕН 02.04 20:56 02.04 20:56 178.20.210.137 19 ОБЕЗВРЕЖЕН 03.04 01:32 03.04 01:51 192.168.0.104 16 СВОБОДЕН 02.04 16:49 03.04 12:19 192.168.0.188 12 СВОБОДЕН 03.04 10:05 03.04 12:02 192.168.0.89 12 СВОБОДЕН 03.04 09:17 03.04 12:04 192.168.0.71 12 СВОБОДЕН 03.04 06:37 03.04 11:58 192.168.0.113 12 СВОБОДЕН 03.04 08:04 03.04 12:24 192.168.0.121 12 СВОБОДЕН 03.04 08:26 03.04 12:05 139.59.42.255 11 ОБЕЗВРЕЖЕН 02.04 19:13 02.04 19:13 192.168.0.80 11 СВОБОДЕН 03.04 08:30 03.04 15:41 66.132.224.226 10 ОБЕЗВРЕЖЕН 02.04 18:17 02.04 18:17 94.26.68.55 10 ОБЕЗВРЕЖЕН 03.04 14:02 03.04 14:10 192.168.0.85 9 СВОБОДЕН 03.04 08:41 03.04 11:12 192.168.0.158 8 СВОБОДЕН 03.04 08:56 03.04 13:39 192.168.0.67 8 СВОБОДЕН 03.04 08:25 03.04 11:59 192.168.0.66 8 СВОБОДЕН 03.04 11:54 03.04 13:46 205.210.31.17 6 СВОБОДЕН 03.04 01:21 03.04 01:21 18.116.101.220 6 СВОБОДЕН 02.04 20:42 02.04 20:47 205.210.31.239 6 СВОБОДЕН 02.04 19:55 02.04 19:55 147.185.132.100 6 СВОБОДЕН 03.04 07:51 03.04 07:51 66.132.172.207 5 СВОБОДЕН 02.04 18:33 02.04 18:33 66.132.186.187 5 СВОБОДЕН 03.04 06:27 03.04 06:27 192.168.0.78 4 СВОБОДЕН 03.04 10:21 03.04 10:21 165.154.120.77 4 ОБЕЗВРЕЖЕН 02.04 17:06 03.04 15:05 192.168.0.95 4 СВОБОДЕН 03.04 11:58 03.04 11:58 192.168.0.198 4 СВОБОДЕН 02.04 16:44 03.04 15:05 80.66.83.75 4 ОБЕЗВРЕЖЕН 02.04 23:13 02.04 23:13 64.227.18.98 3 СВОБОДЕН 03.04 02:50 03.04 03:00 157.254.223.76 3 СВОБОДЕН 03.04 04:12 03.04 07:35 52.140.102.59 3 СВОБОДЕН 03.04 08:09 03.04 14:33 20.219.16.35 3 СВОБОДЕН 03.04 08:26 03.04 14:27 209.126.82.146 2 СВОБОДЕН 02.04 21:09 03.04 14:01 94.72.110.157 2 ОБЕЗВРЕЖЕН 02.04 17:20 02.04 20:11 115.190.244.156 2 СВОБОДЕН 02.04 18:16 02.04 22:33 14.225.222.7 2 СВОБОДЕН 02.04 20:20 02.04 22:30 185.156.73.157 2 СВОБОДЕН 03.04 10:35 03.04 11:38 194.60.254.20 2 СВОБОДЕН 03.04 12:42 03.04 12:42 213.111.91.197 2 СВОБОДЕН 02.04 22:12 03.04 12:37 165.154.120.211 2 СВОБОДЕН 03.04 11:44 03.04 12:12 142.93.145.126 2 СВОБОДЕН 02.04 16:14 02.04 19:59 64.62.197.137 2 СВОБОДЕН 03.04 12:46 03.04 12:46 20.29.22.156 2 СВОБОДЕН 03.04 04:24 03.04 04:24 80.94.95.83 2 СВОБОДЕН 02.04 23:50 03.04 09:53 207.189.16.214 2 СВОБОДЕН 03.04 03:32 03.04 05:43 81.29.142.100 2 СВОБОДЕН 03.04 07:05 03.04 07:54 80.66.83.80 2 СВОБОДЕН 02.04 21:04 03.04 08:41 103.49.131.118 2 СВОБОДЕН 03.04 07:38 03.04 10:55 64.23.226.83 2 СВОБОДЕН 02.04 23:02 02.04 23:02 103.131.85.178 2 ОБЕЗВРЕЖЕН 02.04 19:08 02.04 23:24 135.237.126.209 2 СВОБОДЕН 03.04 10:43 03.04 10:43 84.247.165.192 2 СВОБОДЕН 02.04 18:37 03.04 14:47 ИТОГ: ОБЕЗВРЕЖЕНО: 11 | СВОБОДНО: 42
Ну и, в самом конце ленты, находится статистика лога по типам событий, количеству хитов и уникальных хостов, которые принимали участие в атаках:
=== [ АНАЛИЗАТОР АКТИВНОСТИ ПО ID ] === Статистика событий, содержащих IP-адреса (за последние 24 ч.) Код события Обращений Уникальных IP ----------- --------- ------------- 131 412 86 140 151 39 139 9 7
Обращаем ваше внимание на то, что в этой таблице содержатся не только те события, которые по указанию управляющей таблицы отслеживает ES-RDP, но вообще все события, в которых зафиксирован IP-адрес любого клиента RDP. Из нее, кстати, хорошо видно, что указание в управляющей таблице именно кодов 140 и 131 — является правильным решением.
Выводы и ретроспектива
Система ES-RDP работает в моем окружении уже несколько недель. И вот что я могу сказать по итогам.
Что получилось хорошо
Цель достигнута — ботнет, атаковавший RDP, был нейтрализован. Количество «преступлений» в логах упало с тысяч до десятков в сутки. Работает автономно — скрипт, запущенный через Task Scheduler, не требует ручного вмешательства. Банхаммер сам находит цели, сам блокирует, сам амнистирует старых заключенных. Прозрачность — скрипт stat_all.ps1 позволяет в любой момент заглянуть в «тюрьму» и понять, кто сидит, за что и когда выйдет.
Что можно было сделать лучше
Код PowerShell — да, я новичок. Опытный Windows-админ наверняка найдет, к чему придраться: где-то неоптимальная обработка массивов, где-то можно было обойтись без лишних циклов. Обработка ошибок — сейчас скрипт молча глотает проблемы с чтением логов. В промышленной среде стоило бы добавить уведомления. Масштабирование — при тысячах одновременных банов производительность `Get-NetFirewallRule` может просесть. Мне 108 правил было достаточно, но на больших фермах, возможно, узкое место (на самом деле — не знаю, не уверен).
Стоит ли использовать ES-RDP?
Да, если у вас: небольшой или средний RDP-сервер; нет бюджета на коммерческие системы защиты; есть базовые навыки работы с PowerShell и Task Scheduler; вам нужна бесплатная и прозрачная альтернатива «черным ящикам».
Вместо эпилога
Я не фанат Windows. PowerShell не станет моим любимым инструментом. Но когда задача стоит — защитить сервер здесь и сейчас — приходится брать то, что работает. ES-RDP работает. Я на этом ставлю точку и возвращаюсь к Linux. Пользуйтесь, критикуйте, улучшайте (не забывая ссылаться на первоисточник).