Skip to content

Instantly share code, notes, and snippets.

@Lekensteyn
Last active April 30, 2024 10:40
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save Lekensteyn/f64ba6d6d2c6229d6ec444647979ea24 to your computer and use it in GitHub Desktop.
Save Lekensteyn/f64ba6d6d2c6229d6ec444647979ea24 to your computer and use it in GitHub Desktop.
Extracts a subset of TLS secrets and injects them in an existing capture file (requires Wireshark 3.0).
#!/usr/bin/env python3
# Extracts a subset of TLS secrets and injects them in an existing capture file.
#
# Author: Peter Wu <peter@lekensteyn.nl>
import argparse
import os
import shlex
import subprocess
import sys
import tempfile
parser = argparse.ArgumentParser()
parser.add_argument('--debug', action='store_true',
help='Print tshark and editcap commands')
parser.add_argument('-d', dest='decode_as', default=[], action='append',
help='Decode As options for tshark, e.g. -dudp.port==443,quic')
parser.add_argument('-o', dest='prefs', default=[], action='append',
metavar='preference_setting',
help='Preferences for tshark, e.g. -otcp.reassemble_out_of_order:TRUE')
parser.add_argument('secrets_file', metavar='keylog.txt',
help='File with TLS decryption secrets (from SSLKEYLOGFILE)')
parser.add_argument('input_capture_file',
help='Input file, e.g. some.pcapng')
parser.add_argument('output_capture_file', nargs='?',
help='Output file. Defaults to a name based on the input file, e.g. some-dsb.pcapng')
def getsize(filename):
try:
return os.path.getsize(filename)
except OSError:
return 0
def remove_file(filename):
try:
if filename:
os.remove(filename)
except FileNotFoundError:
pass
def make_output_file(capture_file):
'''
Given an input file some.pcap, some.pcapng, some.pcap.gz, or some.pcapng.gz,
return some-dsb.pcapng. For other files, just append '-dsb.pcapng'.
'''
root, ext = os.path.splitext(capture_file)
if ext == '.gz':
root, ext = os.path.splitext(root)
if ext in ('.pcap', '.pcapng'):
return root + '-dsb.pcapng'
return capture_file + '-dsb.pcapng'
def is_client_random(token):
return len(token) == 64
def read_key_log_file(key_log_file):
secrets = {}
with open(key_log_file) as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
try:
label, client_random, secret = line.split(' ')
except ValueError:
continue
if not is_client_random(client_random):
continue
sub_keys = secrets.setdefault(client_random.lower(), [])
if not line in sub_keys:
sub_keys.append(line)
return secrets
def extract_client_randoms(rands):
valid_rands = []
for rand in rands:
# Duplicates may occur when running the Go test suite with fixed client
# random values. This is not correctly handled by Wireshark, but at
# least ensure that we do not include the same secrets multiple times.
if is_client_random(rand) and rand not in valid_rands:
valid_rands.append(rand)
return valid_rands
def filter_keys(all_keys, client_randoms):
keys = []
nsessions = 0
for client_random in client_randoms:
if not client_random in all_keys:
print("Warning: missing secrets for Client Random", client_random)
continue
nsessions += 1
keys.extend(all_keys[client_random])
return nsessions, keys
def explain_missing_sessions():
print("""
Potential reasons for this:
- TLS runs on a custom port. Use 'Decode As' 'TCP Port' -> TLS.
- The packet capture was started before keys were captured.
- The TLS handshake was not captured, try restarting the connection.
""".strip())
def explain_missing_keys():
print("""
Potential reasons for this:
- The TLS handshake was not completed.
- Traffic goes through multiple hosts or programs and are
reencrypted (proxied), but keys are captured from the wrong one.
""".strip())
def main():
args = parser.parse_args()
keys_file = args.secrets_file
capture_file = args.input_capture_file
output_file = args.output_capture_file or make_output_file(capture_file)
extra_tshark_args = ['-d' + opt for opt in args.decode_as]
extra_tshark_args += ['-otls.keylog_file:' + keys_file]
extra_tshark_args += ['-o' + opt for opt in args.prefs]
def debug_cmd(cmd):
if args.debug:
print(' '.join(shlex.quote(arg) for arg in cmd))
if getsize(keys_file) == 0:
print("Missing or empty keys file")
return 1
if getsize(capture_file) == 0:
print("Missing or empty capture file")
return 1
# Scan for client randoms.
cmd = ["tshark", "-Tfields", "-Ytls.handshake.type==1",
"-etls.handshake.random", "-r", capture_file] + extra_tshark_args
debug_cmd(cmd)
rands = subprocess.check_output(cmd, universal_newlines=True).split()
rands = extract_client_randoms(rands)
# Assume client random to be unique. For TLS 1.3, multiple secrets will
# exist, so extract secrets and group them per client random.
all_keys = read_key_log_file(keys_file)
nsessions, keys = filter_keys(all_keys, rands)
if not rands:
print("No TLS sessions found")
explain_missing_sessions()
return 1
elif not keys:
print("No secrets found for all %d sessions." % (len(rands)))
explain_missing_keys()
return 1
elif len(rands) > nsessions:
print("Note: found keys for %d sessions, but there are more sessions in total (%d)" % (
nsessions, len(rands)))
explain_missing_keys()
print("Continuing anyway, but some sessions might fail to be decrypted.")
# return 1
elif len(rands) < nsessions:
print("Note: found keys for %d sessions, but there are less sessions in total (%d)" % (
nsessions, len(rands)))
explain_missing_sessions()
tmp = output_file + '.tmp'
try:
# Write secrets to a temporary file.
tmp_secrets = tempfile.NamedTemporaryFile('w', delete=False)
tmp_secrets.write('\n'.join(keys) + '\n')
tmp_secrets.close()
# Replace existing secrets with the subset.
cmd = ["editcap", "--discard-all-secrets", "--inject-secrets",
"tls," + tmp_secrets.name, capture_file, tmp]
debug_cmd(cmd)
subprocess.check_call(cmd)
os.replace(tmp, output_file)
print("Injected", len(keys), "secret(s) for", nsessions, "session(s) in",
output_file)
finally:
remove_file(tmp_secrets.name)
remove_file(tmp)
if __name__ == '__main__':
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(130)
except subprocess.CalledProcessError as e:
sys.exit(e.returncode)
#!/bin/bash
# Extracts a subset of TLS secrets and injects them in an existing capture file.
#
# Author: Peter Wu <peter@lekensteyn.nl>
set -eu
if [ $# -lt 2 ]; then
echo "Usage: $0 keylog.txt some.pcapng [output.pcapng]"
echo "Output file is based on input file (e.g. some-dsb.pcapng)."
exit 1
fi
keys_file="$1"
capture_file="$2"
if [ ! -s "$keys_file" ]; then
echo "Missing keys file"
exit 1
fi
if [ ! -s "$capture_file" ]; then
echo "Missing capture file"
exit 1
fi
output_file="${3:-}"
if [ -z "$output_file" ]; then
basename1="${capture_file%.*}"
basename2="${basename1%.*}"
case "$capture_file" in
*.pcap|*.pcapng)
output_file=$basename1-dsb.pcapng
;;
*.pcap.gz|*.pcapng.gz)
output_file=$basename2-dsb.pcapng
;;
*)
output_file=$capture_file-dsb.pcapng
;;
esac
fi
explain_missing_sessions() {
echo "Potential reasons for this:"
echo " - TLS runs on a custom port. Use 'Decode As' 'TCP Port' -> TLS."
echo " - The packet capture was started before keys were captured."
echo " - The TLS handshake was not captured, try restarting the connection."
}
explain_missing_keys() {
echo "Potential reasons for this:"
echo " - The TLS handshake was not completed."
echo " - Traffic goes through multiple hosts or programs and are"
echo " reencrypted (proxied), but keys are captured from the wrong one."
}
rands=$(tshark -otls.keylog_file:"$keys_file" -Tfields -Ytls.handshake.type==1 -etls.handshake.random -r "$capture_file")
keys=$(xargs -n1 grep "$keys_file" -wiFe <<<"$rands") || :
# Assume client random to be unique. For TLS 1.3, multiple secrets will exist,
# so deduplicate those.
nrands=$(echo "$rands" | grep -c .) || :
nkeys=$(echo "$keys" | grep -c .) || :
nkeys_unique=$(echo "$keys" | sort -uk2,2 | grep -c .) || :
if [ $nrands -eq 0 ]; then
echo "No TLS sessions found"
explain_missing_sessions
exit 1
elif [ $nkeys -eq 0 ]; then
echo "No secrets found for $nrands sessions."
explain_missing_keys
exit 1
elif [ $nrands -gt $nkeys_unique ]; then
echo "Note: found keys for $nkeys_unique sessions, but there are more sessions in total ($nrands)"
explain_missing_keys
echo "Continuing anyway, but some sessions might fail to be decrypted."
#exit 1
elif [ $nrands -lt $nkeys_unique ]; then
echo "Note: found keys for $nkeys_unique sessions, but there are less sessions in total ($nrands)"
explain_missing_sessions
fi
tmp1=
tmp2=
trap 'rm -f "$tmp1" "$tmp2"' EXIT
tmp1=$(mktemp)
tmp2=$(mktemp)
echo "$keys" > "$tmp1"
# Replace existing secrets with the subset.
editcap --discard-all-secrets --inject-secrets tls,"$tmp1" "$capture_file" "$tmp2"
mv "$tmp2" "$output_file"
echo "Injected $nkeys secret(s) for $nkeys_unique session(s) in $output_file"
@Lekensteyn
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment