Talos Vulnerability Report

TALOS-2024-2016

OpenPLC OpenPLC_v3 OpenPLC Runtime EtherNet/IP parser invalid pointer dereference vulnerabilities

September 18, 2024
CVE Number

CVE-2024-39590,CVE-2024-39589

SUMMARY

Multiple invalid pointer dereference vulnerabilities exist in the OpenPLC Runtime EtherNet/IP parser functionality of OpenPLC_v3 16bf8bac1a36d95b73e7b8722d0edb8b9c5bb56a. A specially crafted EtherNet/IP request can lead to denial of service. An attacker can send a series of EtherNet/IP requests to trigger these vulnerabilities.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

OpenPLC _v3 16bf8bac1a36d95b73e7b8722d0edb8b9c5bb56a

PRODUCT URLS

OpenPLC_v3 - https://github.com/thiagoralves/OpenPLC_v3

CVSSv3 SCORE

7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CWE

CWE-704 - Incorrect Type Conversion or Cast

DETAILS

OpenPLC is an open-source programmable logic controller (PLC) designed to provide a low cost option for automation. The platform consists of two parts: the Runtime and the Editor. The Runtime can be deployed on a variety of platforms including Windows, Linux, and various microcontrollers. Common uses for OpenPLC include home automation and industrial security research. OpenPLC supports communication across a variety of protocols, including Modbus and EtherNet/IP. The runtime additionally provides limited support for PCCC transported across EtherNet/IP.

When a valid PCCC Protected Logical Read or Protected Logical Write request is sent to OpenPLC, the request data is processed in preparation to create a response. During the course of this processing a few fields of the response buffer are filled with values from the request buffer. Three fields in particular (header.RP_CMD_Code, header.HD_Status, and header.HD_TransactionNum) are populated through use of a memmove call to extract bytes out of the request pccc_header structure. Within these memmove calls the pointer provided as the src argument is cast to the type unsigned int, as shown below:

memmove(&buffer[0], (unsigned int)header.RP_CMD_Code, 1);
memmove(&buffer[1], (unsigned int)header.HD_Status, 1);
memmove(&buffer[2], (unsigned int)header.HD_TransactionNum, 2);

As an unsigned int is typically defined as 32 bits in size this causes the pointer address to get truncated on systems with addresses greater than 32 bits.

CVE-2024-39589 - PCCC Protected Typed Logical Read

When this occurs in a PCCC Protected Logical Read request using function code 0xA2 the response buffer is prepared in the function Protected_Logical_Read_Reply. The snippet below shows the header.RP_CMD_CODE buffer address being loaded into $x0 ([0]) , truncated to a 32-bit value ([1]), and finally the truncated address being dereferenced ([2]).

(gdb) disass                                                                     
Dump of assembler code for function _Z28Protected_Logical_Read_Reply11pccc_headerPhi: 
   ...
   0x0000aaaad58a1678 <+336>:   ldr     x0, [x19, #72]            [0]
   0x0000aaaad58a167c <+340>:   mov     w0, w0                    [1]
   0x0000aaaad58a1680 <+344>:   ldrb    w1, [x0]                  [2]
   ...
(gdb) 

When the truncated address does not exist, and is subsequently dereferenced, a SIGSEV is raised causing the OpenPLC server to crash.

Crash Information

(gdb) x/i $pc
=> 0xaaaad6ab167c <_Z28Protected_Logical_Read_Reply11pccc_headerPhi+340>:       mov     w0, w0
(gdb) i r
x0             0xffff9b31b598      281473285469592
x1             0xaaaad6b210e8      187650723156200
...
sp             0xffff9b31ac20      0xffff9b31ac20
pc             0xaaaad6ab167c      0xaaaad6ab167c <Protected_Logical_Read_Reply(pccc_header, unsigned char*, int)+340>
...
(gdb) stepi
(gdb) x/i $pc
=> 0xaaaad6ab1680 <_Z28Protected_Logical_Read_Reply11pccc_headerPhi+344>:       ldrb    w1, [x0]
(gdb) i r
x0             0x9b31b598          2603726232
x1             0xaaaad6b210e8      187650723156200
...
sp             0xffff9b31ac20      0xffff9b31ac20
pc             0xaaaad6ab1680      0xaaaad6ab1680 <Protected_Logical_Read_Reply(pccc_header, unsigned char*, int)+344>
...
(gdb) stepi
Thread 20 "openplc" received signal SIGSEGV, Segmentation fault.
0x0000aaaad6ab1680 in Protected_Logical_Read_Reply(pccc_header, unsigned char*, int) ()
(gdb) bt
#0  0x0000aaaad6ab1680 in Protected_Logical_Read_Reply(pccc_header, unsigned char*, int) ()
#1  0x0000aaaad6ab1450 in Command_Protocol(pccc_header, unsigned char*, int) ()
#2  0x0000aaaad6ab1390 in ParsePCCCData(unsigned char*, int) ()
#3  0x0000aaaad6ab12c8 in processPCCCMessage(unsigned char*, int) ()
#4  0x0000aaaad6a9f9f4 in sendRRData(int, enip_header*, enip_data_Unknown*, enip_data_Unconnected*, enip_data_Connected*) ()
#5  0x0000aaaad6aa000c in processEnipMessage(unsigned char*, int) ()
#6  0x0000aaaad6ab32f0 in processMessage(unsigned char*, int, int, int) ()
#7  0x0000aaaad6ab3450 in handleConnections(void*) ()
#8  0x0000ffff9cbcd5c8 in start_thread (arg=0x0) at ./nptl/pthread_create.c:442
#9  0x0000ffff9cc35edc in thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone.S:79
(gdb) i r
x0             0x9b31b598          2603726232
x1             0xaaaad6b210e8      187650723156200
x2             0x0                 0
x3             0x1                 1
x4             0xaaaad6b210f8      187650723156216
x5             0x17f               383
x6             0x203a726576726553  2322294337967383891
x7             0x6320646165726854  7142819378486208596
x8             0x3f                63
x9             0x6e65696c6320726f  7954880231060304495
x10            0x6320646165726854  7142819378486208596
x11            0x6620646574616572  7358992178030404978
x12            0x6e65696c6320726f  7954880231060304495
x13            0xa36203a44492074   735811023747489908
x14            0x6461657268742067  7233174018586845287
x15            0x65687420726f6620  7307218078116308512
x16            0x1                 1
x17            0xffff9cbd21b0      281473311383984
x18            0x0                 0
x19            0xffff9b31b080      281473285468288
x20            0xffff9b31f4fc      281473285485820
x21            0xffff9bb2e2be      281473293935294
x22            0x80e920            8448288
x23            0xffff9bb2e2bf      281473293935295
x24            0x0                 0
x25            0xffff9ab10000      281473277034496
x26            0x80e920            8448288
x27            0xffff9bb2f0e0      281473293938912
x28            0xffff9ab10000      281473277034496
x29            0xffff9b31ac20      281473285467168
x30            0xaaaad6ab15b8      187650722698680
sp             0xffff9b31ac20      0xffff9b31ac20
pc             0xaaaad6ab1680      0xaaaad6ab1680 <Protected_Logical_Read_Reply(pccc_header, unsigned char*, int)+344>
cpsr           0x60201000          [ EL=0 BTYPE=0 SSBS SS C Z ]
fpsr           0x3                 [ IOC DZC ]
fpcr           0x0                 [ RMode=0 ]
pauth_dmask    0x7f000000000000    35747322042253312
pauth_cmask    0x7f000000000000    35747322042253312
(gdb) 

Mitigation

Update your version of OpenPLC one that has this issue patched.

If that is not possible, modify the source code to remove the three pointer casts in the Protected_Logical_Read_Reply memmove calls, similar to the following:

memmove(&buffer[0], header.RP_CMD_Code, 1);
memmove(&buffer[1], header.HD_Status, 1);
memmove(&buffer[2], header.HD_TransactionNum, 2);

CVE-2024-39590 - PCCC Protected Typed Logical Write

When this occurs in a PCCC Protected Logical Write request using function code 0xAA or 0xAB the response buffer is prepared in the function Protected_Logical_Write_Reply. The snippet below shows the header.RP_CMD_CODE buffer address being loaded into $x0 ([0]) , truncated to a 32-bit value ([1]), and finally the truncated address being dereferenced ([2]).

(gdb) disass
Dump of assembler code for function _Z29Protected_Logical_Write_Reply11pccc_headerPhi:
   ...
   0x0000aaaae961172c <+48>:    ldr     x0, [x19, #72]            [0]
   0x0000aaaae9611730 <+52>:    mov     w0, w0                    [1]
   0x0000aaaae9611734 <+56>:    ldrb    w1, [x0]                  [2]
   ...
(gdb) 

When the truncated address does not exist, and is subsequently dereferenced, a SIGSEV is raised causing the OpenPLC server to crash.

Crash Information

(gdb) x/i $pc
=> 0xaaaae9611730 <_Z29Protected_Logical_Write_Reply11pccc_headerPhi+52>:       mov     w0, w0
(gdb) i r
x0             0xffff9eb1b598      281473344189848
x1             0xffff9eb1c11d      281473344192797
...
sp             0xffff9eb1b010      0xffff9eb1b010
pc             0xaaaae9611730      0xaaaae9611730 <Protected_Logical_Write_Reply(pccc_header, unsigned char*, int)+52>
...
(gdb) stepi
(gdb) x/i $pc
=> 0xaaaae9611734 <_Z29Protected_Logical_Write_Reply11pccc_headerPhi+56>:       ldrb    w1, [x0]
(gdb) i r
x0             0x9eb1b598          2662446488
x1             0xffff9eb1c11d      281473344192797
...
sp             0xffff9eb1b010      0xffff9eb1b010
pc             0xaaaae9611734      0xaaaae9611734 <Protected_Logical_Write_Reply(pccc_header, unsigned char*, int)+56>
...
(gdb) stepi
Thread 7 "openplc" received signal SIGSEGV, Segmentation fault.
0x0000aaaae9611734 in Protected_Logical_Write_Reply(pccc_header, unsigned char*, int) () 
(gdb) bt
#0  0x0000aaaae9611734 in Protected_Logical_Write_Reply(pccc_header, unsigned char*, int) ()
#1  0x0000aaaae96114c0 in Command_Protocol(pccc_header, unsigned char*, int) ()
#2  0x0000aaaae9611390 in ParsePCCCData(unsigned char*, int) ()
#3  0x0000aaaae96112c8 in processPCCCMessage(unsigned char*, int) ()
#4  0x0000aaaae95ff9f4 in sendRRData(int, enip_header*, enip_data_Unknown*, enip_data_Unconnected*, enip_data_Connected*) ()
#5  0x0000aaaae960000c in processEnipMessage(unsigned char*, int) ()
#6  0x0000aaaae96132f0 in processMessage(unsigned char*, int, int, int) ()
#7  0x0000aaaae9613450 in handleConnections(void*) ()
#8  0x0000ffff9f3ad5c8 in start_thread (arg=0x0) at ./nptl/pthread_create.c:442
#9  0x0000ffff9f415edc in thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone.S:79
(gdb) i r
x0             0x9eb1b598          2662446488
x1             0xffff9eb1c11d      281473344192797
x2             0xbee2              48866
x3             0xffff9eb1b758      281473344190296
x4             0xffff9eb1b7c8      281473344190408
x5             0x4                 4
x6             0x203a726576726553  2322294337967383891
x7             0x6320646165726854  7142819378486208596
x8             0x3f                63
x9             0x6e65696c6320726f  7954880231060304495
x10            0x6320646165726854  7142819378486208596
x11            0x6620646574616572  7358992178030404978
x12            0x6e65696c6320726f  7954880231060304495
x13            0xa34203a44492074   735248073794068596
x14            0x6461657268742067  7233174018586845287
x15            0x65687420726f6620  7307218078116308512
x16            0xaaaae963fbb8      187651036806072
x17            0xffff9f407d10      281473353547024
x18            0x0                 0
x19            0xffff9eb1b080      281473344188544
x20            0xffff9eb1f4fc      281473344206076
x21            0xffff9e30e2be      281473335747262
x22            0x80e920            8448288
x23            0xffff9e30e2bf      281473335747263
x24            0x0                 0
x25            0xffff9e310000      281473335754752
x26            0x80e920            8448288
x27            0xffff9e30f0e0      281473335750880
x28            0xffff9e310000      281473335754752
x29            0xffff9eb1b010      281473344188432
x30            0xaaaae96114c0      187651036615872
sp             0xffff9eb1b010      0xffff9eb1b010
pc             0xaaaae9611734      0xaaaae9611734 <Protected_Logical_Write_Reply(pccc_header, unsigned char*, int)+56>
cpsr           0x60201000          [ EL=0 BTYPE=0 SSBS SS C Z ]
fpsr           0x0                 [ ]
fpcr           0x0                 [ RMode=0 ]
pauth_dmask    0x7f000000000000    35747322042253312
pauth_cmask    0x7f000000000000    35747322042253312
(gdb) 

Mitigation

Update your version of OpenPLC one that has this issue patched.

If that is not possible, modify the source code to remove the three pointer casts in the Protected_Logical_Write_Reply memmove calls, similar to the following:

memmove(&buffer[0], header.RP_CMD_Code, 1);
memmove(&buffer[1], header.HD_Status, 1);
memmove(&buffer[2], header.HD_TransactionNum, 2);
TIMELINE

2024-07-15 - Vendor Disclosure
2024-09-17 - Vendor Patch Release
2024-09-18 - Public Release

Credit

Discovered by Jared Rittle of Cisco Talos.