RC RANDOM CHAOS

CVE-2023-2163 is now a config file away

Zeroserve exposes eBPF program loading through an HTTP scripting surface. The kernel verifier becomes the trust boundary for every web request.

· 7 min read

Zeroserve is a zero-config web server that exposes eBPF hooks as a scripting surface. The design intent is programmable request handling - match a path, attach a BPF program, run logic in the kernel before the request returns to userspace. The security consequence is that web application logic now executes inside the kernel verifier’s trust boundary. That boundary has a long CVE history.

eBPF is not new attack surface. The Linux BPF verifier has shipped exploitable bugs in nearly every kernel release since 5.4. CVE-2021-3490 was an ALU32 bounds tracking flaw that yielded local privilege escalation through arbitrary kernel read/write. CVE-2022-23222 was a pointer arithmetic flaw - *_OR_NULL pointers were treated as scalars after a NULL check, giving the verifier-bypass primitive that Manfred Paul demonstrated at Pwn2Own. CVE-2023-2163 was a branch tracking error in the verifier’s state pruning logic that produced LPE against unprivileged BPF on Ubuntu defaults. Every one of those CVEs assumed the attacker could load a BPF program. Zeroserve hands that capability to any user who can write a configuration file.

The bug class that matters here is verifier state divergence. The eBPF verifier is a static analyser. It walks every reachable path through a BPF program and tracks the possible values of every register and stack slot as abstract domains - tnums for bitwise constraints, signed and unsigned ranges, pointer types with offset bounds. Before the program runs, the verifier proves that every memory access is in-bounds, every helper call has the right argument types, and every loop terminates. The verifier’s correctness is the entire security model. If the verifier’s abstract state disagrees with the JIT’s concrete execution on any path, the attacker has an oracle to construct out-of-bounds reads and writes against kernel memory.

Most verifier bugs follow the same pattern. A new feature is added - bounded loops, dynptr, kfuncs, BPF arena. The verifier is updated to handle the new construct. An edge case in the abstract domain is missed. The JIT emits code that operates on values the verifier thought were impossible. The attacker writes a program that reaches that edge case, gets the JIT to emit a confused load or store, and pivots to arbitrary kernel read/write. From there the playbook is fixed: locate the cred struct of the current task, overwrite uid, gid, and cap_effective, return to userspace as root. T1068, exploitation for privilege escalation. The primitive is kernel R/W. The technique is verifier-state confusion.

Zeroserve’s exposure model puts that primitive behind an HTTP request path. If the server permits remote operators to attach BPF programs through its scripting surface - and the documented use case is dynamic request handling - then a remote compromise of the management interface yields kernel code execution on the host. If the server restricts BPF loading to local operators only, the model degrades to a local privilege escalation vector with a web-shaped attack path. Either way, the trust boundary moves. A previously userspace web server now decides what gets loaded into the kernel verifier.

The attack chain is short. T1190, exploitation of a public-facing application, against whatever authentication Zeroserve ships in front of its scripting endpoint. T1505 class persistence through a server component is bypassed entirely - the BPF program itself is the persistence layer, attached to a hook that fires on every matching request. T1014, rootkit, is reachable directly. Published research on BPF rootkits - bad-bpf, TripleCross, boopkit - demonstrates kprobe and tracepoint attachment to hide processes, intercept getdents64, hook openat, and tamper with TCP SYN responses. A Zeroserve operator scripting request handling has the same load privilege as those research tools.

Real-world exploitation context for BPF-loaded payloads exists. TeamTNT operators used BPF-based hiding in 2022 cloud campaigns. The Symbiote malware family, attributed to operators targeting Latin American financial institutions, used BPF for credential theft and connection hiding. CrowdStrike and Intezer reporting in 2022 documented BPF tracepoint abuse for capturing libpcap-style traffic without raw sockets. The technique is not theoretical. Operators have shipped it. What Zeroserve changes is the on-ramp. Instead of requiring CAP_BPF and a dropped binary, the loader is a feature of the web server.

The specificity of hook type matters. Web request handlers attached via Zeroserve will run under XDP, TC, or socket filter program types depending on the hook point. XDP programs execute in the network driver path before skb allocation. They have direct packet access, they can drop, redirect, or modify frames at line rate, and they share execution context with the NIC’s interrupt path. A verifier bypass in an XDP program does not just give kernel R/W. It gives kernel R/W on the receive path of every packet hitting the interface. Network filter bypass becomes trivial. Connection hiding from netstat, ss, and /proc/net/tcp becomes a matter of dropping packets matching a tuple. The attacker controls what the host sees on the wire before any userspace tool can observe it.

TC programs at the ingress and egress qdisc have similar reach with more flexibility - they see fully formed skbs and can mutate them. Socket filter programs are narrower but still attach to running sockets and inspect every byte. The hook point selects the blast radius. Zeroserve’s documentation will state what hooks operators can request. The threat model has to assume operators will request the most permissive available, because that is what the use case rewards.

Telemetry is where this gets uncomfortable. eBPF programs do not appear in standard Linux audit trails by default. Sysmon for Linux logs process and network events. It does not introspect BPF program load operations. auditd can be configured to log the bpf() syscall - audit rule -a always,exit -F arch=b64 -S bpf - but it logs the syscall, not the program semantics. The verifier accepts a program. auditd records that a program was loaded. The program then mutates packet processing or hides processes. The detection surface is the load event. The behavioural surface is invisible without specialised tooling.

EDR vendors have closed some of this. Falco rules detect bpf() syscall with BPF_PROG_LOAD command. Tetragon, built on eBPF itself, can observe other BPF programs being loaded. Microsoft Defender for Endpoint on Linux added BPF program load telemetry in 2024. CrowdStrike Falcon logs BPF load events. The gap is correlation. A load event in isolation is not malicious. Modern observability stacks load dozens of BPF programs at boot. Cilium, Pixie, Parca, BCC tools, systemd-journald-bpf, every modern eBPF security product, all generate load noise. The signal a defender needs is the program’s intent. The verifier knows the program’s instruction sequence. The EDR sees only the syscall outcome.

What fires reliably: bpf() syscall load events when audit rules are configured. What fires inconsistently: program attachment to specific hook points - kprobes on do_init_module, tracepoints on tcp_v4_connect, uprobes on libssl. What rarely fires: behaviour driven by the program itself, because the program runs in kernel context and its side effects look like kernel side effects. A BPF program that mutates getdents64 return values to hide a process directory produces no process event, no file event, no syscall trace anomaly. The hiding is the absence of evidence. Detection requires either pre-load program analysis - disassembling the BPF bytecode and reasoning about its semantics - or runtime introspection of BPF object map state through bpftool prog show and bpftool map dump. Neither is standard SOC workflow.

The patch boundary for Zeroserve itself is not the interesting boundary. Patching the web server’s authentication or input validation closes the front door. The verifier bugs underneath are kernel CVEs with their own patch cadence. CVE-2024-1086 - netfilter use-after-free, not eBPF directly but reachable through nf_tables which BPF programs can interact with - had a stable exploit, public PoC, and was added to CISA’s KEV catalog within weeks of disclosure. The pattern repeats. A verifier or JIT bug is found, disclosed, patched in mainline, and then sits unpatched on production kernels for months because distribution backports lag and reboots are expensive. The Zeroserve attack surface inherits every one of those unpatched kernels.

Residual exposure post-patch on Zeroserve itself: the model still loads attacker-controlled BPF logic into the kernel under operator authority. That is the design. The verifier is the last line of defence. Treat Zeroserve as a kernel-loading service for threat modelling purposes. Apply the controls that apply to kernel module loading - restricted operator set, signed program manifests, audit on every load, behavioural baselining of which hooks are touched. The web server framing is misleading. The actual service is privileged code injection into the kernel, delivered over HTTP.

The CVEs are coming. Verifier bugs in upstream Linux average four to six disclosed per year. JIT bugs add another two to four. Each one becomes a Zeroserve weaponisation path the moment the affected kernel is in operator inventory. The window between kernel CVE disclosure and Zeroserve operator patch is the exploitation window. That window is measured in weeks at best. The tool is not the vulnerability. The tool is the delivery mechanism for whatever the verifier ships next.

Share

Keep Reading

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.