Skip to content

Malformed CPF and Get_Attribute_List request leads to pointer corruption and denial of service in OpENer #567

@gff-cw

Description

@gff-cw

Summary

I found a remotely reachable server-side crash in the latest master branch of OpENer while sending a crafted EtherNet/IP SendRRData request to a real OpENer server instance.

Although the final crash is observed in AddIntToMessage() (source/src/enet_encap/endianconv.c:136), the root cause is earlier in the request handling path:

  1. CreateCommonPacketFormatStructure() accepts a malformed CPF with an invalid item_count / inconsistent length.
  2. The malformed unconnected Message Router payload reaches GetAttributeList().
  3. GetAttributeList() parses an attacker-controlled, excessively large attribute_count_request and starts building an oversized inner Message Router response.
  4. EncodeMessageRouterResponseData() copies that inner response into the outer ENIPMessage without validating the remaining destination capacity.
  5. The copy overruns message_buffer[512] and overwrites the adjacent current_message_position field.
  6. A later AddIntToMessage() dereferences the corrupted pointer and crashes.

In other words, the visible crash site is AddIntToMessage(), but the actual memory corruption happens earlier during response re-assembly in EncodeMessageRouterResponseData().

Security impact

A remote unauthenticated attacker able to send crafted EtherNet/IP traffic to the OpENer TCP service can cause a server-side crash.

At minimum this is a denial-of-service issue.

Because the bug involves an out-of-bounds write into adjacent stack/object fields, the memory corruption surface is more serious than a simple parser reject/fail condition.

OS

  • Ubuntu 24.04 LTS

Affected Verison

  • Latest submit (commit:76b95cf951a18d0e8481833168ab8c6943ce7c96)

Actual Behavior If Applicable

==101374==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x5e0ac8d3f929 bp 0x7fffe45c6640 sp 0x7fffe45c6640 T0)

Compile Command

cmake -S source -B build-asan \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DOpENer_PLATFORM:STRING=POSIX \
-DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo \
-DBUILD_SHARED_LIBS:BOOL=OFF \
-DOpENer_TRACES:BOOL=OFF \
-DCMAKE_C_FLAGS:STRING="-O1 -g -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=address" \
-DCMAKE_CXX_FLAGS:STRING="-O1 -g -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=address" \
-DCMAKE_EXE_LINKER_FLAGS:STRING="-fsanitize=address -pthread"

cmake --build build-asan --target OpENer -j"$(nproc)"

The Outcome of ASAN

AddressSanitizer:DEADLYSIGNAL
=================================================================
==101374==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x5e0ac8d3f929 bp 0x7fffe45c6640 sp 0x7fffe45c6640 T0)
==101374==The signal is caused by a READ memory access.
==101374==Hint: this fault was caused by a dereference of a high value address (see register values below).  Disassemble the provided pc to learn which register was used.
    #0 0x5e0ac8d3f929 in AddIntToMessage /root/wc/opener-verify/source/src/enet_encap/endianconv.c:136:49
    #1 0x5e0ac8d38072 in EncodeSockaddrInfoItemTypeId /root/wc/opener-verify/source/src/enet_encap/cpf.c:595:3
    #2 0x5e0ac8d38072 in AssembleLinearMessage /root/wc/opener-verify/source/src/enet_encap/cpf.c:696:9
    #3 0x5e0ac8d36eaa in NotifyCommonPacketFormat /root/wc/opener-verify/source/src/enet_encap/cpf.c:70:30
    #4 0x5e0ac8d3b438 in HandleReceivedSendRequestResponseDataCommand /root/wc/opener-verify/source/src/enet_encap/encap.c:558:22
    #5 0x5e0ac8d39aea in HandleReceivedExplictTcpData /root/wc/opener-verify/source/src/enet_encap/encap.c:186:26
    #6 0x5e0ac8d1b30b in HandleDataOnTcpSocket /root/wc/opener-verify/source/src/ports/generic_networkhandler.c:864:30
    #7 0x5e0ac8d19d24 in NetworkHandlerProcessCyclic /root/wc/opener-verify/source/src/ports/generic_networkhandler.c:497:32
    #8 0x5e0ac8d18a44 in executeEventLoop /root/wc/opener-verify/source/src/ports/POSIX/main.c:261:24
    #9 0x5e0ac8d18a44 in main /root/wc/opener-verify/source/src/ports/POSIX/main.c:229:12
    #10 0x733288e2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #11 0x733288e2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #12 0x5e0ac8c3f454 in _start (/root/wc/opener-verify/build-asan-1/src/ports/POSIX/OpENer+0x2e454) (BuildId: 92cc5930c2b20f5b29383eb48552639fdc1a55ab)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /root/wc/opener-verify/source/src/enet_encap/endianconv.c:136:49 in AddIntToMessage
==101374==ABORTING

The Outcome of GDB

Program received signal SIGSEGV, Segmentation fault.
0x000055555567a959 in AddIntToMessage (data=32768, outgoing_message=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/endianconv.c:136
136	  outgoing_message->current_message_position[0] = (unsigned char) data;
#0  0x000055555567a959 in AddIntToMessage (data=32768, outgoing_message=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/endianconv.c:136
#1  0x00005555556762f0 in EncodeSockaddrInfoItemTypeId (item_type=item_type@entry=0, common_packet_format_data_item=common_packet_format_data_item@entry=0x5555560179d0 <g_common_packet_format_data_item>, outgoing_message=0x60600028606003e, outgoing_message@entry=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/cpf.c:595
#2  0x0000555555675333 in AssembleLinearMessage (message_router_response=message_router_response@entry=0x7ffff5f00c20, common_packet_format_data_item=0x5555560179d0 <g_common_packet_format_data_item>, outgoing_message=outgoing_message@entry=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/cpf.c:696
#3  0x000055555567443b in NotifyCommonPacketFormat (received_data=0x7ffff5c004a0, originator_address=<optimized out>, outgoing_message=<optimized out>) at /root/wc/opener-verify/source/src/enet_encap/cpf.c:70
#4  0x0000555555677671 in HandleReceivedSendRequestResponseDataCommand (receive_data=receive_data@entry=0x7ffff5c004a0, originator_address=originator_address@entry=0x7ffff6003a90, outgoing_message=outgoing_message@entry=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/encap.c:558
#5  0x00005555556766de in HandleReceivedExplictTcpData (socket=<optimized out>, buffer=0x7ffff6003830 "o", length=328, number_of_remaining_bytes=<optimized out>, originator_address=<optimized out>, outgoing_message=<optimized out>) at /root/wc/opener-verify/source/src/enet_encap/encap.c:186
data = 32768
outgoing_message = 0x7ffff6003ac0
rcx            0x30300014303001f2  3472275399410450930
rdx            0x60600028606003e   434034424926306366
rdi            0x8000              32768
rip            0x55555567a959      0x55555567a959 <AddIntToMessage+41>
$1 = {
  message_buffer = '\000' <repeats 30 times>, "00\000\000\000\000\262\000\342\001\203\000\n\000\000\20000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024\00000\024",
  current_message_position = 0x30300014303001f2 <error: Cannot access memory at address 0x30300014303001f2>,
  used_message_length = 498
}
$2 = 0x30300014303001f2
0x7ffff6003af0:	0x3030001430300014	0x3030001430300014
$4 = {
  item_count = 12336,
  address_item = {
    type_id = 0,
    length = 0,
    data = {
      connection_identifier = 0,
      sequence_number = 0
    }
  },
  data_item = {
    type_id = 178,
    length = 8,
    data = 0x7ffff6003858 "\003\003 \001$\00100"
  },
  address_info_item = {{
      type_id = 32768,
      length = 12336,
      sin_family = 12336,
      sin_port = 12336,
      sin_addr = 808464432,
      nasin_zero = "00000000"
    }, {
      type_id = 0,
      length = 0,
      sin_family = 0,
      sin_port = 0,
      sin_addr = 0,
      nasin_zero = "\000\000\000\000\000\000\000"
    }}
}

#2  0x0000555555677671 in HandleReceivedSendRequestResponseDataCommand (receive_data=receive_data@entry=0x7ffff5c004a0, originator_address=originator_address@entry=0x7ffff6003a90, outgoing_message=outgoing_message@entry=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/encap.c:558
558	      return_value = NotifyCommonPacketFormat(receive_data, originator_address, outgoing_message);
receive_data = 0x7ffff5c004a0
originator_address = 0x7ffff6003a90
outgoing_message = 0x7ffff6003ac0
$17 = 0x12a
$18 = 0x7ffff600384e
0x7ffff600384e:	0x30	0x30	0x00	0x00	0x00	0x00	0xb2	0x00
0x7ffff6003856:	0x08	0x00	0x03	0x03	0x20	0x01	0x24	0x01
0x7ffff600385e:	0x30	0x30	0x00	0x80	0x30	0x30	0x30	0x30
0x7ffff6003866:	0x30	0x30	0x30	0x30	0x30	0x30	0x30	0x30

Breakpoint 1, CreateCommonPacketFormatStructure (data=<optimized out>, data_length=<optimized out>, common_packet_format_data=0x5555560179d0 <g_common_packet_format_data_item>) at /root/wc/opener-verify/source/src/enet_encap/cpf.c:239
239	  common_packet_format_data->address_info_item[0].type_id = 0;
242	  size_t length_count = 0;
243	  CipUint item_count = GetUintFromMessage(&data);
244	  //OPENER_ASSERT(4U >= item_count);/* Sanitizing data - probably needs to be changed for productive code */
245	  common_packet_format_data->item_count = item_count;
315	  if(length_count == data_length) { /* length of data is equal to length of Addr and length of Data */
316	    return kEipStatusOk;
317	  } else {
318	    OPENER_TRACE_WARN(
319	      "something is wrong with the length in Message Router @ CreateCommonPacketFormatStructure\n");
320	    if(common_packet_format_data->item_count > 2) {
321	      /* there is an optional packet in data stream which is not sockaddr item */
322	      return kEipStatusOk;
323	    } else { /* something with the length was wrong */
324	      return kEipStatusError;
325	    }
326	  }
Value returned is $5 = kEipStatusOk
$7 = {
  item_count = 12336,
  address_item = {
    type_id = 0,
    length = 0,
    data = {
      connection_identifier = 0,
      sequence_number = 0
    }
  },
  data_item = {
    type_id = 178,
    length = 8,
    data = 0x7ffff6003858 "\003\003 \001$\00100"
  },
  address_info_item = {{
      type_id = 32768,
      length = 12336,
      sin_family = 12336,
      sin_port = 12336,
      sin_addr = 808464432,
      nasin_zero = "00000000"
    }, {
      type_id = 0,
      length = 0,
      sin_family = 0,
      sin_port = 0,
      sin_addr = 0,
      nasin_zero = "\000\000\000\000\000\000\000"
    }}
}
$8 = 0x3030

Breakpoint 14, GetAttributeList (instance=0x5040000000d0, message_router_request=0x555556016248 <g_message_router_request>, message_router_response=0x7ffff5f00c20, originator_address=0x7ffff6003a90, encapsulation_session=1) at /root/wc/opener-verify/source/src/cip/cipcommon.c:1123
1129	  CipUint attribute_count_request = GetUintFromMessage(
1130	    &message_router_request->data);
1132	  if(0 != attribute_count_request) {
1137	    CipOctet *attribute_count_responst_position =
1138	      message_router_response->message.current_message_position;
1140	    MoveMessageNOctets(sizeof(CipInt), &message_router_response->message);  // move the message pointer to reserve memory
1142	    for(size_t j = 0; j < attribute_count_request; j++) {
1143	      attribute_number = GetUintFromMessage(&message_router_request->data);
1144	      attribute = GetCipAttribute(instance, attribute_number);
$36 = 0x7ffff6003860
0x7ffff6003860:	0x00	0x80	0x30	0x30	0x30	0x30	0x30	0x30
0x7ffff6003868:	0x30	0x30	0x30	0x30	0x30	0x30	0x30	0x30
$38 = 0x8000

#1  0x0000555555664125 in GetAttributeList (instance=0x5040000000d0, message_router_request=<optimized out>, message_router_response=<optimized out>, originator_address=<optimized out>, encapsulation_session=<optimized out>) at /root/wc/opener-verify/source/src/cip/cipcommon.c:1178
1178	      AddIntToMessage(attribute_number, &message_router_response->message);  // Attribute-ID
1180	      if(NULL != attribute) {
1195	      } else {
1196	        AddSintToMessage(kCipErrorAttributeNotSupported,
1197	                         &message_router_response->message);                                 // status
1198	        AddSintToMessage(0, &message_router_response->message); // Reserved, shall be 0
1199	        message_router_response->general_status = kCipErrorAttributeListError;
attribute_number = 12336
attribute = 0x0
$43 = 0x3030
$44 = (CipAttributeStruct *) 0x0

Breakpoint 17, EncodeMessageRouterResponseData (message_router_response=message_router_response@entry=0x7ffff5f00c20, outgoing_message=outgoing_message@entry=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/cpf.c:574
574	  memcpy(outgoing_message->current_message_position,
message_router_response = 0x7ffff5f00c20
outgoing_message = 0x7ffff6003ac0
$48 = 0x7ffff6003aec
$49 = 0x7ffff6003ac0
$50 = 0x7ffff6003cc0
$51 = 0x1de
$52 = 468
$53 = 0x7ffff6003cc0
0x7ffff6003cc0:	0xec	0x3a	0x00	0xf6	0xff	0x7f	0x00	0x00
0x7ffff6003cc8:	0x14	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff6003cd0:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
576	         message_router_response->message.used_message_length);
$54 = {
  message_buffer = '\000' <repeats 30 times>, "00\000\000\000\000\262\000\342\001\203\000\n", '\000' <repeats 468 times>,
  current_message_position = 0x7ffff6003aec "",
  used_message_length = 20
}
0x7ffff6003cc0:	0xec	0x3a	0x00	0xf6	0xff	0x7f	0x00	0x00
0x7ffff6003cc8:	0x14	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff6003cd0:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
$55 = 0x7ffff6003aec
$56 = 0x14

Watchpoint 6: outgoing_message->current_message_position

Old value = (CipOctet *) 0x7ffff6003aec ""
New value = (CipOctet *) 0x3030001430300014 <error: Cannot access memory at address 0x3030001430300014>
__memcpy_evex_unaligned_erms () at ./sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:497
#0  __memcpy_evex_unaligned_erms () at ./sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:497
#1  0x000055555561aed7 in __asan_memcpy ()
#2  0x000055555567627a in EncodeMessageRouterResponseData (message_router_response=message_router_response@entry=0x7ffff5f00c20, outgoing_message=outgoing_message@entry=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/cpf.c:574
#3  0x00005555556752d0 in AssembleLinearMessage (message_router_response=message_router_response@entry=0x7ffff5f00c20, common_packet_format_data_item=0x5555560179d0 <g_common_packet_format_data_item>, outgoing_message=outgoing_message@entry=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/cpf.c:676
#4  0x000055555567443b in NotifyCommonPacketFormat (received_data=0x7ffff5c004a0, originator_address=<optimized out>, outgoing_message=<optimized out>) at /root/wc/opener-verify/source/src/enet_encap/cpf.c:70
#5  0x0000555555677671 in HandleReceivedSendRequestResponseDataCommand (receive_data=receive_data@entry=0x7ffff5c004a0, originator_address=originator_address@entry=0x7ffff6003a90, outgoing_message=outgoing_message@entry=0x7ffff6003ac0) at /root/wc/opener-verify/source/src/enet_encap/encap.c:558
$29 = 0x3030001430300014

Reproduction

poc:

poc.zip

replay program:

replay.zip

usage:

./OpENer lo
python3 replay.py poc

Root cause analysis

1. Malformed CPF is accepted

In CreateCommonPacketFormatStructure():

CipUint item_count = GetUintFromMessage(&data);
//OPENER_ASSERT(4U >= item_count);
common_packet_format_data->item_count = item_count;

The request begins with attacker-controlled bytes such that item_count becomes 0x3030.

Later, even when the parsed CPF length is inconsistent, the function still returns success if item_count > 2:

if(length_count == data_length) {
  return kEipStatusOk;
} else {
  if(common_packet_format_data->item_count > 2) {
    return kEipStatusOk;
  } else {
    return kEipStatusError;
  }
}

This allows a malformed CPF to continue into the Message Router path.

2. GetAttributeList() trusts attacker-controlled count and attribute IDs

Once the malformed unconnected data item is routed into GetAttributeList(), the request data is parsed as:

CipUint attribute_count_request = GetUintFromMessage(&message_router_request->data);

During debugging, this was observed as:

  • attribute_count_request = 0x8000

Then the loop begins:

for(size_t j = 0; j < attribute_count_request; j++) {
  attribute_number = GetUintFromMessage(&message_router_request->data);
  attribute = GetCipAttribute(instance, attribute_number);

The first parsed attribute ID was:

  • attribute_number = 0x3030

Even when the attribute is not found (attribute == NULL), the code still writes the attacker-controlled attribute_number into the response before adding an error status:

AddIntToMessage(attribute_number, &message_router_response->message);  // Attribute-ID

if(NULL != attribute) {
  ...
} else {
  AddSintToMessage(kCipErrorAttributeNotSupported,
                   &message_router_response->message);
  AddSintToMessage(0, &message_router_response->message);
  message_router_response->general_status = kCipErrorAttributeListError;
}

As a result, the function keeps building a large inner response from attacker-controlled bogus attribute IDs.

3. Oversized inner response is copied into a too-small outer buffer

Later, in EncodeMessageRouterResponseData():

memcpy(outgoing_message->current_message_position,
       message_router_response->message.message_buffer,
       message_router_response->message.used_message_length);

During GDB debugging, the following values were observed immediately before this memcpy:

  • outgoing_message->current_message_position = 0x7ffff6003aec
  • &outgoing_message->message_buffer[512] = 0x7ffff6003cc0
  • remaining space in outer buffer = 468
  • message_router_response->message.used_message_length = 0x1de = 478

Therefore, the code copies 478 bytes into a region with only 468 bytes remaining.

That means this memcpy necessarily overruns the end of message_buffer by 10 bytes.

4. The overflow overwrites the adjacent current_message_position field

The ENIPMessage layout is:

struct enip_message {
    CipOctet message_buffer[512];
    CipOctet *current_message_position;
    size_t used_message_length;
}

So the field current_message_position is located immediately after message_buffer[512].

GDB confirmed:

  • &outgoing_message->message_buffer[512] == &outgoing_message->current_message_position

Before the overflow, memory at that location contained the valid pointer value.
After the memcpy, the bytes at that address changed to:

14 00 30 30 14 00 30 30 ...

which corresponds to the corrupted pointer:

0x3030001430300014

After that corruption, a subsequent AddIntToMessage() attempts to write through this invalid pointer and the process crashes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions