Skip to content

Fix OOB read in TCP/IP interface hostname validation#586

Open
MrAlaskan wants to merge 1 commit into
EIPStackGroup:masterfrom
MrAlaskan:fix/tcpip-hostname-oob-read
Open

Fix OOB read in TCP/IP interface hostname validation#586
MrAlaskan wants to merge 1 commit into
EIPStackGroup:masterfrom
MrAlaskan:fix/tcpip-hostname-oob-read

Conversation

@MrAlaskan

@MrAlaskan MrAlaskan commented Jun 14, 2026

Copy link
Copy Markdown

Summary

This change fixes an out-of-bounds read in the TCP/IP Interface Object Host Name and Domain Name validation paths.

The issue is caused by a semantic mismatch inside OpENer: the code first stores incoming data as length-prefixed CIP strings, but later validates the same data as if it were NUL-terminated C strings. Under AddressSanitizer, this issue is reproducible as a heap-buffer-overflow and can crash the process.

Vulnerability Description

This issue affects OpENer 2.3.0 (commit 36943e6). The vulnerable path is part of the input validation logic for writable attributes of the TCP/IP Interface Object (Class 0xF5). The relevant locations are:

  • source/src/cip/ciptcpipinterface.c: IsValidNameLabel
  • source/src/cip/ciptcpipinterface.c: IsValidDomain
  • source/src/cip/ciptcpipinterface.c: DecodeCipTcpIpInterfaceHostName
  • source/src/cip/ciptcpipinterface.c: DecodeTcpIpInterfaceConfiguration
  • source/src/cip/cipstring.c: SetCipStringByData

When a client sends SetAttributeSingle(0x10) to write the Host Name (Attribute 6) or the Domain Name field inside Interface Configuration, OpENer first allocates and copies the exact number of bytes described by the incoming CIP String length field. No trailing NUL byte is appended. The later validation logic still treats that memory as a C string and continues reading past the end of the heap buffer.

Root Cause

The root cause is an internal API contract mismatch:

  • SetCipStringByData() creates a length-prefixed CipString and guarantees only length valid bytes.
  • It does not guarantee that the stored byte array contains a trailing '\0'.
  • IsValidNameLabel() and IsValidDomain() were implemented with C-string semantics, for example:
    • scanning with while ('\0' != *label)
    • searching for '.' with strchr()
    • temporarily writing '\0' into the buffer to split labels

As a result, even a semantically valid CIP String can trigger an out-of-bounds read if its allocated heap buffer does not happen to be followed by a NUL byte.

Reproduction

1) Build

Build OpENer in a POSIX environment (e.g. Ubuntu 22.04) with OpENer_TRACES and ASan enabled.

Edit bin/posix/setup_posix.sh to ensure the following flags are set:

  -DOpENer_TRACES:BOOL=ON \
  -DOpENer_TRACE_LEVEL_ERROR:BOOL=ON \
  -DCMAKE_C_FLAGS:STRING="-fsanitize=address -fno-omit-frame-pointer -DOPENER_TCPIP_IFACE_CFG_SETTABLE=1" \

Then build the project:

cd /home/user/OpENer/bin/posix
./setup_posix.sh
make

2) Run target

Start the target process:

./src/ports/POSIX/OpENer lo

3) Send attack traffic

In a separate terminal, execute the PoC script:
PoC.py

This PoC performs the following operations:

  1. Target the TCP/IP Interface Object (Class 0xF5), Instance 1
  2. Send SetAttributeSingle(0x10) to Host Name (Attribute 6)
  3. Use a valid CIP String payload, for example a length of 6 with the content "ubuntu", encoded as:
06 00 75 62 75 6e 74 75

4) The request enters the following path:

DecodeCipTcpIpInterfaceHostName
  -> SetCipStringByData
  -> IsValidNameLabel

SetCipStringByData() allocates exactly 6 heap bytes and does not append '\0'. IsValidNameLabel() then keeps scanning as a C string and eventually reads beyond the allocated heap region, producing a heap-buffer-overflow

The full ASan log is shown below:

==995612==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000007f6 at pc 0x56469f4929bc bp 0x7ffe9c861510 sp 0x7ffe9c861500
READ of size 1 at 0x6020000007f6 thread T0
    #0 0x56469f4929bb in IsValidNameLabel /home/user/OpENer/source/src/cip/ciptcpipinterface.c:127
    #1 0x56469f494fef in DecodeCipTcpIpInterfaceHostName /home/user/OpENer/source/src/cip/ciptcpipinterface.c:494
    #2 0x56469f46d093 in SetAttributeSingle /home/user/OpENer/source/src/cip/cipcommon.c:823
    #3 0x56469f465665 in NotifyClass /home/user/OpENer/source/src/cip/cipcommon.c:127
    #4 0x56469f491854 in NotifyMessageRouter /home/user/OpENer/source/src/cip/cipmessagerouter.c:217
    #5 0x56469f4a2f8f in NotifyCommonPacketFormat /home/user/OpENer/source/src/enet_encap/cpf.c:60
    #6 0x56469f4ab886 in HandleReceivedSendRequestResponseDataCommand /home/user/OpENer/source/src/enet_encap/encap.c:558
    #7 0x56469f4a8840 in HandleReceivedExplictTcpData /home/user/OpENer/source/src/enet_encap/encap.c:186
    #8 0x56469f45fbe6 in HandleDataOnTcpSocket /home/user/OpENer/source/src/ports/generic_networkhandler.c:864
    #9 0x56469f45dd8a in NetworkHandlerProcessCyclic /home/user/OpENer/source/src/ports/generic_networkhandler.c:497
    #10 0x56469f45b513 in executeEventLoop /home/user/OpENer/source/src/ports/POSIX/main.c:261
    #11 0x56469f45b3bd in main /home/user/OpENer/source/src/ports/POSIX/main.c:229
    #12 0x7fc7ba148082 in __libc_start_main ../csu/libc-start.c:308
    #13 0x56469f45abed in _start (/home/user/OpENer/bin/posix/src/ports/POSIX/OpENer+0x89bed)

0x6020000007f6 is located 0 bytes to the right of 6-byte region [0x6020000007f0,0x6020000007f6)
allocated by thread T0 here:
    #0 0x7fc7badc6a06 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:153
    #1 0x56469f4a2a5b in CipCalloc /home/user/OpENer/source/src/ports/POSIX/sample_application/sampleapplication.c:199
    #2 0x56469f49a4be in SetCipStringByData /home/user/OpENer/source/src/cip/cipstring.c:179
    #3 0x56469f494fa8 in DecodeCipTcpIpInterfaceHostName /home/user/OpENer/source/src/cip/ciptcpipinterface.c:486
    #4 0x56469f46d093 in SetAttributeSingle /home/user/OpENer/source/src/cip/cipcommon.c:823
    #5 0x56469f465665 in NotifyClass /home/user/OpENer/source/src/cip/cipcommon.c:127
    #6 0x56469f491854 in NotifyMessageRouter /home/user/OpENer/source/src/cip/cipmessagerouter.c:217
    #7 0x56469f4a2f8f in NotifyCommonPacketFormat /home/user/OpENer/source/src/enet_encap/cpf.c:60
    #8 0x56469f4ab886 in HandleReceivedSendRequestResponseDataCommand /home/user/OpENer/source/src/enet_encap/encap.c:558
    #9 0x56469f4a8840 in HandleReceivedExplictTcpData /home/user/OpENer/source/src/enet_encap/encap.c:186
    #10 0x56469f45fbe6 in HandleDataOnTcpSocket /home/user/OpENer/source/src/ports/generic_networkhandler.c:864
    #11 0x56469f45dd8a in NetworkHandlerProcessCyclic /home/user/OpENer/source/src/ports/generic_networkhandler.c:497
    #12 0x56469f45b513 in executeEventLoop /home/user/OpENer/source/src/ports/POSIX/main.c:261
    #13 0x56469f45b3bd in main /home/user/OpENer/source/src/ports/POSIX/main.c:229
    #14 0x7fc7ba148082 in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/user/OpENer/source/src/cip/ciptcpipinterface.c:127 in IsValidNameLabel
Shadow bytes around the buggy address:
  0x0c047fff80a0: fa fa 00 01 fa fa 00 06 fa fa 00 00 fa fa 01 fa
  0x0c047fff80b0: fa fa 01 fa fa fa 01 fa fa fa 01 fa fa fa 01 fa
  0x0c047fff80c0: fa fa 01 fa fa fa 00 00 fa fa 01 fa fa fa 01 fa
  0x0c047fff80d0: fa fa 01 fa fa fa 02 fa fa fa 02 fa fa fa 02 fa
  0x0c047fff80e0: fa fa 00 00 fa fa 00 00 fa fa 00 00 fa fa 00 00
=>0x0c047fff80f0: fa fa 00 00 fa fa 00 00 fa fa 06 fa fa fa[06]fa
  0x0c047fff8100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8110: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8120: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8130: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8140: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==995612==ABORTING

Fix Logic

This fix only modifies source/src/cip/ciptcpipinterface.c and does not change the general memory semantics of CipString.

The fix works as follows:

  1. IsValidNameLabel() is converted to a length-aware validator that takes both label and label_length
  2. IsValidDomain() is converted to a length-aware validator that takes both domain and domain_length
  3. The validation logic no longer depends on a trailing '\0' and no longer uses strchr() or temporary in-place NUL insertion
  4. Domain names are now parsed by explicit-length iteration, and each dot-separated label is validated with the length-aware IsValidNameLabel()
  5. Debug logging for Host Name and Domain Name is changed from %s to %.*s so that logging itself does not treat CipString data as a NUL-terminated C string

After this fix:

  • CipString keeps its original length-prefixed behavior and still does not require a trailing NUL
  • the validation path no longer reads beyond the allocated heap buffer
  • both Host Name and Domain Name paths are fixed consistently

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant