Одним из этапов сканирования узла на наличие уязвимостей является определение его сетевой доступности. Как известно, сделать это можно несколькими способами, в том числе и посредством команды ping.
Для одного из проектов по анализу защищенности в организации с большим количеством рабочих станций нам понадобилось разработать собственный «пингер».
Требования технического задания были следующие:
- Количество одновременно пингуемых узлов должно быть велико (несколько подсетей).
- Количество портов задается пользователем (может быть 65535).
- Пингер не должен «съедать» все время процессора.
- Пингер должен обладать высоким быстродействием.
Способ пинга задается пользователем, доступны различные способы (ICMP ping, TCP port ping и Resolve name). Естественно, первой мыслью было использовать готовое решение, например, nmap, но он тяжеловат и непроизводителен на таких диапазонах узлов (портов).
Чтобы результат соответствовал ТЗ, все выполняемые операции должны быть асинхронными и использовать единый пул потоков.
Последнее обстоятельство подтолкнуло нас к выбору библиотеки Boost.Asio в качестве средства разработки, поскольку она содержит все необходимые асинхронные примитивы.
Реализация пингера
В работе пингера реализована следующая иерархия:
Класс Ping выполняет операции пинга, получения имени, после выполнения заданий инициируется обратный вызов (callback), в который передается результат. Класс Pinger создает операции пинга, инициализирует, помещает новые запросы в очередь, управляет количеством потоков и количеством одновременно открытых сокетов, определяет доступность локальных портов.
Необходимо принимать во внимание тот факт, что количество ожидающих сокетов, а значит и одновременно пингуемых портов, может доходить до нескольких тысяч, при этом загрузка процессора может быть минимальна, в случае если пингуются недоступные узлы (порты).
С другой стороны, если пингуются доступные узлы (порты), то несколько сотен активных сокетов значительно увеличивают нагрузку на процессор. Получается, что зависимость загрузки процессора от количества активных сокетов нелинейна.
Для баланса между ресурсами ЦП и временем пинга используется загрузка процессора, исходя из которой контролируется количество активных сокетов.
Доступность портов
На машине, выполняющей пинг, порты могут быть заблокированы межсетевым экраном, поэтому в нашем пингере необходимо было реализовать механизм определения доступности локальных портов. Чтобы определить доступность порта пытаемся осуществить соединение с невалидным адресом: если удалось — порт эмулирован межсетевым экраном.
typename PortState::Enum GetPortState(const Ports::value_type port)
{
boost::recursive_mutex::scoped_lock lock(m_PortsMutex);
PortState::Enum& state = m_EnabledPorts[port];
if (state == PortState::Unknown)
{
state = PortState::Pending;
const std::size_t service = GetNextService();
const SocketPtr socket(new TCPSocket(GetService(service)));
const TimerPtr timer(new Timer(GetService(service)));
socket->async_connect(
Tcp::endpoint(Address(INVALID_IP), port),
boost::bind(
&PingerImpl::GetPortStateCallback,
this,
ba::placeholders::error,
port,
socket,
timer
)
);
timer->expires_from_now(boost::posix_time::seconds(1));
timer->async_wait(boost::bind(&PingerImpl::CancelConnect, this, socket));
}
return state;
}
void GetPortStateCallback(const boost::system::error_code& e, const Ports::value_type port, const SocketPtr, const TimerPtr)
{
boost::recursive_mutex::scoped_lock lock(m_PortsMutex);
m_EnabledPorts[port] = e ? PortState::Enabled : PortState::Disabled;
}
void CancelConnect(const SocketPtr socket)
{
boost::system::error_code e;
socket->close(e);
}
В процессе пинга довольно часто приходится получать сетевое имя узла, но к сожалению, асинхронная версия getnameinfo отсутствует как таковая.
В Boost.Asio асинхронное получение имен проходит в фоновом потоке, который привязан к объекту boost::asio::io_service. Таким образом количество фоновых операций получения имени равно количеству объектов boost::asio_io_service. Чтобы повысить быстродействие получения имен и пинга в целом, создаем объекты boost::asio::io_service по числу потоков в пуле, при этом каждая операция пинга обрабатывается своим объектом.
Реализация операции пинга
ICMP ping
Все довольно просто: используются сырые сокеты. За основу взята реализация из примеров boost.org. Код достаточно прост и не требует особых пояснений.
TCP ping
Представляет собой попытки установления TCP-соединения с удаленным узлом для каждого порта из диапазона. В случае если попытка соединения хотя бы с одним портом удаленного узла успешна — узел считается доступным. Если же соединение ни с одним портом установить не удалось, количество асинхронных операций становится равным нулю и объект пинга уничтожается. В этом случае в деструкторе пинга выполняется callback c учетом результатов пинга.
Объект операции пинга существует, пока выполняется хотя бы одна асинхронная операция, поскольку в каждую из них передается указатель shared_from_this().
Код, запускающий процесс TCP-пинга:
virtual void StartTCPPing(std::size_t timeout) override
{
boost::mutex::scoped_lock lock(m_DataMutex);
if (PingerLogic::IsCompleted() || m_Ports2Ping.empty())
return;
Ports::const_iterator it = m_Ports2Ping.begin();
const Ports::const_iterator itEnd = m_Ports2Ping.end();
for (; it != itEnd; )
{
const PortState::Enum state = m_Owner.GetPortState(*it); // получаем состояние порта у владельца — пингера
if (state == PortState::Disabled)
{
it = m_Ports2Ping.erase(it);
continue;
}
else
if (state == PortState::Pending) // пропускаем порт, его локальная доступность пока неизвестна
{
++it;
continue;
}
if (m_Owner.CanAddSocket()) // проверяем, можем ли мы создать еще один сокет
{
PingPort(*it);
it = m_Ports2Ping.erase(it);
if (m_Ports2Ping.empty())
break;
}
else
{
break;
}
}
if (!m_Ports2Ping.empty())
{
// остались пропущенные порты, взводим таймер перезапуска пинга m_RestartPingTimer.expires_from_now(boost::posix_time::milliseconds(DELAY_IF_MAX_SOCKETS_REACHED));
m_RestartPingTimer.async_wait(boost::bind(
&Ping::StartTCPPing,
shared_from_this(),
timeout
));
}
// сохраняем время запуска пинга и взводим таймер контроля таймаута пинга
m_StartTime = boost::posix_time::microsec_clock().local_time();
m_PingTimer.expires_from_now(boost::posix_time::seconds(timeout));
m_PingTimer.async_wait(boost::bind(&Ping::OnTimeout, shared_from_this(), ba::placeholders::error, timeout));
}
Код, запускающий асинхронное соединение:
void PingPort(const Ports::value_type port)
{
const Tcp::endpoint ep(m_Address, port);
const SocketPtr socket(new TCPSocket(m_Owner.GetService(m_ServiceIndex)));
m_Sockets.push_back(socket);
m_Owner.OnSocketCreated(); // инкрементируем количество активных сокетов
socket->async_connect(ep, boost::bind(
&Ping::TCPConnectCallback,
shared_from_this(),
boost::asio::placeholders::error,
socket
));
}
Callback:
void TCPConnectCallback(const boost::system::error_code& e, const SocketPtr socket)
{
m_Owner.OnSocketClosed(); // декрементируем количество активных сокетов
if (!e)
TCPPingSucceeded(socket);
else
TCPPingFailed(socket);
}
Соответствующие обработчики:
void TCPPingSucceeded(const SocketPtr socket)
{
const boost::posix_time::time_duration td(boost::posix_time::microsec_clock::local_time() - m_StartTime);
boost::system::error_code error;
socket->shutdown(TCPSocket::shutdown_both, error);
// pinged successfully, close all opened sockets
boost::mutex::scoped_lock lock(m_DataMutex);
CloseSockets();
PingerLogic::OnTcpSucceeded(static_cast<std::size_t>(td.total_milliseconds()));
}
void TCPPingFailed(const SocketPtr socket)
{
// ping on this port fails, close this socket
boost::system::error_code error;
socket->close(error);
boost::mutex::scoped_lock lock(m_DataMutex);
const std::vector<SocketPtr>::const_iterator it = std::remove(
m_Sockets.begin(),
m_Sockets.end(),
socket
);
m_Sockets.erase(it, m_Sockets.end());
if (m_Sockets.empty())
m_PingTimer.cancel(); // all ports failed, cancel timer
}
Name resolving
Бустовый резолвер в зависимости от типа переданного аргумента выполняет функции getaddrinfo или getnameinfo (первый и второй примеры кода ниже соответственно).
virtual void StartResolveIpByName(const std::string& name) override
{
const typename Resolver::query query(Tcp::v4(), name, "");
m_Resolver.async_resolve(query, boost::bind(
&Ping::ResolveIpCallback,
shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::iterator
));
}
virtual void StartResolveNameByIp(unsigned long ip) override
{
const Tcp::endpoint ep(Address(ip), 0);
m_Resolver.async_resolve(ep, boost::bind(
&Ping::ResolveFQDNCallback,
shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::iterator
));
}
Первый пример кода используется для получения IP-адреса; аналогичный код используется для проверки NetBIOS-имени. Код из второго примера используется для получения FQDN узла, в случае если его IP уже известен.
Логика пингера
Собственно, она вынесена в отдельную абстракцию. И у нас для этого есть несколько причин.
- Необходимо отделить выполнение операций с сокетами от логики пингера.
- Нужно предусмотреть возможность использования в будущем нескольких стратегий в ходе работы пингера.
- Реализация условий для покрытия юнит-тестами всей логики работы пингера как отдельной сущности.
Класс, реализующий операцию пинга, унаследован от класса, реализующего логику:
class Ping : public boost::enable_shared_from_this<Ping>, public PingerLogic
При этом в классе Ping переопределяются соответствующие виртуальные методы:
//! Init ports
virtual void InitPorts(const std::string& ports) = 0;
//! Resolve ip
virtual bool ResolveIP(const std::string& name) = 0;
//! Start resolve callback
virtual void StartResolveNameByIp(unsigned long ip) = 0;
//! Start resolve callback
virtual void StartResolveIpByName(const std::string& name) = 0;
//! Start TCP ping callback
virtual void StartTCPPing(std::size_t timeout) = 0;
//! Start ICMP ping
virtual void StartICMPPing(std::size_t timeout) = 0;
//! Start get NetBios name
virtual void StartGetNetBiosName(const std::string& name) = 0;
//! Cancel all pending operations
virtual void Cancel() = 0;
Детально реализацию класса PingerLogic описывать не будем, приведем лишь примеры кода, которые говорят сами за себя.
//! On ping start
void OnStart()
{
InitPorts(m_Request.m_Ports);
const bool ipResolved = ResolveIP(m_Request.m_HostName);
if (!ipResolved)
StartResolveIpByName(m_Request.m_HostName);
}
//! On ip resolved
void OnIpResolved(const unsigned long ip)
{
boost::recursive_mutex::scoped_lock lock(m_Mutex);
m_Result.m_ResolvedIP = ip;
if (m_Request.m_Flags & SCANMGR_PING_RESOLVE_HOSTNAME)
{
m_HasPendingResolve = true;
StartResolveNameByIp(ip);
}
if (m_Request.m_Flags & SCANMGR_PING_ICMP)
{
// if tcp ping needed it will be invoked after icmp completes
StartICMPPing(m_Request.m_TimeoutSec);
return;
}
if (m_Request.m_Flags & SCANMGR_PING_TCP)
{
// in case of tcp ping only
StartTCPPing(m_Request.m_TimeoutSec);
}
}
На сегодня все. Спасибо за внимание! В следующей статье мы расскажем о покрытии процесса сетевого пинга и логики нашего пингера модульным тестированием. Следите за обновлениями.
Автор: Сергей Карнаухов, старший программист Positive Technologies (CLRN).
Автор: ptsecurity