Хроники RDP-тюрьмы: Как усмирить ботнет за 10 минут

После того, как я очень четко (результативно и эффективно) разобрался у себя с 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. Пользуйтесь, критикуйте, улучшайте (не забывая ссылаться на первоисточник).