Skip to content
OT Cybersecurity | | | 8 min read

Fuzzing with AFL - finding vulnerabilities in ICS software

How to use AFL/AFL++ fuzzing to find security vulnerabilities in ICS software. Case study: unauthenticated DoS in DLMS energy meter library.

D
Dominik M.
B
Bartłomiej Bojarczuk
fuzzingAFLDLMSsecurity testingvulnerability researchICS
Fuzzing with AFL - finding vulnerabilities in ICS software
)}

In 2019, while testing GuruxDLMS.c - an open-source implementation of the DLMS protocol used for remote energy meter readings - we found a vulnerability that allowed remote disruption of the meter without any authentication. A single TCP connection was enough to crash the device. The tool was AFL (American Fuzzy Lop), a coverage-guided fuzzer that found the crashing input within hours.

We reported the vulnerability to the vendor and confirmed it was patched before publishing this article. The methodology described here is universal - we apply it in security testing of OT devices.

What is fuzzing

Fuzzing (fuzz testing) is a software testing technique that feeds a program massive amounts of random or modified input data and observes what happens. If the program behaves incorrectly - crashes, leaks memory, enters an infinite loop - we have a potential security bug. Fuzzing is particularly effective at finding memory access errors (buffer overflow, use after free, out-of-bounds read/write), which are the most dangerous class of vulnerabilities in C/C++ programs.

Modern fuzzers like AFL++ go far beyond random data generation. They use instrumentation - the program reports which code paths were executed during each run. Based on this feedback, the fuzzer applies genetic algorithms: it prefers inputs that reached new, unexplored code paths and mutates them further. Instead of shooting randomly, it systematically increases code coverage.

NOTE

The course Fuzzing 1001: Introductory white-box fuzzing with AFL++ (OpenSecurityTraining2, instructor: Francesco Pollicino, ex-Siemens) is a solid introduction covering mutation-based fuzzing, coverage-guided fuzzing, ASAN, and PCGUARD instrumentation.

Why fuzzing matters for OT

Software running on industrial devices - PLCs, RTUs, energy meters, BMS controllers - is often written in C/C++ and processes network data (Modbus, DNP3, DLMS, OPC protocols). Vulnerabilities in protocol parsers can enable remote takeover or denial of service. MITRE ATT&CK for ICS documents techniques like Exploitation of Remote Services (T0866) and Denial of Service (T0814) that directly exploit such bugs.

Fuzzing is particularly valuable because it finds bugs that manual testing and code review miss, it runs automatically for weeks without supervision, every crash is reproducible with the exact input file, and it scales to large codebases.

Case study: GuruxDLMS.c

Target: DLMS library for energy meters

GuruxDLMS.c is an open-source implementation of DLMS/COSEM - a protocol standard used in the energy sector for remote meter readings. In our attack scenario, an attacker connects to a GuruxDLMS.c-based server (energy meter) to disrupt its operation or take control (e.g., falsify readings).

Step 1: Preparing the program for fuzzing

AFL delivers input data via files - it passes the generated file path as a command-line argument. We rewrote the example server (which listens on TCP port 4061) to read from a file instead:

// GuruxDLMSSimpleServerExample/src/main.c (modified)
int main(int argc, char **argv){
  unsigned char data;
  FILE *f;
  if(argc < 2){
    puts("Error. Please provide a filename");
    return 1;
  }
  svr_init(&settings, 1, DLMS_INTERFACE_TYPE_HDLC,
           HDLC_BUFFER_SIZE, PDU_BUFFER_SIZE,
           frame, HDLC_BUFFER_SIZE, pdu, PDU_BUFFER_SIZE);
  svr_InitObjects(&settings);
  if(svr_initialize(&settings)){
    puts("Server init error");
    return 2;
  }
  if(!(f = fopen(argv[1], "rb"))){
    puts("Error reading file");
    return 3;
  }
  while(fread(&data, sizeof(unsigned char), 1, f)){
    if(svr_handleRequest3(&settings, data, &reply)){
      puts("Error when handling request");
      break;
    }
    if(reply.size != 0){
      printf("Server reply: %s\n", reply.data);
      bb_clear(&reply);
    }
  }
  fclose(f);
  return 0;
}

The processing logic remains identical to the original server - the only change is the data source (file instead of TCP). This ensures that any bugs we find apply to the real parser code, not our modifications.

Step 2: Compiling with instrumentation

AFL requires the program to be compiled with instrumentation - special tracking code that reports which code branches are executed. We also used AddressSanitizer (ASAN), which stops the program immediately on any invalid memory access and reports the exact source location.

# In Makefile: CC=afl-clang, CFLAGS+=-fsanitize=address, LFLAGS+=-fsanitize=address
cd development && make
cp lib/libgurux_dlms_c.a ../GuruxDLMSSimpleServerExample/obj/
cd ../GuruxDLMSSimpleServerExample && make

AFL instrumentation records which code branches are visited. ASAN catches invalid memory accesses that might otherwise go unnoticed - without it, a crash could be silently ignored by the program.

Step 3: Preparing seed inputs

AFL needs a directory with sample input data to mutate. Ideally, these should be valid DLMS frames. If we don’t have any, AFL can work even with a single space character:

mkdir input output
echo ' ' > input/1

Thanks to its genetic algorithm, AFL will eventually start generating valid DLMS communication - though the process is much faster with meaningful seed data.

Step 4: Running the fuzzer

afl-fuzz -i input/ -o output/ bin/gurux.dlms.simple.server.bin @@

The @@ symbol marks where AFL substitutes the path to the generated file. After several hours, the fuzzer typically finds hundreds or thousands of unique code paths - and, with sufficient coverage, files that cause crashes.

The checksum problem

After several hours, we noticed the fuzzer wasn’t discovering many new paths. The reason: the DLMS protocol verifies message checksums. Even intelligent fuzzers using genetic algorithms cannot automatically calculate and place a correct CRC in the right position.

TIP

Solutions for checksum-protected protocols:

  • Disable CRC verification in code (simplest - this is what we did)
  • AFL post_library - a script that fixes checksums in generated data
  • Custom mutator in AFL++ - a plugin generating protocol-compliant data
  • Libprotobuf-mutator - for protocols with structure definitions (protobuf)

Disabling CRC is sufficient for finding parser bugs. If we need to exploit found bugs in a real attack scenario (e.g., during a penetration test), we must add correct checksums.

Step 5: The vulnerability

After disabling CRC verification and restarting the fuzzer, AFL found files causing crashes. ASAN analysis revealed the root cause:

==10691==ERROR: AddressSanitizer: stack-buffer-overflow
  on address 0x7fffffffd5c1
READ of size 8187 at 0x7fffffffd5c1 thread T0
    #0 __asan_memcpy
    #1 ba_copy src/bitarray.c:203
    #2 apdu_parseProtocolVersion src/apdu.c:1067
    #3 apdu_parsePDU src/apdu.c:1428
    #4 svr_HandleAarqRequest src/server.c:325
    #5 svr_handleCommand src/server.c:2398
    #6 svr_handleRequest3 src/server.c:2467

ASAN prints the exact call stack at the moment of failure. The key elements: the error type in the first line (stack-buffer-overflow) and the call stack - read bottom-up from where the program started to where the error occurred.

The root cause: in apdu_parseProtocolVersion(), the unusedBits variable (read from input data) could exceed 8, causing an integer overflow. The value passed to ba_copy() exceeded the source array size, resulting in an out-of-bounds read.

Vulnerability assessment

This is an unauthenticated DoS - an attacker could remotely crash an energy meter without knowing any password. The vulnerable function apdu_parseProtocolVersion() is called before authentication checks, meaning anyone who can establish a TCP connection can cause a crash.

The excess data copied from the buffer was not sent back to the attacker (this is not a data leak vulnerability like Heartbleed in OpenSSL, which allowed reading server memory contents over the network). Here the consequence is different: process crash and service disruption.

Fuzzing tools for OT software

ToolTypeOT use caseNotes
AFL++Coverage-guided, mutation-basedProtocol libraries (DLMS, Modbus, OPC)Active successor to AFL. PCGUARD, CMPLOG, RedQueen
libFuzzerIn-process, coverage-guidedParsers, librariesPart of LLVM, faster than AFL for small targets
HonggfuzzCoverage-guided, multi-processServers, daemonsSupports network process fuzzing
BoofuzzGeneration-based, networkOT protocols over network (Modbus TCP, DNP3, OPC)Successor to Sulley, no instrumentation required
Peach FuzzerModel-based, networkComplex OT protocolsCommercial, with protocol format definitions

TIP

For OT protocols with checksums (DLMS, DNP3, Modbus), consider a hybrid approach: AFL++ with a custom mutator for deep parser fuzzing + Boofuzz for over-the-network protocol fuzzing with ready-made field definitions and CRC. Boofuzz doesn’t require instrumentation - it can fuzz a physical device (PLC, meter) without source code access.

Fuzzing as part of OT security testing

Fuzzing should be part of every OT software security testing process - both during development (SDL - Security Development Lifecycle) and during penetration testing of devices.

Key takeaways from our experience:

  1. Fuzzing alone is not enough - it must be part of a broader testing process (code review, manual testing, architecture analysis)
  2. Source code access dramatically increases effectiveness - AFL instrumentation requires compilation with afl-clang. Without source code, use QEMU mode (slower) or network fuzzing (Boofuzz)
  3. Checksums are the main barrier - most OT protocols implement them. Custom mutators or disabling CRC in code is a necessity
  4. Report found bugs responsibly - contact the vendor directly, agree on a publication timeline, and wait for the patch before publishing. That’s what we did with GuruxDLMS.c: the vendor received our report before this article was published. This approach (coordinated disclosure) protects library users and builds trust between researchers and vendors

Sources

Omówimy zakres, metodykę i harmonogram.