Chapter 36

TLS Decryption Lab

TLS decryption in network forensics requires the session keys — either from the server's private key (RSA key exchange only, pre-TLS 1.3) or from a TLS key log file (SSLKEYLOGFILE) that captures the session secrets at the client or server side. The SSLKEYLOGFILE approach works with all modern TLS including PFS cipher suites and TLS 1.3, because it captures the derived session keys rather than the private key.

Scenario

You've compromised a C2 implant in a controlled lab environment. You want to capture and fully decrypt the implant's TLS traffic to analyze the C2 protocol. You configure the system to export TLS session keys using the SSLKEYLOGFILE environment variable, run the implant, capture the traffic with tcpdump, then load both the PCAP and the key log file into Wireshark. Every TLS session decrypts, revealing the HTTP/2 C2 protocol in plaintext.

SSLKEYLOGFILE Approach

  SSLKEYLOGFILE — How Session Key Export Works
  ═══════════════════════════════════════════════════════════════════

  NSS (Network Security Services) key log format:
  CLIENT_RANDOM  
    Used for TLS 1.2 and earlier

  CLIENT_HANDSHAKE_TRAFFIC_SECRET  
  SERVER_HANDSHAKE_TRAFFIC_SECRET  
  CLIENT_TRAFFIC_SECRET_0  
  SERVER_TRAFFIC_SECRET_0  
    Used for TLS 1.3

  How client software exports keys:
  ├── Chrome/Firefox: set SSLKEYLOGFILE env var → writes keys to file
  ├── Python requests/urllib: manually via ssl module hooks
  ├── Java (Cobalt Strike teamserver): -Djavax.net.debug=ssl or JVM agent
  └── .NET/Schannel: ETW logging, harder to access directly

  Applications that support SSLKEYLOGFILE:
  ├── Firefox: built-in support
  ├── Chrome: built-in support (all Chromium-based browsers)
  ├── curl: built-in (--verbose shows, --sslkeylogfile writes)
  ├── Python ssl module: manual hook required (see script below)
  └── Any OpenSSL-based app: set via SSLKEYLOGFILE env before launch

  Applications that do NOT support SSLKEYLOGFILE:
  ├── Windows Schannel-based apps (IE, Edge legacy, many Windows services)
  ├── Java (unless patched or agent injected)
  └── Apps with pinned/custom TLS implementations

Setting Up TLS Decryption

bashtls-decryption-setup.sh
#!/bin/bash
# Setup TLS decryption for a lab session

KEYLOG_FILE="/tmp/tls_session_keys.log"
PCAP_FILE="/tmp/lab_capture.pcap"

echo "[1] Start packet capture in background"
tcpdump -i eth0 -w "$PCAP_FILE" &
TCPDUMP_PID=$!
echo "    tcpdump PID: $TCPDUMP_PID"

echo ""
echo "[2] Set SSLKEYLOGFILE environment variable"
export SSLKEYLOGFILE="$KEYLOG_FILE"
echo "    Keys will be written to: $KEYLOG_FILE"

echo ""
echo "[3] Now run the target application in this shell:"
echo "    Examples:"
echo "      curl https://target.com/"
echo "      python3 script_that_uses_requests.py"
echo "      firefox &"
echo "      ./malware_sample"
echo ""
echo "    Press Enter when done to stop capture..."
read

echo "[4] Stop capture"
kill $TCPDUMP_PID 2>/dev/null
sleep 1

echo ""
echo "[5] Key log file contents:"
wc -l "$KEYLOG_FILE"
head -3 "$KEYLOG_FILE"

echo ""
echo "[6] Analyze with tshark using key log:"
tshark -r "$PCAP_FILE" \
  -o "tls.keylog_file:${KEYLOG_FILE}" \
  -Y "http" \
  -T fields \
  -E separator="\t" \
  -e ip.src -e ip.dst -e http.request.method -e http.host -e http.request.uri \
  | head -30

echo ""
echo "[7] Open in Wireshark:"
echo "    wireshark -r $PCAP_FILE -o \"tls.keylog_file:${KEYLOG_FILE}\""

Exporting Keys from Python Applications

Pythonpython-tls-keylog.py
#!/usr/bin/env python3
"""
Add SSLKEYLOGFILE support to Python ssl/requests.
Wraps ssl.SSLSocket to export session keys on connection.
"""
import ssl, os, requests

KEYLOG_FILE = os.getenv("SSLKEYLOGFILE", "/tmp/tls_keys.log")

def _keylog_callback(connection, line):
    """Callback that writes session key lines to the keylog file."""
    with open(KEYLOG_FILE, "a") as f:
        f.write(line.decode() + "\n")

# Patch the default SSL context to enable key logging
_orig_create_default_context = ssl.create_default_context
def patched_create_default_context(*args, **kwargs):
    ctx = _orig_create_default_context(*args, **kwargs)
    ctx.keylog_filename = KEYLOG_FILE  # Python 3.8+
    return ctx
ssl.create_default_context = patched_create_default_context

# Now use requests normally — all TLS sessions are logged
resp = requests.get("https://example.com/api/checkin", timeout=10)
print(f"Status: {resp.status_code}")
print(f"Response: {resp.text[:200]}")
print(f"\nSession keys written to: {KEYLOG_FILE}")

Decrypting in Wireshark and tshark

bashwireshark-decrypt.sh
#!/bin/bash
PCAP="$1"
KEYLOG="$2"

echo "=== Decrypted HTTP/1.1 requests ==="
tshark -r "$PCAP" \
  -o "tls.keylog_file:${KEYLOG}" \
  -Y "http.request" \
  -T fields \
  -E separator="\t" \
  -e frame.time_epoch \
  -e ip.src \
  -e ip.dst \
  -e http.request.method \
  -e http.host \
  -e http.request.uri \
  -e http.user_agent \
  -e http.file_data

echo ""
echo "=== Decrypted HTTP/2 requests ==="
tshark -r "$PCAP" \
  -o "tls.keylog_file:${KEYLOG}" \
  -Y "http2.headers" \
  -T fields \
  -E separator="\t" \
  -e frame.time_epoch \
  -e ip.src \
  -e ip.dst \
  -e http2.headers.method \
  -e http2.headers.path \
  -e http2.headers.authority \
  -e http2.headers.user_agent

echo ""
echo "=== Export decrypted objects (HTTP files, etc.) ==="
mkdir -p /tmp/decrypted_objects
tshark -r "$PCAP" \
  -o "tls.keylog_file:${KEYLOG}" \
  --export-objects "http,/tmp/decrypted_objects/" 2>/dev/null
ls -la /tmp/decrypted_objects/ | head -20

echo ""
echo "Wireshark command:"
echo "wireshark -r \"$PCAP\" -o \"tls.keylog_file:${KEYLOG}\""
Why PFS makes server private key useless for decryption

Pre-TLS 1.3 RSA key exchange: the client encrypts the session master secret with the server's public key. The server's private key decrypts it. If you have the server's private key, you can decrypt old captured sessions. This is why some organizations archive server private keys for later decryption. TLS 1.3 and TLS 1.2 with DHE/ECDHE cipher suites use Perfect Forward Secrecy (PFS): the session key is derived from a temporary Diffie-Hellman exchange, not from the server's long-term private key. Even if you get the server's private key, you cannot decrypt captured PFS sessions — the ephemeral DH private key was discarded at the end of the handshake. SSLKEYLOGFILE works around PFS because it captures the derived session key at the moment of derivation, before it's used for encryption. This is why SSLKEYLOGFILE is the standard approach for lab analysis of modern TLS traffic.

Q & A

Q: I set SSLKEYLOGFILE but the malware sample doesn't write any keys. Why?

Common reasons a process doesn't write to SSLKEYLOGFILE: (1) Doesn't use NSS or OpenSSL: SSLKEYLOGFILE is a convention implemented by NSS (Firefox, Chrome) and some OpenSSL builds. Apps using Windows Schannel (system TLS on Windows), BoringSSL without key log support, or custom TLS implementations won't check the env var. (2) Env var not inherited: if you set SSLKEYLOGFILE in bash but run the malware via a script or with a different user context, it may not inherit the env var. Check: cat /proc/[pid]/environ | tr '\0' '\n' | grep SSL to verify the running process has the variable. (3) Pinned TLS: some malware uses certificate pinning or a custom TLS implementation specifically to resist analysis. (4) No TLS at all: the malware may be using plaintext, a custom encryption layer over raw TCP, or a protocol your capture isn't matching. Alternatives: use Frida to hook SSL_read/SSL_write at runtime and extract decrypted bytes, or use a man-in-the-middle proxy (mitmproxy, Burp) if the malware doesn't pin certificates.