18 September 2019
Security research: CODESYS Runtime, a PLC control framework. Part 1
- The framework
- CODESYS Runtime: description of the object of research
- Information from public sources
- Investigating the CODESYS PDU protocol stack
- Detected vulnerabilities and potential attacks
- Description of the testing bench
- Attacks at the Datagram layer
- Vulnerability in the channel layer. Predictability of the channel ID
- Vulnerabilities of the Services layer
- In conclusion
Research on the security of technologies used by automation system developers that can potentially be applied at industrial facilities across the globe is a high-priority area of work for the Kaspersky Industrial Systems Emergency Response Team (Kaspersky ICS CERT).
This article continues the discussion of research on popular OEM technologies that are implemented in the products of a large number of vendors. Vulnerabilities in such technologies are highly likely to affect the security of many, if not all, products that use them. In some cases, this means hundreds of products that are used in industrial environments and in critical infrastructure facilities. This is the case with CODESYS Runtime®, a framework by CODESYS designed for developing and executing industrial control system software.
According to the developer’s official information, CODESYS Runtime has already been adapted for more than 350 devices from different vendors, used in the energy sector, industrial manufacturing, internet of things and industrial internet of things systems, etc. It should also be noted that the actual adoption figures are much higher, since many vendors’ PLCs that use the CODESYS Runtime framework are missing from the official list. The number of such devices continues to grow: there were only 140 of them in 2016. We won’t be surprised if this trend continues into the future.
Fragments of technical information were removed at the request of the CODESYS Group. Request more information from security@codesys.com.
The framework
Today, the use of ready-made software code in a product is a rule rather than an exception. This enables the developers of a new product to avoid ‘reinventing the wheel’, helping reduce development time.
The degree to which third-party code affects a software product that incorporates it, and the degree to which it affects the security of a system where that product us used, can vary.
Third-party code is often used to implement a specific function or set of functions, such as rendering images or the user interface, sending files to the printer or saving data in a database. We have conducted security research and identified vulnerabilities in third-party code before. For example, in 2017, in part of SafeNet Sentinel, a hardware-based solution designed to control licensing agreement compliance and protect applications from being ‘cracked’, and in 2018, in the OPC UA library by OPC Foundation.
The situation with the CODESYS framework is different: vendors of CODESYS-based PLCs adapt the framework for their hardware and, if necessary, develop additional modules using services provided by CODESYS. PLC end-users (i.e., engineers) use the CODESYS development environment to develop the code of industrial process automation programs. And the execution flow of the additional modules developed and the industrial process automation program is controlled on PLCs by the versions of CODESYS Runtime adapted for those PLCs.
The fact that the framework controls the execution flow of the program means that using the framework imposes restrictions at the architecture level – at the product design stage. In other words, the framework is a sophisticated mechanism that is already in place, and the user’s code must be designed to be a cog in that mechanism.
In terms of ensuring security when using a framework, the developer must address the following questions:
- What’s inside the framework?
- How does it work?
- How do I make my software secure if there is a vulnerability in the framework, rather than in my code?
This paper is devoted to research on the security of CODESYS Runtime. In it, we address the first two of the above questions: what happens inside the framework and how it works. We also demonstrate a technique for identifying vulnerabilities without being able to analyze the source code.
CODESYS Runtime: description of the object of research
Before discussing the results of research on an object’s security, it is essential to clarify what that object is. We developed the technical description of CODESYS Runtime provided in this chapter in the process of analyzing the framework.
Product bundle offered by the CODESYS Group
The CODESYS Group develops two main products:
- CODESYS Development System, a development environment;
- CODESYS Runtime, an execution environment.
The two products work together. The CODESYS Development System is an IDE used to develop software for controlling devices on which CODESYS Runtime runs. The development environment includes numerous tools designed to simplify the development and testing process.
In the context of our research, it is important that the CODESYS Development System is a customizable development environment. Solutions based on it include IDE SoMachine by Schneider Electric, TwinCAT by Beckhoff Automation, IdraWorks by Bosch, Wagilo Pro by WAGO, IDEs under the name of CODESYS Development System by Owen, STW Technic, and prolog-plc, as well as other IDEs.
To program a controller using the CODESYS Development System IDE, CODESYS Runtime should be running on the controller. For CODESYS Runtime to run correctly on a specific device, it has to be adapted to the operating system and hardware selected. According to information on the CODESYS official website, CODESYS developers themselves have only adapted CODESYS Runtime for 15 devices. However, distributors have adapted CODESYS Runtime for over 350 devices.
CODESYS Runtime adaptations include versions for:
- Single-board Linux-based computers, such as Raspberry Pi, UniPi, and BeagleBone;
- Windows and Linux based Soft PLC installations;
- PLCs by 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, and many other vendors.
Architecture
Components
CODESYS Runtime is based on a component-oriented architecture. This means that each logical or functional part of CODESYS Runtime is divided into one or more components or modules.
Each component is responsible for a specific task in a specific functional area, such as logging, network communication, communication over serial cables, core load balancing, program debugging, etc.
The following modules can be identified as the main CODESYS Runtime components:
- Component Manager or CM – the component that launches and initializes all other components on a system;
- System Components – the group of components that define communication with the operating system and the hardware. Components in this group are responsible for communicating with physical ports and with the file system, for dynamic and static memory allocation, etc.
- Communication Components – the group of components for communicating with the outside world, e.g., over the network or serial cables;
- Application components – components responsible for controlling the PLC program;
- Core components – components for controlling the PLC and its state.
A developer has several ways of extending CODESYS Runtime:
- Replacing existing components;
- Writing custom components;
- Write custom components designed to extend the functionality of existing components.
Component structure
CODESYS components are dynamic libraries (the equivalent of *.dll in Windows and *.so in Linux). All components are loaded by the Component Manager component.
CODESYS Runtime can be built either statically or dynamically.
If CODESYS Runtime is built statically, the components’ code is contained in the executable file itself.
If CODESYS Runtime is built dynamically, a list of components to be loaded is specified in the configuration file and the component files are located separately from the executable file.
The structure of a component file was not an object of this study, but it can be said with confidence that the structure includes:
- The component’s program code;
- The component’s name, the author’s name, component version and description;
- Various checksums and magic numbers.
Structure of communication interfaces
For a researcher, the structure of component files is not as interesting as how CODESYS Runtime communicates with external components and how internal components communicate with each other.
Each component must include the following functions: initialization function, export function, import function, event handling function, and a function to create and remove its instance. A component must also have a unique numeric identifier.
The functions that delete and create a component’s instance are optional. They turned out to be empty for most components. For this reason, they will not be covered by this paper. The remaining functions are discussed below.
Implementation of the initialization function
The initialization function is analogous to an entry point for PE and ELF files; the only difference is that it does not start the component’s actual operation. The function is called by the Component Manager.
01 Removed at the vendor’s request 02: { 03: Removed at the vendor’s request 04: Removed at the vendor’s request 05: Removed at the vendor’s request 06: Removed at the vendor’s request 07: Removed at the vendor’s request 08: Removed at the vendor’s request 09: Removed at the vendor’s request [...] 17: Removed at the vendor’s request 18: }
The function ModuleCmpBlkDrvUdp_entry is an initialization function. The function takes the structure INIT_STRUCT as an argument. The function is usually called using the Component Manager to populate the structure. The initialization function populates all the fields in the structure, including all the functions mentioned above and the component identifier, which is equal to 7 for the CmpBlkDrvUdp component.
Implementation of the event handling function
The next function of interest is the event handling function. For the CmpBlkDrvUdp component, this is the ModuleCmpBlkDrvUdp_hook function. It determines what the Component Manager requires it to do based on the event ID received.
Main event identifiers:
- CH_INIT_SYSTEM – ID 1. If a component is in the System Components group, it must be initialized;
- CH_INIT – ID 2. Components must initialize all local variables;
- CH_INIT2 – ID 3. The component must initialize;
- CH_INIT_TASKS – ID 5. The component can execute its threads;
- CH_INIT_COMM – ID 6. The component can start communication;
- CH_EXIT_COMM – ID 10. The component must close all communication channels;
- CH_EXIT_TASKS – ID 11. The component must stop and terminate all threads of execution created by it;
- CH_EXIT2 – ID 13. The component must save all data before calling CH_EXIT;
- CH_EXIT – ID 14. The component must release memory;
- CH_EXIT_SYSTEM – ID 15. If the component is in the System Components group, it must release memory;
- CH_COMM_CYCLE – ID 20. It is called in every cycle and is used for the execution threads created.
Like events created when calling a DLL in Windows, events handled by a CODESYS component are created in ‘mirror’ pairs: [СH_INIT_SYSTEM – CH_EXIT_SYSTEM], [СH_INIT – CH_EXIT], etc.
001: Removed at the vendor’s request 002: { 003: Removed at the vendor’s request 004: Removed at the vendor’s request 005: Removed at the vendor’s request 006: Removed at the vendor’s request 007: Removed at the vendor’s request 008: 009: Removed at the vendor’s request 010: { 011: Removed at the vendor’s request : // Initialization of the component’s variables 012: Removed at the vendor’s request 013: Removed at the vendor’s request 014: Removed at the vendor’s request 015: Removed at the vendor’s request 016: Removed at the vendor’s request 017: Removed at the vendor’s request 018: Removed at the vendor’s request 019: Removed at the vendor’s request 020: Removed at the vendor’s request 021: Removed at the vendor’s request 022: Removed at the vendor’s request 023: Removed at the vendor’s request 024: Removed at the vendor’s request 025: Removed at the vendor’s request : // Initialization of the component. Setting values from the configuration file 026: Removed at the vendor’s request 027: Removed at the vendor’s request 028: Removed at the vendor’s request 029: Removed at the vendor’s request 030: Removed at the vendor’s request 031: Removed at the vendor’s request 032: Removed at the vendor’s request 033: Removed at the vendor’s request 034: { 035: iRemoved at the vendor’s request 036: Removed at the vendor’s request 037: Removed at the vendor’s request 038: Removed at the vendor’s request 039: Removed at the vendor’s request 040: } 041: Removed at the vendor’s request 042: { 043: Removed at the vendor’s request 044: Removed at the vendor’s request 045: { 046: Removed at the vendor’s request 047: Removed at the vendor’s request 048: Removed at the vendor’s request 049: Removed at the vendor’s request 050: Removed at the vendor’s request 051: Removed at the vendor’s request 052: } 053: } 054: Removed at the vendor’s request 055: Removed at the vendor’s request // Creating a communication thread and starting communication 056: Removed at the vendor’s request 057: Removed at the vendor’s request 058: { 059: Removed at the vendor’s request 060: Removed at the vendor’s request 061: Removed at the vendor’s request 062: } 063: Removed at the vendor’s request 064: Removed at the vendor’s request : // Completing communication 065: Removed at the vendor’s request 066: Removed at the vendor’s request 067: { 068: Removed at the vendor’s request 069: Removed at the vendor’s request 070: Removed at the vendor’s request 071: } 072: Removed at the vendor’s request 073: Removed at the vendor’s request: // Saving data and releasing it 074: Removed at the vendor’s request 075: { 076: Removed at the vendor’s request 077: { 078: Removed at the vendor’s request 079: Removed at the vendor’s request 080: Removed at the vendor’s request 081: } 082: Removed at the vendor’s request 083: Removed at the vendor’s request 084: } 085: Removed at the vendor’s request 086: { 087: Removed at the vendor’s request 088: Removed at the vendor’s request 089: } 090: Removed at the vendor’s request 091: Removed at the vendor’s request // Updating the communication socket 092: Removed at the vendor’s request 093: { 094: Removed at the vendor’s request 095: Removed at the vendor’s request 096: { 097: Removed at the vendor’s request 098: Removed at the vendor’s request 099: } 100: Removed at the vendor’s request 101: } 102: Removed at the vendor’s request 103: Removed at the vendor’s request : 104: Removed at the vendor’s request 105: } 106: }
Using the function ModuleCmpBlkDrvUdp_hook as an example, we can see the following:
- Components can ignore prescribed event handling rules and use one handler to handle several different events, as implemented in the function used in this example: when handling the event CH_INIT_COMM, a thread is both initialized and executed, although the event CH_INIT_TASKS is designed to perform the latter task;
- Components do not necessarily have to handle all events. Specifically, components do not have to handle both ‘symmetrical’ events if one of the two ‘mirror’ events has already been handled. For example, the component CmpBlkDrvUdp does not handle the event CH_EXIT, although it handled the event CH_INIT.
Implementation of import and export functions
The export function and the import function use a mechanism that provides the same overall capabilities as exported and imported functions in Windows and Linux dynamic libraries. The main way in which import and export functions of CODESYS components are different from Windows and Linux libraries is that these functions register exported functions and initialize pointers that point to imported functions.
01: Removed at the vendor’s request 02: { 03: Removed at the vendor’s request 04: Removed at the vendor’s request 05: Removed at the vendor’s request 06: Removed at the vendor’s request 07: } 08: 09: Removed at the vendor’s request 10: Removed at the vendor’s request 11: Removed at the vendor’s request 12: Removed at the vendor’s request 13: 14: Removed at the vendor’s request 15: { 16: Removed at the vendor’s request 17: 18: Removed at the vendor’s request 19: 20: Removed at the vendor’s request 21: } 22:
The function CMRegisterAPI takes a pointer which points to an array populated with exported functions as its first argument and the component identifier as its last argument. Here is an example of an exported function: line 10 of the code fragment shown above contains the structure exported_function populated with the following values: pointer which points to the function sub_84e5bc0, function name “raspiyuv”, hash 0xF81Fd05, and version 0x3050400.
Thus, the CmpRasPi component provides all other CODESYS components and application programs with an API that enables them to communicate with the camera module on the Raspberry PI device. An example of the use of the API is shown below in the sample project Camera.project for 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
Each component’s import function attempts to find functions exported by other components and record their addresses.
01: Removed at the vendor’s request 02: { 03: Removed at the vendor’s request 04: Removed at the vendor’s request 05: Removed at the vendor’s request 06: Removed at the vendor’s request 07: Removed at the vendor’s request 08: Removed at the vendor’s request 09: Removed at the vendor’s request 10: Removed at the vendor’s request 11: [...] 12:
The function CMGetAPI2 looks for a function that was registered by another component. The first argument is the function’s name, the second is the value in which to save the function pointer obtained, the third is the expected hash of the function, if the hash is passed, and the last argument is the expected version.
Prior to this, all these functions were registered by the SysTarget component.
01: Removed at the vendor’s request 02: Removed at the vendor’s request 03: Removed at the vendor’s request 04: Removed at the vendor’s request 05: Removed at the vendor’s request 06: Removed at the vendor’s request 07: Removed at the vendor’s request 08: Removed at the vendor’s request 09: Removed at the vendor’s request 10: Removed at the vendor’s request 11: Removed at the vendor’s request 12: Removed at the vendor’s request 13: Removed at the vendor’s request 14: Removed at the vendor’s request 15: Removed at the vendor’s request 16: Removed at the vendor’s request
The mechanism of importing and exporting functions provides developers with core functionality for creating their own components or extending the capabilities of existing components.
Component configuration
Since the component configuration mechanism demonstrates how part of the CODESYS Runtime architecture operates, it is discussed here as a conclusion to the chapter on the architecture of CODEYS Runtime.
A CODESYS Runtime user can control components via an .ini configuration file. An .ini configuration file is a text file containing keys and parameters used to configure components.
[...] 28: [CmpWebServer] 29: ConnectionType=0 30: 31: [CmpOpenSSL] [...]
The Component Manager initializes all components. System components, such as CmpMemPool, CmpLog, CmpSettings, SysFile, etc., are the first to be initialized.
********* 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> [...]
One of the system components, CmpSettings, is of interest because its export function registers APIs that are used by all other components to get parameters from the configuration file.
01: Removed at the vendor’s request 02: Removed at the vendor’s request 03: Removed at the vendor’s request 04: Removed at the vendor’s request 05: Removed at the vendor’s request 06: Removed at the vendor’s request 07: Removed at the vendor’s request 08: Removed at the vendor’s request 09: Removed at the vendor’s request 10: Removed at the vendor’s request 11: Removed at the vendor’s request 12: Removed at the vendor’s request 13: Removed at the vendor’s request 14: Removed at the vendor’s request 15: Removed at the vendor’s request
The functions SettgGetIntValue and SettgGetStringValue are used by most components to determine their operating parameters. Using cross-references from the calls of these functions, it can be determined which components can be configured via the configuration file and which keys should be included in the configuration file.
By searching for calls of the SettgGetIntValue function using cross-references, it is possible to find the key DemoTimeUnlimited for configuring the ComponentManager component:
01: Removed at the vendor’s request 02: { 03: Removed at the vendor’s request 04: 05: Removed at the vendor’s request 06: Removed at the vendor’s request 07: Removed at the vendor’s request 08: Removed at the vendor’s request 09: Removed at the vendor’s request 10: Removed at the vendor’s request 11: Removed at the vendor’s request 12: Removed at the vendor’s request 13: Removed at the vendor’s request 14: }
Adaptation
Support for adapting CODESYS Runtime for any hardware and operating system is certainly its main feature. Developers of products that use the framework are responsible for adapting CODESYS Runtime to the needs and requirements of the specific application, including the industrial process type. The adapted CODESYS Runtime framework should be able to communicate with hardware interfaces and the Ethernet, release and allocate memory, work with the timer, events, inter-thread communication, etc.
The adaptation of system components is performed by exporting functions required by other components (this process was described in the previous chapter).
Upon analyzing several variants of CODESYS Runtime, we determined that there are a total of 25 system components.
The main system components are listed below:
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
After ensuring that the system components operate properly, the developer should create custom CODESYS Runtime modules for PLCs with the specific functionality required.
Implementation
The first version of CODESYS Control for Raspberry Pi was released in December 2016. In June 2018, a version for Linux (CODESYS Control for Linux SL) was released. There is also a CODESYS Control emulator for Windows, which is part of the CODESYS Development System software package. All these implementations are analogous to the CODESYS Control for Raspberry Pi implementation and have similar or identical implementation elements.
In this chapter, we discuss the implementation of CODESYS Runtime using CODESYS Control for Raspberry Pi and CODESYS Control for Linux SL as examples.
Installer file
The CODESYS Development System transfers a CODESYS Control installer to the Raspberry Pi device using the SSH client. The installer is a .deb (Debian binary package) file.
# 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/
The main elements of the .deb file are the configuration file and the executable file.
Configuration file
The configuration file includes a huge number of different configuration parameters for CODESYS Control. The following conclusions can be made based on the contents of the file:
- CODESYS Control for Raspberry Pi can work as a web server;
- CODESYS Control for Raspberry Pi uses OpenSSL;
- There are logging parameters for the CmpLog component;
- Parameters of the CmpSettings component can include references to other files;
- For the SysProcess component, there is the key Command.%d, the value of whose parameter is the same as the name of the shutdown system utility in Linux OS.
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
Executable file
File protection parameters
An initial analysis of executable files usually includes checking the compilation options for security parameter settings. The checksec tool shows the following security parameter values for executable files.
# ./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
Results produced by the utility demonstrate that the executable files of CODESYS Control for Raspberry Pi v3.5.14.10 were compiled without additional protection that might make exploiting binary vulnerabilities more difficult.
The situation with the compilation of the CODESYS Control for Linux SL file is slightly better, because the file has the option PIE enabled.
# ./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
The state of the executable file
Static analysis of the executable file using the IDA Pro tool shows that 99% of the file is data (shown in green) rather than machine code:
This state is typical of executable files whose machine code is packed. However, all executable files must have an entry point. For the executable file of CODESYS Runtime for Raspberry Pi, the entry point is the start function, so this function can be the one with which to start an analysis.
Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request Removed at the vendor’s request
The start function’s code is recognized normally and IDA Pro suggests that the function sub_86a0840, a.k.a. the main function, also contains valid program code.
1: Removed at the vendor’s request 2: { 3: Removed at the vendor’s request 4: Removed at the vendor’s request 5: Removed at the vendor’s request 6: Removed at the vendor’s request 7: Removed at the vendor’s request 8: }
The main function stores the number of command-line arguments used and a pointer pointing to their values in global variables (lines 3:4). Next, it calls the mprotect function (line 5), which changes the access parameters for the memory area. The first argument is a pointer pointing to the start function, which is also the beginning of the .text segment. The second argument is the size of the memory whose access parameter will be changed. The memory size should also point to the end of the segment. The last argument is the memory access parameters replacing the original parameters. It is equal to 7, i.e., the sum of the values of the parameters PROT_READ | PROT_WRITE | PROT_EXEC.
In other words, line 5 prepares a memory area in which program code is to be unpacked and executed. After that, the next function is called (line 6) and a pointer to the memory area in which the variable dword_86A0460 is stored is passed to it as an argument. The pointer points to the original main function after it has been unpacked.
Thus, for the file to be further analyzed, it needs to be unpacked.
Running process
CODESYS Runtime for Raspberry Pi and for Linux traces its process, i.e., CODESYS Runtime for Raspberry Pi and For Linux debugs itself. This mechanism is used for two purposes: to intercept system calls (syscalls) and to implement primitive anti-debugging protection: it is impossible to connect to a running CODESYS Runtime process using third-party debugging tools, such as gdb, IDA Pro, radare, or strace.
Tracing
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:
It can be seen in the log for executing the strace utility with the key -f that CODESYS Runtime changes memory access parameters (line 06), which, as discussed in the previous section, is necessary to unpack program code.
Next, the clone syscall creates a child process (line 13). The parent process has the identifier 4289. The newly created child process is assigned the identifier 4290. Since the -f key is used, strace attempts to trace child processes, causing a notification that a child process has been attached to be shown in line 14.
After that, the parent process attempts to resume the stopped child process by calling the ptrace function with the argument PTRACE_CONT (line 17). Meanwhile, the child process executes ptrace with the argument PTRACE_TRACEME (line 20), indicating by this that the process should be traced by the parent process.
However, the result of executing the function indicates that the process cannot be traced by the parent process. Due to this, the child process terminates (lines 21:22). After that, the parent process receives a response from the ptrace function (line 23) and determines that the child process no longer exists on the system. Next, the parent process makes one more attempt to call the child process (line 28) and, after failing again to find it, terminates (lines 29:30).
At this point, the strace utility terminates.
Debugging
A similar situation arises when attempting to run the executable file in the gdb debugger.
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.
To debug the child process, the relevant mode should be set for gdb: set follow-fork-mode child (line 17). After that, CODESYS Runtime is executed (line 18). Next, a child process is created (line 22) and, after some time, the program terminates (lines 26:27).
Consequently, to analyze the executable file, after being unpacked it needs to be brought to a state in which it can be debugged.
It should be noted that a successfully created file tracing process can be emulated by specifying a library containing the functions fork, ptrace, getppid and getsid as the LD_PRELOAD environment variable. However, at this stage this would not be particularly effective.
Threads
CODESYS Runtime is a multithreaded application. In addition to the running process being cloned and tracing the child process, the child process creates an enormous number of threads. Linux system utilities ps and htop get a list of threads created by the process.
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
After running the ps utility and filtering the result with the grep utility, it can be seen that the child process has the identifier 5405 (line 03).
Running the htop command for process 5405 (line 06) produces a list of threads created by the child process (lines 09:18).
Some of the component names from the communication group and the name of the OPC UA industrial protocol can be recognized in thread names (e.g., the BlkDrvTcp component and the BlkDrvUdp component).
Network communications
Based on information provided by the netstat utility, CODESYS Runtime listens on the following ports:
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 listens both on TCP and on UDP ports. TCP port 11740 (line 2) is used for TCP communication between CODESYS Runtime and the CODESYS Development System.
UDP port 1740 (line 7) is used for the same purpose, the difference being that the communication is carried out over the UDP protocol.
CODESYS Runtime also listens on a broadcast address on UDP port 1740 (line 6). The purpose of listening on broadcast addresses on the client side is usually to enable servers to discover these clients, i.e. as a discovery service. TCP port 4840 (lines 4:5) is used as an OPC UA discovery service.
Information from public sources
Searching for information in public sources is an integral part of research work. We found:
- Removed at the vendor’s request. Contains a technical overview of the CODESYS Control architecture.
- Removed at the vendor’s request. Contains a large amount of information that is useful for static analysis, such as the purpose of different functions and their arguments.
- CODESYS Control adaptation manual (removed at the vendor’s request). Describes a basic approach to porting CODESYS Runtime to a device without an OS.
Unfortunately, all the information we were able to find dates back to late 2015. However, it needed to be analyzed: although the document versions were outdated, they provided numerous clues that helped us find answers to questions which came up while researching the protocol used for communication between the CODESYS Development System and CODESYS Runtime.