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

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

Скачать PDF версию

 

Исследование стека протоколов CODESYS PDU

Эта глава посвящена исследованию стека протоколов CODESYS PDU (Packet Data Unit). Этот стек протоколов используется для коммуникации между узлами сети CODESYS,
в том числе CODESYS Development System и CODESYS Runtime.

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

Примечание: ввиду того, что исследование стека протоколов CODESYS PDU проходило методом «черного ящика», большинство названий полей и уровней основаны на их назначении. Из-за этого используемые названия в дальнейшем описании могут расходиться с теми, что используются в публичных документациях или были даны другими исследователями.

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

  • Для первого уровня: “datagram layer”, “Layer 2” или “block driver” (далее используется уровень Block Driver (Block Driver Layer))
  • Для второго уровня: “network layer”, “Layer 3” или “router” (далее используется уровень датаграм (datagram layer))
  • Для третьего уровня: “protocol layer”, “Layer 4” или “channel management” (далее используется канальный уровень (channel layer))
  • Для четвертого уровня: “application layer”, “layer 7” или “application services” (далее используется уровень служб (services layer))

Базовое описание протокола

CODESYS PDU (Packet Data Unit) — это стек протоколов, состоящий из четырех различных уровней:

  • уровень Block Driver (Block Driver Layer);
  • уровень датаграм (datagram layer);
  • канальный уровень (channel layer);
  • уровень служб (services layer);

Порядок байт в этом стеке протоколов — little endian, но при необходимости может быть изменен на big endian. Принцип работы — синхронный или асинхронный в зависимости от уровня протокола.

Использование протокола CODESYS PDU не ограничивается сетевой коммуникацией. Он используется также для коммуникации по USB, Can–шине и последовательным портам. При этом, среда исполнения CODESYS Runtime всегда используют возможности ОС, под которую она была адаптирована. Таким образом, конечный информационный пакет будет содержать сформированные данные CODESYS Runtime
и данные, сформированными драйверами ОС под определенный физический интерфейс.

Так, например, конечный CODESYS PDU пакет, отправленный через сетевой интерфейс по TCP, будет содержать сразу два стека протокола — TCP и CODESYS PDU.

Пример использования стеков протоколов TCP и CODESYS PDU в одном пакете

Способность общения по физическим интерфейсам реализуется компонентами группы Communication — Block Drivers. Помимо этого, любой разработчик может разработать свой Block Driver и использовать протокол CODESYS PDU в нем.

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

Ниже — схематичное представление того, как компоненты участвуют в разборе входящего пакета, сформированного по протоколу CODESYS PDU.

Схематичное представление разбора компонентами пакета CODESYS PDU

Совокупная работа этих компонентов определяет мощность стека протокола CODESYS PDU.

Далее мы рассмотрим каждый из уровней в стеке протокола CODESYS PDU.

Разбор стека протоколов

Уровень Block driver (Block driver layer)

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

Основная задача компонентов из группы Block Drivers — создание возможности коммуникации через физический или программный интерфейс. Любой компонент Block Drivers является «входной точкой» для получения информационного пакета и точкой его отправки. Поэтому эти компоненты перед отправкой пакета могут добавить дополнительные поля в протоколе.

Схематичное представление разбора компонентами пакета CODESYS PDU на уровне Block Driver (Block Driver Layer)

Так работает, например, компонент Block Driver CmpBlkDrvTcp. Этот компонент реализует коммуникацию по TCP-протоколу. В каждом сообщении CmpBlkDrvTcp добавляет два поля, каждый из которых является 4-байтовым числом:


Пример использования двух дополнительных полей на уровне Block Driver

  • magic — магическое число. Константное число 0xe8170100 вставляется и проверяется компонентом CmpBlkDrvTcp каждый раз, когда компонент получает сетевой пакет.
  • length — суммарное количество байт в пакете, включая размеры полей magic и length (оба поля размером по 4 байта).

Ниже представлена трассировка вызова функции Receive(), которая принадлежит компоненту CmpBlkDrvTcp. Функция Receive() обрабатывает поля magic и length.

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

Получение всех данных из сети для дальнейшей обработки компонентом CmpBlkDrvTcp происходит в два шага:

  1. На первом шаге компонент достает первые 8 байт (строка 068) из полученных по сети данных через функцию SysSockRecv, которая была экспортирована системным компонентом SysSocket. Максимальное количество байт, которые можно получить, передается в третьем аргументе функции SysSockRecv. Далее первые 4 байта сравниваются с магической константой (строка 099). Вторые 4 байта сравниваются с числом 520. Число 520 было получено путем сложения максимально возможного размера пакета, сформированного по протоколу CODESYS PDU (512 байт), и суммарного размера полей magic и length (8 байт).
  2. На втором шаге извлекаются оставшиеся данные. Ожидается, что размер данных будет равен разнице значения поля length и суммарного размера полей magic и length (строка 144).

Далее компонент CmpBlkDrvTcp передает управление функции RouterHandleData (строка 196), которая зарегистрирована компонентом CmpRouter, — на уровень датаграм (datagram layer).

Стоит учесть, что в случае коммуникации по UDP протоколу поля magic и length будут отсутствовать.


Пример отсутствия дополнительных полей на уровне Block Driver

Функция UdpReceiveBlock() компонента CmpBlkDrvUdp аналогична функции Receive() компонента CmpBlkDrvTcp.

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

Функция UdpReceiveBlock() не выполняет каких-либо проверок полученных данных. Помимо этого, у компонента CmpBlkDrvUdp есть другая особенность. Она заключается в том, что функция UdpReceiveBlock() слушает широковещательные сообщения (строка 047). Если данных не оказалось, то компонент пытается считать отправленные конкретно ему данные (строка 64). Если в одном из случаев данные были, то компонент CmpBlDrvUdp вызывает функцию RouterHandleData для дальнейшей обработки (строка 111).

Уровень датаграм (Datagram layer)

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

Cхематичное представление разбора компонентами пакета CODESYS PDU на уровне датаграм (Datagram layer)

Компоненты группы Block Drivers обязаны вызвать функцию RouterHandleData, которая действует на уровне датаграм. В аргументах вызова функции компоненты передают полученные данные.


Используемые поля на уровне датаграм (datagram layer)

С точки зрения трафика эта функция обрабатывает следующие поля и данные:

  • magic — магическое число пакета, сформированного по протоколу CODESYS PDU. Размер этого поля — один байт, он вставляется компонентом CmpRouter.
  • hop_info — битовая структура, которая состоит из двух полей: 5-битового поля hop_count и 3-битового поля header_length:
    1. Поле hop_count отвечает за возможное количество передач полученного по сети пакета. Каждый раз, когда один узел сети CODESYS получает пакет
      и перенаправляет его другому узлу сети CODESYS, он декрементирует значение поля hop_count. Если узел получил пакет и не является его конечным получателем, при этом значение поля hop_count равно 0, то узел сбросит этот пакет. По сути, это поле защищает сеть, в которой есть узлы CODESYS, от бесконечной пересылки пакета;
    2. Поле header_length указывает на количество байт до следующего поля с размером данных (lengths). Ожидается, что при сложении значения поля header_length с его позицией в пакете получится позиция на поле lengths.
  • packet_info — параметры пакета. Это поле также является битовой структурой:
  1. Первые два бита — это поле priority. Оно обозначает приоритетность обрабатываемого пакета. Для обозначения приоритетности используются следующие числовые значения: 0 — низкая (low), 1 — обычная (normal),
    2 — высокая (high), 3 — срочная (emergency);
  2. Следующий бит signal используется компонентом CmpRouter в качестве возвращаемого статуса обработки пакета, в котором можно указать на ошибки;
  3. Поле type_address указывает на тип передаваемого адреса. Это поле нужно для понимания компонентом CmpRouter содержимого полей sender
    и receiver. Есть два значения для поля type_address: 0 — полный адрес,
    1 — относительный адрес;
  4. Последнее поле length_data_block указывает на максимальный размер данных, которые может принять получатель.
  • service_id — идентификатор службы. Указывает на то, какая именно служба должна обработать полученные данные. CODESYS Runtime содержит и определяет следующие службы:
  1. Служба с идентификатором 1 для запроса и 2 для ответа — служба адресов (address service). Эта служба используется для обнаружения «живых» узлов в сети и для построения самой информационной сети из узлов. Узел в этой сети представляет собой участника с запущенным CODESYS Runtime или CODESYS Development System.
  2. Служба с идентификатором 3 для запроса и 4 для ответа — служба имен (name services). Эта служба используется для получения информации об узле.
  3. Служба с идентификатором 64 (0x40) как для запроса, так и для ответа — служба канала (channel services). Эта служба используется для обращения
    к серверу и к менеджеру канала связи.
  • message_id — идентификатор сообщения. Это значение указывается отправителем и используется для идентификации сообщения. Обычно в качестве идентификатора сообщения CmpRouter передает 4-битное значение текущего времени. Таким образом достигается уникальность идентификатора сообщения.
  • lengths — размеры полей receiver и sender. Поле lengths — это битовая структура, в которой старшие 4 бита содержат значение, соответствующее половине количества байт в поле receiver, а младшие 4 бита — половине количества байт в поле sender. То есть, количество байт в поле receiver и в поле sender будет в два раза больше указанных в поле lengths. Например, для рассматриваемого пакета значение старших 4 битов поля lengths (0x53) равно 5. Это значит, что итоговое количество байт для поля receiver будет 10.
  • sender — адрес, которому предназначено сообщение.
  • receiver — адрес, которому необходимо отправить ответ на сообщение.
  • Поле padding добавляется в конец пакета. Не является обязательным.

Поля sender и receiver имеют свой формат данных, который зависит от используемого компонента Block Driver. Например, CmpBlkDrvTcp ожидает в этих полях полный сетевой адрес узла и номер сетевого порта. То есть, байты поля receiver (2ddcc0a80058) на самом деле содержат порт 11740 (2ddc) и адрес получателя 192.168.0.88 (c0a80058).

CmpBlkDrvUdp использует другой формат. Вместо полного адреса он использует относительный адрес, а вместо двух байт для значения порта — один. Этот байт порта указывает на индекс порта. CmpBlkDrvUdp определяет четыре индекса портов: 0, 1, 2, 3. Каждый из индексов соответствуют одному UDP порту: 0 — 1740, 1 — 1741, 2 — 1742,
3 — 1743. Относительный сетевой адрес — это последний байт в числовом формате физического адреса.


Пример содержимого полей Sender и Receiver при использовании компонента CmpBlkDrvUdp

Таким образом, значение байт поля sender (0058) будут содержать порт 1740 (значение поля port_index равно 0x0) и адрес 192.168.0.88 (0x58 — последний байт адреса, первые три байта извлекаются из адреса интерфейса). Получатель будет ожидать ответ на порт 1743 (значение поля port_index равно 0x3) и на адрес 192.168.0.33 (0x21).

Назначение и формат всех оставшихся данных в пакете зависит от службы (service_id), для которой этот пакет предназначен. Если этот идентификатор равен 1, 2, 3 или 4, то обработчиком остается компонент CmpRouter или его вспомогательные компоненты CmpNameServiceClient и CmpNameServiceServer. Если идентификатор равен 64 (0x40), то все оставшиеся данные передаются компоненту CmpChannelManager.

Основываясь на поле sender, компонент CmpRouter определяет, нужно ли переслать пакет другому узлу или же пакет предназначен ему. В первом случае компонент CmpRouter сначала декрементирует значение hop_count в пакете, затем отправляет его как есть на узел, указанный в поле sender. Во втором случае компонент CmpRouter обрабатывает пакет и возвращает результат на адрес, указанный в поле receiver. Обработкой пакета, который был предназначен узлу, занимается функция HandleLocally.

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

Функция HandleLocally по значению поля service_id определяет, какой именно обработчик необходимо вызвать:

  • Для значения, равного 1 или 2, — функция AddrSrvcHandlePackage (строка 103). Это обработчик службы адресов (address service).
  • Для значения, равного 3, – функция NSServerHandleData (строка 51). Это обработчик службы имен (Name Service), который обрабатывает входящие запросы, то есть работает в качестве сервера.
  • Для значения, равного 4, — функция NSClientHandleData (строка 69). Это обработчик службы имен (Name Service), который обрабатывает результаты выполненных запросов, то есть работает в качестве клиента.
  • Для значения, равного 0x40, — функция ChannelMgrHandleData (строка 86). Это обработчик службы канала (Channel Service).

Если для полученного service_id не было найдено подходящего обработчика, то происходит его поиск среди дополнительных обработчиков, зарегистрированных функцией RouterRegisterProtocolHandler. При наличии такового, выполняется его вызов (строки 125:127).

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

Функция RouterRegisterProtocolHandler в качестве аргументов (строка 1) принимает идентификатор сервиса (service_id) и обработчик (handler). Эта функция не позволяет зарегистрировать обработчик (строка 16) для следующих идентификаторов: 1, 2, 3, 4
и 64 (0x40). В случае если обработчик для данного идентификатора не установлен (строка 18), то обработчик будет добавлен в глобальный словарь обработчиков (s_protocolHandlers), где ключ для обработчика — первый аргумент service_id
(строка 24).

Далее будут рассмотрены обработчики системных служб CODESYS Runtime, а именно обработчики службы адресов (address service) и обработчики службы имен (name service)

Обработчик службы каналов (channel service) рассмотрен в главе «Канальный уровень».

Служба адресов (address service)

Обработчик службы адресов определяет по идентификатору service_id две возможные команды: команду «запрос» с идентификатором service_id 1 и команду «ответ»
с идентификатором service_id 2. Обработчиком является функция AddrSrvcHandlePackage.

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

Команда «запрос» с идентификатором 1 (строка 23) отправляется узлом сети CODESYS для оповещения остальных узлов о своем существовании. Этот запрос можно постоянно наблюдать в трафике по широковещательному адресу: он отправляется
с одинаковой периодичностью всеми узлами сети CODESYS на широковещательный адрес.

Широковещательные оповещения узлов сети CODESYS

Если среди узлов сети CODESYS есть родительский узел, то он узнает о существовании узла, который отправил запрос. Этот же запрос может отправить родительский узел. Если такой запрос получат дочерние узлы, то они оповестят его о своем существовании. Дополнительных полей для этого запроса не используется.

Команда «ответ» с идентификатором 2 (строка 28) обычно отправляется родительским узлом. Эта команда используется для построения информационной сети CODESYS. Она использует следующие поля:

  • version_major — поле, которое используется для обозначения версии информационной сети CODESYS, которая будет сформирована. Для протокола CODESYS PDU значение version_major всегда равно 1 (стока 75 декомпилированного псевдокода функции AddrSrvcHandlePackage).
  • version_minor — поле, указывающее на используемую версию команды. Она определяет дополнительные поля в команде и в ответе. Например, при её положительном значении в пакете будет использоваться поле parent_subnet_params.
  • address_len — указывает на количество байт поля parent_address, которое нужно обработать. Итоговое количество байт умножается на два.
  • address — адрес родительского узла.
  • subnet_id — идентификатор сформированной подсети.
  • subnet_params и parent_subnet_params — параметры текущей подсети и подсети родительского узла.

Узел сети CODESYS, получив такое сообщение и при отсутствии заранее указанного родительского узла, установит в качестве родительского узла источник этого сообщения.

Служба имен (name service)

Обработку сообщений, которые предназначены для службы имен (name services), выполняют вспомогательные компоненты CmpNameServiceClient и CmpNameServiceServer. Сообщения службы имен (name services) также условно разделяются на команду «запрос» и команду «ответ». Входящие «запросы» от других узлов обрабатывается компонентом CmpNameServiceServer. Запросы, которые были отправлены узлом CODESYS Runtime, обрабатываются как «ответы» компонентом CmpNameServiceClient.

Общий заголовок сообщений службы имен (name_service_header) использует следующие поля:

  • subcmd — идентификатор команды, которую необходимо выполнить службе имен.
  • version — номер версии команды. Это поле определяет наличие дополнительных полей в поле message_data, характерных для указанной версии команды.
  • message_id — идентификатор сообщения. Это значение возвращается в ответе. Для создания значения этого поля используется числовой идентификатор текущего времени.
  • message_data — поля команды, формат которых определяет сама команда (subcmd).

Компонент CmpNameServiceServer экспортирует функцию NSServerHandleData. Функция NSServerHandleData выступает в качестве обработчика запросов к службе имен (name service) от других узлов.

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

Обработчик NsServerHandleData определяет два возможных идентификатора поля subcmd и для каждого вызывает соответствующий вспомогательный обработчик:

  • Для идентификатора 0xc202 (строка 20) — обработчик HandleResolveAddrReq (строка 18). Это запрос для получения информации об узле. Ответ на этот запрос использует значение идентификатора service_id 4 и будет обработан функцией NsClientHandleData.
  • Для идентификатора 0xc201 (строка 14) — обработчик HandleResolveNameReq (строка 18). Этот запрос аналогичен запросу с идентификатором 0xc202, с той лишь разницей, что в теле запроса передается имя узла. Если переданное имя узла не совпадает с именем узла, который получил запрос, то узел игнорирует запрос. Ответ на этот запрос использует значение идентификатора service_id 4 и будет обработан функцией NsClientHandleData.

Компонент CmpNameServiceClient экспортирует функцию NSClientHandleData которая выступает в качестве обработчика полученных от узлов ответов на запросы службы имен (name services). Ответ на запрос использует общий заголовок сообщения (name_service_header). В зависимости от указанного значения в поле version, ответ может различаться. Для поля version, равного 0x103, ответ будет содержать следующие поля:

  • max_channels — значение поля указывает на количество одновременно поддерживаемых каналов связи. Это количество регулируется настройками компонента CmpChannelMgr. Сам же канал связи используется на канальном уровне (Chanel layer) стека протоколов CODESYS PDU.
  • byte_order — указывает на используемый порядок байт в протоколе. Как было сказано ранее, по умолчанию CODESYS PDU использует в качестве порядка байт little — endian, однако порядок байт может быть изменен. Значение поля byte_order, равное 1, указывает на использование little-endian.
  • Unknown — назначение поле в ходе исследования распознать не удалось.
  • node_name_length — размер поля node_name.
  • device_name_length – размер поля device_name.
  • vendor_name_length – размер поля vendor_name.
  • target_type – тип устройства.
  • target_id – идентификатор устройства.
  • target_version – версия устройства.
  • node_name — сетевое имя устройства.
  • device_name – имя устройства.
  • vendor_name — имя организации, которая разработала устройство или внедрила в устройство CODESYS Runtime.

Если в поле version запроса было указано значение 0x400, то ответ будет содержать следующие поля: адрес родительского узла, номер лицензии, тип компонента Block Driver.

Канальный уровень (Channel layer)

Канальный уровень — следующий уровень в стеке протоколов CODESYS PDU.

Схематичное представление разбора компонентами пакета CODESYS PDU на канальном уровне (Channel Layer)

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

Основным компонентом на этом уровне является компонент CmpChannelMgr (Component Channel Manager). Этот компонент является менеджером канала связи. Он следит за синхронизацией общения между узлами и целостностью получаемых данных или передает управление на сервер каналов (компонент CmpChannelServer) или клиенту каналов связи (компонент CmpChannelClient).

Компонент CmpChannelServer является сервером каналов. Он ответственен за:

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

Компонент CmpChannelClient является клиентом каналов. Он формирует необходимые запросы и занимается обработкой ответов от сервера каналов.

Компонент CmpChannelMgr экспортирует функцию ChannelMgrHandleData, к которой обращается компонент CmpRouter в случае если на уровне датаграм значение поля service_id равно 0x40.

Местоположения передачи управления функции ChannelMgrHandleData

Для канального уровня используется общий заголовок (channel_common_header). Он содержит следующие поля:

  • Поле package_type определяет тип пакета. В случае если в значение этого поля установлен старший бит, то пакет является командой для сервера канала. При отсутствии старшего бита пакет предназначен для менеджера канала.
  • Поле flags имеет различное назначение, которое зависит от поля package_type.
  • Поле packet_data содержит оставшиеся данные пакета и определяется полем packet_type.

Экспортируемая компонентом CmpChannelMgr функция ChannelMgrHandleData является обработчиком на канальном уровне (Channel layer). Работу этой функции можно условно разделить на три категории: работу с сервером каналов, работу с клиентом каналов и работу с менеджером каналов.

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

Работа с сервером каналов происходит в обработчике NetServerHandleMetaRequest.

Ответ от сервера канала обрабатывается клиентом канала в обработчике NetClientHandleMetaResponse (строка 35) и (строка 27).

Работа с менеджером каналов происходит в функции HandleL4Data (строка 43).

Команды для сервера канала связи

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

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

Обработчиком команд для сервера канала связи является функция NetServerHandleMetaRequest, а команд для клиента — NetClientHandleMetaResponse. Первая функция обрабатывает входящие запросы, то есть реализует серверную часть. Вторая функция обрабатывает ответы на запросы, то есть реализует клиентскую часть. Далее будут рассмотрены обе эти функции.

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

Функция NetServerHandleMetaRequest (строка 01) определяет три возможных идентификатора команды (command_id) для обработки:

  • 0xC2 (строка 16) для команды GET_INFO. Обработчиком команды выступает функция HandleInfoReq (строка 17).
  • 0xC3 (строка 10) для команды OPEN_CHANNEL. Обработчиком команды выступает функция HandleOpenChannelReq (строка 11).
  • 0xC4 для команды CLOSE_CHANNEL. Обработчиком команды выступает функция HandleCloseChannelReq (строка 14).

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

При этом клиентская функция NetClientHandleMetaResponse (строка 23) определяет только две возможные команды для клиента:

  • 0xC3 (строка 30) на команду OPEN_CHANNEL. Обработчиком команды выступает функция HandleOpenChannelResp (стока 32).
  • 0xC4 (строка 34) на команду CLOSE_CHANNEL. Обработчиком команды выступает функция HandleCloseChannelResp (строка 36).

Сообщение канального уровня, которое предназначено для сервера и клиента канала, имеет свой заголовок (channel_header). В заголовке channel_header используются следующие поля:

  • Поле command_id определяет идентификатор команды и обозначает, является ли сообщение ответом на запрос или самим запросом. Выставленный 7-й бит указывает на то, что сообщение является ответом. Остальные первые 6 бит обозначают идентификатор команды. Существуют следующие идентификаторы команд для сервера канала:
    • 0xc2 (GET_INFO) – информационная команда для получения количества единовременно поддерживаемых каналов на узле;
    • 0xc3 (GET_CHANNEL) – запрос на создание канала связи между узлами;
    • 0xc4 (CLOSE_CHANNEL) – запрос на закрытие канала связи между узлами.
  • Во время исследования не было обнаружено использования значения, установленного в поле flags.
  • Поле version указывает на идентификатор версии команды. В зависимости от этого поля могут быть использованы дополнительные поля в теле сообщения команды.
  • Оставшиеся данные команды определяются командой (command_id).

Например, при запросе открытия канала связи серверная функция NetServerHandleMetaRequest обрабатывает следующие поля и данные:


Используемые поля на канальном уровне (Channel layer)

  • command_id с идентификатором 0xc3 означает, что сообщение является запросом на открытие канала коммуникации (GET_CHANNEL).
  • Поле flags будет проигнорировано при обработке команды GET_CHANNEL.
  • Поле version определяет наличие дополнительных полей в сообщении. Для текущего значения будет использовано два дополнительных поля.
  • checksum — контрольная сумма пакета. В качестве контрольной суммы используется алгоритм CRC
  • command_data — поле рассмотрено ниже.

Для запроса команды GET_CHANNEL используются следующие поля и данные:


Используемые поля запроса с командой GET_CHANNEL

  • Поле datagram_layer_fields — поля уровня датаграм (datagram layer).
  • Поле channel_header — заголовок команды серверу канала.
  • Message_id — идентификатор сообщения. Обычно в качестве значения этого поля используется 4-битное представление текущего времени.
  • Receiver_buffer_size — максимально возможное количество данных, которое может накапливать получатель в канале связи.

В ответе на этот запрос поле command_id заголовка channel_header установит 7-й бит. Поля command_data в ответе на запрос будут следующими:


Используемые поля ответа на команду GET_CHANNEL

  • Поле datagram_layer_fields — поля уровня датаграм (datagram layer).
  • Поле channel_header — заголовок команды клиенту канала.
  • Message_id — возвращаемый идентификатор сообщения. Это значение эквивалентно значению, которое было получено в запросе.
  • Reason – статус обработки команды.
  • Channel_id — идентификатор открытого канала связи.
  • Receiver_buffer_size — максимально возможное количество данных, которое может накапливать получатель в канале связи.

Другие команды используют свой набор полей в поле command_data. Исключением является команда GET_INFO. Для получения результата этой команды достаточно отправить заполненный channel_header с указанным в нем идентификатором команды GET_INFO. В ответе будет содержаться одно поле:

  • Поле Max_channels содержит максимальное количество единовременно поддерживаемых каналов связи.

Запрос на закрытие канала (CLOSE_CHANNEL) использует следующие поля в command_data:

  • channel_id — идентификатор канала, который необходимо закрыть.
  • reason — причина закрытия канала.

Узел, получивший такой запрос, не возвращает какой-либо ответ.

Команды для менеджера канала связи

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

Команды для менеджера канала связи используют общий заголовок (channel_manager_header) со следующими полями:

  • packet_type — идентификатор типа пакета. Определяются следующие типы пакетов и их назначения:
    • BLK – передача данных для следующего уровня в стеке протоколов;
    • ACK – уведомление о получении данных;
    • KEEPALIVE – поддержка жизни канала связи.
  • Flags — дополнительные параметры или указатели, которые специфичны для типа пакета (packet_type).
  • packet_data — специфичные данные для типа пакета (packet_type).

Функция HandleL4Data обрабатывает все команды рассматриваемой группы.

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

Функция HandleL4Data (строка 01) определяет три возможных идентификатора packet_type для обработки BLK (0x1), ACK (0x2), KEEPALIVE (0x3).

Каждый тип пакета имеет специфичное для этого типа тело данных. Так, типа пакета BLK использует следующие поля в теле сообщения:


Используемые поля для пакета типа BLK на канальном уровне (channel layer)

  • Packet_type — тип пакета BLK (0x1), который означает передачу данных.
  • Flags — флаги пакета для типа пакета BLK. Указанное значение 0x81 означает, что:
    • Узел, который получил этот пакет, выступает в качестве сервера (старший бит), данные запросы являются первыми в передачи (младший бит);
    • Если младший бит не установлен, то сообщение содержит продолжение данных последнего пакета. Младший бит данного пакет указывает, что этот пакет первый раз передает данные следующему уровню.
  • Channel_id — идентификатор открытого канала, по которому осуществляется передача данных.
  • Blk_id — идентификатор текущего BLK-сообщения. Этот идентификатор инкрементируется каждый раз стороной, которая инициировала начало общения по каналу.
  • Ack_id — идентификатор последнего ACK-сообщения. Этот идентификатор меняется каждый раз отвечающей стороной. После получения последнего пакета для передачи данных службе на уровне приложений, отвечающая сторона меняет значение этого идентификатора на значение Blk_id.
  • Remaining_data_size — размер ожидаемых данных, который содержит поле remaining_data.
  • Checksum — контрольная сумма данных, содержащихся в поле remaining_data. Для подсчета контрольной суммы используется алгоритм CRC32.

Если пакет типа BLK содержал данные, размер которых не превышает максимального размера пакета CODESYS PDU (512 байт), то ответ будет содержать измененное значение поля flags, в котором старший бит не будет выставлен, т.к. получатель теперь является клиентом. Значение поля ack_id будет изменено на значение поля blk_id.

Несмотря на то, что максимальный размер пакета CODESYS PDU 512 байт, по самому протоколу CODESYS PDU возможно передавать данные очень больших размеров. Это работает путем накопления входящих данных на получающей стороне. Получающая сторона понимает, что данные необходимо накапливать, за счет значений в поле flags. Понимание того, что пакет содержит последние для команды данные, осуществляется по полям checksum и remaining_data_size.

Тип сообщения ACK используется для уведомления отправляющей стороны о том, что часть данных была получена и ожидается следующая часть данных. Это сообщение использует следующие поля:

  • Channel_id — идентификатор канала, по которому был получен пакет типа BLK.
  • Blk_id — значение поля Blk_id из пакета типа BLK, который был получен принимающей стороной.

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

Тип сообщения KEEPALIVE использует одно поле:

  • Channel_id — идентификатор канала, время которого необходимо продлить.

Уровень служб (services layer)

Следующий уровень в стеке протоколов CODESYS PDU — уровень служб (services layer).

Схематичное представление разбора компонентами пакета CODESYS PDU на уровне служб (Services Layer)

Уровень служб — это условно объединенный уровень из нескольких уровней модели ISO/OSI: сессии, представлений и приложений. Основная задача этого уровня — вызов запрашиваемой службы и передача параметров для её работы. Дополнительные задачи уровня служб — это кодирование, декодирование, шифрование и расшифрование передаваемых на этом уровне данных. Другая дополнительная задача — поддержка сессионности на устройстве.

В последних реализациях CODESYS Runtime поддерживает шифрование данных на уровне служб. Компонент CmpSecureChannel шифрует и расшифровывает данные на этом уровне. Это происходит в экспортируемой им функции SecChServerHandleRequest. Если данные были успешно расшифрованы или если они не были зашифрованы изначально, то они передаются функции ServerAppHandleRequest, которую экспортировал компонент CmpSrv.

В случае отсутствия компонента CmpSecureChannel, компонент CmpChannelServer передает управление функции ServerAppHandleRequest самостоятельно.

Сам же формат заголовка сообщения (protocol_header), как для зашифрованного, так и для незашифрованного сообщения, следующий:

  • protocol_id — идентификатор используемого протокола. Этот идентификатор указывает на то, какой обработчик протокола изменил данные или какому протоколу необходимо передать данные службам. Существуют два системных идентификатора протокола:
    • HeaderTagProtocol с идентификатором 0xcd Этот идентификатор протокола указывает на то, что данные поля protocol_data содержат теги (tags).
    • SecureProtocol с идентификатором 0x7557 – протокол для защищенной передачи данных. Этот идентификатор указывает на то, что данные поля protocol_data подлежат расшифровке.
  • Header_size – размер заголовка protocol_header. Значение этого поля не содержит размеры предыдущих полей и текущего поля
  • service_group — идентификатор вызываемой службы. Если в идентификаторе установлен старший бит, то сообщение является ответом от службы. По идентификатору службы в качестве службы определяются следующие компоненты:
    • CmpAlarmManager – 0x18;
    • CmpApp – 0x2;
    • CmpAppBP – 0x12;
    • CmpAppForce – 0x13;
    • CmpCodeMeter – 0x1d;
    • CmpCoreDump – 0x1f;
    • CmpDevice – 0x1;
    • CmpFileTransfer – 0x8;
    • CmpIecVarAccess – 0x9;
    • CmpIoMgr – 0xb;
    • CmpLog – 0x5;
    • CmpMonitor – 0x1b;
    • CmpOpenSSL – 0x22;
    • CmpSettings – 0x6;
    • CmpTraceMgr – 0xf;
    • CmpTraceMgr – 0xf;
    • CmpUserMgr – 0xc;
    • CmpVisuServer – 0x4;
    • PlcShell – 0x11;
    • SysEthernet – 0x7.
  • service_id — идентификатор команды. Этот идентификатор определяет, что именно служба должна сделать.
  • session_id — идентификатор сессии. Содержит значение полученной или пустой сессии. Это значение проверяется обработчиками протоколов и большинством команд, которые требуют повышенных привилегий пользователя.
  • content_size – размер данных в поле
  • additional_data — поле для дополнительных данных.
  • protocol_data — данные, сформированные по использованному протоколу (protocol_id).

Если сообщение было зашифровано по протоколу SecureProtocol, то почти все поля заголовка protocol_header будут содержать нулевые байты. Исключением будут поля header_size и content_size, которые работают в обычном режиме, и поле protocol_data, которое содержит зашифрованный заголовок protocol_header. После расшифровки поля protocol_data расшифрованный заголовок protocol_header будет обработан обработчиком протокола HeaderTagProcol.

Если сообщение не было зашифровано и был использован протокол HeaderTagProcol, то поле protocol_data будет содержать теги (tags).

Пользователь может зарегистрировать свой обработчик для protocol_id с помощью функции ServerRegisterProtocolHandler, которая экспортирована компонентом CmpSrv:

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

Функция ServerRegisterProtocolHandler довольно простая, и её алгоритм заключается в следующем:

  1. На строках с 10 по 12 функция сравнивает каждый из зарегистрированных обработчиков для поля protocol_id с обработчиком, который планируется зарегистрировать. В случае обнаружения среди зарегистрированных такого же обработчика, возвращает соответствующий статус (строка 13).
  2. Далее она пытается найти не занятую ячейку для регистрации обработчика (строки 15:21).
  3. Регистрирует новый обработчик в свободной ячейке (строка 25:26).

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

Например, для не зашифрованного запроса прохождения аутентификации функция ServerAppHandleRequest обрабатывает пакет следующим образом:


Пример содержимого полей на уровне служб (services layer)

  • Protocol_id — содержит идентификатор 0xcd55. Это значит, что был использован протокол HeaderTagProtocol: данные поля protocol_data не зашифрованы, а поля service_group и service_id содержат значения запрашиваемой службы.
  • Header_size — содержит значение 0x Это значит, что размер используемого заголовка (protocol_header) 16 (0x10) байт.
  • Service_group — указывает на идентификатор службы, которая была зарегистрирована компонентом CmpDevice.
  • Service_id — указывает на идентификатор запрашиваемой команды для службы. Значение 2 указывает на то, что зарегистрированной службе компонентом CmpDevice необходимо выполнить команду AUTH.
  • Protocol_data_size — указывает, что размер данных поля protocol_data 72 (0x48) байта.
  • Поле additional_data не было использовано.

Теги (tags)

Последний рассматриваемый слой в стеке протоколов CODESYS PDU — это теги (tags).

Схематичное представление разбора компонентами пакета CODESYS PDU на уровне служб

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

Типы тегов

Теги передаются в поле protocol_data заголовка protocol_header. Они бывают двух видов: тег данных и родительский тег.

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

  • tag_id — идентификатор тега. Идентификаторы родительского тега и тега данных различают по значению старшего бита. Если значение старшего бита выставлено, то тег является родительским, и все оставшиеся данные характерны для родительского тега. В противном случае тег является тегом данных, и его данные содержат конечные параметры для службы.
  • tag_size — размер данных. Это поле определяет количество данных в поле tag_data. Помимо этого, значение старшего бита поля tag_size определяет существование дополнительного поля additional_data: если значение старшего бита установлено, то поле additional_data существует.
  • additional_data — дополнительное поле. Имеет динамический размер, который не может быть больше 10 байт. Окончание этого поля определяется нулевым байтом.
  • tag_data — данные родительского тега или данные тега данных.

Данные, извлеченные службой из тега данных, преобразуются в данные определенного типа. Например, тег, содержащий 4 байта в поле tag_data, может быть приведен службой к одному из числовых типов. Сам тип переменной в структуре тега не передается. Однако, множество раз было замечено, что службы CODESYS Runtime передают группу тегов, из которых один тег может содержать значение, второй — идентификатор типа, третий — размер. Такие связки тегов обычно объединены под одним родительским тегом.

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

Обработка тегов

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


Пример разбора содержимого поля protocol_data на теги в запросе прохождения аутентификации

Внутри заголовка services_layer_fields есть поле protocol_data. Если разбить это поле на теги, то в нем находятся 4 тега данных и 1 родительский тег.

Иерархия тегов будет следующей:

  • Data_tag_1
  • Data_tag_2
  • Parent_tag_1
    • Data_tag_3
    • Data_tag_4

Теги данных Data_tag_1 и Data_tag_2 находятся на одном уровне с родительским тегом Parent_tag_1.

Data_tag_3 и Data_tag_4 находятся внутри родительского тега.

Для взаимодействия с тегами используется множество API-функций, среди которых есть функции для взаимодействия с входящими тегами с префиксом BTagReader (Binary Tag Reader) и для взаимодействия с исходящими тегами с префиксом BtagWriter (Binary Tag Writer).

Основные API-функции для взаимодействия с тегами:

Для входящих тегов

  1. BTagReaderInit – инициализация структуры чтения полученных данных. Структура чтения данных хранит в себе следующие элементы: сами данные, текущую позицию в данных, возможную конечную позицию данных и размер данных. В большинстве случаев, все функции для взаимодействия со входящими тегами используют для работы только указатель на текущую позицию в данных — так происходит «перемещение» по тегам в чистых данных.
  2. BTagReaderGetTagId – получение идентификатора тега.
  3. BTagReaderGetContent – получение данных тега.
  4. BTagReaderMoveNext – переход к следующему тегу.
  5. BTagReaderSkipContent – перемещение к концу данных текущего тега.

Для исходящих тегов

  1. BTagWriterInit — инициализация структуры записи исходящих данных. Структура записи данных хранит в себе следующие элементы: изначально полученные данные, указатель на конец данных и текущий размер данных. Все функции для взаимодействия с исходящими тегами меняют все элементы структуры.
  2. BTagWriterStartTag – открытие нового тега для исходящих данных. Открытие тега должно сопровождаться вызовом функции закрытия тега — BTagWriterEndTag.
  3. BTagWriterAppendBlob – добавление данных для созданного тега.
  4. BTagWriterEndTag – закрытие созданного тега данных.
  5. BTagWriterFinish – завершение записи тегов в качестве исходящих данных. По сути, эта функция проверяет, что все добавленные теги имеют валидную структуру и были закрыты функцией BTagWriterEndTag.

В пакете есть заголовок services_layer_fields, в котором значение поля service_group равно 1. Этот идентификатор зарегистрирован компонентом CmpDevice. Сам компонент во время своей инициализации регистрирует функцию DeviceServiceHandler в качестве службы, устанавливая ей идентификатор, равный 1:

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

В этом же пакете команда, указанная в поле идентификатора команды (service_id), равна 2. Функция DeviceServiceHandler определяет 9 команд, среди которых есть команда с идентификатором 2 (строка 254):

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

Функция DeviceServiceHandler условно выполняет команду с идентификатором 2 в три этапа:

  1. Извлекает параметры и устанавливает локальные значения;
  2. Исполняет команды с локальными значениями и полученными параметрами;
  3. Возвращает результат исполнения команды.

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

Алгоритм работы команды с идентификатором 2 для каждого этапа следующий:

Этап извлечения параметров

  1. На строке 131 происходит инициализация структуры для записи исходящих данных (writer), которая будет использоваться на этапе возвращения результата. На строке 132 — инициализация структуры для чтения входящих данных (reader), которая используется на текущем этапе.
  2. Происходит попытка распознать теги во входящих данных (строка 266). В случае успеха из первого тега извлекается идентификатор тега (269).
  3. В зависимости от полученного на предыдущем шаге идентификатора тега происходит запись данных тега в соответствующие переменные.
    Для идентификатора 0x23 (строка 272) происходит запись в переменную pulChallenge (строка 273), для идентификатора 0x22 (строка 298) — в переменную pulCrypeType (строка 299).
    Если идентификатор тега равен 0x81 (строка 275), то тег является родительским
    и в нем происходит поиск тегов данных (строка 279) с идентификаторами 16
    (строка 280). Данные из найденных тегов записываются в переменную user_name (строка 282).
    Из найденного тега с идентификатором 17 (строка 284) данные записываются
    в переменную encrypted_password (строка 286).
  4. На строке 315 происходит проверка того, что переменные, необходимые для исполнения команды, заполнены данными из тегов.

Этап исполнения команды

  1. Полученные переменные encrypted_password, pulCrypeType и pulChallenge используются в функции расшифровки пароля UserMgrDecryptPassword (строка 318). Расшифрованный пароль будет записан в переменную decrypted_password.
  2. Переменная user_name будет использована в функции проверки существования пользователей FindUser. Эта функция ищет запись
    о пользователях в базе данных.
  3. В случае нахождения записи о пользователе с именем из переменной user_name, происходит проверка соответствия пароля пользователя
    с расшифрованным паролем, который был сохранен в переменную decrypted_password.
  4. Если расшифрованный пароль совпадает с паролем из базы данных,
    то для текущего пользователя происходит проверка прав на объект “Device”
    (строка 357).
  5. При наличии прав у пользователя на объект “Device”, сгенерированный идентификатор сессии (переменная ulSessionId) присваивается текущему пользователю (строка 365) и используемому каналу связи (строка 367).

Этап возвращения результата

  1. Для исходящих данных открывается тег с идентификатором 0x82 (строка 376).
    Этот тег является родительским, и внутри него последовательно открываются
    и закрываются теги с идентификаторами 0x20 (открытие на строке 377 и закрытие
    на строке 379), 0x24 (открытие на строке 389 и закрытие на строке 391) и 0x21 (открытие на строке 392 и закрытие на строке 394).
  2. В теги данных записываются следующие значения: с идентификатором 0x20 — значение статуса выполнения команды (строка 378); с идентификатором 0x24 — значение настроек устройства (строка 390); с идентификатором 0x21 — значение сгенерированной сессии (строка 393).
  3. На строке 395 закрывается родительский тег с идентификатором 0x82
    и завершается запись исходящих данных (строка 396).

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

На запрос прохождения аутентификации CODESYS Runtime при верных данных аутентификации пользователя возвращается следующий пакет:


Пример разбора на теги содержимого поля protocol_data в ответе на запрос прохождения аутентификации

Таким образом, мы можем однозначно определить, в каких тегах с какими идентификаторами будут переданы определенные параметры для служб. Основные параметры в этом ответе будут находиться в теге с идентификатором 0x21. Этот тег будет содержать идентификатор сессии, который потом будет использован в качестве значения поля session_id на уровне служб (services layer).

 
Назад к первой части | Продолжение в третьей части

Скачать PDF версию