SSH (OpenSSH & libssh) capture¶
friTap can capture plaintext SSH traffic from OpenSSH and libssh on Linux and Android (Termux) in the same shape it captures plaintext TLS: synthetic TCP/22 frames written into a PCAPNG file, plus a side-car keylog file that Wireshark's SSH dissector can read to decrypt a paired ciphertext capture (produced separately, e.g. via tcpdump).
Two SSH implementations
The bulk of this page documents the OpenSSH hook ladder, which is the most mature path. libssh (used by the Qt/KDE ecosystem, Ansible, and various git clients) is detected and routed through the same SSH pipeline — see libssh support below. Dropbear and libssh2 are not supported.
Quick start¶
# Linux x86_64, client side
fritap --protocol ssh --include-loopback \
-p out.pcapng \
-- /usr/bin/ssh user@host 'echo hello'
# Linux server side (sshd) — friTap auto-enables --enable_child_gating
sudo fritap -s --protocol ssh \
-p sshd-out.pcapng \
-- /usr/sbin/sshd -D -p 2222
# Android via Termux
fritap -m --protocol ssh -p /sdcard/ssh.pcapng \
-- /data/data/com.termux/files/usr/bin/ssh user@host 'date'
After a run the working directory contains:
| File | Producer | Consumer |
|---|---|---|
out.pcapng | friTap PCAPNG sink | Open in Wireshark — plaintext TCP/22 frames |
keys.log (when -k set) | friTap keylog handler | Human-readable per-direction SSH keys |
out.ssh.log (auto-derived from -p) | friTap SSH keylog side-car | Wireshark Edit → Preferences → Protocols → SSH → Key log filename |
What the PCAPNG contains¶
friTap hooks ssh_packet_send2_wrapped (write path) and ssh_packet_read_poll2 (read path) inside OpenSSH. These are the points where the fully-assembled SSH binary packet is in memory in cleartext — post-compression on the send side, post-decryption + decompression on the receive side. friTap reads state->outgoing_packet / state->incoming_packet via sshbuf_ptr + sshbuf_len, fabricates an IPv4/IPv6 + TCP frame using the same 5-tuple lookup TLS uses (getsockname / getpeername on the SSH socket fd), and emits an Enhanced Packet Block into the PCAPNG file.
Wireshark's SSH dissector expects ciphertext + a keylog file. It will not auto-decode the plaintext that friTap embeds in this PCAPNG (it sees non-encrypted bytes where it expects encryption to have happened). To inspect:
# Follow the TCP stream — the binary packet protocol is visible
tshark -r out.pcapng -Y "tcp.port == 22" -q -z follow,tcp,raw,0
# Extract TCP payloads as raw bytes
tshark -r out.pcapng -Y "tcp.port == 22" -T fields -e tcp.payload \
| tr -d '\n' | xxd -r -p > /tmp/payload.bin
The bytes form the RFC 4253 §6 SSH binary packet protocol:
The first message in each direction is the SSH version banner; subsequent messages are KEXINIT, KEXDH_INIT, KEXDH_REPLY, NEWKEYS, USERAUTH_REQUEST, CHANNEL_DATA, etc.
The Wireshark side-car keylog¶
If you also have a paired ciphertext PCAP (e.g. you ran tcpdump -i any tcp port 22 alongside friTap), Wireshark can use the side-car keylog friTap auto-generates to decrypt and fully dissect that capture.
# 1. Capture ciphertext separately
sudo tcpdump -i any -w /tmp/ssh-cipher.pcap "tcp port 22" &
# 2. friTap produces the keylog (and a plaintext PCAPNG we don't need here)
fritap --protocol ssh -p /tmp/dummy.pcapng \
-- /usr/bin/ssh user@host 'echo decryptme'
# → /tmp/dummy.ssh.log is auto-derived from /tmp/dummy.pcapng
# 3. Open /tmp/ssh-cipher.pcap in Wireshark
# 4. Edit → Preferences → Protocols → SSH → Key log filename
# → /tmp/dummy.ssh.log
# 5. Reload (Ctrl-R). Encrypted SSH packets are now dissected.
Format details: - One line per (re)keying: <32-hex-cookie> SHARED_SECRET <K-hex>. - cookie is the 16-byte random from SSH_MSG_KEXINIT (hex-encoded). - SHARED_SECRET is the post-DH shared key K. Wireshark performs the RFC 4253 §7.2 KDF internally — friTap does not emit pre-derived per-direction encryption / IV / MAC keys here. - Wireshark accepts a match against either side's cookie; friTap writes both when it can recover them.
Requirements: Wireshark ≥ 4.0 (introduced the SSH keylog file preference in mid-2023). Earlier Wireshark versions see ciphertext only.
Cipher coverage¶
Wireshark's SSH dissector currently decrypts:
| Cipher | Status |
|---|---|
chacha20-poly1305@openssh.com | Supported |
aes128-ctr / aes192-ctr / aes256-ctr + hmac-sha2-256 / hmac-sha2-512 (incl. -etm) | Supported |
aes128-gcm@openssh.com, aes256-gcm@openssh.com | Supported |
aes*-cbc, 3des-cbc, deprecated MACs | Not supported |
zlib@openssh.com compression | Partial; may break dissection mid-session |
friTap always emits the keylog and the plaintext PCAPNG regardless of which cipher is negotiated — even when Wireshark can't decrypt the paired ciphertext PCAP, the plaintext file from friTap is usable.
CLI / API integration¶
--protocol ssh # Install SSH hooks only (excludes TLS/QUIC/OHTTP)
--protocol all # Install everything; prompts unless -y/--yes
--protocol auto # Script-friendly alias of --protocol all (no prompt)
--ssh-keylog <path> # Override the side-car file path
-y, --yes # Auto-confirm --protocol all
When --protocol ssh -p OUT.pcapng is set and --ssh-keylog is omitted, friTap auto-derives the side-car path: OUT.ssh.log.
Programmatic API mirror:
from friTap.api import FriTap
(
FriTap("ssh")
.protocol("ssh")
.pcap("out.pcapng")
.keylog("keys.log") # SSH side-car (keys.ssh.log) is auto-derived
.start("ssh user@host 'echo hello'")
)
How keys are extracted¶
friTap's SSH hook ladder (all symbol-based, no struct offsets in the primary path):
| Hook | Source | Purpose |
|---|---|---|
cipher_init(..., key, keylen, iv, ivlen, do_encrypt) | cipher.c | Per-direction key/IV (direct args). Mapped to C2S/S2C using /proc/self/comm process role. |
kex_send_kexinit(ssh) | kex.c | Capture local SSH_MSG_KEXINIT cookie. |
kex_input_kexinit(type, seq, ssh) | kex.c | Capture peer SSH_MSG_KEXINIT cookie. |
kex_derive_keys(ssh, hash, hashlen, shared_secret) | kex.c | Capture shared secret K (arg 3 is an sshbuf*). Correlates with cookies and emits the Wireshark keylog line. |
ssh_packet_send2_wrapped(ssh) | packet.c | Plaintext send path. |
ssh_packet_read_poll2(ssh, typep) | packet.c | Plaintext recv path. |
cipher_crypt(cc, ...) (fallback) | cipher.c | Used when wrapper symbols are stripped — emits plaintext with a synthetic loopback 5-tuple. |
Signatures are stable across OpenSSH 7.6 → 10.x (verified against upstream tags V_7_6_P1 → V_10_0_P1). The legacy kex_derive_keys_bn (BIGNUM-based, older OpenSSH) is supported as an automatic fallback.
libssh support¶
In addition to OpenSSH, friTap recognises libssh — the standalone SSH library used by the Qt/KDE ecosystem, Ansible, and a number of git clients. libssh is selected by the same --protocol ssh selector and flows through the same SSH capture pipeline; no separate flag is required.
libssh's key-derivation entry points differ from OpenSSH (there is no kex_derive_keys / sshenc; instead libssh uses ssh_make_sessionkey plus a packet-callback model). friTap advertises the libssh symbol set so the loader's address-resolution surface is accurate:
| Symbol | Role |
|---|---|
ssh_make_sessionkey | Session-key derivation |
ssh_packet_kexdh_init / ssh_packet_newkeys | Key-exchange state transitions |
ssh_socket_unbuffered_write | Send path |
ssh_packet_socket_callback | Receive path |
ssh_channel_read / ssh_channel_write | Channel-level plaintext |
The SSH executor probes each symbol family independently and no-ops the hooks that don't resolve, so an OpenSSH binary and a libssh-linked binary can both be attached with the same --protocol ssh invocation.
Maturity
The OpenSSH path (above) is the most battle-tested. libssh detection and symbol resolution are wired through the same pipeline; treat libssh plaintext/keylog coverage as newer and verify against your target build.
Server-side caveat: sshd privilege separation¶
sshd forks a pre-auth child for KEX, then re-execs into sshd-session post-auth. The plaintext + keylog material is generated in the first fork (KEX completes before authentication). When --protocol ssh is set and the target binary basename matches sshd or sshd-session, friTap auto-enables --enable_child_gating so Frida follows the fork chain.
Symbol availability¶
friTap resolves symbols via Module.findExportByName and falls back to a .symtab scan (findNonExportedSymbol). This covers:
| Build | Status |
|---|---|
Debian / Ubuntu / Fedora / Arch (default sshd/ssh packages) | Works out of the box |
| Termux openssh (Android) | Works out of the box |
| Alpine Linux (stripped sshd) | Pattern matching required; v1 ships placeholder entries — see the pattern file format and the pattern derivation guide |
| Custom statically-stripped builds | Same as Alpine |
When all primary plaintext hooks fail to resolve, friTap falls back to cipher_crypt (always exported) and emits plaintext with a synthetic loopback 5-tuple — Wireshark's SSH dissector won't dissect it but tshark's hex view still shows the cleartext.
Limitations¶
- Wireshark dissection of the friTap PCAPNG itself: the friTap PCAPNG is plaintext-where-Wireshark-expects-ciphertext. The SSH dissector won't engage on it directly. Use the side-car keylog against an independent ciphertext capture.
- No PCAPNG DSB block for SSH: the pcapng spec has not assigned an SSH Decryption Secrets Block type. The side-car file is the only delivery channel for now.
- Dropbear (LineageOS, OpenWrt) is not covered. Different codebase, different struct layouts. Out of scope.
- libssh is supported via the same
--protocol sshpipeline (see libssh support); its coverage is newer than OpenSSH's. libssh2 is not covered. - iOS is not supported (no native OpenSSH on iOS).