Home / Reports / Security research: CODESYS Runtime, a PLC control framework. Part 1

Security research: CODESYS Runtime, a PLC control framework. Part 1

Download PDF version

 

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:

  1. CODESYS Development System, a development environment;
  2. 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.

CODESYS Runtime component-oriented architecture (source)

The following modules can be identified as the main CODESYS Runtime components:

  1. Component Manager or CM – the component that launches and initializes all other components on a system;
  2. 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.
  3. Communication Components – the group of components for communicating with the outside world, e.g., over the network or serial cables;
  4. Application components – components responsible for controlling the PLC program;
  5. Core components – components for controlling the PLC and its state.

A developer has several ways of extending CODESYS Runtime:

  1. Replacing existing components;
  2. Writing custom components;
  3. 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.

Decompiled code of the inutialization function of CmpBlkDrvUdp component from the Communication group

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.

CODESYS component lifecycle compared to Windows Dynamic Link Library lifecycle

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.

Decompiled code of the ModuleCmpBlkDrvUdp_hook function, demomstrating the handling of event by the CmpBlkDrvUdp component

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.

Pseudocode for the export function of the CmpRasPi component (available only in CODESYS Runtime for Raspberry PI)

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.

Camera.project program code

Each component’s import function attempts to find functions exported by other components and record their addresses.

Import function in the CmpApp module

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.

Fragment of the exported functions array

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.

Fragment of configuration file for CODESYS Control for Raspberry Pi

The Component Manager initializes all components. System components, such as CmpMemPool, CmpLog, CmpSettings, SysFile, etc., are the first to be initialized.

Fragment of CODESYS Control for Raspberry Pi startup log

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.

Fragment of the exported functions array of the CmpSettings component

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:

Configuration keys for the ComponentManager component

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.

System components from the CODESYS Runtime component-oriented architecture

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:

The main system components

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.

Contents of the .deb file

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.

Contents of the configuration file for CODESYS Control For Raspberry Pi v3.5.14.10

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.

Result of checking CODESYS Control for Raspberry Pi v3.5.14.10 executable files with the checksec utility

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.

Result of running the checksec utility on the executable files of CODESYS Control for Linux SL v3.5.14.10

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:

State of the packed executable file of CODESYS Runtime For Raspberry Pi

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.

Assembler code of the start function

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.

Decompiled pseudocode of the function sub_86a0840

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

Launching the strace utility with the key –f with the executable file of CODESYS Control For Raspberry Pi v3.5.14.00

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.

Starting the gdb debugger with the executable file of CODESYS Control For Raspberry Pi v3.5.14.00

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.

Getting a list of threads created by the process of the executable file of CODESYS Control for Raspberry Pi v3.5.14.00

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:

List of listening ports opened by the process of the executable file of CODESYS Control for Raspberry Pi v3.5.14.00

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:

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.

 
Continue to part 2

Download PDF version
Alexander Nochvay

Alexander Nochvay

Security Researcher, KL ICS CERT