18 September 2019
Security research: CODESYS Runtime, a PLC control framework. Part 3
- 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
- In conclusion
Detected vulnerabilities and potential attacks
After we created the conditions for conducting a static and dynamic analysis and peeled back the layers of the protocol, we were able to search for vulnerabilities.
This chapter will examine several detected logical vulnerabilities that could be exploited to take over a device on which CODESYS Runtime software is installed.
Based on the results of our research, we also plan to publish an article devoted to an automatic search of network binary vulnerabilities in conditions when the source code is not available.
Description of the testing bench
To demonstrate the exploitation of vulnerabilities, we will use a testing bench consisting of the following network nodes:
- Attacking computers with network addresses 192.168.0.2 and 192.168.0.30. The attacking nodes are designated as Attacker №1 and Attacker №2.
- Raspberry Pi device running CODESYS Control For Raspberry PI that has the network address 192.168.0.91. The abbreviated name Client №1 will be used hereinafter for this node.
- Raspberry Pi device running CODESYS Control For Raspberry PI that has the network address 192.168.0.92. The abbreviated name Client №2 will be used hereinafter for this node.
- Computer with CODESYS Development System installed and the network address 192.168.0.39. The abbreviated name IDE will be used for this node.
Attacks at the Datagram layer
The CODESYS PDU protocol is based on the ISO/OSI model. Therefore, we can presume that, along with the concept of the ISO/OSI model and its stack of protocols, the CODESYS PDU protocol also inherited the shortcomings of this model and the security threats corresponding to these shortcomings.
IP spoofing
Each protocol in the ISO/OSI model has its own set of threats. IP spoofing refers to an attack on the ISO/OSI model at the network layer consisting of forging the address of the message source (IP SRC) for the purpose of concealing the sender’s address. The victims who receive such a packet will process the request and return a response to the source address indicated in the field (IP SRC).
The sender’s address is concealed for the purpose of deceiving security systems and hindering discovery of the attack.
Attacks analogous to IP spoofing can be conducted over the CODESYS PDU protocol.
The following two attacks on the CODESYS PDU protocol will be examined:
- Attack aimed at concealing the address of the message source.
- Attack aimed at taking control over the existing channel of communication between nodes of the CODESYS network.
Attack aimed at concealing the address of the message source
The CmpRouter component processes fields at the Datagram layer. The header of the datagram layer contains fields for addressing, packet parameterization, channel layer service ID, and others. Among the addressing fields is the receiver field, which indicates to which address a response to a request should be sent.
As evident from the example data stream presented above, the receiver field contains the value of the last bit of the numerical representation of the IDE node address (0x27) and the value of the port index equal to 2. Both of these values match the numerical value of the message source address (the src field, which is equal to 192.168.0.39) and the value of the port from which the request was sent (the src port field, which is equal to 1742).
By changing the value in the receiver field, an attacker can implement a classic IP spoofing attack.
The CODESYS PDU protocol has another architectural vulnerability that could be exploited to implement an advanced IP spoofing attack. It is based on routing, which is one of the responsibilities of the CmpRouter component.
The sender field indicates the address of the node for which the packet is intended. The CmpRouter component redirects a received CODESYS PDU packet to a different node in the network corresponding to the one indicated in the sender field if the value of the sender field does not match the address of the node that received the packet.
By manipulating the sender field, an attacker can modify the IP spoofing attack by adding an intermediate node. The intermediate node will serve as a proxy for redirecting a malicious packet to other nodes in the CODESYS network.
The finishing stroke of the IP spoofing attack over the CODESYS PDU protocol will be the concealed receipt of a response to the redirected request. The receipt of the response can be concealed by specifying a broadcast address as the response recipient in the message.
After receiving a request in which the last byte of a broadcast address (0xff) is indicated as the response recipient in the receiver field, the node will return a response to the broadcast address.
The attacker can conceal the address of its node by receiving responses of the victim’s nodes from the broadcast node.
By automating the exploitation of detected vulnerabilities and implementing the described scheme of attack, an attacker could make it more difficult for analysts to investigate an attack.
Attack aimed at taking control of an existing channel of communication between nodes of the CODESYS network
In light of the fact that the CmpRouter component performs its functions based only on data in the CODESYS PDU packet, an attacker can infiltrate the existing communications between nodes. However, because each layer of the CODESYS PDU protocol is dependent on the previous layer, control of the last layer (Services layer) can be taken over only by taking control at all previous layers.
To interact at the Channel layer, network participants have to establish a communication channel. To query most services at the Services layer, one node must complete authentication on the other node and receive a session ID. The value of the channel ID is transmitted in the header at the Channel layer. The value of the session ID is transmitted in the header of the Services layer.
Taking control in the CODESYS PDU protocol can be divided into several tasks:
- Receiving addresses of communication participants
- Receiving the channel ID
- Receiving the BLK and ACK ID
- Receiving the session ID
The first task can be accomplished using the standard capabilities of the CODESYS PDU protocol. The third is accomplished by finding out IDs.
To accomplish the second and fourth tasks, you must know and exploit several detected vulnerabilities.
If an attacker has detected the vulnerabilities that we found and can automate an attack, he could implement the following attack scenario:
The attack algorithm may be as follows:
- OPEN_CHANNEL request:
The IDE node requests open channels on the Client #1 node. - OPEN_CHANNEL response:
The Client #1 node opens a communication channel for the IDE node and sends it the channel ID (channel_id) equal to 4. - AUTH request:
The IDE node is authenticated on the Client #1 node by sending the password and user name. - AUTH response:
The Client #1 node searches the database for an entry on the received user name. After finding the entry and comparing the password, Client #1 returns a session ID equal to 0x3456789a to the IDE node. - PROGRAM_STOP request:
An attacker from the Attacker #1 node sends a message with the command PROGRAM_STOP to the Client #1 node. This packet must indicate an incremental BLK ID from the last message of the IDE node and an ACK ID analogous to the ID in the last message of the IDE node. The IDs of the session (session_id) and channel (channel_id) match the IDs used in the communication between the nodes. - PROGRAM_STOP response:
The Client #1 node processes the request containing the PROGRAM_STOP command received from the Attacker #1 node as if the request came from the IDE node. This is because the received ID of the communication channel (channel_id) matches the existing ID of the communication channel between the IDE node and Client #1 node, the received BLK and ACK IDs are correct for the utilized channel, and the session ID is available. As a result, the Client #1 node returns a positive response about stopping the program.
Setting an arbitrary parent node
Network traffic interception is one of the threats for the data link layer of the ISO/OSI model. An attack that implements the threat of network traffic interception is called a “Man-in-the-middle” (MITM) attack.
In the ARP protocol, which operates at the data link layer of the ISO/OSI model, one of the implementations of a man-in-the-middle attack is ARP poisoning. This attack consists of modifying the ARP table on the victim’s computer by sending specially generated ARP responses to network nodes of victims. After modifications are made to the ARP table, all outbound and inbound traffic of the victim will pass through the network address of the attacker.
CODESYS Runtime does not have an ARP table. However, it has a mechanism for changing the route of a CODESYS network for which the CmpRouter component is responsible.
The address of any node in a CODESYS network consists of the addresses of all previous parent nodes and its own address as the terminal address. A CODESYS network node generates and remembers its full address after receiving a series of requests for the address service. After generating an address, the node will send all outgoing packets to the source of the request, assuming that it is the parent node.
The figure above shows 5 packets and the contents of the last packet. Below is a description of each of the packets and the sequence of actions taken by CODESYS network nodes on our test bench:
- The IDE node sends packet №1 to a broadcast address. This packet contains a request to receive information about nodes in the network. The request is intended for the name service.
- The Client #1 node receives packet №1 from a broadcast address. When processing the request, the node extracts from the receiver field the address of the node to which a response must be sent. The value of the receiver field indicates the address of the IDE node. Therefore, the Client #1 node responds with packet №2 sent to the IDE node. This packet contains information about the Client #1 node.
- Attacker #2 sends packet №3 to the broadcast address. This packet contains a request for the address service to change the parent address (service_id 2). Packet №3 is received by the Client #1 node. After processing packet №3, the Client #1 node modifies the route for its network interface and sets the address of Attacker #2 as the parent node.
- The IDE node sends a request (packet №4) to the broadcast address to receive information about nodes in the network. Packet №4 is identical to packet №1.
- The Client #1 node receives packet №4 from the broadcast address. When processing this request, the node generates a response and sends it in packet №5. This packet is sent to the address of the parent node that was assumed by the Attacker #2 node instead of the address of the IDE node specified in the receiver
Despite the fact that the Client #1 node received both requests to receive information from the same address but it sent responses to different addresses, the contents of these responses are identical. The values of the sender and receiver fields were also unchanged. In other words, the node with the modified route and the set parent node awaits delivery of the packet sent from its parent node, therefore the node does not change the values of the receiver and sender fields.
This means that an attacker can change the route of CODESYS network traffic without any privileges at the node running CODESYS Runtime. By making his machine the parent node, the attacker can infiltrate already existing or future traffic by implementing a man-in-the-middle attack.
Vulnerability in the channel layer. Predictability of the channel ID
Initially we assumed that the value of a created communication channel ID is always incremented by four from the value of the last communication channel ID. This was indicated by the log returned by the program:
To confirm this hypothesis, we researched the HandleOpenChannelReq function, which serves as the handler at the channel layer for the command to open channels (OPEN_CHANNEL). This function belongs to the CmpChannelServer component.
Call trace: 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 Pseudocode: 001: Removed at the vendor’s request 002: { [...] 064: Removed at the vendor’s request 065: { [...] 086: Removed at the vendor’s request 087: { [...] 094: Removed at the vendor’s request 095: Removed at the vendor’s request 096: Removed at the vendor’s request 097: Removed at the vendor’s request [...] 128: Removed at the vendor’s request 129: Removed at the vendor’s request 130: Removed at the vendor’s request 131: Removed at the vendor’s request 132: Removed at the vendor’s request 133: Removed at the vendor’s request 134: Removed at the vendor’s request 135: } [...] 148: Removed at the vendor’s request 149: Removed at the vendor’s request 150: Removed at the vendor’s request 151: Removed at the vendor’s request 152: Removed at the vendor’s request 153: Removed at the vendor’s request 154: { 155: Removed at the vendor’s request 156: Removed at the vendor’s request 157: } 158: }
While researching the code of the HandleOpenChannelReq function, we discovered that each new ID of the channel actually depends on the value of the previous communication channel ID. However, it is incremented not by a fixed value equal to four, but by the number of simultaneously supported communication channels (line 94).
The event processing function of the CmpChannelServer component assigns the number of simultaneously supported channels in the global variable s_iMaxServerChannels.
001: Removed at the vendor’s request 002: { [...] 008: 009: Removed at the vendor’s request 010: { 011: 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 026: Removed at the vendor’s request 027: Removed at the vendor’s request 028: Removed at the vendor’s request 029: { 030: Removed at the vendor’s request 031: Removed at the vendor’s request 032: { 033: Removed at the vendor’s request 034: Removed at the vendor’s request 035: } 036: } 037: Removed at the vendor’s request 038: Removed at the vendor’s request 039: Removed at the vendor’s request 040: Removed at the vendor’s request 041: Removed at the vendor’s request 042: Removed at the vendor’s request [...] 124: Removed at the vendor’s request 125: }
Line 24 of the pseudocode of the CmpChannelServer_hook function shows that the value of the global variable s_iMaxServerChannels is regulated by the settings of the configuration file. If a section named CmpChannelServer and a setting named MaxChannels are missing, the default value, which is equal to four, is set in the s_iMaxServerChannels variable.
An attacker can obtain the value of the s_iMaxServerChannels variable by contacting the communication channel manager using the GET_INFO command at the Channel layer or using any command for the name service at the Datagram layer. Privileges are not required for execution of these commands.
Call trace: 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 Pseudocode: 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: }
The HandleInfoReq function processes the GET_INFO command for the communication channel manager at the Channel layer. The global variable s_iMaxServerChannels will be written to the last two bytes of the response (line 08).
Depending on the settings used in the configuration file, CODESYS Runtime may fail to process information requests at the Datagram layer and at the Channel layer. However, an attacker can always send multiple paired requests to open and close a channel. The difference between two consecutively received IDs of channels will unequivocally identify the number of simultaneously supported channels on a CODESYS Runtime node.
Vulnerabilities of the Services layer
Vulnerabilities in the authentication system
This chapter will examine two detected vulnerabilities in the authentication system, namely the vulnerability in session ID generation and in password decryption. Exploitation of the first vulnerability can lead to prediction of a session ID. Exploitation of the second vulnerability can lead to decryption of an intercepted password.
Vulnerability in the predictability of session ID generation
For most services, the node must complete authentication. A request to complete authentication is processed by the DeviceServiceHandler function, which is registered by the CmpDevice component as a service with the ID of 1. The DeviceServiceHandler function was examined as an example of tag processing in the chapter titled “Processing tags”.
When the DeviceServiceHandler function processes an incoming request containing a command whose ID is equal to 2, the DeviceServiceHandler function transfers management to the ServerGenerateSessionId function (examined below).
001: Removed at the vendor’s request 002: { [...] 129: Removed at the vendor’s request 130: Removed at the vendor’s request 131: Removed at the vendor’s request 132: Removed at the vendor’s request 133: Removed at the vendor’s request 134: { [...] 254: Removed at the vendor’s request [...] 262: Removed at the vendor’s request 263: Removed at the vendor’s request [...] 365: Removed at the vendor’s request 366: } 367: Removed at the vendor’s request [...] 389: Removed at the vendor’s request 390: Removed at the vendor’s request 391: Removed at the vendor’s request 392: Removed at the vendor’s request 393: Removed at the vendor’s request 394: Removed at the vendor’s request 395: Removed at the vendor’s request 396: Removed at the vendor’s request 397: Removed at the vendor’s request 398: Removed at the vendor’s request [...] 761: } 762: Removed at the vendor’s request 763: }
A response to a successful authentication request contains a data tag with the ID 0x21 (line 392). The data of this tag contains a generated ID for the created session (line 393). The session ID itself is created within the HandleLoginSessionId function even before the received authentication data is verified (line 263). The generated ID is returned in the ulSessionId variable.
01: Removed at the vendor’s request 02: { [...] 09: Removed at the vendor’s request 10: Removed at the vendor’s request 11: Removed at the vendor’s request 12: { 13: Removed at the vendor’s request 14: Removed at the vendor’s request 15: } 16: Removed at the vendor’s request 17: { 18: *Removed at the vendor’s request 19: Removed at the vendor’s request 20: } 21: Removed at the vendor’s request 22: { 23: *Removed at the vendor’s request 24: Removed at the vendor’s request 25: } 26: Removed at the vendor’s request 27: Removed at the vendor’s request 28: }
The ServerGenerateSessionId function (line 13), which generates the session ID, is queried within the HandleLoginSessionId function. This function is queried under the condition that the received session ID is equal to one of the following numbers: 0x0, 0x11 or 0x815. An additional condition for creating a session ID is that a session ID must not already exist for the received channel ID (line 10).
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: } 08: 09: Removed at the vendor’s request 10: { 11: Removed at the vendor’s request [...] 15: Removed at the vendor’s request 16: Removed at the vendor’s request 17: Removed at the vendor’s request 18: Removed at the vendor’s request 19: *Removed at the vendor’s request 20: *Removed at the vendor’s request 21: Removed at the vendor’s request 22: }
The ServerGenerateSessionId function is exported by the CmpSrv component. Its algorithm is described below:
- Verify the presence of an argument handle (lines 15 and 16).
- Receive the current time by calling the SysTimeGetMs function (line 17).
- Initialize the pseudo-random number generator. Set the value received at the preceding step as the seed value for the generator (line 18).
- Add the received time value to the random number and write it to the value for the argument handle (line 19).
- Set the most significant bit in the value for the argument handle (line 20).
This generator algorithm uses a pseudo-random number generator. This means that the generated random number depends on the set seed value and may be restored. An attacker can find out session IDs by decrementing the seed values and recreating the session ID for the modified seed. This can be done within an acceptable time interval, even though the attack will occur remotely.
The second weakness of this algorithm is the use of the SysTimeGetMs function. This function is exported by the system component SysTimer. To correctly run CODESYS Runtime, the developer must implement all system components. This means that the provided implementation of the SysTimeGetMs function may differ from the implementation of this function in a different CODESYS Runtime. In terms of security, this implementation of the function that generates a session ID imposes additional responsibility on the developer that will adapt the system components, including the SysTimer component.
For instance, in the analogous example of a response to an authentication request presented in the chapter titled “Processing tags”, the fragment of the response contains a data tag with the session ID setting, and a seed with the value 356267299 was used to generate a session ID value equal to 0xc5946b05.
Vulnerabilities in password encryption
In the DeviceServiceHandler function registered as a service by the CmpDevice component, multiple vulnerabilities in the password encryption mechanism were detected. These vulnerabilities can be exploited to decrypt an intercepted password.
A request to complete authentication is processed by the DeviceServiceHandler function, which is registered by the CmpDevice component as a service with the ID of 1. The DeviceServiceHandler function was examined as an example of tag processing in the chapter titled “Processing tags”.
To complete authentication in CODESYS Development System, a request must be sent for the DeviceServiceHandler service containing a command with an ID equal to 2. The data sent for authentication will include the encrypted password that will be decrypted during processing by the service.
001: Removed at the vendor’s request 002: { [...] 130: Removed at the vendor’s request [...] 133: Removed at the vendor’s request 134: { [...] 254: Removed at the vendor’s request [...] 266: Removed at the vendor’s request 267: Removed at the vendor’s request 268: { 269: Removed at the vendor’s request 270: Removed at the vendor’s request 271: { 272: Removed at the vendor’s request 273: Removed at the vendor’s request 274: Removed at the vendor’s request 275: Removed at the vendor’s request 276: Removed at the vendor’s request 277: Removed at the vendor’s request 278: { 279: Removed at the vendor’s request 280: Removed at the vendor’s request 281: { 282: Removed at the vendor’s request 283: } 284: Removed at the vendor’s request 285: { 286: Removed at the vendor’s request 287: Removed at the vendor’s request 288: } 289: Removed at the vendor’s request 290: { 291: Removed at the vendor’s request 292: } 293: Removed at the vendor’s request 294: Removed at the vendor’s request 295: Removed at the vendor’s request 296: } 297: Removed at the vendor’s request 298: Removed at the vendor’s request 299: Removed at the vendor’s request 300: Removed at the vendor’s request 301: Removed at the vendor’s request 302: Removed at the vendor’s request 303: Removed at the vendor’s request 304: } 305: Removed at the vendor’s request 306: Removed at the vendor’s request 307: Removed at the vendor’s request 308: } [...] 315: Removed at the vendor’s request 316: { 317: Removed at the vendor’s request 318: Removed at the vendor’s request [...] 327: } [...] 761: } 762: Removed at the vendor’s request 763: }
The received password is decrypted in the UserMgrDecryptPassword function (line 318). This function uses the following values as arguments:
- encrypted_password – value of the encrypted password that is extracted from the data tag with the ID 17 (line 286).
- pulCrypeType – ID of the encryption algorithm that was used to encrypt the transmitted password. The value of the ID is extracted from the data tag with the ID 0x22 (line 299).
- pulChallenge – random number value (nonce) involved in password decryption. The value of the random number is extracted from the data tag with the ID 0x23.
01: Removed at the vendor’s request 02: { [...] 16: Removed at the vendor’s request 17: Removed at the vendor’s request 18: Removed at the vendor’s request 19: Removed at the vendor’s request 20: Removed at the vendor’s request 21: Removed at the vendor’s request 22: { 23: Removed at the vendor’s request 24: { 25: Removed at the vendor’s request 26: Removed at the vendor’s request 27: Removed at the vendor’s request 28: Removed at the vendor’s request 29: Removed at the vendor’s request 30: Removed at the vendor’s request 31: { 32: Removed at the vendor’s request 33: Removed at the vendor’s request 34: Removed at the vendor’s request 35: Removed at the vendor’s request 36: Removed at the vendor’s request 37: Removed at the vendor’s request 38: } 39: Removed at the vendor’s request 40: } [...]
The UserMgrDecryptPassword function performs the following actions:
- Compares the values of the pulCrypeType argument with 1 (line 16). If these values are different, the function is not processed further. In other words, CODESYS Runtime provides only one encryption algorithm for a transmitted password, and the pulCrypeType value must be transmitted each time during authentication, despite the lack of alternatives for selecting the password encryption algorithm.
- Writes a fixed key to the local key variable (line 18).
- From the 4-byte value of the pulChallenge argument, writes only the least significant bit to the 4-byte aChallenge array (line 25). Zero values are set for the remaining three bytes of the array (lines 26-28).
- Then the function uses three indexes: index – password index, index_key – key index, and challenge_index – random number index. The password index identifies the end of decryption of an encrypted password. When the password index is equal to the size of the encrypted password, the UserMgrDecryptPassword function terminates and returns management to the DeviceServiceHandler function (line 32). The remaining two indexes (random number index and key index) will be zeroed each time the size of their variables (aChallenge and key) are equal to the value of the index (line 34 for the key variable and line 36 for the aChallenge variable).
- A character-by-character decryption of a password is provided below. Each decrypted character is obtained by adding one byte taken from the ulChallenge array for the random number index (challenge_index) and one byte taken from the key variable for the key index (index_key). For the obtained number, an XOR operation is performed with the byte taken from the encrypted_key password argument for the encrypted password index (line 32).
If the UserMgrDecryptPassword function is successfully performed, the obtained password will be decrypted and written to the decrypted_password argument.
From the described algorithm of the function, three vulnerabilities can immediately be emphasized:
- Using a weak random number. Despite the fact that the DeviceServiceHandler function registered by the CmpDevice component for the authentication command extracts a 4-byte numerical value from received data tags, only one of the four bytes is used to decrypt the obtained password. Because the encryption algorithm is symmetrical, only one byte is involved in password encryption as well.
Use of a random one-byte value for password protection does not increase the security of a transmitted password because this value can be obtained by brute force within a very short period of time.
- Using an arbitrary value as the random number. The examined algorithm of the DeviceServiceHandler function shows that the service authentication command of the CmpDevice component uses the obtained numerical parameter from the authenticated node as the random number (aChallenge) for password decryption.
In other words, despite the fact that the side performing the authentication sends a random number that must be used in password encryption when it receives an authentication request, the side that is being authenticated can encrypt the password with its own number and transmit this number for decryption.
This means that an attacker who has intercepted the authentication data one time can resend it without modifications to complete authorization.
- Using a fixed key. The examined decryption algorithm uses a fixed key for password encryption and decryption. This means that an attacker who has received the encrypted authentication data can always decrypt an intercepted password.
Based on the example of a packet containing an authentication request examined in the chapter titled “Processing tags”, we will show how a transmitted password can be partially decrypted.
The encrypted password is transmitted in the data tag with the ID 0x11, which is designated as Data_tag_4. You can partially restore a password by extracting data of the encrypted password from the data tag and perform the XOR operation with bytes of the key for the same indexes for each byte of data.
1: encrypted_password = "\xce\x01\x29\x3b\x20\x5f\x36\x12\x18\x42\x46\x58\xf9\x75\x70\x68\x4c\x54\x68\x75\x77\x3f\x70\x68\x76\x44\x72\x2a\x87\x55\x62\x52" 2: KEY = "zeDR96EfU#27vuph7Thub?phaDr*rUbR" 3: for c, s in enumerate(encrypted_password): 4: print chr(ord(KEY[c]) ^ ord(encrypted_password[c])), 5: 6: � d m i i s t M a t o �
A partially restored password contains the letters d, m, I, I, s, t, a, t, o (line 6). These characters are included in the “Administrator” string, which is the default password for an Administrator.
Vulnerability of application code
One of the tasks performed by CODESYS Runtime is loading, managing, and executing applications. CODESYS Development System compiles an application for CODESYS Runtime and loads it over the CODESYS PDU protocol. A loaded application is a stream of binary data.
During our research, we did not focus on research of the structure of a compiled application. (It should be noted that the U.S. Office of Naval Research supported research work that was conducted to analyze the contents of a compiled application for CODESYS Runtime. This work resulted in the development of the ICSREF framework. )
When studying the contents of packets sent from CODESYS Development System to CODESYS Runtime while an application is being loaded, we detected places in the binary stream where we could inject arbitrary machine code – shellcode.
Two functions turned out to be our backdoors: the function for initialization of global variables (named Global INIT in the cited work) and the program start function (named PLC_PRG in the cited work).
Header: 1: PROGRAM PLC_PRG 2: VAR 3: magic: DWORD:= 16#DEADBEEF; 4: END_VAR Body: 5: magic := magic + 16#BEEF;
To confirm the capability of injecting arbitrary machine code for the purpose of executing it in the binary stream of an application, we used CODESYS Development System to compile the program and remotely downloaded it to a Raspberry PI device running CODESYS Runtime. In the variable declaration block, the magic variable was declared with the value 0xDEADBEEF (line 3). The program body block indicates that the value of the magic variable must be permanently stored with the value 0xBEEF and the obtained result must be written to the magic variable (line 5).
After downloading the compiled program to the Raspberry Pi device, the traffic was searched for EF BE bytes. These bytes are contained in the numbers 0xDEADBEEF and 0xBEEF when the bytes are ordered from least significant to most significant (little endian). Only two references of these bytes were detected in traffic (highlighted in red). It should be noted that in one packet, AD DE bytes (highlighted in blue) were detected next to the sought EF BE bytes.
Then the entire loaded stream of binary data was analyzed for the presence of machine instructions of the ARM processor on which the Raspberry Pi is running. While searching for EF BE bytes, we detected an instruction querying the number 0xDEADBEEF and an instruction querying the number 0xBEEF.
We will examine the instructions of the first query.
01: 00 00 00 60 ANDVS R0, R0, R0 02: A0 01 D8 00 SBCEQS R0, R8, R0,LSR#3 03: 21 06 03 00 ANDEQ R0, R3, R1,LSR#12 04: 50 8A 01 00 ANDEQ R8, R1, R0,ASR R10 05: 22 CC 80 00 ADDEQ R12, R0, R2,LSR#24 06: 48 00 00 00 ANDEQ R0, R0, R8,ASR#32 07: 00 44 2D E9 STMFD SP!, {R10,LR} 08: 0D A0 A0 E1 MOV R10, SP 09: 08 D0 4D E2 SUB SP, SP, #8 10: 10 08 2D E9 STMFD SP!, {R4,R11} 11: 00 40 A0 E3 MOV R4, #0 12: 09 40 CA E5 STRB R4, [R10,#9] 13: 00 40 A0 E3 MOV R4, #0 14: 08 40 0A E5 STR R4, [R10,#-8] 15: 00 40 A0 E3 MOV R4, #0 16: 04 40 4A E5 STRB R4, [R10,#-4] 17: 14 40 9F E5 LDR R4, =0xDEADBEEF 18: 0C B0 9F E5 LDR R11, =0x3870 19: 00 40 8B E5 STR R4, [R11] 20: 10 08 BD E8 LDMFD SP!, {R4,R11} 21: 08 D0 8D E2 ADD SP, SP, #8 22: 00 84 BD E8 LDMFD SP!, {R10,PC}
The constant 0xDEADBEEF is written to register R4 at line 17. The constant 0x3870 is written to register R11 at line 18. At line 19, the value of the R4 register (0xDEADBEEF) is written at the address of the value of the R11 register (0x3870).
01: 00 00 00 60 ANDVS R0, R0, R0 02: A0 01 C0 00 SBCEQ R0, R0, R0,LSR#3 03: 21 06 03 00 ANDEQ R0, R3, R1,LSR#12 04: 28 15 01 00 ANDEQ R1, R1, R8,LSR#10 05: 22 B4 80 00 ADDEQ R11, R0, R2,LSR#8 06: 30 00 00 00 ANDEQ R0, R0, R0,LSR R0 07: 00 44 2D E9 STMFD SP!, {R10,LR} 08: 0D A0 A0 E1 MOV R10, SP 09: 30 00 2D E9 STMFD SP!, {R4,R5} 10: 18 B0 9F E5 LDR R11, =0x3870 11: 00 40 9B E5 LDR R4, [R11] 12: 0C 50 9F E5 LDR R5, =0xBEEF 13: 05 40 84 E0 ADD R4, R4, R5 14: 00 40 8B E5 STR R4, [R11] 15: 30 00 BD E8 LDMFD SP!, {R4,R5} 16: 00 84 BD E8 LDMFD SP!, {R10,PC}
In contrast to the machine code of the first query, the machine code of the second query works in reverse. First the constant 0x3870 is written to the R11 register (line 10). Then the memory cell contents based on the address of the constant 0x3870 (line 11) is written to the R4 register. Then the constant 0xBEEF is written to the R5 register (line 12). The value of the R5 register (0xBEEF) is added to the value at the address of 0x3870 (line 13). The resulting sum of the saved value from memory and the constant 0xBEEF is written to the R4 register, whose value is then written to the memory cell at the address of 0x3870.
Based on the examined queries, we can assume that the address of the declared global magic variable is located at the address of 0x3870. The value 0xDEADBEEF is written to the magic variable in the examined machine code of the first detected query. In the second detected query, the number 0xBEEF is added to the magic variable. The result of this addition is again written to the address of the global magic variable (0x3870). Both fragments of machine code correspond to the source code of the program PLC_PRG.
Machine instructions of an ARM processor are also transmitted over the CODESYS PDU protocol. In the example of the second query, the instruction ADD R4, R4, R5 (line 13) corresponds to bytes 05 40 84 E0 (highlighted in green in the packet), and the next instruction STR R4, [R11] (line 14) corresponds to bytes 00 40 8B E5 (highlighted in orange).
Therefore, an attacker can inject his own machine code and execute it on a target device. It should be mentioned that the system daemons of CODESYS Runtime on a Raspberry Pi device are run as a background process (daemon) under the user name root, which has the highest level of privileges in Linux OS. Therefore, executable machine code will also run with the highest privileges in the system. The CODESYS Runtime emulator in Windows OS is also run with the highest permissions in the system: SYSTEM permissions.
In conclusion
CODESYS Runtime is a sophisticated and powerful tool designed for developing PLC programs and controlling PLCs. At the same time, it implements an effective architectural approach that enables the capabilities of CODESYS Runtime itself to be expanded. The protocol used for communication between the development environment, i.e., the CODESYS Development System, and the execution environment, i.e., CODESYS Runtime, is multi-layer, dynamic and, most importantly, proprietary.
Ultimately, the use of proprietary protocols negatively affects PLC vendors, because they have limited knowledge of the software they use and have to entrust the initial protection of their PLCs blindly to the framework’s developers.
CODESYS has made many steps to improve the security of their products. However, as our research of the security of CODESYS Runtime has demonstrated, these steps are insufficient.
While analyzing the security of CODESYS Runtime, we identified 15 vulnerabilities and reported them to the software vendor. Of those vulnerabilities reported, the vendor was already aware of 5 (i.e., duplicates), 2 were referred to as architectural features by the CODESYS security group and the remaining 8 were fixed. The vulnerabilities that were fixed had CVSS base scores of 5.4 to 9.
It is worth noting that CODESYS subsequently did fix one of the vulnerabilities they had called architectural features.
Our research was based on the black box method, which means that we had originally had no information on CODESYS Runtime. Any information we have has been obtained from public sources and from technical research.
After analyzing the data transferred over the CODESYS protocol and correlating CODESYS Runtime software code with the data received over the network, we were able to identify four vulnerabilities in the authentication mechanism – in the Services layer (which is the last of four layers in the CODESYS protocol stack). These vulnerabilities were assigned the following IDs: KLCERT-18-037 (CVE-2018-20025) and KLCERT-19-031 (CVE-2019-9013). By exploiting these vulnerabilities in the authentication system, an attacker could be able to decrypt the password being sent, implement an attack in which encrypted authentication data is reused without being modified and predict the session ID.
CODESYS developers built their protocol on the TCP/IP protocol stack. As a result of this, CODESYS has inherited some of the issues characteristic of TCP/IP: we identified vulnerabilities in the Datagram layer (the second of four layers) and the Channel layer (the third of four layers), the possible existence of which in the TCP/IP protocol stack was reported back in 1989 (see Security Problems in the TCP/IP Protocol Suite).
In the Datagram layer of the CODESYS protocol stack, we determined that an attack identical to IP spoofing was possible. This vulnerability was assigned the ID KLCERT-18-036 (CVE-2018-20026). By automating the exploitation of this vulnerability, attackers could disguise their activity on the network for a long time, manipulating devices with CODESYS Runtime running on them and making these devices send malicious packets to each other.
We also discovered that an attack similar to the infamous ARP spoofing attack was possible against the Datagram layer: the routing mechanism used on the CODESYS network makes it possible to build an information network based on the tree topology from CODESYS Runtime nodes. If the parent node can be changed without authentication, this makes man-in-the-middle attacks possible. Thus, an attacker can use the protocol’s capabilities at the datagram level to inform a CODESYS Runtime host that it has become that host’s new parent, which will result in the host sending all of its outgoing traffic via the new parent.
The last specimen in the classical vulnerability zoo is the absence of a sandbox for the program downloaded to the device. In the process of analyzing the protocol, it was determined that some fragments of the program downloaded to the device are machine instructions. The hypothesis that arbitrary code (shellcode) can be injected instead of these instructions was confirmed. The vulnerability was assigned the ID KLCERT-18-035 (CVE-2018-10612).
Since the CODESYS Runtime daemon in Linux and the CODESYS Runtime service in Windows run with the highest privileges (root and SYSTEM, respectively), arbitrary code will also run with the highest privileges. This means that the attacker will not need to perform any additional manipulations in the system or exploit any further vulnerabilities to achieve the highest privilege level.
As information security experts know from their many years of experience, the ‘security by obscurity’ approach is not the best strategy for protecting information. This is certainly true of undocumented, proprietary network communication protocols. Any such protocol will eventually be analyzed and its vulnerabilities identified. Unfortunately, in many cases threat actors will do this sooner than white-hat researchers, if only because they have a much stronger motivation.
We believe that all the vulnerabilities described in this article, and possibly others as well, could have been found by the community of information security experts and enthusiasts at the early stages of the protocol’s design, development and use. If the protocol specification had been available to potential users, its vulnerabilities could have been identified during its discussion and analysis and would not have affected products by hundreds of developers installed at tens or even hundreds of thousands of industrial facilities.
At the current stage, such work takes much greater effort and requires much more specialist knowledge, which, unfortunately, may be inaccessible to many developers who use CODESYS in their solutions. Perhaps, in this respect, the development approach selected was not optimal.
The CODESYS security group responded to information on the vulnerabilities identified promptly and responsibly.
We sincerely thank the CODESYS team for their cooperation.
pi@raspberrypi:~ $ ./opt/codesys/bin/codesyscontrol.bin -vvvvvvv CODESYS Control V3.5.12.0 for ARM - build Dec 18 2017 type:4102 id:0x00000010 name:CODESYS Control for Raspberry Pi SL vendor: 3S - Smart Software Solutions GmbH buildinformation: <none> _________ < ... bye > --------- \ ^__^ \ (--)\_______ (__)\ )\/\ ||----w | || ||