17 сентября 2019

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

 

Исследование безопасности технологий, которые используются разработчиками систем автоматизации и имеют потенциал применения на промышленных объектах по всему миру, является одним из приоритетных направлений работы Центра реагирования на инциденты информационной безопасности промышленных инфраструктур «Лаборатории Касперского» (Kaspersky ICS CERT).

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

Согласно официальной информации от компании-разработчика, CODESYS Runtime уже адаптирован для более чем 350 устройств от различных вендоров, которые используются в энергетики, промышленном производстве, системах интернета вещей и промышленного интернета вещей и т.д. Отметим также, что в реальности эта цифра значительно больше, так как устройства множества компаний, которые используют фреймворк CODESYS Runtime для своих ПЛК, не находятся в официальном списке. Количество таких устройств постоянно растет — в 2016 году их было всего 140, — и мы не будем удивлены, если оно продолжит расти и в будущем.

Фрагменты технической информации были удалены в связи с требованием CODESYS Group. Более подробную информацию можно запросить по адресу security@codesys.com.

Фреймворк

В современном мире использование готового программного кода в своем продукте — скорее правило, чем исключение. Это позволяет разработчикам нового продукта не заниматься «изобретением велосипеда» и сокращает время на разработку.

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

Сторонний код часто используется для реализации конкретной функции или набора функций, таких как, отрисовка изображения, отображение пользовательского интерфейса, печать на принтере или сохранение данных в БД. Мы уже проводили исследования безопасности и обнаруживали уязвимости в различном стороннем коде. Например, в 2017 году — для части программно-аппаратного комплекса контроля соблюдения лицензионных соглашений и защиты приложения от «взлома» SafeNet Sentinel, а в 2018 — для библиотеки OPC UA от OPC Foundation.

С фреймворком CODESYS ситуация другая: разработчик ПЛК на основе CODESYS адаптирует фреймворк для работы на своих аппаратных комплексах и разрабатывает при необходимости дополнительные модули, используя предоставляемые CODESYS сервисные функции. Конечный пользователь ПЛК (инженер) разрабатывает при помощи среды разработки CODESYS код программы автоматизации технологического процесса. И поток выполнения разработанных дополнительных модулей и программы автоматизации технологического процесса контролируется на ПЛК адаптированным под него вариантом CODESYS Runtime.

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

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

  • Что внутри этого фреймворка?
  • Как он работает?
  • Как мне сделать свое ПО безопасным, если уязвимость не в моем коде, а во фреймворке?

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

Описание объекта исследования: CODESYS Runtime

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

Связка продуктов компании CODESYS Group

Компания CODESYS Group разрабатывает два основных продукта:

  1. Среда разработки — CODESYS Development System
  2. Среда исполнения — CODESYS Runtime

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

В контексте нашего исследования важно, что CODESYS Development System — это кастомизируемая среда разработки. На её основе созданы IDE SoMachine от компании Schneider Electric, TwinCAT от компании Beckhoff Automation, IdraWorks от компании Bosch, Wagilo Pro от компании WAGO, одноименные IDE CODESYS от компаний Owen, STW Technic, prolog-plc и другие IDE.

Для программирования контроллера с помощью IDE CODESYS Development System, на нем должен быть запущен CODESYS Runtime. Для корректного запуска CODESYS Runtime на конкретном устройстве его необходимо адаптировать под выбранную операционную систему и железо. Согласно информации на официальном сайте CODESYS, сами разработчики CODESYS адаптировали CODESYS Runtime только для 15 устройств. Дистрибьютерами же CODESYS Runtime был адаптирован для более чем 350 устройств.

Среди адаптированных CODESYS Runtime есть версии:

  • Для одноплатных компьютеров с операционной системой Linux, такие как Raspberry Pi, UniPi, BeagleBone;
  • Для Soft PLC на Windows и Linux;
  • Для ПЛК компаний ASEM S.p.A, exceet electronics AG, Hitachi Europe GmbH, Hans Turck GmbH & Co. KG, elrest Automationssysteme GmbH, Janz Tec AG, Kendrion Kuhnke Automation GmbH, Beijer Electronics, ifm electronic gmbh, Nidec Control Techniques Limited, Advantech Europe B.V, WAGO Kontakttechnik GmbH & Co. KG, KEB Automation KG, Berghof Automation GmbH и многих других.

Архитектура

Компоненты

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

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

Компонентно-ориентированная архитектура CODESYS Runtime (источник)

Среди всех компонентов CODESYS Runtime можно выделить основные:

  1. Component Manager, или CM — компонент для запуска и инициализации всех остальных компонентов в системе;
  2. System Components — группа компонентов для описания взаимодействий с операционной системой и с железной составляющей. Компоненты из этой группы отвечают за взаимодействие с физическими портами, с файловой системой, с работой по динамическому и статическому выделению памяти и т.д.
  3. Communication Components – группа компонентов для взаимодействия с внешним миром, например по сети или через последовательный кабель;
  4. Application components — компоненты управления программой ПЛК;
  5. Core components — компоненты для управления ПЛК и его состоянием.

У разработчика есть несколько способов расширить CODESYS Runtime:

  1. Заменить существующие компоненты;
  2. Написать собственные компоненты;
  3. Написать собственные компоненты, которые будут расширять функциональность уже существующих компонентов.

Структура компонентов

Компоненты CODESYS — это динамические библиотеки (аналог *.dll в ОС Windows и *.so в ОС Linux). Все компоненты загружаются компонентом Component Manager.

CODESYS Runtime может иметь статическую и динамическую сборку.

Если CODESYS Runtime имеет статическую сборку, то программный код компонентов содержится в самом исполняемом файле.

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

Структура самого файла компонента не являлась объектом данного исследования, однако можно с уверенностью сказать, что в этой структуре содержится:

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

Структура интерфейсов взаимодействия

Более интересным, чем структура файла компонента, для исследования является то, каким образом CODESYS Runtime взаимодействует с внешними компонентами, и как внутренние компоненты взаимодействуют друг с другом.

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

Функции удаления и создания своей сущности — опциональны. Они оказались пустыми для большинства компонентов. Поэтому мы не станем здесь их рассматривать. Остальные же функции будут рассмотрены далее.

Реализация функции инициализации

Функция инициализации — аналог entry point для PE и ELF файлов, с той лишь разницей, что непосредственную работу компонента она не начинает. Вызов этой функции осуществляется компонентом Component Manager.

01 Удалено по требованию вендора 
02: {
03:   Удалено по требованию вендора 
04:   Удалено по требованию вендора 
05:   Удалено по требованию вендора 
06:   Удалено по требованию вендора 
07:   Удалено по требованию вендора 
08:   Удалено по требованию вендора 
09:   Удалено по требованию вендора 
[...]
17:   Удалено по требованию вендора 
18: }

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

Функция ModuleCmpBlkDrvUdp_entry — функция инициализации. Аргумент для этой функции структура INIT_STRUCT. Обычно эта функция вызывается с использованием Component Manager для заполнения структуры. Функция инициализации заполняет поля в структуре, среди которых — все вышеперечисленные функции и идентификатор компонента, который для компонента CmpBlkDrvUdp равен 7.

Реализация функции обработки событий

Следующая интересная функция — это функция обработки событий. Для компонента CmpBlkDrvUdp этой функцией является ModuleCmpBlkDrvUdp_hook. Эта функция определяет по полученному идентификатору события, что от неё требует выполнить Component Manager.

Идентификаторы основных событий:

  • CH_INIT_SYSTEM — идентификатор 1. В случае, если компонент относится к группе системных компонентов (SystemComponents), то ему необходимо инициализироваться;
  • CH_INIT — идентификатор 2. Компоненты должны инициализировать все локальные переменные;
  • CH_INIT2 — идентификатор 3. Компонент должен инициализироваться;
  • CH_INIT_TASKS — идентификатор 5. Компонент может запустить свои потоки выполнения;
  • CH_INIT_COMM — идентификатор 6. Компонент может начать коммуникацию;
  • CH_EXIT_COMM — идентификатор 10. Компоненту необходимо завершить все каналы коммуникаций;
  • CH_EXIT_TASKS — идентификатор 11. Компоненту необходимо остановить и завершить все созданные им потоки выполнения;
  • CH_EXIT2 — идентификатор 13. Компоненту необходимо сохранить все данные перед вызовом CH_EXIT;
  • CH_EXIT — идентификатор 14. Компоненту необходимо освободить память;
  • CH_EXIT_SYSTEM — идентификатор 15. В случае если компонент относится к группе системных компонентов (SystemComponents), то ему необходимо освободить память;
  • CH_COMM_CYCLE — идентификатор 20. Вызывается каждый цикл. Используется для созданных потоков выполнения.

Сравнение жизненного цикла работы компонента CODESYS с циклом работы динамически подключаемой библиотеки Windows (Dynamic Link Library)

События для обработки компонентом, как и события вызова DLL Windows, созданы «зеркальными» парами: [СH_INIT_SYSTEM — CH_EXIT_SYSTEM], [СH_INIT — CH_EXIT] и т.д.

001: Удалено по требованию вендора 
002: {
003:   Удалено по требованию вендора 
004:   Удалено по требованию вендора 
005:   Удалено по требованию вендора 
006:   Удалено по требованию вендора 
007:   Удалено по требованию вендора 
008: 
009:   Удалено по требованию вендора 
010:   {
011:     Удалено по требованию вендора : // Initialization of the component’s variables
012:       Удалено по требованию вендора 
013:       Удалено по требованию вендора 
014:       Удалено по требованию вендора 
015:       Удалено по требованию вендора 
016:       Удалено по требованию вендора 
017:       Удалено по требованию вендора 
018:       Удалено по требованию вендора 
019:       Удалено по требованию вендора 
020:       Удалено по требованию вендора 
021:       Удалено по требованию вендора 
022:       Удалено по требованию вендора 
023:       Удалено по требованию вендора 
024:       Удалено по требованию вендора 
025:     Удалено по требованию вендора : // Initialization of the component. Setting values from the configuration file
026:       Удалено по требованию вендора 
027:       Удалено по требованию вендора 
028:       Удалено по требованию вендора 
029:       Удалено по требованию вендора 
030:         Удалено по требованию вендора 
031:       Удалено по требованию вендора 
032:       Удалено по требованию вендора 
033:       Удалено по требованию вендора 
034:       {
035:         iУдалено по требованию вендора 
036:           Удалено по требованию вендора 
037:         Удалено по требованию вендора 
038:           Удалено по требованию вендора 
039:         Удалено по требованию вендора 
040:       }
041:       Удалено по требованию вендора 
042:       {
043:         Удалено по требованию вендора 
044:         Удалено по требованию вендора 
045:         {
046:           Удалено по требованию вендора 
047:           Удалено по требованию вендора 
048:             Удалено по требованию вендора 
049:           Удалено по требованию вендора 
050:           Удалено по требованию вендора 
051:           Удалено по требованию вендора 
052:         }
053:       }
054:       Удалено по требованию вендора 
055:     Удалено по требованию вендора  // Creating a communication thread and starting communication
056:       Удалено по требованию вендора 
057:       Удалено по требованию вендора 
058:       {
059:         Удалено по требованию вендора 
060:           Удалено по требованию вендора 
061:         Удалено по требованию вендора 
062:       }
063:       Удалено по требованию вендора 
064:     Удалено по требованию вендора : // Completing communication
065:       Удалено по требованию вендора 
066:       Удалено по требованию вендора 
067:       {
068:         Удалено по требованию вендора 
069:         Удалено по требованию вендора 
070:         Удалено по требованию вендора 
071:       }
072:       Удалено по требованию вендора 
073:     Удалено по требованию вендора:  // Saving data and releasing it
074:       Удалено по требованию вендора 
075:       {
076:         Удалено по требованию вендора 
077:         {
078:           Удалено по требованию вендора 
079:             Удалено по требованию вендора 
080:           Удалено по требованию вендора 
081:         }
082:         Удалено по требованию вендора 
083:         Удалено по требованию вендора 
084:       }
085:       Удалено по требованию вендора 
086:       {
087:         Удалено по требованию вендора 
088:         Удалено по требованию вендора 
089:       }
090:       Удалено по требованию вендора 
091:     Удалено по требованию вендора  // Updating the communication socket
092:       Удалено по требованию вендора 
093:       {
094:         Удалено по требованию вендора 
095:         Удалено по требованию вендора 
096:         {
097:           Удалено по требованию вендора 
098:           Удалено по требованию вендора 
099:         }
100:         Удалено по требованию вендора 
101:       }
102:       Удалено по требованию вендора 
103:     Удалено по требованию вендора :
104:       Удалено по требованию вендора 
105:   }
106: }

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

На примере функции ModuleCmpBlkDrvUdp_hook видно следующее:

  • Компоненты могут пренебрегать предусмотренными правилами работы с событиями и объединять обработку нескольких событий в одном обработчике, как это сделано, например, в рассматриваемой функции: при обработке события CH_INIT_COMM происходит одновременно инициализация потока выполнения и его непосредственный запуск, хотя для последней задачи предназначено событие CH_INIT_TASKS;
  • Компоненты не обязательно должны обрабатывать все события. В том числе, компонентам не обязательно обрабатывать оба «симметричных» события, если одно событие из «зеркальной» пары уже было обработано — например, компонент CmpBlkDrvUdp не обрабатывает событие CH_EXIT, хотя им было обработано событие CH_INIT.
Реализация функций импорта и экспорта

Функция экспорта и функция импорта используют механизм, который в совокупности дает эффект работы экспортируемых и импортируемых функций в динамических библиотеках ОС Windows и Linux. Главная разница функций экспорта и импорта с библиотеками ОС Windows и Linux в том, что во время работы этих функций происходит регистрация экспортируемых функций и инициализация указателей на импортируемые функции.

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

Псевдо-код функции экспорта компонента CmpRasPi (есть исключительно в CODESYS Runtime for Raspberry PI)

Функция CMRegisterAPI использует в качестве первого аргумента указатель на заполненный массив, содержащий сведения об экспортируемых функциях, а в качестве последнего — идентификатор компонента. Для примера экспортируемой функции: на строке 10 есть заполненная структура exported_function с указателем на функцию sub_84e5bc0, именем функции “raspiyuv”, хешем 0xF81Fd05 и версией 0x3050400.

Таким образом компонент CmpRasPi предоставляет API для взаимодействия с модулем камеры на устройстве Rapsberry PI для всех остальных компонентов СodeSys и прикладных программ. Пример использования этого API продемонстрирован в примере проекта Camera.project для CODESYS Control for Raspberry Pi.

1: PROGRAM PLC_PRG
2: VAR
3:   xTakePicture: BOOL;
4: END_VAR
5: 
6: IF xTakePicture THEN
7:   Raspberry_Pi_Camera.Still('-o Picture.jpg');
8:   xTakePicture := FALSE;
9: END_IF

Код программы проекта Camera.project

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

01: Удалено по требованию вендора 
02: {
03:   Удалено по требованию вендора 
04:   Удалено по требованию вендора 
05:   Удалено по требованию вендора 
06:   Удалено по требованию вендора 
07:   Удалено по требованию вендора 
08:   Удалено по требованию вендора 
09:   Удалено по требованию вендора 
10:   Удалено по требованию вендора 
11: [...]
12:

Функция импорта модуля CmpApp

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

Перед этим все эти функции были зарегистрированы компонентом SysTarget.

01: Удалено по требованию вендора 
02: Удалено по требованию вендора 
03: Удалено по требованию вендора 
04: Удалено по требованию вендора 
05: Удалено по требованию вендора 
06: Удалено по требованию вендора 
07: Удалено по требованию вендора 
08: Удалено по требованию вендора 
09: Удалено по требованию вендора 
10: Удалено по требованию вендора 
11: Удалено по требованию вендора 
12: Удалено по требованию вендора 
13: Удалено по требованию вендора 
14: Удалено по требованию вендора 
15: Удалено по требованию вендора 
16: Удалено по требованию вендора

Фрагмент массива с экспортируемыми функциями

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

Настройка компонентов

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

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

[...]
28: [CmpWebServer] 
29: ConnectionType=0 
30:
31: [CmpOpenSSL] 
[...]

Фрагмент файла конфигурации для CODESYS Control for Raspberry Pi

Component Manager инициализирует все компоненты. В первую очередь он инициализирует системные компоненты, такие как CmpMemPool, CmpLog, CmpSettings и SysFile и т.д.

********* CoDeSysControl DEMO VERSION - runs 2 hours*********
[...]
=======================================================================
1526222855: Cmp=CM, Class=1, Error=0, Info=4, pszInfo= CODESYS Control V3
1526222855: Cmp=CM, Class=1, Error=0, Info=5, pszInfo= Copyright (c) 3S - Smart Software Solutions GmbH
1526222855: Cmp=CM, Class=1, Error=0, Info=6, pszInfo= <version>3.5.12.0</version> <builddate>Dec 18 2017</builddate>
=======================================================================
1526222855: Cmp=CM, Class=1, Error=0, Info=10, pszInfo= System: <cmp>CM</cmp>, <id>0x00000001</id> <ver>3.5.12.0</ver>
1526222855: Cmp=CM, Class=1, Error=0, Info=10, pszInfo= System: <cmp>CmpMemPool</cmp>, <id>0x0000001e</id> <ver>3.5.12.0</ver>
1526222855: Cmp=CM, Class=1, Error=0, Info=10, pszInfo= System: <cmp>CmpLog</cmp>, <id>0x00000013</id> <ver>3.5.12.0</ver>
1526222855: Cmp=CM, Class=1, Error=0, Info=10, pszInfo= System: <cmp>CmpSettings</cmp>, <id>0x0000001a</id> <ver>3.5.12.0</ver>
1526222855: Cmp=CM, Class=1, Error=0, Info=10, pszInfo= System: <cmp>SysFile</cmp>, <id>0x00000104</id> <ver>3.5.12.0</ver>
1526222855: Cmp=CM, Class=1, Error=0, Info=10, pszInfo= System: <cmp>SysTimer</cmp>, <id>0x00000116</id> <ver>3.5.12.0</ver>
1526222855: Cmp=CM, Class=1, Error=0, Info=10, pszInfo= System: <cmp>SysTimeRtc</cmp>, <id>0x00000127</id> <ver>3.5.12.0</ver>
[...]

Фрагмент лога запуска исполняемого файла CODESYS Control for Raspberry Pi

Среди системных компонентов есть компонент CmpSettings. Он интересен нам тем, что функция экспорта этого компонента регистрирует API, которыми пользуются все остальные компоненты для получения параметров из файла конфигурации.

01: Удалено по требованию вендора 
02: Удалено по требованию вендора 
03: Удалено по требованию вендора 
04: Удалено по требованию вендора 
05: Удалено по требованию вендора 
06: Удалено по требованию вендора 
07: Удалено по требованию вендора 
08: Удалено по требованию вендора 
09: Удалено по требованию вендора 
10: Удалено по требованию вендора 
11: Удалено по требованию вендора 
12: Удалено по требованию вендора 
13: Удалено по требованию вендора 
14: Удалено по требованию вендора 
15: Удалено по требованию вендора

Фрагмент массива экспортируемых функций компонента CmpSettings

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

Используя метод поиска вызовов функции SettgGetIntValue, по перекрестным ссылкам можно найти ключ DemoTimeUnlimited для настройки компонента ComponentManager:

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

Ключи настройки компонента ComponentManager

Адаптация

Возможность адаптировать CODESYS Runtime под любое железо и операционную систему — несомненно, главная его особенность. На разработчиках продукта с использованием фреймворка лежит ответственность по адаптации системных компонентов CODESYS Runtime под нужды и требования конкретного применения, включая тип технологического процесса. Адаптированный фреймворк CODESYS Runtime должен уметь взаимодействовать с аппаратными интерфейсами, Ethernet, освобождать и выделять память, работать с таймером, событиями, межпотоковым взаимодействием и т.д.

Системные компоненты из компонентно-ориентированной архитектуры CODESYS Runtime

Адаптация системных компонентов происходит путем экспортирования функций, которые требуют остальные компоненты (этот путь описан в предыдущей главе).

Проведя анализ нескольких вариантов CODESYS Runtime, мы выяснили, что системных компонентов всего 25.

Список основных системных компонентов:

SysTimer, SysTimeRtc, SysTime, SysTask, SysTarget, SysSocket, SysShm, SysSemProcess, SysSemCount, SysSem, SysReadWriteLock, SysProcess, SysOut, SysMutex, SysMsgQ, SysModule, SysMem, SysInternalLib, SysFile, SysExcept, SysEvent, SysEthernet, SysDir, SysCpuHandling, SysCom

После обеспечения работоспособности системных компонентов разработчик должен создать собственные модули CODESYS Runtime для ПЛК со специфичной функциональностью.

Реализация

В декабре 2016 вышла первая версия CODESYS Control for Raspberry Pi. В июне 2018 года — версия для Linux (CODESYS Control for Linux SL). Помимо этого, еще есть эмулятор CODESYS Control для Windows, который входит в пакет программ CODESYS Development System. Все эти реализации однотипны с реализацией CODESYS Control for Raspberry Pi и имеют схожие или полностью идентичные элементы реализации.

В этой главе мы рассказываем о реализации CODESYS Runtime на примерах CODESYS Control for Raspberry Pi и CODESYS Control for Linux SL.

Файл — установщик

CODESYS Development System передает на устройство Raspberry Pi установщик CODESYS Control, используя SSH client. Сам установщик представляет собой .deb-файл (Debian binary package).

# dpkg -c codesyscontrol_arm_raspberry_V3.5.12.0.deb
drwxr-xr-x root/root         0 2017-12-18 09:22 ./
drwxr-xr-x root/root         0 2017-12-18 09:22 ./var/
drwxr-xr-x root/root         0 2017-12-18 09:22 ./var/opt/
drwxr-xr-x root/root         0 2017-12-18 09:22 ./var/opt/codesys/
drwxr-xr-x root/root         0 2017-12-18 09:22 ./var/opt/codesys/backup/
drwxr-xr-x root/root         0 2017-12-18 09:22 ./var/opt/codesys/cmact_licenses/
-rwxr-xr-x root/root      2640 2017-12-18 09:22 ./var/opt/codesys/bacstacd.ini
-rw-r--r-- root/root     20736 2017-12-18 09:22 ./var/opt/codesys/3SLicense.wbb
drwxr-xr-x root/root         0 2017-12-18 09:22 ./var/opt/codesys/restore/
drwxr-xr-x root/root         0 2017-10-09 14:16 ./etc/
-rw-r--r-- root/root       216 2017-10-09 14:16 ./etc/CODESYSControl_User.cfg
drwxr-xr-x root/root         0 2017-12-18 09:22 ./etc/init.d/
-rw-r--r-- root/root      3355 2017-12-18 09:22 ./etc/init.d/codesyscontrol
-rw-r--r-- root/root       158 2017-10-09 14:16 ./etc/3S.dat
-rw-r--r-- root/root       943 2017-10-09 14:16 ./etc/CODESYSControl.cfg
drwxr-xr-x root/root         0 2017-12-18 09:22 ./opt/
drwxr-xr-x root/root         0 2017-12-18 09:22 ./opt/codesys/
drwxr-xr-x root/root         0 2017-12-18 09:22 ./opt/codesys/bin/
-rwxr-xr-x root/root   7330296 2017-12-18 09:22 ./opt/codesys/bin/codesyscontrol.bin
drwxr-xr-x root/root         0 2017-12-18 09:22 ./opt/codesys/scripts/

Содержимое .deb-файла

Основными элементами .deb-файла являются файл конфигурации и исполняемый файл.

Файл конфигурации

Файл конфигурации содержит огромное количество различных параметров конфигурации для CODESYS Control. По содержимому этого файла можно сделать следующие выводы:

  • CODESYSControlforRaspberryPi способен работать в качестве веб-сервера;
  • CODESYS Control for Raspberry Pi использует OpenSSL;
  • Есть параметры логирования для компонента CmpLog;
  • Параметры компонента CmpSettings могут ссылаться на другие файлы;
  • Для компонента SysProcess есть ключ Command.%d, значение параметра которого совпадает с именем системной утилиты shutdown в ОС Linux.
01: # cat etc/CODESYSControl_User.cfg
02: [SysCom]
03: ;Linux.Devicefile=/dev/ttyS
04: 
05: [CmpBlkDrvCom]
06: ;Com.0.Name=MyCom
07: ;Com.0.Baudrate=115200
08: ;Com.0.Port=3
09: ;Com.0.EnableAutoAddressing=1
10: 
11: [SysProcess]
12: Command.0=shutdown
13: 
14: [CmpApp]
15: Bootproject.RetainMismatch.Init=1
01: # cat etc/CODESYSControl.cfg
02: [SysFile]
03: FilePath.1=/etc/, 3S.dat
04: PlcLogicPrefix=0
05: 
06: [CmpLog]
07: Logger.0.Name=/tmp/codesyscontrol.log
08: Logger.0.Filter=0x0000000F
09: Logger.0.Enable=1
10: Logger.0.MaxEntries=1000
11: Logger.0.MaxFileSize=1000000
12: Logger.0.MaxFiles=1
13: Logger.0.Backend.0.ClassId=0x00000104 ;writes logger messages in a file
14: Logger.0.Type=0x314 ;Set the timestamp to RTC
15: 
16: [CmpSettings]
17: FileReference.0=SysFileMap.cfg, SysFileMap
18: FileReference.1=/etc/CODESYSControl_User.cfg
19: 
20: [SysExcept]
21: Linux.DisableFpuOverflowException=1
22: Linux.DisableFpuUnderflowException=1
23: Linux.DisableFpuInvalidOperationException=1
24: 
25: [CmpBACnet]
26: IniFile=bacstacd.ini
27: 
28: [CmpWebServer]
29: ConnectionType=0
30: 
31: [CmpOpenSSL]
32: WebServer.Cert=server.cer
33: WebServer.PrivateKey=server.key
34: WebServer.CipherList=HIGH
35: 
36: [SysMem]
37: Linux.Memlock=0
38: 
39: [CmpCodeMeter]
40: InitLicenseFile.0=3SLicense.wbb
41: 
42: [SysEthernet]
43: Linux.ProtocolFilter=3
44: 
45: [CmpSchedule]
46: ProcessorLoad.Enable=1
47: ProcessorLoad.Maximum=95
48: ProcessorLoad.Interval=5000

Содержимое файла конфигурации CODESYS Control For Raspberry Pi v3.5.14.10

Исполняемый файл

Параметры защиты файла

Первичный анализ исполняемых файлов, как правило, включает в себя проверку опций компиляции на предмет установки параметров защиты. Инструмент checksec показывает эти значения параметров защиты для исполняемых файлов.

# ./checksec.sh/checksec -o csv -f codesyscontrol_armv6l_raspberry.bin
No RELRO,No Canary found,NX disabled,No PIE,RPATH,No RUNPATH,No SYMTABLES,No Fortify,0,23,codesyscontrol_armv6l_raspberry.bin

# ./checksec.sh/checksec -o csv -f codesyscontrol_armv7l_raspberry.bin
No RELRO,No Canary found,NX disabled,No PIE,RPATH,No RUNPATH,No SYMTABLES,No Foritfy,0,23,codesyscontrol_armv7l_raspberry.bin

Результат обработки утилитой checksec исполняемых файлов CODESYS Control for Raspberry Pi v3.5.14.10

По результатам работы утилиты видно, что исполняемые файлы CODESYS Control for Raspberry Pi v3.5.14.10 были скомпилированы без дополнительной защиты, которая могла бы усложнить эксплуатацию бинарных уязвимостей.

Чуть лучше ситуация с компиляцией файла CODESYS Control for Linux SL, потому что в этом файле содержится установленная опция PIE.

# ./checksec.sh/checksec -o csv -f codesyscontrol.bin
Partial RELRO,No Canary found,NX disabled,PIE enabled,No RPATH,No RUNPATH,No SYMTABLES,No Fortify,0,23,codesyscontrol.bin

Результат обработки утилитой checksec исполняемых файлов CODESYS Control for Linux SL v3.5.14.10

Состояние исполняемого файла

Статический анализ исполняемого файла с помощью инструмента IDA Pro показывает, что этот файл на 99% состоит из данных (зеленый цвет), а не из машинного кода:

Состояние запокованного исполняемого файла CODESYS Runtime For Raspberry Pi

Обычно такое состояние характерно для исполняемых файлов, машинный код которых был запакован. Тем не менее, для всех исполняемых файлов обязательным атрибутом является наличие точки входа (Entry point). Для исполняемого файла CODESYS Runtime for Raspberry Pi точкой входа является функция start. С этой функции можно начать анализ.

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

Ассемблированный код функции start

Код функции start нормально распознается, и IDA Pro подсказывает, что функция sub_86a0840, она же функция main, также содержит валидный программный код.

1: Удалено по требованию вендора 
2: {
3:   Удалено по требованию вендора 
4:   Удалено по требованию вендора 
5:   Удалено по требованию вендора 
6:   Удалено по требованию вендора 
7:   Удалено по требованию вендора 
8: }

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

Функция main записывает в глобальные переменные количество аргументов командной строки и указатель на их значения (строки 3:4). Затем она вызывает функцию mprotect (строка 5), которая изменяет параметры доступа к области памяти. Первый аргумент — указатель на функцию start или же начало сегмента .text. Второй аргумент — размер памяти, параметр доступа к которой будет изменен. Размер памяти также должен указывать на конец сегмента. Последний аргумент — это параметры доступа к памяти, на которые меняются исходные параметры. Его значение равно 7, то есть суммарному значению параметров PROT_READ | PROT_WRITE | PROT_EXEC.

Иначе говоря, строка 5 готовит область памяти к распаковке и возможности выполнения программного кода в этом участке памяти. Затем вызывается следующая функция (строка 6) и в качестве аргумента передается указатель на участок памяти, которым является переменная dword_86A0460, – это указатель на оригинальную функцию main после распаковки.

Таким образом, для дальнейшего исследования файла необходимо его распаковать.

Запущенный процесс

CODESYS Runtime for Raspberry Pi и for Linux выполняет трассировку своего процесса, то есть CODESYS Runtime for Raspberry Pi и For Linux занимается отладкой самого себя. Данный механизм используется в двух случаях: для перехвата системных вызовов (syscalls) и для реализации простейшей защиты от отладки: к запущенному процессу CODESYS Runtime нельзя присоединиться, используя сторонние инструменты отладки, такие как gdb, IDA Pro, radare или же strace.

Трассировка
01: # strace -f ./codesyscontrol.bin
02: execve("./codesyscontrol.bin", ["./codesyscontrol.bin"], [/* 18 vars */]) = 0
03: brk(NULL)                               = 0x90ce000
04: uname({sysname="Linux", nodename="raspberrypi", ...}) = 0
05: [...]
06: mprotect(0x8050000, 6495192, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
07: cacheflush(0x8050000, 0x8681bd8, 0, 0x8681bd8, 0x8681828) = 0
08: open("/home/pi/", O_RDONLY)             = 3
09: rt_sigaction(SIGTERM, {sa_handler=0x8050a40, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x76d436b0}, NULL, 8) = 0
10: rt_sigaction(SIGINT, {sa_handler=0x8050a40, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x76d436b0}, NULL, 8) = 0
11: rt_sigaction(SIGPIPE, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x76d436b0}, NULL, 8) = 0
12: rt_sigaction(SIGABRT, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x76d436b0}, NULL, 8) = 0
13: clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x76faabf8) = 4290
14: strace: Process 4290 attached
15: [pid  4290] set_robust_list(0x76faac00, 12) = 0
16: [pid  4290] getppid( <unfinished ...>
17: [pid  4289] ptrace(PTRACE_CONT, 4290, NULL, SIG_0 <unfinished ...>
18: [pid  4290] <... getppid resumed> )     = 4289
19: [pid  4290] getsid(4289)                = 3663
20: [pid  4290] ptrace(PTRACE_TRACEME)      = -1 EPERM (Operation not permitted)
21: [pid  4290] getpid()                    = 4290
22: [pid  4290] kill(4290, SIGKILL)         = ?
23: [pid  4289] <... ptrace resumed> )      = -1 ESRCH (No such process)
24: [pid  4289] wait4(-1,  <unfinished ...>
25: [pid  4290] +++ killed by SIGKILL +++
26: <... wait4 resumed> [{WIFSIGNALED(s) && WTERMSIG(s) == SIGKILL}], 0, NULL) = 4290
27: --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=4290, si_uid=0, si_status=SIGKILL, si_utime=0, si_stime=1} ---
28: ptrace(PTRACE_CONT, 4290, NULL, SIG_0)  = -1 ESRCH (No such process)
29: getpid()                                = 4289
30: kill(4289, SIGKILL)                     = ?
31: +++ killed by SIGKILL +++
32: Killed
33:

Запуск утилиты strace с ключом –f на исполняемый файл CODESYS Control For Raspberry Pi v3.5.14.00

Из лога запуска утилиты strace с ключом -f видно, что CODESYS Runtime меняет параметры доступа к памяти (строка 06), что, как известно из предыдущего пункта, нужно для распаковки программного кода.

Далее syscall clone создает дочерний процесс (строка 13). Родительский процесс имеет идентификатор 4289. Созданному дочернему процессу присвоен идентификатор 4290. Из-за наличия ключа -f strace пытается трассировать дочерние процессы, поэтому на строке 14 происходит оповещение о подключении к созданному дочернему процессу.

Далее родительский процесс пытается возобновить работу остановленного дочернего процесса вызовом функции ptrace с аргументом PTRACE_CONT (строка 17). Дочерний процесс в это время выполняет ptrace с аргументом PTRACE_TRACEME (строка 20), обозначив этим, что этот процесс должен быть трассирован родительским процессом.

Однако, результат выполнения функции указывает на то, что процесс не может быть трассирован родительским процессом. Из-за этого дочерний процесс завершает свою работу (строки 21:22). После этого родительский процесс получает ответ от функции ptrace (строка 23) и узнает, что дочернего процесса уже нет в системе. Далее родительский процесс пытается еще раз обратиться к дочернему процессу (строка 28) и, снова не обнаружив его, завершает свою работу (строки 29:30).

На этом утилита strace завершает свою работу.

Отладка

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

01: # gdb ./codesyscontrol.bin
02: GNU gdb (Raspbian 7.12-6) 7.12.0.20161007-git
03: Copyright (C) 2016 Free Software Foundation, Inc.
04: License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
05: This is free software: you are free to change and redistribute it.
06: There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
07: and "show warranty" for details.
08: This GDB was configured as "arm-linux-gnueabihf".
09: Type "show configuration" for configuration details.
10: For bug reporting instructions, please see:
11: <http://www.gnu.org/software/gdb/bugs/>.
12: Find the GDB manual and other documentation resources online at:
13: <http://www.gnu.org/software/gdb/documentation/>.
14: For help, type "help".
15: Type "apropos word" to search for commands related to "word"...
16: Reading symbols from ./codesyscontrol.bin...(no debugging symbols found)...done.
17: (gdb) set follow-fork-mode child
18: (gdb) run
19: Starting program: /home/pi/ggasss/codesyscontrol.bin
20: [Thread debugging using libthread_db enabled]
21: Using host libthread_db library "/lib/arm-linux-gnueabihf/libthread_db.so.1".
22: [New process 5379]
23: [Thread debugging using libthread_db enabled]
24: Using host libthread_db library "/lib/arm-linux-gnueabihf/libthread_db.so.1".
25: 
26: Program terminated with signal SIGKILL, Killed.
27: The program no longer exists.

Запуск отладчика gdb на исполняемый файл CODESYS Control For Raspberry Pi v3.5.14.00

Для отладки дочернего процесса для gdb необходимо выставить соответствующий режим set follow-fork-mode child (строка 17). После этого запустить CODESYS Runtime (строка 18). Далее создается дочерний процесс (строка 22), и спустя некоторое время программа самозавершается (строки 26:27).

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

Необходимо сказать, что существует возможность сэмулировать успешно созданный процесс трассировки файла, указав в качестве переменной окружения LD_PRELOAD библиотеку, которая будет содержать функции fork, ptrace, getppid и getsid. Однако на этом этапе это не будет иметь особой эффективности.

Потоки

CODESYS Runtime — многопоточное приложение. Помимо того, что запущенный процесс клонируется и выполняет трассировку дочернего процесса, дочерний процесс порождает огромное количество потоков. Системные утилиты Linux ps и htop получают список потоков, созданных процессом.

01: # ps aux | grep -i codesyscontrol
02: root      5404 10.0  0.7  11184  7448 pts/0    S    04:25   0:02 ./codesyscontrol.bin
03: root      5405  5.6  1.3  14852 13172 pts/0    SLl  04:25   0:01 ./codesyscontrol.bin
04: root      5419  0.0  0.0   4372   540 pts/0    S+   04:25   0:00 grep --color=auto -i codesyscontrol
05: 
06: # htop -p 5405
07: 
08:   PID USER      PRI  NI  VIRT   RES   SHR S CPU% MEM%   TIME+  Command
09:  5405 root       20   0 14852 13404  2684 S  4.7  1.4  0:54.62 │  └─ ./codesyscontrol.bin
10:  5416 root       20   0 14852 13404  2684 S  0.0  1.4  0:00.32 │     ├─ BlkDrvTcp
11:  5415 root       20   0 14852 13404  2684 S  0.0  1.4  0:00.37 │     ├─ BlkDrvUdp
12:  5414 root       20   0 14852 13404  2684 S  0.0  1.4  0:00.00 │     ├─ GwCommDrvTcp
13:  5413 root       20   0 14852 13404  2684 S  0.7  1.4  0:01.07 │     ├─ OPCUAServer
14:  5412 root       20   0 14852 13404  2684 S  0.7  1.4  0:00.19 │     ├─ WebServerCloseC
15:  5411 root      -70   0 14852 13404  2684 S  0.0  1.4  0:00.00 │     ├─ CAAEventTask
16:  5410 root      -95   0 14852 13404  2684 S  2.7  1.4  0:29.29 │     ├─ Schedule
17:  5409 root      -69   0 14852 13404  2684 S  0.0  1.4  0:00.00 │     ├─ SchedException
18:  5408 root       20   0 14852 13404  2684 S  1.3  1.4  0:11.77 │     └─ SchedProcessorL

Получение списка потоков, порожденных процессом исполняемого файла CODESYS Control for Raspberry Pi v3.5.14.00

По результату выполнения утилиты ps и фильтрации вывода утилитой grep видно, что дочерний процесс имеет идентификатор 5405 (строка 03).

Результат выполнения команды htop для процесса 5405 (строка 06) выводит потоки, созданные дочерним процессом (строки 09:18).

Из имен потоков частично узнаются имена компонентов группы коммуникации (компонент BlkDrvTcp, компонент BlkDrvUdp), имя индустриального протокола OPC UA.

Сетевые коммуникации

По результатам работы утилиты netstat, CODESYS Runtime слушает следующие порты:

1: # netstat -ntupl | grep -i codesys
2: tcp        0      0 0.0.0.0:11740           0.0.0.0:*               LISTEN      5405/./codesyscontr
3: tcp        0      0 0.0.0.0:1217            0.0.0.0:*               LISTEN      5405/./codesyscontr
4: tcp        0      0 127.0.0.1:4840          0.0.0.0:*               LISTEN      5405/./codesyscontr
5: tcp        0      0 192.168.0.92:4840       0.0.0.0:*               LISTEN      5405/./codesyscontr
6: udp        0      0 192.168.0.255:1740      0.0.0.0:*                           5405/./codesyscontr
7: udp        0      0 192.168.0.92:1740       0.0.0.0:*                           5405/./codesyscontr

Список прослушивающих портов, открытых процессом исполняемого файла CODESYS Control for Raspberry Pi v3.5.14.00

CODESYS Runtime слушает как TCP, так и UDP порты. TCP порт 11740 (строка 2) используется для TCP-связи между CODESYS Runtime и CODESYS Development System.

UDP порт 1740 (строка 7) используется для этой же цели, только общение осуществляется по UDP-протоколу.

Также CODESYS Runtime слушает UDP порт 1740 на широковещательном адресе (строка 6). Широковещательные адреса на стороне клиентов обычно слушаются для возможности обнаружения этих клиентов серверами, т.е. в качестве discovery service. TCP порт 4840 (строки 4:5) используется в качестве discovery service OPC UA.

Информация в открытых источниках

Поиск информации в открытых источниках — неотъемлемая часть исследовательской работы. Нами было найдено:

К сожалению, вся обнаруженная документации датируется концом 2015 года. Однако её изучение было необходимо: несмотря на устаревшие версии документов, они дали множество подсказок на вопросы, которые возникали во время исследования протокола общения между CODESYS Development System и CODESYS Runtime.

 
Продолжение во второй части

Авторы
  • Александр Ночвай