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:
CreateCommonPacketFormatStructure() accepts a malformed CPF with an invalid item_count / inconsistent length.
- The malformed unconnected Message Router payload reaches
GetAttributeList().
GetAttributeList() parses an attacker-controlled, excessively large attribute_count_request and starts building an oversized inner Message Router response.
EncodeMessageRouterResponseData() copies that inner response into the outer ENIPMessage without validating the remaining destination capacity.
- The copy overruns
message_buffer[512] and overwrites the adjacent current_message_position field.
- 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
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:
After that corruption, a subsequent AddIntToMessage() attempts to write through this invalid pointer and the process crashes.
Summary
I found a remotely reachable server-side crash in the latest master branch of OpENer while sending a crafted EtherNet/IP
SendRRDatarequest 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:CreateCommonPacketFormatStructure()accepts a malformed CPF with an invaliditem_count/ inconsistent length.GetAttributeList().GetAttributeList()parses an attacker-controlled, excessively largeattribute_count_requestand starts building an oversized inner Message Router response.EncodeMessageRouterResponseData()copies that inner response into the outerENIPMessagewithout validating the remaining destination capacity.message_buffer[512]and overwrites the adjacentcurrent_message_positionfield.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 inEncodeMessageRouterResponseData().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-serviceissue.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 LTSAffected Verison
76b95cf951a18d0e8481833168ab8c6943ce7c96)Actual Behavior If Applicable
Compile Command
The Outcome of ASAN
The Outcome of GDB
Reproduction
poc:
poc.zip
replay program:
replay.zip
usage:
Root cause analysis
1. Malformed CPF is accepted
In
CreateCommonPacketFormatStructure():The request begins with attacker-controlled bytes such that
item_countbecomes 0x3030.Later, even when the parsed CPF length is inconsistent, the function still returns success if
item_count > 2:This allows a malformed CPF to continue into the Message Router path.
2.
GetAttributeList()trusts attacker-controlled count and attribute IDsOnce the malformed unconnected data item is routed into
GetAttributeList(), the request data is parsed as:During debugging, this was observed as:
attribute_count_request = 0x8000Then the loop begins:
The first parsed attribute ID was:
attribute_number = 0x3030Even when the attribute is not found (
attribute == NULL), the code still writes the attacker-controlledattribute_numberinto the response before adding an error status: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():During GDB debugging, the following values were observed immediately before this
memcpy:outgoing_message->current_message_position = 0x7ffff6003aec&outgoing_message->message_buffer[512] = 0x7ffff6003cc0468message_router_response->message.used_message_length = 0x1de = 478Therefore, the code copies 478 bytes into a region with only 468 bytes remaining.
That means this
memcpynecessarily overruns the end ofmessage_bufferby 10 bytes.4. The overflow overwrites the adjacent
current_message_positionfieldThe
ENIPMessagelayout is:So the field
current_message_positionis located immediately aftermessage_buffer[512].GDB confirmed:
&outgoing_message->message_buffer[512] == &outgoing_message->current_message_positionBefore the overflow, memory at that location contained the valid pointer value.
After the
memcpy, the bytes at that address changed to:which corresponds to the corrupted pointer:
After that corruption, a subsequent
AddIntToMessage()attempts to write through this invalid pointer and the process crashes.