Привет, Хабр! Я Роман Сафиуллин, руковожу отделом защиты информации InfoWatch ARMA. Мы занимаемся защитой инфраструктуры промышленных предприятий от киберугроз, и сегодня хочу поделиться с вами инструкцией по разбору промышленных протоколов на примере протоколов IEC104 и Fanuc Focas. В основе статьи – мой доклад на конференции Industrial++. Совместно с коллегами из отдела разработки ARMA – Сергеем Калдыркаевым и Алексеем Пуцем, собрали для вас немного цифр по атакам на промышленность, примеры интересной малвари и, собственно, туториал по разбору пром. протоколов.

Что происходит?
Пару слов о том, что мы сейчас наблюдаем в промышленности с точки зрения безопасности. По данным экспертно-аналитического центра InfoWatch, одна из основных проблем ИБ на предприятиях состоит в том, что устройства АСУ ТП, как правило, имеют ряд уязвимостей, которые уже хорошо известны злоумышленниками и активно ими эксплуатируются “in the wild” (в реальной среде). При этом около половины из известных уязвимостей не исправляются по тем или иным причинам. Это могут быть как проблемы с получением обновлений для зарубежных ПЛК, так и несовместимость обновленного ПО с остальной системой. Также часто встречается простое нежелание перестраивать уже работающую инфраструктуру под новую версию ПО. В настоящее время известные уязвимости имеют более 60% устройств АСУ ТП. Оборудование применяемое в АСУ ТП очень часто сильно ограниченно с программной и аппаратной точки зрения, не может применять шифрование, или протоколы, требующие сложной реализации. Оборудование медленно эволюционирует по причине необходимости обеспечения совместимости нового внедряемого оборудования со старым, уже установленным. Многие комплексы АСУ ТП не меняются с момента запуска вовсе. Стоит также отметить, что многие промышленные протоколы вообще не применяют шифрование.
В целом, атаки на промышленность переживают период своего расцвета. В России число атак на промышленные объекты год к году выросло практически в 12 раз в 2022 году. В 2023 году рост числа атак уже не такой выразительный – всего 4%, но это можно связать с эффектом высокой базы 2022 года.
Вот пара примеров того, с какими именно угрозами сталкивается промышленность.
Примеры атак

BlackEnergy – Малварь, наиболее «ярко» проявившая себя в конце декабря 2015 года, в ходе атак на СМИ и энергосети Украины. C использованием похожего вредоносного ПО (ВПО) уже осуществлялись атаки на SCADA-системы в США и Европе, однако инцидент с энергосетями в 2015 году впервые повлек за собой массовое отключение электроэнергии. Специалисты «Лаборатории Касперского» обнаружили в исследованной версии ВПО функции, позволяющие саботировать работу промышленных систем. В частности, после заражения малварь отключает ряд процессов, относящихся к работе автоматизированной системы управления ASEM Ubiquity и повреждает ответственные за их запуск файлы.

Triton – ещё один интересный пример ВПО. По данным FireEye, Triton маскируется под легитимное ПО для Triconex SIS, предназначенное для рабочих станций под управлением Windows, и задействует проприетарный протокол TriStation. Исследователи отмечают, что TriStation недокументирован публично, то есть разработчики малвари провели большую работу по реверс-инжинирингу (как раз то, что мы будем разбирать в этой статье, но в благих целях). Если ВПО обнаруживает на зараженной машине файлы конфигурации SIS, он пытается перепрограммировать контроллеры. В итоге, Triton либо вообще останавливает весь производственный процесс, либо вынуждает оборудование работать в небезопасном состоянии.

Snake - Вымогатель Snake был впервые обнаружен в январе 2020 года, и представляет особую угрозу для промышленных систем управления (ICS), так как малварь ориентирована на процессы, специфичные для этих сред. Также известно, что малварь обычно похищает данные компаний, перед тем как приступить к шифрованию файлов, а затем операторы вымогателя требуют выкуп за эту информацию.

Прочие угрозы - кроме ВПО, угрозу представляет и банальная, но оттого не менее страшная ошибка инженера, ведь если что-то можно сделать не так, в какой-то момент — это обязательно будет сделано не так. При этом, далеко не на всех объектах АСУ ТП можно встретить «защиту от дурака».
Разбор промышленного протокола: сценарий атаки
Поняв, что атаки на промку – явление достаточно регулярное, требуется выяснить, а как от всего этого защищаться. Протоколы – основа автоматизации на предприятии, и чтобы эффективно их защищать, нужно понимать, что у них внутри и каким образом их могут атаковать злоумышленники. Поэтому первый этап – это построение плана атаки.
Рассмотрим примерный сценарий атаки на протокол IEC, в результате которой злоумышленник может вклиниться в поток данных и изменить значимые параметры. Если разбить подобный сценарий на этапы, то получается следующая последовательность:

Этап 1 – проникновение в сетевую структуру. Этот шаг может быть осуществлён как изнутри сети, так и из-за её пределов.
Этап 2 – получение доступа к трафику промышленной сети. Основной целью этого этапа является получение доступа к контролю служебного трафика. Это может быть реализовано путём проведения атак типа MITM или ARP-spoofing.
Этап 3 – обнаружение устройств IEC 104. Основная цель данного этапа – это обнаружение подключённых устройств, которые работают по протоколу IEC 104. Это можно сделать одним из двух способов – пассивным или активным. Пассивный подразумевает прослушивание и анализ всех полученных пакетов. Активное обнаружение – это сканирование сетевого пространства на предмет наличия устройств, работающих с протоколом IEC 104.
Этап 4 – сброс существующей сессии TCP. TCP-соединения используются для всех коммуникаций протокола IEC 104. Одновременно к главной контролирующей станции, мастер-устройству, могут подключаться несколько подчинённых устройств, но для передачи данных может использоваться только одно активное соединение. Для внедрения ложной команды в существующее соединение необходимо, чтобы действующее TCP-соединение было прервано. В дальнейшем атакующий ПК должен создать постоянное TCP-соединение.
Этап 5 – создание постоянного TCP-соединения с устройством IEC 104. Для отправки данных необходимо обеспечение непрерывного соединения с целевым устройством. В противном случае данные будут отклонены принимающей стороной, так как порядок передачи данных будет нарушен.
Этап 6 – отправка ложных данных. На этом этапе целевое атакуемое устройство уже известно. Фактически необходимо просто создать корректное вредоносное сообщение IEC 104.
Отсутствие механизмов защиты протокола IEC 104 (или любого другого распространённого протокола без шифрования) от атак различных уровней может привести к тому, что критически важные инфраструктурные объекты и системы могут столкнуться с достаточно серьёзными угрозами кибератак.
Разбор промышленного протокола: общий подход
Для того, чтобы написать парсер под определённый протокол - необходимо знать его структуру. Её можно узнать двумя основными способами - изучив документацию (при её наличии) или проведя реверс инжиниринг.
До начала реверса
В первую очередь стоит попробовать найти спецификацию для протокола. Большинство протоколов имеют спецификацию в свободном доступе. Примером такого протокола является OPC UA, имеющий спецификацию (https://reference.opcfoundation.org/) с удобной навигацией. Эта спецификация содержит и структуру пакетов протокола. А для того же IEC 104 даже есть отечественный ГОСТ. Если протокол популярный, да ещё и открытый, найти статьи с описанием его структуры не составит труда. В случае, если для протокола найдена спецификация – проводить реверс инжиниринг не требуется. Однако, это не распространяется на проприетарные протоколы, в том числе, допустим, Fanuc FOCAS.
Если документация для протокола отсутствует, то стоит приступать к реверс инжинирингу протокола. Реверс промышленного протокола делится на несколько основных этапов:
Сперва мы исследуем структуру команд с помощью библиотеки для взаимодействия с ЧПУ-станками. Чтобы понять, что значат аргументы в командах, используем общедоступные руководства.
Узнаём типы данных полей, представленных в командах. Это нужно, чтобы понимать их размер и найти в сообщениях протокола.
Формируем список программных эмуляторов и файловых менеджеров, как в случае с Fanuc FOCAS. Найти софт для промышленных протоколов можно на профильных форумах по типу cccp3d.ru.
Затем подключаемся утилитой для передачи программ на ЧПУ станок, это и будет нашим тестовым стендом.
Записываем дампы обособленно по командам. Это необходимо чтобы знать, какую команду содержат пакеты в дампе. Захват трафика производим с помощью Wireshark — инструмента для анализа сетевых пакетов. Эта утилита, в том числе, может видеть содержимое многих промышленных протоколов, но не Fanuc FOCAS. Видимо ещё не нашлось желающих написать плагин для Wireshark, как раз в силу отсутствия спецификаций в открытом доступе.
Проводим сам реверс-инижиниринг протокола. Сравниваем пакеты между собой, чтобы определить следующие поля: начало протокола, длину сообщения, количество подпакетов, номер команды, аргументы и т.д. Чуть подробнее про то, как проводить реверс опишем ниже.
Итак, мы определили структуру протокола. Как минимум, сигнатуру протокола. Это первые байты, по которым можно если не однозначно определить протокол, то как минимум отбрасывать гарантировано неподходящие. Например, в случае с протоколом DNP3, можно посмотреть через Wireshark, что сигнатурой протокола является поле Start Bytes: 0x0564.
Другим необходимым полем является длина, позволяющая не выйти при чтении пакета за его границы. Также необходимо знать расположение поля с командой и её структуру. Зная команду и используя библиотеку для связи с ЧПУ-станками, можно будет попробовать «увидеть» поля команды, сопоставляя их с полями соответствующих структур в библиотеке.
Дальнейший процесс разработки относительно тривиален. Необходимо написать функции, последовательно, поле за полем, читающие пакет.
Также необходимо написать функции для парсинга строк с правилами — сигнатур. Для этого используются всем знакомые регулярки. Сигнатуры можно задавать с помощью условий и диапазонов. В таком случае необходимо иметь функции, которые могут парсить строки вида: VALUE = 10 и VALUE [10:15]. И так для каждого поля протокола. То есть, будь у нас поле с номером команды, парсер должен уметь распознавать сигнатуры вида: "CMD = 5" и "CMD [5:5]". А будь у нас поле в протоколе с GUID, наша функция должна уметь парсить сигнатуру вида: "GUID {b0d4ce5d-2757-4699-948c-cfa72ba94f86}".
Завершаем написание парсера функцией для сравнения значений в пакете с сигнатурами-образцами, по которым мы понимаем, надо ли блокировать пакет.
А как проводится сам реверс инжиниринг протокола, на что требуется смотреть?
Делаем мы это путём поиска несовпадений от пакета к пакету или наоборот, повторяемости значений.
Определим поля протокола:

Начало протокола — по расположению (в начале) и неизменяемости от пакета к пакету.
Длину пакета и подпакета по тому признаку, что они равны длине последующих байт.
Количество подпакетов — по самому факту наличия подпакета.
Поле с командой, как можно заметить, различается от команды к команде, однако оно не изменяется, если послать несколько одинаковых команд. Это позволяет сделать вывод о том, что это поле команды.
Конечно же, есть и другие поля, такие как аргументы к команде, но принцип их определения такой же.
Разбор промышленного протокола: разработка парсера правил
Разработка начинается с написания парсеров строк с правилами. Здесь и далее мы будем использовать синтаксис IDS/IPS с открытым исходным кодом - Suricata. Чтобы начать работу с неподдерживаемым "из коробки" протоколом, необходимо разработать набор правил, предварительно продумав, какие конкретно параметры трафика мы хотели бы определять. Для каждого интересующего нас поля протокола необходимо придумать правило в виде метки, условия или диапазона. И написать для них парсеры, которые смогут извлечь значение из строки и записать его в соответствующие поля структуры правила. Далее это правило и будет сопоставляться с пакетами протокола.
Сигнатуры используются для фильтрации пакетов по полям протокола. Общий вид сигнатуры для протокола IEC будет выглядеть следующим образом:alert tcp any any <> any any (iec104: ASDU, I, ASDU_TYPE C_ST_NA_1; sid: 1; rev: 1;)
Перечисление правил идёт после ключевого слова «iec104». Они перечисляются через запятую в виде меток, условий и диапазонов.
Для фильтрации по типу пакета используются метки:
APCI (для пакетов, где есть только заголовок);
ASDU (для пакетов с ASDU, содержащим Information Objects).
Пример использования:alert tcp any [1024:] <> any 2404 (iec104: ASDU; msg: "IEC 104 ASDU"; sid: 1; rev: 1;)
alert tcp any [1024:] <> any 2404 (iec104: APCI; msg: "IEC 104 APCI"; sid: 2; rev: 1;)
Для фильтрации по формату пакета можно использовать метки:
I для формата передачи информации (только для пакетов с ASDU)
S для формата функций контроля с нумерацией (только для пакетов APCI)
U для формата функций управления без нумерации (только для пакетов APCI)
Пример использования:alert tcp any [1024:] <> any 2404 (iec104: ASDU, I; msg: "IEC 104 APCI Type I"; sid: 1; rev: 1;)
alert tcp any [1024:] <> any 2404 (iec104: APCI, S; msg: "IEC 104 APCI Type S"; sid: 2; rev: 1;)
alert tcp any [1024:] <> any 2404 (iec104: APCI, U; msg: "IEC 104 APCI Type U"; sid: 3; rev: 1;)
Чтобы не затягивать статью, остановимся на этих двух типах полей в сигнатуре. Для более глубокого погружения в структуру протокола и его полей, можно изучить общедоступные спецификации по IEC 104 и дампы.
Для парсинга строки используются регулярные выражения. Предположим, мы хотим задавать правило фильтрации по типу ASDU в формате: ASDU_TYPE ТИП.
Полностью сигнатура с таким правилом будет выглядеть так:alert tcp any any <> any any (iec104: ASDU_TYPE M_SP_NA_1, ПРАВИЛО 2, ПРАВИЛО 3, ПРАВИЛО...; sid: 1; rev: 1;)
Надо заметить, что также необходимо написать функцию, которая будет разделять правила, перечисляемые после ключевого слова и передавать их парсерам правил по типу того, что сейчас напишем мы. Представим, что у нас уже есть функция, которая это умеет. И перейдём непосредственно к парсеру нашего правила.
Напишем простую регулярку и добавим PCRE объект для работы с ней:
static const char* iec_104_regex_text = "^\sASDU_TYPE\s+(.+)\s$";
static DetectParseRegex iec_104_regex;
Данное регулярное выражение можно проверить на соответствующих сайтах, например, regex101.com.
Убеждаемся, что регулярка действительно может искать строки вида: ASDU_TYPE M_SP_NA_1. Обратим внимание, что извлекаться будет текст в скобках. Создаём перечисление типов ASDU:
typedef enum IEC_ASDU_TYPE
{
IEC_ASDU_TYPE_ERROR = 0x00,
IEC_ASDU_TYPE_M_SP_NA_1 = 0x01,
IEC_ASDU_TYPE_M_SP_TA_1 = 0x02,
<...>
} iec_asdu_type;
Значения этого перечисления и будем записывать в наше правило, при обнаружении соответствующего типа в строке с правилом. Пример парсера строки с правилом для указания типа кадра ASDU протокола IEC 104:
int Detect_IEC104_ASDU_TYPE(const char* str, iec_asdu_type* type)
{
// Инициализируем PCRE объект для поиска
if(iec_104_regex.regex == NULL)
{
DetectSetupParseRegexes(iec_104_regex_text, &iec_104_regex);
}
if(str == NULL)
{
SCLogDebug("Didn't got input string");
return 0;
}
const int LOCAL_MAX_SUBSTRINGS = 30;
char buffer[LOCAL_MAX_SUBSTRINGS];
int ovector[LOCAL_MAX_SUBSTRINGS];
unsigned int offset = 0;
unsigned int len = strlen(str);
int rc = 0;
if(len == 0)
{
SCLogDebug("Got empty string");
return 0;
}
// Совершаем поиск регуляркой в строке
while(offset < len && (rc = DetectParsePcreExec(&iec_104_regex, str, offset, 0, ovector, LOCAL_MAX_SUBSTRINGS)) >= 0)
{
// Найденных групп должно быть две, первая по умолчанию вся строка, вторая то, что заключено в скобки
if(rc != 2)
{
SCLogDebug("Bad input string. Got %i groups, must be 2. Str: \"%s\" ", rc, str);
return 0;
} else
{
memset(buffer, 0, sizeof(buffer));
pcre_copy_substring(str, ovector, rc, 1, buffer, sizeof(buffer));
// Поиск типа ASDU в строке с правилом, для дальнейшей записи в структуру правила
if(strcmp(buffer, "M_SP_NA_1") == 0)
*type = IEC_ASDU_TYPE_M_SP_NA_1;
else if(strcmp(buffer, "M_SP_TA_1") == 0)
*type = IEC_ASDU_TYPE_M_SP_TA_1;
// И т.д.
else
// Тип не найден
return 0;
// Тип найден
return 1;
}
offset = ovector[1];
}
return 0;
}
Если строка соответствует регулярке и найден тип ASDU, он будет записан в аргумент type, и функция вернёт 1 (найдено). Если строка не соответствует регулярке, либо тип не найден, функция вернёт 0. Заметим, что полученное значение типа ASDU надо где-то хранить. Создадим для этого структуру, которая будет хранить правила для фильтрации:
typedef struct iec_104_rule_
{
iec_asdu_type* asdu_type;
// Правило 2
// Правило 3
// И т.д.
} iec_104_rule;
Поскольку полей в протоколе IEC 104 много, в реальности данная структура бы содержала правила и для других полей.
Разбор промышленного протокола: разработка парсера пакетов
После разработки парсера правила, пишем парсер для пакетов. Он необходим для анализа полей и сопоставления их с правилами в структуре iec_104_rule, которое должно быть заполнено после обработки сигнатуры вида:alert tcp any any <> any any (iec104: ...; sid: 1; rev: 1;)
Парсер пакетов IEC 104, с проверкой типа ASDU:
int DetectIec104Match(DetectEngineThreadCtx* det_ctx, Packet* p, const Signature* s, const SigMatchCtx* ctx)
{
SCEnter();
const iec_104_rule* rule = (const iec_104_rule*)ctx;
const int signature = 0x68;
// Проверяем, действительно ли это пакет протокола IEC 104
if(p->payload[0] != signature)
{
SCLogDebug("Bad protocol signature. This is not IEC 104.");
return 0;
}
const int apci_size = 6;
// Проверяем, если этот пакет содержит только APCI часть (6 байт)
if(p->payload_len == apci_size)
{
SCLogDebug("This package contains only APCI.");
return 0;
}
uint8_t asdu_type = p->payload[6];
if(rule->asdu_type != NULL)
{
// Проверяем, соответствует ли тип ASDU в пакете правилу
if(asdu_type == rule->asdu_type)
{
return 1;
}
else
{
return 0;
}
}
return 0;
}
В начале мы проверяем первый байт пакета, на соответствие сигнатуре протокола IEC 104 - байт должен иметь значение 0x68. Далее мы проверяем размер пакета, чтобы отбросить пакеты IEC 104, содержащие только APCI часть (такие пакеты имеют размер в 6 байт). Эта проверка выполняется, т.к. нам нужно прочитать тип ASDU, содержащийся, очевидно, в идущем далее блоке ASDU, а в пакетах, содержащих только APCI часть этот блок отсутствует. Далее дело за малым - мы получаем тип ASDU и сравниваем со значением из правила, которое попало туда при парсинге сигнатуры.
Всю архитектуру парсера передать в статье крайне сложно из-за её объёма (и рекомендаций корпоративных юристов), однако структура парсеров в целом похожа, и для её понимания вы можете изучить, допустим, парсер DNP3, идущий в комплекте с Suricata. Чтобы понять, какое место занимала бы наша воображаемая функция DetectIec104Match в реальном парсере, вы можете обратится к соответствующей функции DetectDNP3FuncMatch протокола DNP3.
С использованием написанного парсера и гибких настроек самой Suricata, можно составлять правила, подходящие для ограничения возможностей потенциального злоумышленника по воздействию на информационную инфраструктуру.
Заключение
Современные промышленные системы сталкиваются с множеством угроз, начиная от целевых атак с использованием вредоносного ПО, до ошибок в эксплуатации оборудования. Уязвимости в промышленных протоколах, в частности в IEC 104, и недостаточная защита инфраструктуры предоставляют злоумышленникам возможность осуществлять атаки, способные нанести значительный ущерб объектам, эксплуатирующим АСУ ТП, в т. ч. объектам КИИ.
Для противодействия этим угрозам необходимо не только внедрять современные технологии защиты, продумывать защиту периметра и проводить регулярный мониторинг состояния систем, но и углубленно изучать внутреннюю структуру промышленных протоколов. Реверс-инжиниринг таких протоколов позволяет выявлять их слабые места и создавать эффективные решения для предотвращения атак.
Комбинированный подход, включающий проактивное исследование, постоянную доработку существующих инструментов и тестирование в изолированных условиях реальных предприятий, позволяет минимизировать риски и повысить безопасность промышленных предприятий, создавая надежный барьер для киберугроз.