17 сентября 2019

Исследование безопасности: CODESYS Runtime — фреймворк для управления ПЛК. Часть 3

Обнаруженные уязвимости и возможные атаки

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

В этой главе будет рассмотрено несколько обнаруженных логических уязвимостей, эксплуатация которых приводит к захвату устройства, на котором установлено ПО CODESYS Runtime.

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

Описание стенда

Для демонстрации эксплуатации уязвимостей мы будем использовать стенд, состоящий из следующих сетевых узлов:

  1. Компьютеры атакующего с сетевыми адресами 192.168.0.2 и 192.168.0.30. Узлы атакующего обозначены как Attacker №1 и Attacker №2;
  2. Устройство Raspberry Pi с запущенным CODESYS Control For Raspberry PI, на котором установлен сетевой адрес 192.168.0.91. Далее для этого узла будет использоваться сокращенное имя Client №1;
  3. Устройство Raspberry Pi с запущенным CODESYS Control For Raspberry PI, на котором установлен сетевой адрес 192.168.0.92. Далее для этого узла будет использоваться сокращенное имя Client №2;
  4. Компьютер с установленным CODESYS Development System с сетевым адресом 192.168.0.39. Для этого узла будет использоваться сокращенное имя IDE.

Cхема стенда

Атаки на уровне датаграм (datagram layer)

Протокол CODESYS PDU основан на модели ISO/OSI. Исходя из этого, можно предположить, что вместе с идеей модели ISO/OSI и её стеком протоколов, протокол CODESYS PDU унаследовал слабости этой модели и соответствующие этим слабостям угрозы безопасности.

Подмена IP-адреса (IP-spoofing)

Для каждого из протокола в модели ISO/OSI есть свой перечень угроз. Подмена IP-адреса (IP-spoofing) — атака модели ISO/OSI на сетевом уровне (network layer), которая заключается в подмене адреса источника сообщения (IP SRC) с целью сокрытия адреса отправителя. Жертва, получив такой пакет, обработает запрос и вернет ответ на указанный в поле адрес источника (IP SRC).

Cхема атаки подмены IP-адреса (IP-spoofing) для модели ISO/OSI

Сокрытие адреса отправителя используется для обмана систем безопасности и усложнения раскрытия атаки.

Атаки, аналогичные IP-spoofing, можно провести по протоколу CODESYS PDU.

Будут рассмотрены две атаки на протокол CODESYS PDU:

  1. Атака с целью сокрытия адреса источника сообщения;
  2. Атака с целью перехвата управления в существующем канале общения между узлами сети CODESYS.
Атака с целью сокрытия адреса источника сообщения

Компонент CmpRouter обрабатывает поля на уровне датаграм (datagram layer).

В заголовке уровня датаграм (datagram layer) находятся поля адресации, параметризации пакета, идентификатор службы канального уровня и другие. Среди полей адресации есть поле receiver, которое указывает, на какой адрес послать ответ на запрос.


Местоположение поля receiver в заголовке уровня датаграм (datagram layer)

Как видно на примере потока данных выше, в поле receiver содержится значение последнего байта числового представления адреса узла IDE (0x27) и значение индекса порта, равное 2. Оба эти значения совпадают с числовым значением адреса источника сообщения (поле src, равное 192.168.0.39) и со значением источника порта, с которого был отправлен запрос (поле src port, равно 1742).

Изменив значение в поле receiver, злоумышленник может реализовать классическую схему атаки по подмене IP-адреса (IP-spoofing):

Cхема классического варианта атаки подмены IP-адреса (IP-spoofing) по протоколу CODESYS PDU

Существует еще одна архитектурная уязвимость протокола CODESYS PDU, приводящая к реализации расширенного варианта атаки подмены IP-адреса. Она заключается в одной из обязанностей компонента CmpRouter — маршрутизации.


Местоположение поля sender в заголовке уровня датаграм (datagram layer)

Поле sender указывает на адрес узла, для которого предназначен пакет. Компонент CmpRouter перенаправляет полученный пакет CODESYS PDU на другой узел в сети, соответствующий указанному в поле sender, если значение поля sender не совпадает с адресом узла, получившего пакет.

Манипулируя полем sender, злоумышленник может модифицировать атаку по подмене IP-адреса, дополнив её промежуточным узлом. Промежуточный узел будет выступать в качестве прокси для перенаправления вредоносного пакета на другие узлы в сети CODESYS.

Cхема модифицированного варианта атаки подмены IP-адреса (IP-spoofing) по протоколу CODESYS PDU

Последним штрихом в атаке по подмене IP-адреса по протоколу CODESYS PDU будет скрытое получение ответа на перенаправленный запрос. Скрытое получение ответа можно осуществить, указав в сообщении в качестве получателя ответа широковещательный адрес.


Пример пакета, в котором широковещательный адрес указан в качестве значения для поля receiver в заголовке уровня датаграм (datagram layer)

Получив запрос, в котором в качестве получателя ответа в поле receiver указан последний байт широковещательного адреса (0xff), узел вернет ответ на широковещательный адрес.

Пример отправки узлом ответа на широковещательный адрес

Злоумышленник может скрыть адрес своего узла, получая с широковещательного узла ответы от узлов жертвы.

Схема атаки подмены IP-адреса (IP-spoofing) по протоколу CODESYS PDU со скрытым получением ответа на запрос

Таким образом, автоматизировав эксплуатацию обнаруженных уязвимостей и реализовав описанную схему атаки, злоумышленник мог усложнить аналитикам проведение расследования атаки.

Атака с целью с целью перехвата управления в существующем канале общения между узлами сети CODESYS

Ввиду того, что компонент CmpRouter выполняет свою работу, основываясь только на данных в пакете CODESYS PDU, злоумышленник может внедриться в существующую коммуникацию между узлами. Однако из-за того, что каждый уровень протокола CODESYS PDU зависим от предыдущего уровня, для перехвата управления на самом последнем уровне (уровне служб) необходимо перехватить управление на всех предыдущих уровнях.

Для взаимодействия на канальном уровне (channel layer) участникам сети необходимо установить канал связи. Для вызова большинства служб на уровне служб (services layer) одному узлу необходимо пройти аутентификацию на другом узле и получить идентификатор сессии. Значение идентификатора канала передается в заголовке на канальном уровне. Значение идентификатора сессии передается в заголовке уровня служб.

Таким образом, перехват управления в протоколе CODESYS PDU можно разбить на несколько задач:

  1. Получение адресов участников общения;
  2. Получение идентификатора канала;
  3. Получение идентификатора BLK и ACK;
  4. Получение идентификатора сессии.

Первая задача может быть решена штатными возможностями протокола CODESYS PDU. Третья решается перебором идентификаторов.

Чтобы решить вторую и четвертую задачи необходимо было обнаружить и проэксплуатировать несколько обнаруженных уязвимостей.

Если бы злоумышленник обнаружил найденные нами уязвимости и автоматизировал выполнение атаки, то он смог бы реализовать следующую схему атаки:

Схема атаки с целью перехвата управления

Алгоритм атаки может быть следующим:

  1. Запрос OPEN_CHANNEL:
    Узел IDE запрашивает открытие канала на узле Client #1.
  2. Ответ OPEN_CHANNEL:
    Узел Client #1 открывает канал связи узлу IDE и возвращает ему идентификатор канала (channel_id), равный 4.
  3. Запрос AUTH:
    Узел IDE аутентифицируется на узле Client #1, передавая ему пароль (Password) и имя пользователя (Username).
  4. Ответ AUTH:
    Узел Client #1 ищет в базе данных запись о полученном пользователе. После нахождения записи и сравнении пароля Client #1 возвращает узлу IDE идентификатор сессии (Session), равный 0x3456789a.
  5. Запрос PROGRAM_STOP:
    Злоумышленник от узла Attacker #1 отправляет на узел Client #1 сообщение с командой PROGRAM_STOP. В этом пакете должен быть указан инкрементированный идентификатор BLK из последнего сообщения узла IDE
    и идентификатор ACK, аналогичный идентификатору в последнем сообщении узла IDE. Идентификаторы сессии (session_id) и канала (channel_id) совпадают
    с идентификаторами, используемыми в коммуникации между узлами.
  6. Ответ PROGRAM_STOP:
    Узел Client #1 обрабатывает полученный от узла Attacker #1 запрос с командой PROGRAM_STOP, как если бы этот запрос пришел от узла IDE, потому что полученный идентификатор канала связи (channel_id) совпадает с существующим идентификатором канала связи между узлами IDE и Client #1, полученные идентификаторы BLK и ACK являются правильными для используемого канала, а идентификатор сессии существует. В результате узел Client #1 вернет положительный ответ об остановке программы.

Установка произвольного родительского узла

Перехват сетевого трафика является одной из угроз для канального уровня модели ISO/OSI (data link layer). Атаку, которая реализует угрозу перехвата сетевого трафика, называют «человек посередине» (Man in the Middle, MitM).

В протоколе ARP, который работает на канальном уровне ISO/OSI, одна из реализаций атак «человек посередине» — ARP-poising. Эта атака заключается в изменении ARP-таблицы на компьютере жертвы путем отправки специально сформированных ARP-ответов на сетевые узлы жертв. После изменений ARP-таблицы, весь исходящий трафик и входящий трафик жертвы будет проходить через сетевой адрес атакующего.


Cхема атаки ARP-poisoning по протоколу ARP. Шаг 1 — изменение ARP-таблицы

Cхема атаки ARP-poisoning по протоколу ARP. Шаг 2 — измененный маршрут сетевого потока

В CODESYS Runtime отсутствует ARP-таблица, однако присутствует механизм смены маршрута сети CODESYS, за который ответственен компонент CmpRouter.

Адрес любого узла сети CODESYS состоит из адресов всех предыдущих родительских узлов и своего адреса в качестве конечного. Узел сети CODESYS формирует и запоминает свой полный адрес после получения серии запросов на службу адресов (address service). После формирования адреса узел будет отправлять все исходящие пакеты на источник запроса, считая его родительским узлом.


Реализация атаки по установке произвольного родительского узла

На рисунке выше показаны 5 пакетов и содержимое последнего пакета. Ниже дано описание каждого из пакетов и последовательность действий узлов сети CODESYS на нашем стенде:

  1. Узел IDE отправляет пакет №1 на широковещательный адрес (broadcast). Этот пакет содержит запрос на получение информации об узлах в сети. Сам запрос предназначен для сервиса имен (name service).
  2. Узел Client #1 получает пакет №1 с широковещательного адреса. Обработав запрос, узел извлекает из поля receiver адрес узла, на который необходимо отправить ответ. В значении поля receiver указан адрес узла IDE, поэтому узел Client #1 отвечает пакетом №2 узлу IDE. Этот пакет содержит информацию об узле Client #1.
  3. Attacker #2 отправляет пакет №3 на широковещательный адрес. Этот пакет содержит запрос для сервиса адресов (address service) на изменение родительского адреса (service_id 2). Пакет №3 получает узел Client #1. После обработки пакета №3 узел Client #1 изменяет маршрут для своего сетевого интерфейса и устанавливает адрес Attacker #2 в качестве родительского узла.
  4. Узел IDE отправляет на широковещательный адрес запрос (пакет №4) на получение информации об узлах в сети. Пакет №4 идентичен пакету №1.
  5. Узел Client #1 получает пакет №4 с широковещательного адреса. Обработав этот запрос, узел формирует ответ и отправляет его в пакете №5. Этот пакет отправляется уже не на указанный в поле receiver адрес узла IDE, а на адрес родительского узла, которым стал узел Attacker #2.

Несмотря на то, что узел Client #1 получил оба запроса на получение информации с одного и того же адреса, а ответы отправил на разные адреса, содержимое этих ответов одинаково. Не изменились в них и значения полей sender и receiver. То есть, узел с измененной маршрутизацией и установленным родительским узлом ожидает от своего родительского узла доставку отправленного пакета, поэтому узел и не изменяет значения полей receiver и sender.

Таким образом злоумышленник может изменить маршрут сетевого трафика CODESYS без каких-либо привилегий на стороне узла с запущенным CODESYS Runtime. Сделав свою машину родительским узлом, злоумышленник может внедриться в уже существующий или в будущий поток, реализовав атаку типа «человек посередине».

Схема измененного маршрута сетевого потока после установки произвольного родительского узла

Уязвимость в канальном уровне (Channel layer). Предсказуемость идентификатора канала

Изначально мы предполагали, что значение идентификатора созданного канала связи всегда инкрементировалось на четыре от значения последнего идентификатора канала связи. На это указывал выводимый программой лог:

Фрагмент лога ПО CODESYS Runtime

Для подтверждения этой гипотезы мы исследовали функцию HandleOpenChannelReq, которая выступает в качестве обработчика на канальном уровне для команды открытия каналов (OPEN_CHANNEL). Сама функция принадлежит компоненту CmpChannelServer.

Call trace: 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 

Pseudocode: 
001: Удалено по требованию вендора 
002: {
[...]
064:   Удалено по требованию вендора 
065:   {
[...]
086:   Удалено по требованию вендора 
087:   {
[...]
094:       Удалено по требованию вендора 
095:       Удалено по требованию вендора 
096:         Удалено по требованию вендора 
097:       Удалено по требованию вендора 
[...]
128:       Удалено по требованию вендора 
129:       Удалено по требованию вендора 
130:       Удалено по требованию вендора 
131:       Удалено по требованию вендора 
132:       Удалено по требованию вендора 
133:       Удалено по требованию вендора 
134:       Удалено по требованию вендора 
135:     }
[...]
148:   Удалено по требованию вендора 
149:   Удалено по требованию вендора 
150:   Удалено по требованию вендора 
151:   Удалено по требованию вендора 
152:   Удалено по требованию вендора 
153:   Удалено по требованию вендора 
154:   {
155:     Удалено по требованию вендора 
156:     Удалено по требованию вендора 
157:   }
158: }

Декомпилированный псевдокод функции HandleOpenChannelReq компонента CmpChannelServer

Исследовав код функции HandleOpenChannelReq, мы обнаружили, что каждый новый идентификатор канала действительно зависит от значения предыдущего идентификатора канала связи, однако он инкрементируется не на фиксированное значение, равное четырем, а на количество единовременно поддерживаемых каналов связи (строка 94).

Функцией обработки событий компонента CmpChannelServer устанавливается количество одновременно поддерживаемых каналов в глобальную переменную s_iMaxServerChannels.

001: Удалено по требованию вендора 
002: {
[...]
008: 
009:   Удалено по требованию вендора 
010:   {
011:     Удалено по требованию вендора 
[...]
023:         Удалено по требованию вендора 
024:         Удалено по требованию вендора 
025:         Удалено по требованию вендора 
026:           Удалено по требованию вендора 
027:         Удалено по требованию вендора 
028:         Удалено по требованию вендора 
029:         {
030:           Удалено по требованию вендора 
031:           Удалено по требованию вендора 
032:           {
033:             Удалено по требованию вендора 
034:             Удалено по требованию вендора 
035:           }
036:         }
037:         Удалено по требованию вендора 
038:         Удалено по требованию вендора 
039:         Удалено по требованию вендора 
040:         Удалено по требованию вендора 
041:         Удалено по требованию вендора 
042:         Удалено по требованию вендора 
[...]
124:   Удалено по требованию вендора 
125: }

Дизассемблированный код функции CmpChannelServer_hook

Из строки 24 псевдокода функции CmpChannelServer_hook видно, что значение глобальной переменной s_iMaxServerChannels регулируется параметрами конфигурационного файла. В случае отсутствия секции с именем CmpChannelServer и параметра с именем MaxChannels, в переменной s_iMaxServerChannels устанавливается значение по умолчанию, которое равно четырем.

Злоумышленник может получить значение переменной s_iMaxServerChannels, обратившись с командой GET_INFO к менеджеру канала связи на канальном уровне (channel layer) или же с любой командой для службы имен (name service) на уровне датаграм (datagram layer). Для выполнения этих команд не требуются привилегии.

Call trace: 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 
	Удалено по требованию вендора 

Pseudocode: 
01: Удалено по требованию вендора 
02: {
03:   Удалено по требованию вендора 
04: 
05:   Удалено по требованию вендора 
06:   Удалено по требованию вендора 
07:   Удалено по требованию вендора 
08:   Удалено по требованию вендора 
09:   Удалено по требованию вендора 
10: }

Дизассемблированный псевдокод функции HandleInfoReq

Функция HandleInfoReq обрабатывает команду GET_INFO для менеджера канала связи на канальном уровне. Глобальная переменная s_iMaxServerChannels будет записана в последние два байта ответа (строка 08).

В зависимости от используемых настроек в файле конфигурации CODESYS Runtime может не обрабатывать информационные запросы на уровне датаграм (datagram layer) и на канальном уровне (channel layer). Но злоумышленник всегда может отправить несколько парных запросов на открытие и закрытие канала. Разница между двумя подряд полученными идентификаторами каналов будет однозначно идентифицировать количество одновременно поддерживаемых каналов на узле CODESYS Runtime.

Уязвимости уровня служб (services layer)

Уязвимости в системе аутентификации

В этой главе будут рассмотрены две обнаруженные уязвимости в системе аутентификации: в генерации идентификатора сессии и в шифровании пароля. Эксплуатация первой уязвимости приводит к предугадыванию идентификатора сессии. Эксплуатация второй уязвимости приводит к расшифровке перехваченного пароля.

Уязвимость в предсказуемости генерации идентификатора сессии

Для выполнения большинства служб узлу необходимо пройти аутентификацию. Обработку запроса на прохождение аутентификации выполняет функция DeviceServiceHandler, которая зарегистрирована компонентом CmpDevice в качестве службы с идентификатором 1. Функция DeviceServiceHandler рассматривалась в качестве примера обработки тегов в главе «Обработка тегов».

При обработке функцией DeviceServiceHandler входящего запроса с командой, идентификатор которой равен 2, функция DeviceServiceHandler передает управление в функцию ServerGenerateSessionId (будет рассмотрена далее).

001: Удалено по требованию вендора 
002: {
[...]
129:   Удалено по требованию вендора 
130:   Удалено по требованию вендора 
131:   Удалено по требованию вендора 
132:   Удалено по требованию вендора 
133:   Удалено по требованию вендора 
134:   {
[...]
254:     Удалено по требованию вендора 
[...]
262:      Удалено по требованию вендора 
263:      Удалено по требованию вендора 
[...]
365:         Удалено по требованию вендора 
366:       }
367:       Удалено по требованию вендора 
[...]
389:       Удалено по требованию вендора 
390:       Удалено по требованию вендора 
391:       Удалено по требованию вендора 
392:       Удалено по требованию вендора 
393:       Удалено по требованию вендора 
394:       Удалено по требованию вендора 
395:       Удалено по требованию вендора 
396:       Удалено по требованию вендора 
397:       Удалено по требованию вендора 
398:       Удалено по требованию вендора 
[...]
761:   }
762:   Удалено по требованию вендора 
763: }

Фрагмент декомпилированного псевдокода функции DeviceServiceHandler компонента CmpDevice

В ответе на успешное выполнение запроса на прохождение аутентификации содержится тег данных с идентификатором 0x21 (строка 392). В данных этого тега находится сгенерированный идентификатор созданной сессии (строка 393). Сам же идентификатор сессии создается внутри функции HandleLoginSessionId еще до проверки полученных данных аутентификации (строка 263). Сгенерированный идентификатор возвращается в переменной ulSessionId.

01: Удалено по требованию вендора 
02: {
[...]
09:   Удалено по требованию вендора 
10:   Удалено по требованию вендора 
11:   Удалено по требованию вендора 
12:   {
13:     Удалено по требованию вендора 
14:     Удалено по требованию вендора 
15:   }
16:   Удалено по требованию вендора 
17:   {
18:     *Удалено по требованию вендора 
19:     Удалено по требованию вендора 
20:   }
21:   Удалено по требованию вендора 
22:   {
23:     *Удалено по требованию вендора 
24:     Удалено по требованию вендора 
25:   }
26:   Удалено по требованию вендора 
27:   Удалено по требованию вендора 
28: }

Фрагмент декомпилированного псевдокода функции HandleLoginSessionId

Внутри функции HandleLoginSessionId вызывается функция ServerGenerateSessionId (строка 13), которая генерирует идентификатор сессии. Вызов этой функции происходит при условии, что полученный идентификатор сессии равен одному из чисел: 0x0, 0x11 или 0x815. Дополнительным условием создания идентификатора сессии является то, что для полученного идентификатора канала не должно уже существовать идентификатора сессии (строка 10).

01: Удалено по требованию вендора 
02: {
03:   Удалено по требованию вендора 
04: 
05:   Удалено по требованию вендора 
06:   Удалено по требованию вендора 
07: }
08: 
09: Удалено по требованию вендора 
10: {
11:   Удалено по требованию вендора 
[...]
15:   Удалено по требованию вендора 
16:     Удалено по требованию вендора 
17:   Удалено по требованию вендора 
18:   Удалено по требованию вендора 
19:   *Удалено по требованию вендора 
20:   *Удалено по требованию вендора 
21:   Удалено по требованию вендора 
22: }

Фрагменты декомпилированного псевдокода функции SysTimeGetMs компонента SysTimer и декомпилированного псевдокода функции ServerGenerateSessionId компонента CmpSrv

Функция ServerGenerateSessionId экспортируется компонентом CmpSrv. Её алгоритм описан ниже:

  1. Проверить наличие указателя аргумента (строки 15 и 16);
  2. Получить текущее время через вызов функции SysTimeGetMs (строка 17);
  3. Инициализировать генератор псевдослучайных чисел. В качестве значения seed для генератора выставить полученное на предыдущем шаге значение (строка 18);
  4. Сложить полученное значение времени со случайным числом и записать его в значение по указателю аргумента (строка 19);
  5. Установить старший бит в значение по указателю аргумента (строка 20).

Данный алгоритм генерации использует генератор псевдослучайный чисел. Это значит, что сгенерированное случайно число зависит от установленного значения seed и может быть восстановлено. Злоумышленник может перебирать идентификаторы сессий, декрементируя значения seed и воссоздавая сам идентификатор сессии для измененного seed. Такой подбор может быть осуществлен за адекватный промежуток времени, даже несмотря на то, что атака будет происходит удаленно.

Вторая слабость этого алгоритма заключается в использовании функции SysTimeGetMs. Эта функция экспортируется системным компонентом SysTimer. Ввиду того, что для корректного запуска CODESYS Runtime разработчику необходимо реализовать все системные компоненты, представленная реализация функции SysTimeGetMs может отличаться от реализации этой же функции в другом CODESYS Runtime. C точки зрения безопасности такая реализация функции, которая генерирует сессионный идентификатор, накладывает дополнительную ответственность на разработчика, который будет адаптировать системные компоненты, в том числе компонент SysTimer.


Пример фрагмента пакета, содержащего сгенерированный идентификатор сессии (session_id)

Например, в аналогичном приведенному в главе «Обработка тегов» примере ответа на запрос на аутентификацию, фрагмент ответа содержит тег данных с параметром идентификатора сессии, а для генерации значения идентификатора сессии, равного 0xc5946b05, был использован seed со значением 356267299.

Уязвимости в шифровании пароля

В зарегистрированной компонентом CmpDevice функции DeviceServiceHandler в качестве службы было обнаружено несколько уязвимостей в механизме шифрования пароля. Эксплуатация этих уязвимостей приводит к расшифровке перехваченного пароля.

Обработку запроса на прохождение аутентификации выполняет функция DeviceServiceHandler, которая зарегистрирована компонентом CmpDevice в качестве службы с идентификатором 1. Функция DeviceServiceHandler рассматривалась в качестве примера обработки тегов в главе «Обработка тегов».

Для прохождения аутентификации CODESYS Development System необходимо отправить запрос для службы DeviceServiceHandler с командой, идентификатор которой равен 2. В отправленных данных для прохождения аутентификации будет передан зашифрованный пароль, который во время обработки службой будет расшифрован.

001: Удалено по требованию вендора 
002: {
[...]
130:   Удалено по требованию вендора 
[...]
133:   Удалено по требованию вендора 
134:   {
[...]
254:     Удалено по требованию вендора 
[...]
266:       Удалено по требованию вендора 
267:       Удалено по требованию вендора 
268:       {
269:         Удалено по требованию вендора 
270:         Удалено по требованию вендора 
271:         {
272:           Удалено по требованию вендора 
273:             Удалено по требованию вендора 
274:             Удалено по требованию вендора 
275:           Удалено по требованию вендора 
276:             Удалено по требованию вендора 
277:             Удалено по требованию вендора 
278:             {
279:               Удалено по требованию вендора 
280:               Удалено по требованию вендора 
281:               {
282:                 Удалено по требованию вендора 
283:               }
284:               Удалено по требованию вендора 
285:               {
286:                 Удалено по требованию вендора 
287:                 Удалено по требованию вендора 
288:               }
289:               Удалено по требованию вендора 
290:               {
291:                 Удалено по требованию вендора 
292:               }
293:               Удалено по требованию вендора 
294:               Удалено по требованию вендора 
295:               Удалено по требованию вендора 
296:             }
297:             Удалено по требованию вендора 
298:           Удалено по требованию вендора 
299:             Удалено по требованию вендора 
300:             Удалено по требованию вендора 
301:           Удалено по требованию вендора 
302:             Удалено по требованию вендора 
303:             Удалено по требованию вендора 
304:         }
305:         Удалено по требованию вендора 
306:         Удалено по требованию вендора 
307:         Удалено по требованию вендора 
308:       }
[...]
315:       Удалено по требованию вендора 
316:       {
317:         Удалено по требованию вендора 
318:         Удалено по требованию вендора 
[...]
327:       }
[...]
761:   }
762:   Удалено по требованию вендора 
763: }

Фрагмент декомпилированного кода функции DeviceServiceHandler компонента CmpDevice

Расшифровка полученного пароля осуществляется в функции UserMgrDecryptPassword (строка 318). В качестве аргументов эта функция использует следующие значения:

  1. encrypted_password — значение зашифрованного пароля, который извлекается из тега данных с идентификатором 17 (строка 286);
  2. pulCrypeType — идентификатор алгоритма шифрования, которым был зашифрован переданный пароль. Значение идентификатора извлекается из тега данных с идентификатором 0x22 (строка 299);
  3. pulChallenge — значение случайного числа (nonce), которое участвует в шифровании пароля. Значение случайного числа извлекается из тега данных с идентификатором 0x23.
01: Удалено по требованию вендора 
02: {
[...]
16:   Удалено по требованию вендора 
17:     Удалено по требованию вендора 
18:   Удалено по требованию вендора 
19:   Удалено по требованию вендора 
20:   Удалено по требованию вендора 
21:   Удалено по требованию вендора 
22:   {
23:     Удалено по требованию вендора 
24:     {
25:       Удалено по требованию вендора 
26:       Удалено по требованию вендора 
27:       Удалено по требованию вендора 
28:       Удалено по требованию вендора 
29:       Удалено по требованию вендора 
30:       Удалено по требованию вендора 
31:       {
32:         Удалено по требованию вендора 
33:         Удалено по требованию вендора 
34:         Удалено по требованию вендора 
35:           Удалено по требованию вендора 
36:         Удалено по требованию вендора 
37:           Удалено по требованию вендора 
38:       }
39:       Удалено по требованию вендора 
40:     }
[...]

Фрагмент декомпилированного псевдокода функции UserMgrDecryptPassword

Функция UserMgrDecryptPassword выполняет следующие действия:

  1. Сравнивает значения аргумента pulCrypeType с 1 (строка 16). Если эти значения разные, то функция дальше не отрабатывается. Иначе говоря, CODESYS Runtime предоставляет только один алгоритм шифрования для передаваемого пароля, а значение pulCrypeType необходимо передавать каждый раз во время аутентификации, несмотря на отсутствие альтернатив в выборе алгоритма шифрования пароля.
  2. Записывает фиксированный ключ в локальную переменную key (строка 18).
  3. Из 4-байтового значения аргумента pulChallenge записывает только младший байт в 4-байтовый массив aChallenge (строка 25). Для остальных трех байт массива устанавливаются нулевые значения (строки 26-28).
  4. Далее функция использует три индекса: index — индекс пароля, index_key — индекс ключа и challenge_index — индекс случайного числа. Индекс пароля определяет окончание расшифровки зашифрованного пароля. Когда индекс пароля будет равен размеру зашифрованного пароля, функция UserMgrDecryptPassword завершится и вернет управление функции DeviceServiceHandler (строка 32). Остальные два индекса (индекс случайного числа и индекс ключа) будут обнуляться всякий раз, когда размер их переменных (aChallenge и key) будет равен значению индекса (строка 34 для переменной key и строка 36 для переменной aChallenge).
  5. Далее посимвольно расшифровывается пароль. Каждый расшифрованный символ получается из сложения одного байта, взятого из массива ulChallenge по индексу случайного числа (challenge_index), и одного байта, взятого из переменной key по индексу ключа (index_key). Для полученного числа выполняется операция XOR с байтом, взятым из аргумента пароля encrypted_key по индексу зашифрованного пароля (строка 32).

При успешном выполнении функции UserMgrDecryptPassword будет расшифрован полученный пароль, который будет записан в аргумент decrypted_password.

Из описанного алгоритма функции можно выделить сразу три уязвимости:

  1. Использование слабого значения случайного числа. Несмотря на то, что зарегистрированная компонентом CmpDevice функция DeviceServiceHandler для команды прохождения аутентификации извлекает из полученных тегов данных
    4-байтовое числовое значение, для расшифровки полученного пароля используется только один байт из четырех. Ввиду того, что алгоритм шифрования симметричен,
    в шифровании пароля участвует также один байт.

Использование случайного однобайтового значения для защиты пароля не повышает защищенность передаваемого пароля, т.к. это значение может быть подобрано за очень короткое время.

  1. Использование произвольного значения в качестве случайного числа. Из рассмотренного алгоритма функции DeviceServiceHandler видно, что команда аутентификации службы компонента CmpDevice в качестве случайного числа (aChallenge) для расшифровки пароля использует полученный числовой параметр от проходящего аутентификацию узла.

Другими словами, несмотря на то что при получении запроса на прохождение аутентификации сторона, на которой проходят аутентификацию, отправляет случайное число, которое должно быть использовано в шифровании пароля, другая сторона, которая проходит аутентификацию, может зашифровать пароль со своим числом и передать это число для расшифровки.

Это означает, что злоумышленник, единожды перехватив данные аутентификации, может повторно отправлять их без изменений для прохождения аутентификации.

  1. Использование фиксированного ключа. Рассмотренный алгоритм расшифровки использует фиксированный ключ для шифрования и расшифровки пароля. Это означает, что злоумышленник, получивший зашифрованные данные аутентификации, всегда может расшифровать перехваченный пароль.

На примере рассмотренного в главе «Обработка тегов» пакета с запросом на прохождение аутентификацию мы покажем, как можно частично расшифровать передаваемый пароль.


Пакет с запросом на прохождение аутентификации

Зашифрованный пароль передается в теге данных с идентификатором 0x11, который обозначен как Data_tag_4. Если извлечь данные зашифрованного пароля из тега данных и для каждого байта данных выполнить операцию XOR с байтами ключа по таким же индексам, то можно частично восстановить пароль.

1: encrypted_password = "\xce\x01\x29\x3b\x20\x5f\x36\x12\x18\x42\x46\x58\xf9\x75\x70\x68\x4c\x54\x68\x75\x77\x3f\x70\x68\x76\x44\x72\x2a\x87\x55\x62\x52"
2: KEY = "zeDR96EfU#27vuph7Thub?phaDr*rUbR"
3: for c, s in enumerate(encrypted_password):
4:     print chr(ord(KEY[c]) ^ ord(encrypted_password[c])),
5:     
6: � d m i  i s t M a t o �

Скрипт на языке программирования Python для частичной расшифровки пароля

Частично восстановленный пароль содержит символы d, m, I, I, s, t, a, t, o (строка 6). Эти символы входят в строку “Administrator”, которая является паролем по умолчанию для пользователя Administrator.

Уязвимость кода приложения

Одной из задач, которую выполняет CODESYS Runtime, является загрузка, управление и исполнения приложений. CODESYS Development System компилирует приложение для CODESYS Runtime и загружает его по протоколу CODESYS PDU. Загружаемое приложение представляет собой поток бинарных данных.

Во время нашего исследования мы не фокусировались на исследовании структуры скомпилированного приложения. (Отметим, что при поддержке Организации по управлению военно-морских исследований США (U.S. Office of Naval Research) была проведена исследовательская работа по анализу содержимого скомпилированного приложения для CODESYS Runtime. Результатом этой работы стала разработка фреймворка ICSREF.)

Мы же, изучив содержимое отправляемых пакетов от CODESYS Development System на CODESYS Runtime во время загрузки приложения, обнаружили места в бинарном потоке, куда можно внедрить произвольный машинный код — shellcode.

Лазейками стали две функции: функция инициализации глобальных переменных (в упомянутой работе названа Global INIT) и стартовая функция программы (в упомянутой работе названа PLC_PRG).

Header:
1: PROGRAM PLC_PRG
2: VAR
3: 	  magic: DWORD:= 16#DEADBEEF;
4: END_VAR

Body:
5: magic := magic + 16#BEEF;

Исходный код программы PLC_PRG

Чтобы подтвердить возможность внедрения произвольного машинного кода с целью его исполнения в бинарный поток приложения, мы, используя CODESYS Development System, скомпилировали программу и удаленно загрузили ее на устройство Raspberry PI с запущенным CODESYS Runtime. В блоке объявления переменных была объявлена переменная magic со значением 0xDEADBEEF (строка 3). В блоке тела программы указано, что значение переменной magic необходимо постоянно складывать со значением 0xBEEF и записывать получившийся результат в переменную magic (строка 5).

Фрагмент трафика при загрузке приложения с упоминаниями использования байт EF BE

После загрузки скомпилированной программы на устройство Raspberry Pi в трафике был произведен поиск байт EF BE. Эти байты содержатся в числах 0xDEADBEEF и 0xBEEF при порядке байт в числах от младшего к старшему (little-endian). Всего было обнаружено два упоминания этих байт в трафике (выделено красным). Необходимо сказать, что в одном пакете рядом с искомыми байтами EF BE были обнаружены байты AD DE (выделено синим).

Далее весь загружаемый поток бинарных данных был проанализирован на наличие машинных инструкций процессора ARM, на котором работает Raspberry Pi. По поиску тех же байт EF BE была обнаружена инструкция с обращением к числу 0xDEADBEEF и инструкция с обращением к числу 0xBEEF.

Рассмотрим инструкции первого обращения.

01: 00 00 00 60       ANDVS           R0, R0, R0
02: A0 01 D8 00       SBCEQS          R0, R8, R0,LSR#3
03: 21 06 03 00       ANDEQ           R0, R3, R1,LSR#12
04: 50 8A 01 00       ANDEQ           R8, R1, R0,ASR R10
05: 22 CC 80 00       ADDEQ           R12, R0, R2,LSR#24
06: 48 00 00 00       ANDEQ           R0, R0, R8,ASR#32
07: 00 44 2D E9       STMFD           SP!, {R10,LR}
08: 0D A0 A0 E1       MOV             R10, SP
09: 08 D0 4D E2       SUB             SP, SP, #8
10: 10 08 2D E9       STMFD           SP!, {R4,R11}
11: 00 40 A0 E3       MOV             R4, #0
12: 09 40 CA E5       STRB            R4, [R10,#9]
13: 00 40 A0 E3       MOV             R4, #0
14: 08 40 0A E5       STR             R4, [R10,#-8]
15: 00 40 A0 E3       MOV             R4, #0
16: 04 40 4A E5       STRB            R4, [R10,#-4]
17: 14 40 9F E5       LDR             R4, =0xDEADBEEF
18: 0C B0 9F E5       LDR             R11, =0x3870
19: 00 40 8B E5       STR             R4, [R11]
20: 10 08 BD E8       LDMFD           SP!, {R4,R11}
21: 08 D0 8D E2       ADD             SP, SP, #8
22: 00 84 BD E8       LDMFD           SP!, {R10,PC}

Обнаруженные ассемблерные инструкции для процессора ARM с обращением к числу 0xDEADBEEF

На строке 17 в регистр R4 записывается константа 0xDEADBEEF. На строке 18 в регистр R11 записывается константа 0x3870. На строке 19 значение регистра R4 (0xDEADBEEF) записывается по адресу значения регистра R11 (0x3870).

01: 00 00 00 60       ANDVS           R0, R0, R0
02: A0 01 C0 00       SBCEQ           R0, R0, R0,LSR#3
03: 21 06 03 00       ANDEQ           R0, R3, R1,LSR#12
04: 28 15 01 00       ANDEQ           R1, R1, R8,LSR#10
05: 22 B4 80 00       ADDEQ           R11, R0, R2,LSR#8
06: 30 00 00 00       ANDEQ           R0, R0, R0,LSR R0
07: 00 44 2D E9       STMFD           SP!, {R10,LR}
08: 0D A0 A0 E1       MOV             R10, SP
09: 30 00 2D E9       STMFD           SP!, {R4,R5}
10: 18 B0 9F E5       LDR             R11, =0x3870
11: 00 40 9B E5       LDR             R4, [R11]
12: 0C 50 9F E5       LDR             R5, =0xBEEF
13: 05 40 84 E0       ADD             R4, R4, R5
14: 00 40 8B E5       STR             R4, [R11]
15: 30 00 BD E8       LDMFD           SP!, {R4,R5}
16: 00 84 BD E8       LDMFD           SP!, {R10,PC}

Обнаруженные ассемблерные инструкции для процессоар ARM с обращением к числу 0xBEEF

В отличие от машинного кода первого обращения, машинный код второго обращения действует от обратно. Сначала происходит запись константы 0x3870 в регистр R11 (строка 10). Далее в регистр R4 записывается содержимое ячейки памяти по адресу константы 0x3870 (строка 11). После происходит запись константы 0xBEEF в регистр
R5 (строка 12). Значение регистра R5 (0xBEEF) суммируется со значением по адресу 0x3870 (строка 13). Результат суммы сохраненного значения из памяти и константы 0xBEEF записывается в регистр R4, значение которого потом записывается в ячейку памяти по адресу 0x3870.

Исходя из рассмотренных обращений можно предположить, что по адресу 0x3870 находится адрес объявленной глобальной переменной magic. Значение 0xDEADBEEF записывается в переменную magic в рассмотренном машинном коде первого найденного обращения. Во втором найденном обращении в переменную magic прибавляется число 0xBEEF. Результат сложения снова записывается по адресу глобальной переменной magic (0x3870). Оба фрагмента машинного кода соответствуют исходному коду программы PLC_PRG.

Фрагмент трафика при загрузке приложения с ассемблерными инструкциями

Машинные инструкции процессора ARM также передаются по протоколу CODESYS PDU. В примере второго обращения инструкции ADD R4, R4, R5 (строка 13) соответствуют байты 05 40 84 E0 (выделены в пакете зеленым), следующей инструкции STR R4, [R11] (строка 14) соответствуют байты 00 40 8B E5 (выделены оранжевым).

Таким образом, злоумышленник может вставить свой машинный код и выполнить его на целевом устройстве. Необходимо сказать, что системные демоны CODESYS Runtime на устройстве Raspberry Pi запускаются в качестве фонового процесса (daemon) от имени пользователя root, который имеет наивысший уровень привилегий в ОС Linux. Поэтому исполняемый произвольный машинный код будет также работать с наивысшими правами в системе. Эмулятор CODESYS Runtime на ОС Windows также запускается с наивысшими правами в системе — SYSTEM.

Итоги

CODESYS Runtime представляет собой сложный и мощный инструмент для разработки программы для ПЛК и управления ПЛК. При этом в CODESYS Runtime реализован эффективный архитектурный подход, который позволяет расширять возможности самого CODESYS Runtime. Протокол общения между средой разработки CODESYS Development System и средой исполнения CODESYS Runtime является многоуровневым, динамичным, а главное — закрытым.

В результате использования закрытых протоколов в конечном счете страдают производители ПЛК, потому что они ограничены в знаниях об используемом ими программного обеспечения. Из-за этого они вынуждены вслепую доверить начальную защиту ПЛК разработчикам фреймворка.

Компания CODESYS сделала множество шагов для повышения безопасности своих продуктов. Однако, как показало проведенное нами исследование безопасности CODESYS Runtime, этих шагов оказалось недостаточно.

В результате исследования безопасности CODESYS Runtime мы обнаружили 15 уязвимостей, о которых сообщили производителю ПО. О 5 из обнаруженных уязвимостей вендору уже было известно (являлись дубликатами), 2 были признаны группой безопасности CODESYS архитектурными особенностями, а остальные 8 были исправлены. Исправленные уязвимости были оценены по шкале CVSS от 5.4 балла до 9.

Примечательно, что одну уязвимость, ранее отмеченную как «архитектурная особенность», компания CODESYS позже все же исправила.

Наше исследование проходило методом «черного ящика», то есть изначально у нас не было никакой информации о CODESYS Runtime, мы получали её из открытых источников информации и в ходе технического исследования.

После изучения передаваемых данных по протоколу CODESYS и сопоставления программного кода CODESYS Runtime с обработкой получаемых данных по сети, нами было обнаружено четыре уязвимости в механизме аутентификации — в уровне служб
(в стеке протокола CODESYS это последний уровень из четырех). Этим уязвимостям были присвоены идентификаторы KLCERT-18-037 (CVE-2018-20025) и KLCERT-19-031 (CVE-2019-9013). Их эксплуатация в системе аутентификации приводит к расшифровке передаваемого пароля, позволяет реализовать атаку повторного использования зашифрованных данных аутентификации без их изменения и предугадать идентификатор сессии.

Разработчики CODESYS взяли за основу своего протокола стек протоколов TCP/IP.

В результате CODESYS унаследовал некоторые проблемы TCP/IP: на уровне датаграм (второй из четырех уровней) и на канальном уровне (третий из четырех уровней) нами были обнаружены уязвимости, про возможность которых в стеке протоколов TCP/IP было известно еще в 1989 году (см. Security Problems in the TCP/IP Protocol Suite).

На уровне датаграм в стеке протоколов CODESYS мы обнаружили возможность проведения атаки, идентичной IP-spoofing. Этой уязвимости был присвоен идентификатор KLCERT-18-036 (CVE-2018-20026). Автоматизировав эксплуатацию этой уязвимости, злоумышленник мог бы долго скрывать свои действия в сети, манипулируя устройствами с запущенным CODESYS Runtime и заставляя их отправлять вредоносные пакеты друг другу.

Мы также обнаружили возможность проведения атаки на уровень датаграм, которая похожа на хорошо известную атаку ARP spoofing: механизм маршрутизации сети CODESYS позволяет выстроить информационную сеть с топологией дерево из узлов CODESYS Runtime. Отсутствие необходимости прохождения аутентификации для изменения родительского узла приводит к возможности проведения атаки типа «человек посередине». Так, злоумышленник, используя мощности протокола на уровне датаграм, может сообщить узлу CODESYS Runtime, что стал его новым родителем, и в дальнейшем этот узел будет пересылать весь исходящий трафик через нового родителя.

Последний зверь в зоопарке классических уязвимостей — отсутствие песочницы для загружаемой программы. В ходе исследования протокола обнаружилось, что некоторые фрагменты загружаемой программы представляют собой машинные инструкции. Гипотеза о том, что вместо этих инструкций можно внедрить произвольный код (шеллкод) подтвердилась. Этой уязвимости был присвоен идентификатор KLCERT-18-035 (CVE-2018-10612).

Из-за того, что фоновый процесс (daemon) CODESYS Runtime в ОС Linux и сервис CODESYS Runtime в ОС Windows работают с наивысшими привилегиями в системе (root и SYSTEM соответственно), произвольный код будет также выполняться с наивысшими привилегиями. Таким образом, злоумышленнику уже не надо будет производить дополнительные манипуляции с системой или эксплуатировать уязвимости для получения максимальных привилегий.

Как говорит многолетний опыт специалистов по информационной безопасности, подход «security by obscurity» — не лучшая стратегия защиты информации. Это в полной мере касается недокументированных, закрытых протоколов сетевой коммуникации. Рано или поздно протокол будет исследован, а уязвимости в нём найдены. К сожалению, во многих случаях злоумышленники сделают это раньше добропорядочных исследователей. Как минимум, потому что обычно они гораздо лучше мотивированы.

Мы считаем, что все описанные в статье уязвимости, и, возможно, какие-то ещё, могли бы быть обнаружены сообществом специалистов и энтузиастов информационной безопасности ещё на ранних этапах проектирования протокола, его разработки и использования. Если бы спецификация протокола существовала в открытом для потенциальных пользователей доступе, то уязвимости могли бы быть обнаружены при ее обсуждении и анализе и не затронули бы продукты сотен разработчиков, установленные на десятках и сотнях тысяч промышленных объектов.

Сейчас эта работа требует существенно больших затрат и гораздо более специфических знаний, которые, к сожалению, могут оказаться труднодоступными для многих из разработчиков, использующих CODESYS в своих решениях. Возможно, выбранный подход к разработке протокола оказался не самым оптимальным в этом смысле.

Группа безопасности CODESYS Group оперативно и ответственно отреагировала на информацию об обнаруженных уязвимостях.

Мы искренне благодарим CODESYS Group за сотрудничество.

pi@raspberrypi:~ $ ./opt/codesys/bin/codesyscontrol.bin -vvvvvvv
CODESYS Control V3.5.12.0 for ARM - build Dec 18 2017
type:4102 id:0x00000010 name:CODESYS Control for Raspberry Pi SL vendor: 3S - Smart Software Solutions GmbH              
buildinformation: <none>
 _________
< ... bye >
 ---------
        \   ^__^
         \  (--)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Назад ко второй части

Авторы