вторник, 9 августа 2016 г.

Powershell. Контролируем сетевую активность.

Приветствую, уважаемый читатель!

Хотел бы сегодня поделится опытом решения следующей задачи: запрет соединения с ресурсами сети Интернет, при установлении соединения с конкретным IP-адресом. Не скажу, что данная задача является типовой и очень востребованной, но процесс её решения заставил придумывать нестандартные ходы. Будут освещены вопросы архитектуры скрипта, который сможет выполнять данную задачу, и показаны примеры работы с брандмауэром Windows из powershell. Кто заинтересовался, прошу ниже.





Начнем с постановки задачи и описания последовательности действий. Предположим, что соединение с указанным IP-адресом происходит с использованием специального программного обеспечения.

Задача: Контроль сетевой активности Windows-машины, с целью выявления конкретного TCP-соединения с указанным IP-адресом и блокирование всех остальных соединений. Как только соединение с конкретным адресом прекратилось, блокировка остальных соединений должна сниматься.
Средство реализации задачи: Учитывая, что в последнее время мне крайне полюбился powershell, программное обеспечение решающее данную задачу будет реализовано на нём. Исходя из постановки задачи понятно, что скрипт на powershell должен выполняться в операционной системе с определённой периодичностью.
Обобщённый алгоритм работы: Скрипт просматривает список всех установленных соединений в системе, с целью поиска интересующего. После нахождения такого соединения, скрипт разрывает все остальные соединения, кроме интересующего. После окончания работы с указанным IP-адресом обычная работа пользователя с ресурсами сети Интернет восстанавливается.


Теперь, последовательность действий.
  1. Получение списка всех текущих TCP-соединений, с указанием приложения их создавшего, и поиск нужного соединения.
  2. Определение списка приложений, которым мы запрещаем сетевую активность и добавляем запрещающие правила брандмауера.
  3. Отмена запрещающих правил после окончания конкретного соединения.
  4. Настройка скрипта на постоянное выполнение.

Приступим к первому пункту. Получение списка всех текущих TCP-соединений, с указанием приложения их создавшего, и поиск нужного соединения.

Частичное решение этой задачи я описывал в этом посте - http://unitybas.blogspot.ru/2015/11/powershell-netstat.html

Всё просто, получаем список установленных TCP-соединений с помощью Netstat, приводим его в удобный вид и связываем по полю ProcessID со списком запущенных в операционной системе процессов.

Хотел бы сделать небольшое отступление. Изначально я не планировал использовать брандмауер Windows для блокирования соединений, и собирался просто разрывать установленные соединения (ESTABLISHED) с удаленными IP-адресами. Но как оказалось, в Powershell нет командлетов или функций, которым можно сказать: Разорви конкретное соединение с этим IP-адресом.

Немного погуглив, я нашел несколько решений данной проблем, но оба они использовали отдельное консольное приложение. Вот список приложений:


Использование приложений:
Разорвать соединение по 22 порту с IP-адресом 100.120.200.18 при помощи wkillcx  
C:\scripts\wkillcx-1.0.2\wkillcx.exe 100.120.200.18:22

Разорвать соединение по 22 порту с IP-адресом 100.120.200.18 при помощи cports  
C:\scripts\cports\cports.exe /close 192.168.1.30 47000 100.120.200.18 22

Варианты использования сторонних приложений мне изначально были не интересны, и я решил ограничиться только средствами powershell.


Но хватит с лирикой. Код, реализующий первый пункт приведен ниже:
     #Получаем список текущих TCP/UDP-соединений и список запущенных в системе процессов    
    $sockets_all = netstat -ano | Select-String -Pattern '\s+(TCP|UDP)' | Select-String -Pattern '\:\:' -notmatch
    $process_list = Get-WmiObject -Class Win32_Process
   
    #Создаём пустую коллекцию для списка соединений
    $ProcessCollection=@()

    foreach($socket in $sockets_all) {
        $parse = $socket.line.split(' ',[System.StringSplitOptions]::RemoveEmptyEntries)
        $ID_tcp = $parse[4]
        $ID_udp = $parse[3]
        $local = $parse[1] -split ":"
        $remote = $parse[2] -split ":"
 
        $local_ip = $local[0]
        $local_port = $local[1]
        $remote_ip = $remote[0]
        $remote_port = $remote[1]

        #Соединяем по ProcessID между собой список TCP-сессий и список процессов в системе
        foreach($process in $process_list) {
             if (($process.ProcessID -eq $ID_tcp) -and ($parse[0] -eq "TCP"))
                {
                  #Данные о сессии будем хранить в PS объектах и добавлять их в коллекцию
                  $obj_proc = New-Object psobject
                  $obj_proc | Add-Member -type noteproperty -name Protocol -Value $parse[0]
                  $obj_proc | Add-Member -type noteproperty -name LocalIP -Value $local_ip
                  $obj_proc | Add-Member -type noteproperty -name LocalPort -Value $local_port
                  $obj_proc | Add-Member -type noteproperty -name RemoteIP -Value $remote_ip
                  $obj_proc | Add-Member -type noteproperty -name RemotePort -Value $remote_port
                  $obj_proc | Add-Member -type noteproperty -name PID -Value $ID_tcp
                   $obj_proc | Add-Member -type noteproperty -name ProcName -Value $process.ProcessName
                  $obj_proc | Add-Member -type noteproperty -name procPath -Value $process.Path
                   $obj_proc | Add-Member -type noteproperty -name CompName -Value $process.CSName
                  $ProcessCollection += $obj_proc
                }

           }
      }

    #IP-адрес, соединение с которым будет контролироваться
    $ControlIP = '100.120.220.18'

    #Ищем, есть ли соединение с данным адресом в текущий момент
    $ResMonitor = Get-NetTcpConnection | where {$_.RemoteAddress -eq $ControlIP}


Приступаем ко второму пункту. Определение списка приложений, которым мы запрещаем сетевую активность и добавляем запрещающие правила брандмауера.

Вообще, управление правилами брандмауера из powershell оказалось очень удобным. Есть целая куча командлетов для управления брандмауером, приведу пару наиболее полезных:
Get-NetFirewallRule (получить список правил МЭ)
New-NetFirewallRule (создать новое правило МЭ)
Set-NetFirewallRule (изменить правило МЭ)
Remove-NetFirewallRule (удалить правило МЭ)

Примеры:
Получить список активных исходящих правил МЭ.
GetNetFirewallRule -Direction Outbound -Enabled True

Добавить новое запрещающее исходящее правило для исполняемого файла.
New-NetFirewallRule -Program “C:\Temp\Test.exe” -Action Block -DisplayName “Test” -Description “Block” -Direction Outbound

Отключить правило.
Set-NetFirewallRule –DisplayName "Test" -Enabled False

Удалить правило.
Remove-NetFirewallRule -DisplayName "Test" 


На основе этих базовых правил для любого МЭ, мы и реализуем блокировку. Общий алгоритм, как был описан выше: блокируем активность для всех приложений, за исключением породившего сессию с контролируемым IP-адресом. Код реализующий данную задачу приведен ниже:

 #IP-адрес, соединение с которым будет котролироваться
    $ControlIP = '100.120.220.18'

    #Ищем, есть ли соединение с данным адресом в текущий момент
    $ResMonitor = Get-NetTcpConnection | where {$_.RemoteAddress -eq $ControlIP}

    #Если соединение с контролируемым IP-адресом установлено
    if ($ResMonitor.count -ne 0) {
       
        #Просматриваем все установленные соединения с IP-адресами в сети Интернет, и получаем для каждого соединения имя породившего его процесса, и путь с исполняемому файлу
        foreach($proc in ($ProcessCollection | where {($_.RemoteIP -ne "0.0.0.0") -and ($_.RemoteIP -ne "127.0.0.1") -and ($_.RemoteIP -ne $ControlIP)} | select PID,ProcName,procPath -Unique)) {
            $RuleCaption = "Block App - " + $proc.ProcName

            #Если процесс породивший соединение уже заблокирован брандмауером Windows, то выводим сообщение
            if (Get-NetFirewallRule | Where {$_.DisplayName -eq $RuleCaption}) {Write-Host "Already ban"}
                else {
                        #Если процесс породивший соединение не заблокирован брандмауером Windows, то добавляем новое правило
                        New-NetFirewallRule -Program $proc.procPath -Action Block -DisplayName $RuleCaption -Description “Automatical Block” -Direction Outbound
                     }
                }
    }


Приступаем к третьему пункту. Отмена запрещающих правил после окончания конкретного соединения.

Тут всё просто, как только соединение с контролируемым IP-адресом разорвано, то мы удаляем блокирующие правила из МЭ. Код представлен ниже:

#Если соединение с контролируемым IP-адресом не установлено
    elseif ($ResMonitor.count -eq 0) {
        #Получаем список текущий блокирующих правил
        $blockRule = Get-NetFirewallRule | Where {$_.DisplayName -like "Block App - *"}
              foreach ($br in $blockRule) {
                        #Удаляем блокирующее правило
                        Remove-NetFirewallRule -DisplayName $br.DisplayName
               }
          }
    }



Приступаем к четвертому пункту. Настройка скрипта на постоянное выполнение.

Есть несколько вариантов решения данной задачи, а именно:
  •   Добавить запуск скрипта в планировщик заданий и настройка периодичности запуска. Прочитать поподробнее про это можно тут - http://windowsnotes.ru/powershell-2/zapusk-powershell-skripta-po-raspisaniyu/
  •   Добавить в скрипте бесконечный цикл и постоянно выполнять код с использованием командлета Start-Sleep. Учитывая, что нам нужно запускать скрипт каждые 5-10 секунд, этот вариант вполне подходит


    #Запускаем скрипт в бесконечном цикле
    $count = 0
    while ($count -le 1) {
            Block
            Start-Sleep -Seconds 5
    }


Таким образом, общая финальная версия скрипта приведена ниже:

function Block {
   
    #Получаем список текущих TCP/UDP-соединений и список запущенных в системе процессов    
    $sockets_all = netstat -ano | Select-String -Pattern '\s+(TCP|UDP)' | Select-String -Pattern '\:\:' -notmatch
    $process_list = Get-WmiObject -Class Win32_Process
   
    #Создаём пустую коллекцию для списка соединений
    $ProcessCollection=@()

    foreach($socket in $sockets_all) {
        $parse = $socket.line.split(' ',[System.StringSplitOptions]::RemoveEmptyEntries)
        $ID_tcp = $parse[4]
        $ID_udp = $parse[3]
        $local = $parse[1] -split ":"
        $remote = $parse[2] -split ":"
 
        $local_ip = $local[0]
        $local_port = $local[1]
        $remote_ip = $remote[0]
        $remote_port = $remote[1]

        #Соединяем по ProcessID между собой список TCP-сессий и список процессов в системе
        foreach($process in $process_list) {
             if (($process.ProcessID -eq $ID_tcp) -and ($parse[0] -eq "TCP"))
                {
                  #Данные о сессии будем хранить в PS объектах и добавлять их в коллекцию
                  $obj_proc = New-Object psobject
                  $obj_proc | Add-Member -type noteproperty -name Protocol -Value $parse[0 ]
                  $obj_proc | Add-Member -type noteproperty -name LocalIP -Value $local_ip
                  $obj_proc | Add-Member -type noteproperty -name LocalPort -Value $local_port
                  $obj_proc | Add-Member -type noteproperty -name RemoteIP -Value $remote_ip
                  $obj_proc | Add-Member -type noteproperty -name RemotePort -Value $remote_port
                  $obj_proc | Add-Member -type noteproperty -name PID -Value $ID_tcp
                  $obj_proc | Add-Member -type noteproperty -name ProcName -Value $process.ProcessName
                  $obj_proc | Add-Member -type noteproperty -name procPath -Value $process.Path
                  $obj_proc | Add-Member -type noteproperty -name CompName -Value $process.CSName
                  $ProcessCollection += $obj_proc
                }

           }
      }

    #IP-адрес, соединение с которым будет котролироваться
    $ControlIP = '100.120.220.18'

    #Ищем, есть ли соединение с данным адресом в текущий момент
    $ResMonitor = Get-NetTcpConnection | where {$_.RemoteAddress -eq $ControlIP}

    #Если соединение с контролируемым IP-адресом установлено
    if ($ResMonitor.count -ne 0) {
       
        #Просматриваем все установленные соединения с IP-адресами в сети Интернет, и получаем для каждого соединения имя породившего его процееса, и путь с исполняемому файлу
        foreach($proc in ($ProcessCollection | where {($_.RemoteIP -ne "0.0.0.0") -and ($_.RemoteIP -ne "127.0.0.1") -and ($_.RemoteIP -ne $ControlIP)} | select PID,ProcName,procPath -Unique)) {
            $RuleCaption = "Block App - " + $proc.ProcName

            #Если процесс породивший соединение уже заблокирован брандмауером Windows, то выводим сообщение
            if (Get-NetFirewallRule | Where {$_.DisplayName -eq $RuleCaption}) {Write-Host "Already ban"}
                else {
                        #Если процесс породивший соединение не заблокирован брандмауером Windows, то добавляем новое правило
                        New-NetFirewallRule -Program $proc.procPath -Action Block -DisplayName $RuleCaption -Description “Automatical Block” -Direction Outbound
                     }
                }
    }

    #Если соединение с контролируемым IP-адресом не установлено
    elseif ($ResMonitor.count -eq 0) {
        #Получаем список текущий блокирующих правил
        $blockRule = Get-NetFirewallRule | Where {$_.DisplayName -like "Block App - *"}
            foreach ($br in $blockRule) {
                #Удаляем блокирующее правило
                Remove-NetFirewallRule -DisplayName $br.DisplayName
            }
         }

    }

    #Запускаем скрипт в бесконечном цикле
    $count = 0
    while ($count -le 1) {
        Block
        Start-Sleep -Seconds 5
    }


Результат работы скрипта в настройка брандмауэра приведены на скриншоте:


Предвижу, что может появиться закономерный вопрос: Что делать если контролируемое соединение породило не отдельное приложение, а, например, браузер Chrome? Для данной ситуации код скрипта необходимо доработать, и блокирующие правила в МЭ должны добавляться на основании внешних IP-адресов в соединении, а не по приложениям. Делается это не трудно, позднее напишу.  

На этом пока всё, может кому пригодится.


2 комментария:

  1. Интересно.
    Я на той неделе тоже парсил netstat
    https://www.powershellgallery.com/packages/Get-NetStat/1.0.0/DisplayScript

    В последнее время использую Add-Member только если нужно добавить метод или скрытое свойство, присмотритесь к формированию объектов через Select-String.

    И еще про запутку while:
    Вы создаете переменную со значением 0
    Потом вы говорите выполнять цикл, пока эта переменная меньше 1
    Вы усложняете, поэтому перед началом такого цикла обязательно нужно писать коментарий "#Запускаем скрипт в бесконечном цикле"
    Но, как известно, хороший код сам по себе является коментарием))
    Так вот можно было бы обойтись и без коментария, если бы вы написали

    while ($true) {
    Block
    Start-Sleep -Seconds 5
    }

    ОтветитьУдалить
    Ответы
    1. Благодарю за комментарий) по предложению с while согласен, так по понятнее.

      Удалить