alosafuzz
ICS/OT protocol security research — fuzzing, scanning, and coordinated disclosure.

A Researcher's Guide to LwM2M & CoAP

Vendor: cross · Sector: iiot · Transport: coap · Project: icsfuzzer

Part of the alosafuzz ICS protocol-guide series — written for fuzzing and vulnerability researchers. Defensive posture: wire format, fuzz surface, and published bug classes only; no weaponized exploit or DoS code.


1. What it is & where it runs

LwM2M (OMA SpecWorks Lightweight M2M) is the device-management protocol for constrained IIoT devices — smart metering, utility/telemetry gateways, remote RTU-class sensors, asset tracking. A client (the managed device) registers with a server and exposes a tree of Objects/Instances/Resources by URI path (/3/0/0 = Device object, instance 0, Manufacturer). It is a scope-expansion for an ICS fuzzer — adjacent to OPC-UA/BACnet’s IIoT lean, not a classic floor protocol.

The single most important finding for a researcher: LwM2M adds almost no new wire format of its own — LwM2M messages are CoAP messages (RFC 7252). A “LwM2M fuzzer” is ~90% a CoAP parser fuzzer plus a thin semantic layer (which URI paths/content-formats/interfaces mean what). So the byte-level attack surface is shared with every CoAP product, and the verified CVE record (§5) lands exactly where that implies: CoAP option parsing, block-wise reassembly, and DTLS.

2. Session & transport model

Three transports, three framings, one CoAP semantic core:

LwM2M interfaces on top: Bootstrap, Registration (POST /rd?ep=<name>&lt=<lt> with a CoRE-link body), Device Management (Read/Write/Execute/Create/Delete on object URIs), and Information Reporting (Observe/Notify).

3. Wire format — CoAP (the real target)

4-byte CoAP/UDP header: Ver(2) | T(2) | TKL(4) | Code(8) | Message-ID(16) + Token (TKL bytes) + options + 0xFF payload marker + payload.

Options — the dominant bug surface. Each option byte = delta(4) | length(4) with escape nibbles: 13 → +8-bit extension byte (+13), 14 → +16-bit extension (+269), 15 → reserved (legal only as the whole 0xFF payload marker; a 15 in either nibble otherwise is a format error the parser must reject). This delta/length nibble encoding with extension bytes is historically the richest CoAP parser-bug source.

Sub-surfaces with their own cases: Block-wise transfer (RFC 7959) — Block1/Block2 options (NUM/M/SZX), reassembly of out-of-order/overlapping/ oversized-total blocks (a known memory-safety sink, and the path for LwM2M’s Firmware Update object 5); Content-Format/Accept steering the body; Observe state machine.

LwM2M payload parsers (the thin semantic part, each its own sub-surface): LwM2M-TLV (content-format 11542, custom type-length-value — length fields are a separate integer surface), SenML-CBOR (1.1+, CBOR: indefinite-length items, deep nesting, huge declared lengths), SenML-JSON, plain/opaque, and CoRE Link (the registration body).

4. The fuzz surface

Classification line: a CoAP error response code (4.xx/5.xx) is a spec-compliant exception, not a crash. But over UDP, “timeout” is NOT by itself a crash — see the liveness heuristic in §6; this is the single most important calibration lesson of the module.

Structured vs. coverage-guided: both, strongly. The CoAP option/block/CBOR decoders are ideal coverage-guided/ASan targets against Wakaama / libcoap / Zephyr (all open C). The LwM2M semantic layer (firmware-object Write, interface state machine) and the stateful DTLS/registration flows are the structured black-box niche — and that niche is exactly where the recent 2024–26 semantic/ block-wise CVEs landed despite OSS-Fuzz coverage of the parsers.

5. Known vulnerabilities & bug classes (verified)

Every confirmed bug sits in a surface §3/§4 names in advance — strong validation:

6. Building a test harness

A framing-only software responder exists (scripts/lwm2m_responder.py, UDP), but LwM2M is lucky: mature open-source stacks are the real oracle — Eclipse Leshan (demo server), Eclipse Wakaama and AVSystem Anjay (example server/client), Zephyr’s client. The module was live-validated against a real Wakaama stack (built from source): plain-CoAP 80-case corpus, server survived all 80, 36 real interop responses, and the 5.00-Internal-Server-Error responses concentrated in the block-wise cases — the fuzzer puts signal on the historically right surface.

Two harness lessons that generalize:

DTLS-PSK transport is wrapped via a persistent openssl s_client -dtls1_2 -psk session (CPython has no DTLS; libcoap’s coap-client is one-shot and unusable for a fuzzing session) — socket-duck-typed so the send path is unchanged.

7. Open-source implementations & tooling

8. Spec references & sources

9. Open questions / under-explored surface

Worked example: protocols/lwm2m/fuzzer.py (8 categories × 80 cases × 3 transports), with the liveness-confirmation heuristic and --source-port; live-validated vs. Eclipse Wakaama. Responder scripts/lwm2m_responder.py (framing-only).