Uprobe Walkthrough - NormB/sipnab GitHub Wiki

A step-by-step walkthrough of sipnab's uprobe and eBPF capture: what it is, what it is not, whether your machine can run it at all, and how to use it.


Read this first

sipnab can show you SIP that only ever exists encrypted on the wire โ€” no private key, no SSLKEYLOGFILE, and no restart of the daemon.

This is not an attack on TLS. Nothing here decrypts a captured packet, and none of it works against traffic on the wire or against another machine. What it does is far simpler: it reads the plaintext inside a process you already control, at the moment that process hands the bytes to its TLS library, and before any encryption happens.

What it needs root on the machine running the SIP daemon
What it reads memory of processes on that machine
What it cannot do decrypt a pcap, read another host, or recover a session key

If you have root on a box, you can already read any process's memory with a debugger. This is the same authority, aimed at one function and filtered to SIP. It gives you nothing you did not already have โ€” it just makes it practical.

Security implications, stated plainly

Because it is easy to under-read the sentence above, here is what it means in operational terms.

  • Anyone who can run this can read every SIP session on that host, for every process that uses a probed TLS library โ€” not only the daemon you had in mind. That includes Authorization headers, Proxy-Authorization, any credential a UA sends, and the contents of MESSAGE bodies.
  • The captured plaintext is as sensitive as the session itself. Treat a capture from this feature the way you would treat a keylog: it is not a redacted summary, it is the traffic.
  • It leaves kernel state. A probe attaches to a library FILE, not to your process, so it costs every process mapping that library for as long as it stays there. sipnab removes them on exit โ€” see Cleaning up for how to confirm, and what to do if a crash left some behind.
  • It cannot transmit. sipnab refuses to send anything derived from this input. Scanner-kill and every other response path stays shut for uprobe captures, and no flag opens it, because sipnab never observed a peer to answer to.
  • Least privilege. Prefer CAP_SYS_ADMIN + CAP_PERFMON (and CAP_BPF for the eBPF backend) over blanket root, and run the capture for as long as you need it rather than leaving it attached.

If your threat model does not allow reading process memory on that host, this feature is not for you โ€” use --keylog with a key the endpoint produces deliberately.

Can your system run this at all?

It does not work everywhere, and it fails closed rather than quietly. Three requirements, in the order they bite.

1. Linux with tracefs โ€” required for either backend

uname -s; ls -d /sys/kernel/tracing

Expect Linux and the directory to exist. macOS, Windows and any BSD have no uprobes at all: sipnab refuses with a message saying so, rather than starting a capture that could never see anything. If the directory is missing, mount it:

sudo mount -t tracefs nodev /sys/kernel/tracing

2. Root, at the moment sipnab attaches a probe

id -u

Expect 0. sipnab drops privileges after it attaches, so a server started with --user cannot attach probes later โ€” which is why the MCP tool refuses on a dropped server rather than failing halfway.

3. BTF โ€” required only for the eBPF backend

ls -l /sys/kernel/btf/vmlinux

BTF is the BPF Type Format: the kernel's description of its own structs. sipnab reads the socket layout from it at load time, so the program keeps working across kernels instead of matching only the one that compiled it.

If this file does not exist, the eBPF backend cannot run on this machine. That is a property of the kernel build (CONFIG_DEBUG_INFO_BTF=y), not something sipnab can work around, and no amount of privilege changes it. The tracefs backend still works โ€” you simply do not get peer addresses.

A container cannot have BTF its host lacks: it shares the kernel. Checking inside a container tells you about the host.

The quickest answer to all three

sudo sipnab --uprobe-list

It installs nothing, and prints what a capture would probe:

FLAVOR        INODE  PIDS  LIBRARY
OpenSSL        21143    12  /proc/954/root/usr/lib/x86_64-linux-gnu/libssl.so.3
wolfSSL     17433084     1  /proc/982702/root/usr/lib/libwolfssl.so.42.2.0

There is no silent empty answer. When nothing matches, --uprobe-list says so and names both readings:

No TLS library is mapped by any process sipnab can see.
Either nothing here uses OpenSSL or wolfSSL, or sipnab cannot read
/proc/<pid>/maps โ€” try again as root.

An unprivileged run sees only its own processes, so run it as root before concluding the host uses no TLS. A library sipnab can see but cannot reach from its own mount namespace โ€” almost always one inside a container โ€” lists as (UNREACHABLE) with a count of how many, rather than quietly dropping out.

Walkthrough: the tracefs backend

The default. Works on any Linux with tracefs, needs no BTF and no special build.

Step 1 โ€” see what is there

sudo sipnab --uprobe-list

Step 2 โ€” capture

sudo sipnab -N --uprobe-tls

sipnab probes every TLS library it finds, because a host commonly runs more than one. Narrow it if only one stack is yours to read:

sudo sipnab -N --uprobe-tls --uprobe-flavor openssl

Step 3 โ€” read the output, and know what is missing

12:57:47.360 0.0.0.0:0 -> 0.0.0.0:0 REGISTER TCP origin=uprobe

Dialogs carry no addresses and port 0, labeled uprobe:<comm>/<pid>. A uprobe sees the bytes an application handed its TLS library and nothing about the socket beneath, so sipnab names the process rather than inventing a peer.

origin=uprobe closes the last gap that leaves. Without it, 0.0.0.0:0 -> 0.0.0.0:0 reads exactly like a wire capture whose addressing sipnab failed to parse, and the reader cannot tell the two apart. Every surface carries the same mark: the --json line adds "input_origin": "uprobe", and the TUI's raw viewer appends it to the info line. A wire capture stays unmarked.

The frame pointer beside it is honest about the same thing. --show-frame on a uprobe: pointer refuses and names the process the bytes came out of, because there is no capture to seek into and nothing to verify the bytes against:

sipnab --show-frame 'uprobe:opensips/954#3'

If you need the peer, that is the eBPF backend below.

Step 4 โ€” the symbol may not be the one you expect

A probe on the wrong symbol captures nothing while looking perfectly healthy โ€” the capture runs, attaches, and reports zero messages. OpenSSL 3 applications increasingly call SSL_write_ex rather than SSL_write:

nm -D --undefined-only /usr/sbin/opensips | grep -i ssl_write

Then pass what you found:

sudo sipnab -N --uprobe-tls --uprobe-symbol SSL_write_ex

Walkthrough: the eBPF backend

Adds the one thing tracefs cannot: who the peer was.

What it needs beyond the above

  • a sipnab whose build produced the kernel programs, which takes --features bpf and a nightly toolchain and cargo install bpf-linker on the build host;
  • CONFIG_DEBUG_INFO_BTF=y at run time.

Without either, sipnab refuses rather than falling back to tracefs. The addresses are the only reason to choose this backend, so a silent downgrade would leave you with the worst outcome: a capture with no peers and no reason given.

--features bpf alone is not enough, and the build says so quietly

Read this before trusting a binary you built yourself.

The feature compiles the loader. The kernel programs come from a second, nightly build that the main build runs for you, and that inner build needs bpf-linker and a nightly toolchain. When either is missing the outer build still succeeds. It prints a cargo:warning, embeds an empty placeholder object, and carries on โ€” so the binary advertises bpf in --version and has no programs to load.

sipnab catches that at attach time and refuses by name rather than attaching to nothing:

this binary carries the `bpf` feature but no kernel programs: it was built on a
machine without bpf-linker. Rebuild where `cargo install bpf-linker` has run, or
use --uprobe-backend tracefs, which needs neither it nor BTF

Degrading is the right trade for a contributor's laptop, because --all-features sweeps this feature into every clippy and rustdoc run. It is the wrong trade for an artifact other people install. Set SIPNAB_BPF_REQUIRED=1 in the build environment and the same missing prerequisite becomes a hard build failure instead:

SIPNAB_BPF_REQUIRED=1 cargo build --release --features bpf

The release workflow sets it on exactly the targets that ship the feature โ€” the Linux glibc builds โ€” so a published binary advertising bpf carries the programs. The static musl and macOS artifacts do not carry the feature at all.

Run it

sudo sipnab -N --uprobe-tls --uprobe-backend bpf --portrange 0-65535
200 OK     127.0.0.1:15061 -> 127.0.0.1:36160  TCP  uprobe:python3/349147#0
REGISTER   127.0.0.1:36160 -> 127.0.0.1:15061  TCP  uprobe:python3/349147#1

Widen --portrange: a TLS trunk on 5061 is the exception, and the port a uprobe reports is whatever the socket actually used โ€” often ephemeral.

How sipnab recovers the peer, and when it cannot

A uprobe on the write symbol sees plaintext and no socket. A kernel probe on tcp_sendmsg sees the socket and only ciphertext. sipnab pairs them by thread: a TLS library encrypts and sends on the calling thread, back to back.

When that pairing does not hold โ€” a write the library buffered rather than sent โ€” sipnab reports the message with no addresses at all, exactly as a tracefs capture does. sipnab does not guess a peer, because a guessed one would be indistinguishable from an observed one.

Cleaning up

Probes are kernel state that outlives the process which created them. sipnab removes its own on exit. Confirm with:

sudo grep sipnab_ /sys/kernel/tracing/uprobe_events

That should print nothing. If a crash left some behind, remove each by name:

sudo sh -c 'echo "-:sipnab_1234_l0_b64" >> /sys/kernel/tracing/uprobe_events'

Never truncate that file. Every other tracer on the host shares it, and emptying it takes their probes with it.

The eBPF backend needs none of this: the kernel frees its programs and maps when sipnab exits.

Troubleshooting

What you see What it means What to do
--uprobe-list finds no TLS library Unprivileged, so /proc/<pid>/maps opens only for your own processes Re-run as root before concluding the host uses no TLS
no kernel programs from a binary you built --features bpf compiled the loader, and the inner nightly build found no bpf-linker Rebuild on a host carrying bpf-linker, with SIPNAB_BPF_REQUIRED=1 so the gap fails the build
Capture attaches, reports 0 messages Almost always the wrong symbol nm -D --undefined-only <binary> | grep -i ssl_write, then --uprobe-symbol
needs this kernel's BTF ... could not read it No CONFIG_DEBUG_INFO_BTF Use --uprobe-backend tracefs; no privilege fixes this
no kernel programs: built on a machine without bpf-linker The binary carries the feature but not the programs Rebuild where cargo install bpf-linker has run, or use tracefs
Addresses are 0.0.0.0:0 on the eBPF backend The write and its send did not pair on one thread Expected, and honest โ€” sipnab never guesses a peer
Messages missing under load The kernel dropped records the reader could not keep up with sipnab reports the count on exit, so the loss is visible rather than hidden
A container's traffic is missing The path in maps names a different file in sipnab's namespace Handled automatically by inode; if naming a library by hand, use the /proc/PID/root/... form

What this cannot do

  • Decrypt a pcap. There is no key here: sipnab reads the plaintext before the library encrypts it.
  • Read another host. The probe is on a library file on the local machine.
  • Recover a session key. sipnab never sees one.
  • Work without root, or without Linux.
  • Answer a scanner. This input can never transmit, by design.

See also

โš ๏ธ **GitHub.com Fallback** โš ๏ธ