CVE-2024-36981,CVE-2024-36980
An out-of-bounds read vulnerability exists in the OpenPLC Runtime EtherNet/IP PCCC parser functionality of OpenPLC_v3 b4702061dc14d1024856f71b4543298d77007b88. A specially crafted network request can lead to denial of service. An attacker can send a series of EtherNet/IP requests to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
OpenPLC _v3 b4702061dc14d1024856f71b4543298d77007b88
OpenPLC_v3 - https://github.com/thiagoralves/OpenPLC_v3
7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-125 - Out-of-bounds Read
OpenPLC is an open-source programmable logic controller (PLC) designed to provide a low cost option for automation. 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.
When a PCCC request with an unsupported command/function pair is sent to the runtime, an error is raised within the controller by returning the value -1
.
/* Determine the Command that is being requested to execute */
uint16_t Command_Protocol(pccc_header header, unsigned char *buffer, int buffer_size)
{
uint16_t var_pccc_length;
/*If Statement to determine the command code from the Command Packet*/
if(((unsigned int)*header.HD_CMD_Code == 0x0f) && ((unsigned int)*header.HD_Data_Function_Code == 0xA2))//Protected Logical Read
{
var_pccc_length = Protected_Logical_Read_Reply(header,buffer,buffer_size);
return var_pccc_length;
}
else if(((unsigned int)*header.HD_CMD_Code == 0x0f) && ( ((unsigned int)*header.HD_Data_Function_Code == 0xAA) || ((unsigned int)*header.HD_Data_Function_Code == 0xAB)))//Protected Logical Write
{
var_pccc_length = Protected_Logical_Write_Reply(header,buffer,buffer_size);
return var_pccc_length;
}
else
{
/*initialize logging system*/
char log_msg[1000];
sprintf(log_msg, "PCCC: Unsupportedd Command/Data Function Code!\n");
log(log_msg);
return -1;
}//return length as -1 to signify that the CMD Code/Function Code was not recognize
}
That length of -1
gets returned into the ParsePCCCData
function, and subsequently returned again
uint16_t ParsePCCCData(unsigned char *buffer, int buffer_size)
{
/*Variables*/
int new_pccc_length; //Variable for new PCCC length
pccc_header header;
header.HD_CMD_Code = &buffer[0];//[1] -> Command Code
header.HD_Status = &buffer[1];////[1] -> Status Code
header.HD_TransactionNum = &buffer[2];//[2] -> Transaction Number
header.HD_Data_Function_Code = &buffer[4];//[1] -> Data Function Code
/*Determine what command is being requested*/
new_pccc_length = Command_Protocol(header,buffer,buffer_size);
return new_pccc_length; //Return the new pccc length
}
The processPCCCMessage
function then proceeds to return the length back to the encapsulating EtherNet/IP processing.
//This function takes in the data from enip.cpp and places the data in the appropriate structure variables
uint16_t processPCCCMessage(unsigned char *buffer, int buffer_size)
{
/* Variables */
int new_pccc_length; //New PCCC Length
pccc_header header;
header.Data = buffer;
header.Data_Size = buffer_size;
/*Determine the new pccc length*/
new_pccc_length = ParsePCCCData(buffer,buffer_size);
return new_pccc_length; //Return the length to enip.cpp
}
processPCCCMessage
is called from three places where the underlying issue is introduced:
* Twice in the sendRRData
processing
* Once in the sendUnitData
processing
In all three locations the length value returned by processPCCCMessage
is cast as a uint16_t
and subsequently compared to the signed value -1
to check for an error case.
//send pccc Data to pccc.cpp to be parsed and craft response
// returns the new PCCC data size
uint16_t newPcccSize = processPCCCMessage(pcccData, currentPcccSize);
if (newPcccSize == -1)
return -1; //error in PCCC.cpp
The issue arises here as a uint16_t
is being compared with a signed value that isn’t cast, which is always going to cause the if
statement to fail, bypassing the error in PCCC.cpp
return. When this occurs, execution is allowed to continue with a newPcccSize
variable containing the value 0xFFFF
.
When this occurs in a SendRRData
request with an enipType
of 0x02, the large newPcccSize
value is used directly in a memmove
call after being increased by 0x07.
int sendRRData(int enipType, struct enip_header *header, struct enip_data_Unknown *enipDataUnknown, struct enip_data_Unconnected *enipDataUnconnected, struct enip_data_Connected *enipDataConnected)
{
if (enipType == 1)
{
...
}
else if (enipType == 2)
{
...
//send pccc Data to pccc.cpp to be parsed and craft response
// returns the new PCCC data size
uint16_t newPcccSize = processPCCCMessage(pcccData, currentItem2Size - 13); // get length of new pccc size
if (newPcccSize == -1) [1]
return -1; //error in PCCC.cpp
...
//move data forward
memmove(&enipDataUnconnected->request_path[2], enipDataUnconnected->requestor_idLength, newPcccSize + 7);//11);
...
}
Since the third parameter of memmove
is typed as a size_t
this allows the count
to be set to the value 0x10006.
Thread 5 "openplc" hit Breakpoint 1, __memmove_generic () at ../sysdeps/aarch64/multiarch/../memcpy.S:211
211 ../sysdeps/aarch64/multiarch/../memcpy.S: No such file or directory.
(gdb) i r
x0 0xffff817ac114 281472854049044 // memmove param 1 (*dst)
x1 0xffff817ac116 281472854049046 // memmove param 2 (*src)
x2 0x10006 65542 // memmove param 3 (count)
...
x30 0xaaaac25ffaf0 187650382232304
sp 0xffff817ab630 0xffff817ab630
pc 0xffff82867cd0 0xffff82867cd0 <__memmove_generic>
cpsr 0x60001000 [ EL=0 BTYPE=0 SSBS C Z ]
fpsr 0x0 [ ]
fpcr 0x0 [ RMode=0 ]
pauth_dmask 0x7f000000000000 35747322042253312
pauth_cmask 0x7f000000000000 35747322042253312
(gdb)
This creates a memmove call similar to the following:
memmove(0xffff817ac114, 0xffff817ac116, 0x10006)
With such a large count
value, memmove
eventually attempts to access data outside of the memory region containing the PCCC request.
Thread 8 "openplc" hit Breakpoint 2, __memcpy_generic () at ../sysdeps/aarch64/multiarch/../memcpy.S:184
184 in ../sysdeps/aarch64/multiarch/../memcpy.S
(gdb) i r
x0 0xffffa515c114 281473451409684
x1 0xffffa515ffd2 281473451425746
x2 0xc0fa 49402
x3 0xffffa515ff90 281473451425680
x4 0xffffa516c11c 281473451475228
x5 0xffffa516c11a 281473451475226
x6 0x0 0
x7 0x0 0
x8 0x0 0
x9 0x0 0
...
(gdb) x/i $pc
=> 0xffffa6a27c7c <__memcpy_generic+316>: ldp x8, x9, [x1, #32]
(gdb)
In the ldp
instruction above, 0x08
bytes starting from address 0xFFFFA515FFF2
($x1+32) are loaded into $x8
. Subsequently another 0x08
bytes are attempted to be loaded from address 0xFFFFA515FFFA
($x1+32+8) into $x9
.
Inspecting the process’ memory map reveals the following two regions of note:
user@machine:$ cat /proc/705332/maps
...
ffffa4960000-ffffa5160000 rw-p 00000000 00:00 0
ffffa5160000-ffffa5170000 ---p 00000000 00:00 0
...
In the second operation of the ldp
instruction above, the tail bytes are being read from the second of the two memory regions listed. Since this region does not have read permissions, a SIGSEV
is thrown and the runtime crashes.
(gdb) stepi
Thread 8 "openplc" received signal SIGSEGV, Segmentation fault.
__memcpy_generic () at ../sysdeps/aarch64/multiarch/../memcpy.S:184
184 in ../sysdeps/aarch64/multiarch/../memcpy.S
(gdb) bt
#0 __memcpy_generic () at ../sysdeps/aarch64/multiarch/../memcpy.S:184
#1 0x0000aaaaded7faf0 in sendRRData(int, enip_header*, enip_data_Unknown*, enip_data_Unconnected*, enip_data_Connected*) ()
#2 0x0000aaaaded8000c in processEnipMessage(unsigned char*, int) ()
#3 0x0000aaaaded930c8 in processMessage(unsigned char*, int, int, int) ()
#4 0x0000aaaaded93228 in handleConnections(void*) ()
#5 0x0000ffffa6a0d5c8 in start_thread (arg=0x0) at ./nptl/pthread_create.c:442
#6 0xf0e00000ffffa6a7 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb) i r
x0 0xffffa515c114 281473451409684
x1 0xffffa515ffd2 281473451425746
x2 0xc0fa 49402
x3 0xffffa515ff90 281473451425680
x4 0xffffa516c11c 281473451475228
x5 0xffffa516c11a 281473451475226
x6 0x0 0
x7 0x0 0
x8 0x0 0
x9 0x0 0
x10 0x0 0
x11 0x0 0
x12 0x0 0
x13 0x0 0
x14 0x4 4
x15 0x65687420726f6620 7307218078116308512
x16 0xaaaadedc0cc0 187650860125376
x17 0xffffa6a27cd0 281473477410000
x18 0x0 0
x19 0x0 0
x20 0xffffa515f4fc 281473451422972
x21 0xffffa596e2be 281473459872446
x22 0x80e920 8448288
x23 0xffffa596e2bf 281473459872447
x24 0x0 0
x25 0xffffa4950000 281473442971648
x26 0x80e920 8448288
x27 0xffffa596f0e0 281473459876064
x28 0xffffa4950000 281473442971648
x29 0xffffa515b630 281473451406896
x30 0xaaaaded7faf0 187650859858672
sp 0xffffa515b630 0xffffa515b630
pc 0xffffa6a27c7c 0xffffa6a27c7c <__memcpy_generic+316>
cpsr 0x20201000 [ EL=0 BTYPE=0 SSBS SS C ]
fpsr 0x0 [ ]
fpcr 0x0 [ RMode=0 ]
pauth_dmask 0x7f000000000000 35747322042253312
pauth_cmask 0x7f000000000000 35747322042253312
(gdb)
Update your version of OpenPLC one that has this issue patched. If that is not possible, modify the source code to cast -1
to a matching type in the affected comparision, similar to the snippet below.
uint16_t newPcccSize = processPCCCMessage(pcccData, currentItem2Size - 13); // get length of new pccc size
if (newPcccSize == (uint16_t) -1)
return -1; //error in PCCC.cpp
When this occurs in a SendUnitData
request, the large newPcccSize
value is used directly in a memmove
call after being increased by 0x07.
int sendUnitData(struct enip_header *header, struct enip_data_Connected_0x70 *enipDataConnected_0x70)
{
...
//send pccc Data to pccc.cpp to be parsed and craft response
// returns the new PCCC data size
uint16_t newPcccSize = processPCCCMessage(pcccData, currentPcccSize);
if (newPcccSize == -1) [2]
return -1; //error in PCCC.cpp
...
//move data forward
memmove(&enipDataConnected_0x70->request_path[2], enipDataConnected_0x70->requestor_id, newPcccSize + 7);
...
}
Since the third parameter of memmove
is typed as a size_t
this allows the count
to be set to the value 0x10006.
Thread 5 "openplc" hit Breakpoint 1, 0x0000aaaab8d8fc9c in sendUnitData(enip_header*, enip_data_Connected_0x70*) ()
(gdb) b memmove
Breakpoint 2 at 0xffff9e987cd0: memmove. (2 locations)
(gdb) c
Continuing.
Thread 5 "openplc" hit Breakpoint 2, __memmove_generic () at ../sysdeps/aarch64/multiarch/../memcpy.S:211
211 ../sysdeps/aarch64/multiarch/../memcpy.S: No such file or directory.
(gdb) i r
x0 0xffff9e0dc11a 281473333444890 // memmove param 1 (*dst)
x1 0xffff9e0dc11c 281473333444892 // memmove param 2 (*src)
x2 0x10006 65542 // memmove param 3 (count)
...
x30 0xaaaab8d8fe7c 187650222390908
sp 0xffff9e0db660 0xffff9e0db660
pc 0xffff9e987cd0 0xffff9e987cd0 <__memmove_generic>
cpsr 0x60001000 [ EL=0 BTYPE=0 SSBS C Z ]
fpsr 0x0 [ ]
fpcr 0x0 [ RMode=0 ]
pauth_dmask 0x7f000000000000 35747322042253312
pauth_cmask 0x7f000000000000 35747322042253312
(gdb)
This creates a memmove call similar to the following:
memmove(0xffff9e0dc11a, 0xffff9e0dc11c, 0x10006)
With such a large count
value, memmove
eventually attempts to access data outside of the memory region containing the PCCC request.
(gdb) c
Continuing.
[Thread 0xffff9c8af0e0 (LWP 716796) exited]
Thread 5 "openplc" hit Breakpoint 3, __memcpy_generic () at ../sysdeps/aarch64/multiarch/.
./memcpy.S:184
184 in ../sysdeps/aarch64/multiarch/../memcpy.S
(gdb) i r
x0 0xffff9e0dc11a 281473333444890
x1 0xffff9e0dffd2 281473333460946
x2 0xc100 49408
x3 0xffff9e0dff90 281473333460880
x4 0xffff9e0ec122 281473333510434
x5 0xffff9e0ec120 281473333510432
x6 0x0 0
x7 0x0 0
x8 0x0 0
x9 0x0 0
...
(gdb) x/i $pc
=> 0xffff9e987c7c <__memcpy_generic+316>: ldp x8, x9, [x1, #32]
(gdb)
In the ldp
instruction above, 0x08
bytes starting from address 0xFFFF9E0DFFF2
($x1+32) are loaded into $x8
. Subsequently another 0x08
bytes are attempted to be loaded from address 0xFFFF9E0DFFFA
($x1+32+8) into $x9
.
user@machine:$ cat /proc/716795/maps
...
ffff9d8e0000-ffff9e0e0000 rw-p 00000000 00:00 0
ffff9e0e0000-ffff9e0f0000 ---p 00000000 00:00 0
...
In the second operation of the ldp
instruction above, the tail bytes are being read from the second of the two memory regions listed. Since this region does not have read permissions, a SIGSEV
is thrown and the runtime crashes.
Thread 5 "openplc" hit Breakpoint 3, __memcpy_generic () at ../sysdeps/aarch64/multiarch/.
./memcpy.S:184
184 in ../sysdeps/aarch64/multiarch/../memcpy.S
(gdb) stepi
Thread 5 "openplc" received signal SIGSEGV, Segmentation fault.
__memcpy_generic () at ../sysdeps/aarch64/multiarch/../memcpy.S:184
184 in ../sysdeps/aarch64/multiarch/../memcpy.S
(gdb) i r
x0 0xffff9e0dc11a 281473333444890
x1 0xffff9e0dffd2 281473333460946
x2 0xc100 49408
x3 0xffff9e0dff90 281473333460880
x4 0xffff9e0ec122 281473333510434
x5 0xffff9e0ec120 281473333510432
x6 0x0 0
x7 0x0 0
x8 0x0 0
x9 0x0 0
x10 0x0 0
x11 0x0 0
x12 0x0 0
x13 0x0 0
x14 0xa 10
x15 0x65687420726f6620 7307218078116308512
x16 0xaaaab8dd0cc0 187650222656704
x17 0xffff9e987cd0 281473342536912
x18 0x0 0
x19 0x0 0
x20 0xffff9e0df4fc 281473333458172
x21 0xffff9d8ce2be 281473324999358
x22 0x80e920 8448288
x23 0xffff9d8ce2bf 281473324999359
x24 0x0 0
x25 0xffff9d8d0000 281473325006848
x26 0x80e920 8448288
x27 0xffff9d8cf0e0 281473325002976
x28 0xffff9d8d0000 281473325006848
x29 0xffff9e0db660 281473333442144
x30 0xaaaab8d8fe7c 187650222390908
sp 0xffff9e0db660 0xffff9e0db660
pc 0xffff9e987c7c 0xffff9e987c7c <__memcpy_generic+316>
cpsr 0x20201000 [ EL=0 BTYPE=0 SSBS SS C ]
fpsr 0x0 [ ]
fpcr 0x0 [ RMode=0 ]
pauth_dmask 0x7f000000000000 35747322042253312
pauth_cmask 0x7f000000000000 35747322042253312
(gdb) bt
#0 __memcpy_generic () at ../sysdeps/aarch64/multiarch/../memcpy.S:184
#1 0x0000aaaab8d8fe7c in sendUnitData(enip_header*, enip_data_Connected_0x70*) ()
#2 0x0000aaaab8d8ff50 in processEnipMessage(unsigned char*, int) ()
#3 0x0000aaaab8da30c8 in processMessage(unsigned char*, int, int, int) ()
#4 0x0000aaaab8da3228 in handleConnections(void*) ()
#5 0x0000ffff9e96d5c8 in start_thread (arg=0x0) at ./nptl/pthread_create.c:442
#6 0xf0e00000ffff9e9d in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb)
Update your version of OpenPLC one that has this issue patched. If that is not possible, modify the source code to cast -1
to a matching type in the affected comparision, similar to the snippet below.
uint16_t newPcccSize = processPCCCMessage(pcccData, currentPcccSize);
if (newPcccSize == (uint16_t) -1)
return -1; //error in PCCC.cpp
2024-06-10 - Initial Vendor Contact
2024-06-10 - Vendor Disclosure
2024-09-17 - Vendor Patch Release
2024-09-18 - Public Release
Discovered by Jared Rittle of Cisco Talos.