17 сентября 2019
Исследование безопасности: CODESYS Runtime — фреймворк для управления ПЛК. Часть 1
- Фреймворк
- Описание объекта исследования: CODESYS Runtime
- Информация в открытых источниках
- Исследование стека протоколов CODESYS PDU
- Обнаруженные уязвимости и возможные атаки
- Итоги
Исследование безопасности технологий, которые используются разработчиками систем автоматизации и имеют потенциал применения на промышленных объектах по всему миру, является одним из приоритетных направлений работы Центра реагирования на инциденты информационной безопасности промышленных инфраструктур «Лаборатории Касперского» (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 разрабатывает два основных продукта:
- Среда разработки — CODESYS Development System
- Среда исполнения — 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 можно выделить основные:
- Component Manager, или CM — компонент для запуска и инициализации всех остальных компонентов в системе;
- System Components — группа компонентов для описания взаимодействий с операционной системой и с железной составляющей. Компоненты из этой группы отвечают за взаимодействие с физическими портами, с файловой системой, с работой по динамическому и статическому выделению памяти и т.д.
- Communication Components – группа компонентов для взаимодействия с внешним миром, например по сети или через последовательный кабель;
- Application components — компоненты управления программой ПЛК;
- Core components — компоненты для управления ПЛК и его состоянием.
У разработчика есть несколько способов расширить CODESYS Runtime:
- Заменить существующие компоненты;
- Написать собственные компоненты;
- Написать собственные компоненты, которые будут расширять функциональность уже существующих компонентов.
Структура компонентов
Компоненты 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: }
Функция 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. Вызывается каждый цикл. Используется для созданных потоков выполнения.
События для обработки компонентом, как и события вызова 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 видно следующее:
- Компоненты могут пренебрегать предусмотренными правилами работы с событиями и объединять обработку нескольких событий в одном обработчике, как это сделано, например, в рассматриваемой функции: при обработке события 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:
Функция 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
Функция импорта в компонентах пытается найти функции, экспортируемые другими компонентами, и запомнить их местоположение.
01: Удалено по требованию вендора 02: { 03: Удалено по требованию вендора 04: Удалено по требованию вендора 05: Удалено по требованию вендора 06: Удалено по требованию вендора 07: Удалено по требованию вендора 08: Удалено по требованию вендора 09: Удалено по требованию вендора 10: Удалено по требованию вендора 11: [...] 12:
Функция 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] [...]
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> [...]
Среди системных компонентов есть компонент CmpSettings. Он интересен нам тем, что функция экспорта этого компонента регистрирует API, которыми пользуются все остальные компоненты для получения параметров из файла конфигурации.
01: Удалено по требованию вендора 02: Удалено по требованию вендора 03: Удалено по требованию вендора 04: Удалено по требованию вендора 05: Удалено по требованию вендора 06: Удалено по требованию вендора 07: Удалено по требованию вендора 08: Удалено по требованию вендора 09: Удалено по требованию вендора 10: Удалено по требованию вендора 11: Удалено по требованию вендора 12: Удалено по требованию вендора 13: Удалено по требованию вендора 14: Удалено по требованию вендора 15: Удалено по требованию вендора
Функции SettgGetIntValue и SettgGetStringValue используются большинством компонентов для определения параметров своей работы. По перекрестным ссылкам вызовов этих функций можно определить, какие компоненты могут быть настроены через файл конфигурации, и какие ключи необходимо указать в файле конфигурации.
Используя метод поиска вызовов функции SettgGetIntValue, по перекрестным ссылкам можно найти ключ DemoTimeUnlimited для настройки компонента ComponentManager:
01: Удалено по требованию вендора 02: { 03: Удалено по требованию вендора 04: 05: Удалено по требованию вендора 06: Удалено по требованию вендора 07: Удалено по требованию вендора 08: Удалено по требованию вендора 09: Удалено по требованию вендора 10: Удалено по требованию вендора 11: Удалено по требованию вендора 12: Удалено по требованию вендора 13: Удалено по требованию вендора 14: }
Адаптация
Возможность адаптировать CODESYS Runtime под любое железо и операционную систему — несомненно, главная его особенность. На разработчиках продукта с использованием фреймворка лежит ответственность по адаптации системных компонентов CODESYS Runtime под нужды и требования конкретного применения, включая тип технологического процесса. Адаптированный фреймворк CODESYS Runtime должен уметь взаимодействовать с аппаратными интерфейсами, Ethernet, освобождать и выделять память, работать с таймером, событиями, межпотоковым взаимодействием и т.д.
Адаптация системных компонентов происходит путем экспортирования функций, которые требуют остальные компоненты (этот путь описан в предыдущей главе).
Проведя анализ нескольких вариантов 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-файла являются файл конфигурации и исполняемый файл.
Файл конфигурации
Файл конфигурации содержит огромное количество различных параметров конфигурации для 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
Исполняемый файл
Параметры защиты файла
Первичный анализ исполняемых файлов, как правило, включает в себя проверку опций компиляции на предмет установки параметров защиты. Инструмент 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
По результатам работы утилиты видно, что исполняемые файлы 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
Состояние исполняемого файла
Статический анализ исполняемого файла с помощью инструмента IDA Pro показывает, что этот файл на 99% состоит из данных (зеленый цвет), а не из машинного кода:
Обычно такое состояние характерно для исполняемых файлов, машинный код которых был запакован. Тем не менее, для всех исполняемых файлов обязательным атрибутом является наличие точки входа (Entry point). Для исполняемого файла CODESYS Runtime for Raspberry Pi точкой входа является функция start. С этой функции можно начать анализ.
Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора Удалено по требованию вендора
Код функции start нормально распознается, и IDA Pro подсказывает, что функция sub_86a0840, она же функция main, также содержит валидный программный код.
1: Удалено по требованию вендора 2: { 3: Удалено по требованию вендора 4: Удалено по требованию вендора 5: Удалено по требованию вендора 6: Удалено по требованию вендора 7: Удалено по требованию вендора 8: }
Функция 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 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 необходимо выставить соответствующий режим 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
По результату выполнения утилиты 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 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.
Информация в открытых источниках
Поиск информации в открытых источниках — неотъемлемая часть исследовательской работы. Нами было найдено:
- Удалено по требованию вендора. Содержит технический обзор архитектуры CODESYS Control.
- Удалено по требованию вендора. Содержит много полезной информации для статического анализа, например, аргументы и назначения функций.
- Руководство по адаптации CODESYS Control (удалено по требованию вендора). Описывает базовый подход по портированию CODESYS Runtime на устройство без ОС.
К сожалению, вся обнаруженная документации датируется концом 2015 года. Однако её изучение было необходимо: несмотря на устаревшие версии документов, они дали множество подсказок на вопросы, которые возникали во время исследования протокола общения между CODESYS Development System и CODESYS Runtime.