summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcrafted-pkt/tls-handshake-fragments.py10
-rw-r--r--doc/wireshark-dissection-and-reassembly.md134
-rwxr-xr-xexportpdu.py45
-rwxr-xr-xextcap/ssh-tcpdump4
-rw-r--r--lua/doh-get.lua54
-rw-r--r--lua/file-zip.lua32
-rw-r--r--lua/tls-alerts.lua136
-rwxr-xr-xreordertcp.py58
-rw-r--r--src/Makefile20
-rw-r--r--src/sslkeylog.c19
-rwxr-xr-xsrc/sslkeylog.sh51
-rwxr-xr-xsync-build.sh10
12 files changed, 528 insertions, 45 deletions
diff --git a/crafted-pkt/tls-handshake-fragments.py b/crafted-pkt/tls-handshake-fragments.py
index 2883933..ad35dfe 100755
--- a/crafted-pkt/tls-handshake-fragments.py
+++ b/crafted-pkt/tls-handshake-fragments.py
@@ -50,17 +50,21 @@ if args.seed is not None:
hsPerStream = 10
maxRecordSize = len(clientHelloMsg) * 4
-# Fragment handshake message over TLS records,
-# fragment TLS records over TCP segments.
packets = []
for i in range(args.count):
hs = b''.join(CH(hsPerStream * i + j + 1) for j in range(hsPerStream))
seq = 0x1000
+ records = b''
+ # Fragment handshake message over TLS records.
while hs:
# Does not matter that n > maxRecordSize, it is capped anyway.
n = random.randint(1, maxRecordSize)
recordData, hs = hs[:n], hs[n:]
- seg = TLSRecord(recordData)
+ records += TLSRecord(recordData)
+ # Fragment TLS records over TCP segments.
+ while records:
+ n = random.randint(1, maxRecordSize)
+ seg, records = records[:n], records[n:]
pkt = IP()/TCP(flags='A', seq=seq, sport=0xc000 + i, dport=443)/seg
packets.append(pkt)
seq += len(seg)
diff --git a/doc/wireshark-dissection-and-reassembly.md b/doc/wireshark-dissection-and-reassembly.md
new file mode 100644
index 0000000..df5f464
--- /dev/null
+++ b/doc/wireshark-dissection-and-reassembly.md
@@ -0,0 +1,134 @@
+# Wireshark dissection and reassembly
+Wireshark's current dissection engine and stream reassembly functionality has
+been the same for a long time, but it is showing its age. This document
+describes the current implementation (Wireshark 3.2.x), related research, and
+attempts to provide a solution for identifies problems.
+
+Status: **DRAFT**.
+
+## Overview
+The primary unit of work is a frame, sometimes referred to as packet. These are
+passed to the frame dissector which will:
+
+- Add metadata such as timing.
+- Pass the buffer to the next dissector. The dissector is usually Ethernet or
+ IP, depending on how the capture file was created.
+- Once done, any post-dissectors will be invoked with the same buffer.
+
+The "next dissector" above will typically parse some data, and pass the
+remaining data to the next. This is the case for Ethernet -> IPv4/IPv6 -> TCP
+for example. All of these are currently done serially, the next packet cannot be
+processed until the current one is finished. One reason is that the dissection
+of subsequent packets may depend on previous ones. This limits parallel
+processing, something which is also made difficult due to implementation details
+such as use of global data.
+
+Aside from per-packet processing, dissectors may maintain state:
+
+- The TCP dissector reconstructs flows, performing reassembly of TCP segments.
+- The TLS dissector reconstructs a TLS handshake and uses the information to
+ build a cipher for decrypting application data. This decrypted application
+ data is remembered for later use.
+- The DNS dissector remembers message identifiers to find retransmissions and to
+ calculate response times.
+- The WireGuard dissector processes handshake messages and creates a cipher for
+ a session. Decrypted data is not saved due to memory usage concerns, instead
+ decryption is performed every time the packet is accessed. This is possible
+ because a single packet contains the counter value required for decryption.
+ The TLS dissector on the other hand cannot read the counter from a TLS record.
+
+Reliable TCP stream reassembly is required for proper functionality of
+higher-level protocols. Typically, the initial part of a higher-level PDU (such
+as the start of HTTP/1.1 headers) are aligned with a TCP segment payload. If all
+headers fit in a single TCP segment, then the HTTP dissector is able to dissect
+the full headers without further state. However, if the HTTP request is split
+over multiple segments, then these segments have to be collected and merged
+based on their sequence numbers. This introduces its own share of problems:
+
+- TCP segments may be overlapping.
+- TCP segments may appear out of order. Out-of-order SYN or (more likely) FIN
+ may result in wrongly reconstructed streams
+ ([Bug 16289](https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=16289)).
+- TCP segments may be missing from the capture file.
+- TCP segments may be duplicated due to retransmission.
+- TCP segments may be overlapping, and contain conflicting data. Either due to
+ bitflips or malicious actors in a network.
+- The packet capture could start in midst of a sessions. If multiple HTTP
+ messages are sent over one stream, the start of a TCP segment may not
+ coincidence with the start of a HTTP message. That means that the stream
+ cannot be recovered from a naive assumption.
+
+Assuming a mechanism that properly reassembles the above complete TCP stream
+into a sequential stream, the higher-level protocols may bring additional
+problems. Consider TLS:
+
+- TLS records can be split over multiple TCP segments.
+- Multiple TLS records may be present in one TCP segment.
+- The start of a TLS record may not coincidence with the start of a TCP segment.
+- Decrypted application data may not be uniquely identifiable by the frame
+ number (the position of a packet in the capture file).
+
+And after TLS, the next application data protocol may also bring additional
+problems. Consider HTTP/2:
+
+- HTTP/2 multiplexes a TCP/TLS stream into multiple logical streams which are
+ contained in HTTP/2 frames.
+- A single TLS record might contain multiple HTTP/2 stream frames which are
+ identified by a 31-bit Stream Identifier.
+- HTTP/2 stream frames may be split over multiple TLS records.
+- The frame number may not uniquely identify a HTTP/2 frame.
+
+Finally, all of the previous network protocols may not be useful to the
+end-user. They may be more interested in data such as reconstructed HTML, CSS,
+JavaScript, JSON, JPEG, etc. files. In those cases, they may not be interested
+in the exact TCP segment. On the other hand, the start of a TCP segment, a TLS
+record, or a HTTP/2 frame may be interesting for performance measurements. For
+that to happen, precise tracking of the individual protocol data parts may be
+necessary. This may be complicated by out-of-order receipt of TCP segments,
+especially when multiple PDUs are in flight.
+
+Wireshark has features to handle aggregates of individual packets:
+
+- "Follow TCP Stream" reads through a whole capture and extracts a single TCP
+ stream.
+- "Export Objects" may be used to extract HTTP objects (HTML, CSS, etc.), IMF
+ (email data from SMTP), etc.
+- A Follow HTTP/2 Stream is available since Wireshark 3.2, but merges data from
+ other streams in the reassembled packet
+ ([Bug 16093](https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=16093)).
+
+The state tracking required for the above functionality requires resources,
+trading off memory cost against CPU time. With new protocols such as QUIC and
+HTTP/3, the complexity of decryption, providing stream reassembly and accurate
+metadata such as timing seem to warrant significant dissection engine changes in
+order to simplify the implementation of new features.
+
+Large objects such as Docker image layers and videos also require more efficient
+implementations:
+
+- Memoization to speed up reassembly.
+- Reduce memory usage by sharing buffers where possible.
+- Consider folding or eliding fields. For example, a large object of hundreds of
+ megabytes likely consists of several 100k TCP segments, displaying all of
+ these in a single view is impossible.
+
+## Ideas
+To speed up processing, parallelism is needed. In the common case with no
+malicious packets, packet processing should be postponed until flow
+reconstruction has happened.
+
+
+## Related work
+This section covers other works from which lessons can potentially be learned.
+
+### tcpflow
+Passive TCP Reconstruction and Forensic Analysis with tcpflow, 2013-09
+https://calhoun.nps.edu/bitstream/handle/10945/36026/NPS-CS-13-003.pdf
+
+https://github.com/simsong/tcpflow
+
+### binpac
+binpac: A yacc for Writing Application Protocol Parsers, 2006-10
+https://www.icsi.berkeley.edu/pubs/networking/binpacIMC06.pdf
+
+https://github.com/zeek/binpac
diff --git a/exportpdu.py b/exportpdu.py
index cb910d7..62c3fb0 100755
--- a/exportpdu.py
+++ b/exportpdu.py
@@ -1,18 +1,19 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import argparse
import struct
# So slow... let's import what we need.
#from scapy.all import *
+from scapy.config import conf
from scapy.fields import StrField
from scapy.packet import Packet
from scapy.utils import wrpcap
-# From epan/exported_pdu.h
+# From wsutil/exported_pdu_tlvs.h (used in epan/exported_pdu.h)
EXP_PDU_TAG_END_OF_OPT = 0
EXP_PDU_TAG_OPTIONS_LENGTH = 10
EXP_PDU_TAG_LINKTYPE = 11
-EXP_PDU_TAG_PROTO_NAME = 12
-EXP_PDU_TAG_HEUR_PROTO_NAME = 13
+EXP_PDU_TAG_DISSECTOR_NAME = 12
+EXP_PDU_TAG_HEUR_DISSECTOR_NAME = 13
EXP_PDU_TAG_DISSECTOR_TABLE_NAME = 14
EXP_PDU_TAG_IPV4_SRC = 20
EXP_PDU_TAG_IPV4_DST = 21
@@ -27,24 +28,14 @@ EXP_PDU_TAG_ORIG_FNO = 30
EXP_PDU_TAG_DVBCI_EVT = 31
EXP_PDU_TAG_DISSECTOR_TABLE_NAME_NUM_VAL = 32
EXP_PDU_TAG_COL_PROT_TEXT = 33
+EXP_PDU_TAG_TCP_INFO_DATA = 34
+EXP_PDU_TAG_P2P_DIRECTION = 35
+EXP_PDU_TAG_COL_INFO_TEXT = 36
-class TagField(StrField):
- def __init__(self, name, default):
- StrField.__init__(self, name, default)
-
- def m2i(self, pkt, x):
- tag_type, tag_len = struct.unpack_from('!HH', x)
- x = x[4:]
- if tag_len > len(x):
- # XXX error?
- return
- tag_data, x = x[:tag_len], x[tag_len:]
- return[tag_type, tag_data]
+# For backwards compatibility, since Wireshark v4.1.0rc0-197-ge5951765d8.
+EXP_PDU_TAG_PROTO_NAME = EXP_PDU_TAG_DISSECTOR_NAME
+EXP_PDU_TAG_HEUR_PROTO_NAME = EXP_PDU_TAG_HEUR_DISSECTOR_NAME
- def i2m(self, pkt, x):
- tag_type, tag_data = x
- tag_len = len(tag_data)
- return struct.pack('!HH', tag_type, tag_len) + tag_data
class TagsField(StrField):
islist = 1
@@ -69,6 +60,15 @@ class TagsField(StrField):
def _convert_data(self, tag_type, tag_data):
if type(tag_data) is int:
return struct.pack('!I', tag_data)
+ # Wireshark pads some strings to align them at four bytes. Although not
+ # strictly necessary for use in Wireshark, replicate it. See
+ # https://gitlab.com/wireshark/wireshark/-/issues/19284
+ tag_len = len(tag_data)
+ if tag_type in (EXP_PDU_TAG_DISSECTOR_NAME,
+ EXP_PDU_TAG_HEUR_DISSECTOR_NAME,
+ EXP_PDU_TAG_DISSECTOR_TABLE_NAME) and (tag_len & 3):
+ pad_len = 4 - (tag_len & 3)
+ tag_data += pad_len * b'\0'
return tag_data
def i2m(self, pkt, x):
@@ -85,6 +85,9 @@ class WiresharkUpperPdu(Packet):
name = "WiresharkUpperPdu"
fields_desc = [ TagsField("tags", []) ]
+DLT_WIRESHARK_UPPER_PDU = 252
+conf.l2types.register(DLT_WIRESHARK_UPPER_PDU, WiresharkUpperPdu)
+
udp_bootp = WiresharkUpperPdu(tags = [
(EXP_PDU_TAG_DISSECTOR_TABLE_NAME, b'udp.port'),
#(EXP_PDU_TAG_PORT_TYPE, 3), # UDP (3)
@@ -101,7 +104,7 @@ ip_udp = WiresharkUpperPdu(tags = [
def make_pcap(filename, pkt):
# Link Type: Wireshark Upper PDU export (252)
- wrpcap(filename, pkt, linktype=252)
+ wrpcap(filename, pkt, linktype=DLT_WIRESHARK_UPPER_PDU)
parser = argparse.ArgumentParser()
parser.add_argument("filename")
diff --git a/extcap/ssh-tcpdump b/extcap/ssh-tcpdump
index 02fcca6..d04b5e0 100755
--- a/extcap/ssh-tcpdump
+++ b/extcap/ssh-tcpdump
@@ -22,6 +22,7 @@ parser.add_argument('--extcap-interfaces', action='store_true')
parser.add_argument('--extcap-dlts', action='store_true')
parser.add_argument('--extcap-config', action='store_true')
parser.add_argument('--capture', action='store_true')
+parser.add_argument('--extcap-version')
parser.add_argument('--extcap-interface', metavar='IFACE')
@@ -72,13 +73,14 @@ def extcap_capture(iface, cfilter, outfile):
else:
ssh_user = os.getenv('USER')
tcpdump_args = [
- "sudo",
"tcpdump",
"-i", iface,
"-p",
"-U",
"-w", "-",
]
+ if ssh_user != 'root':
+ tcpdump_args = ["sudo"] + tcpdump_args
# Change to a less-privileged user
if ssh_user:
tcpdump_args += ["-Z", ssh_user]
diff --git a/lua/doh-get.lua b/lua/doh-get.lua
new file mode 100644
index 0000000..d95b542
--- /dev/null
+++ b/lua/doh-get.lua
@@ -0,0 +1,54 @@
+--
+-- Support for DoH GET dissection in Wireshark. Wireshark already supports
+-- dissection of the application/dns-message POST request and response bodies,
+-- but it does not yet support the GET request parameter. This Lua plugin
+-- provides a workaround for that.
+-- https://tools.ietf.org/html/rfc8484#section-4.1
+--
+
+local doh_get = Proto.new("doh-get", "DNS over HTTPS (GET)")
+local media_type = DissectorTable.get("media_type")
+local http_path = Field.new("http.request.uri")
+local http2_path = Field.new("http2.headers.path")
+
+-- Converts "base64url" to standard "base64" encoding.
+local function from_base64url(b64url)
+ local lastlen = string.len(b64url) % 4
+ local b64 = string.gsub(string.gsub(b64url, "-", "+"), "_", "/")
+ if lastlen == 3 then
+ b64 = b64 .. "="
+ elseif lastlen == 2 then
+ b64 = b64 .. "=="
+ end
+ return b64
+end
+
+function doh_get.dissector(tvb, pinfo, tree)
+ local path = http2_path() or http_path()
+ if not path then
+ return
+ end
+
+ local dns_b64, sep = string.match(path.value, "[%?&]dns=([A-Za-z0-9_=-]+)(.?)")
+ if not dns_b64 then
+ return
+ end
+ -- Check for forbidden values in query string.
+ if sep ~= "" and sep ~= "&" then
+ return
+ end
+
+ -- Convert base64url to standard base64 with +/ and padding
+ dns_b64 = from_base64url(dns_b64)
+
+ local dns_tvb = ByteArray.new(dns_b64, true):base64_decode():tvb("Base64-decoded DNS")
+
+ -- Allow HTTP GET line to be replaced with the DNS one in the Info column.
+ pinfo.columns.info:clear_fence()
+
+ -- Call media_type table instead of dns directly, this ensures that the
+ -- protocol is properly displayed as "DoH".
+ media_type:try("application/dns-message", dns_tvb, pinfo, tree)
+end
+
+register_postdissector(doh_get)
diff --git a/lua/file-zip.lua b/lua/file-zip.lua
index 3b58b4e..99eb4a5 100644
--- a/lua/file-zip.lua
+++ b/lua/file-zip.lua
@@ -116,6 +116,7 @@ make_fields("zip_archive", {
version_req = version_req_def,
flag = general_purpose_flags_def,
comp_method = compr_method_def,
+ lastmod = {ProtoField.absolute_time, base.UTC},
lastmod_time = {ProtoField.uint16, base.HEX},
lastmod_date = {ProtoField.uint16, base.HEX},
crc32 = {ProtoField.uint32, base.HEX},
@@ -140,6 +141,7 @@ make_fields("zip_archive", {
version_req = version_req_def,
flag = general_purpose_flags_def,
comp_method = compr_method_def,
+ lastmod = {ProtoField.absolute_time, base.UTC},
lastmod_time = {ProtoField.uint16, base.HEX},
lastmod_date = {ProtoField.uint16, base.HEX},
crc32 = {ProtoField.uint32, base.HEX},
@@ -261,6 +263,30 @@ local function dissect_extra(hfs, tvb, tree)
end
end
+-- Given two 16-bit unsigned values, return the seconds since Epoch.
+local function parse_msdos_datetime(date, time)
+ -- Parses MS-DOS date and time to a more common format. See
+ -- https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-dosdatetimetofiletime
+ return os.time({
+ year = bit.rshift(date, 9) + 1980,
+ month = bit.rshift(bit.band(date, 0x01e0), 5),
+ day = bit.band(date, 0x001f),
+ hour = bit.rshift(time, 11),
+ min = bit.rshift(bit.band(time, 0x07e0), 5),
+ sec = bit.band(time, 0x001f) * 2,
+ })
+end
+
+local function add_lastmod(fields, tree, tvb, offset)
+ local tvb_time = tvb(offset, 2)
+ local tvb_date = tvb(offset + 2, 2)
+ local secs = parse_msdos_datetime(tvb_date:le_uint(), tvb_time:le_uint())
+ local time = NSTime.new(secs)
+ local subtree = tree:add(fields.lastmod, tvb(offset, 4), time)
+ subtree:add_le(fields.lastmod_time, tvb_time)
+ subtree:add_le(fields.lastmod_date, tvb_date)
+end
+
local function dissect_one(tvb, offset, pinfo, tree)
local orig_offset = offset
local magic = tvb(offset, 4):le_int()
@@ -271,8 +297,7 @@ local function dissect_one(tvb, offset, pinfo, tree)
dissect_version(hf.entry.version_req, tvb(offset + 4, 2), subtree)
dissect_flags(hf.entry.flag, tvb(offset + 6, 2), subtree)
subtree:add_le(hf.entry.comp_method, tvb(offset + 8, 2))
- subtree:add_le(hf.entry.lastmod_time, tvb(offset + 10, 2))
- subtree:add_le(hf.entry.lastmod_date, tvb(offset + 12, 2))
+ add_lastmod(hf.entry, subtree, tvb, 10)
subtree:add_le(hf.entry.crc32, tvb(offset + 14, 4))
subtree:add_le(hf.entry.size_comp, tvb(offset + 18, 4))
subtree:add_le(hf.entry.size_uncomp, tvb(offset + 22, 4))
@@ -334,8 +359,7 @@ local function dissect_one(tvb, offset, pinfo, tree)
dissect_version(hf.cd.version_req, tvb(offset + 6, 2), subtree)
dissect_flags(hf.cd.flag, tvb(offset + 8, 2), subtree)
subtree:add_le(hf.cd.comp_method, tvb(offset + 10, 2))
- subtree:add_le(hf.cd.lastmod_time, tvb(offset + 12, 2))
- subtree:add_le(hf.cd.lastmod_date, tvb(offset + 14, 2))
+ add_lastmod(hf.cd, subtree, tvb, offset + 12)
subtree:add_le(hf.cd.crc32, tvb(offset + 16, 4))
subtree:add_le(hf.cd.size_comp, tvb(offset + 20, 4))
subtree:add_le(hf.cd.size_uncomp, tvb(offset + 24, 4))
diff --git a/lua/tls-alerts.lua b/lua/tls-alerts.lua
new file mode 100644
index 0000000..be79dc9
--- /dev/null
+++ b/lua/tls-alerts.lua
@@ -0,0 +1,136 @@
+--
+-- Wireshark listener to identify unusual TLS Alerts and associated domains.
+-- Author: Peter Wu <peter@lekensteyn.nl>
+--
+-- Load in Wireshark, then open the Tools -> TLS Alerts menu, or use tshark:
+--
+-- $ tshark -q -Xlua_script:tls-alerts.lua -r some.pcapng
+-- shavar.services.mozilla.com 1x Bad Certificate (42)
+-- aus5.mozilla.org 3x Bad Certificate (42), 1x Unknown CA (48)
+--
+
+--local quic_stream = Field.new("quic.stream")
+local tls_sni = Field.new("tls.handshake.extensions_server_name")
+local tls_alert = Field.new("tls.alert_message.desc")
+
+-- Map from TCP stream -> SNI
+local snis
+-- Map from SNI -> (map of alerts -> counts)
+local alerts
+
+local tw
+local function reset_stats()
+ snis = {}
+ alerts = {}
+ if gui_enabled() then
+ tw:clear()
+ end
+end
+
+local function tap_packet(pinfo, tvb, tcp_info)
+ local tcp_stream = tcp_info.th_stream
+ if not tcp_stream then
+ print('TCP stream somehow not found, is this QUIC? pkt=' .. pinfo.number)
+ return
+ end
+
+ local f_sni = tls_sni()
+ if f_sni then
+ snis[tcp_stream] = f_sni.value
+ end
+ -- Ignore "Close Notify (0)" alerts since these are not unusual.
+ local f_alert = tls_alert()
+ if f_alert and f_alert.value ~= 0 then
+ local sni = snis[tcp_stream] or string.format("<unknown SNI on tcp.stream==%d>", tcp_stream)
+ -- Store counters for SNI -> Alerts mappings
+ local sni_alerts = alerts[sni]
+ if not alerts[sni] then
+ sni_alerts = {}
+ alerts[sni] = sni_alerts
+ end
+ local count = sni_alerts[f_alert.display]
+ if not count then
+ sni_alerts[f_alert.display] = 1
+ else
+ sni_alerts[f_alert.display] = count + 1
+ end
+ end
+end
+
+local function round_to_multiple_of(val, multiple)
+ local rest = val % multiple
+ if rest == 0 then
+ return val
+ else
+ return val - rest + multiple
+ end
+end
+
+local function output_all(callback, need_newline)
+ -- Align the domain to a multiple of four columns
+ local max_length = 16
+ for sni in pairs(alerts) do
+ if #sni > max_length then
+ max_length = round_to_multiple_of(#sni + 1, 4) - 1
+ end
+ end
+ local fmt = "%-" .. max_length .. "s %s"
+ if need_newline then fmt = fmt .. "\n" end
+
+ for sni, alert_counts in pairs(alerts) do
+ table.sort(alert_counts, function(a, b) return a > b end)
+ local all_alerts
+ for alert, count in pairs(alert_counts) do
+ local sep = ""
+ local chunk = string.format("%dx %s", count, alert)
+ if all_alerts then
+ all_alerts = all_alerts .. ", " .. chunk
+ else
+ all_alerts = chunk
+ end
+ end
+ callback(string.format(fmt, sni, all_alerts))
+ end
+end
+
+-- Called periodically in the Wireshark GUI
+local function gui_draw()
+ tw:clear()
+ output_all(function(text)
+ tw:append(text .. "\n")
+ end)
+end
+
+-- Called at the end of tshark
+local function cli_draw()
+ output_all(print)
+end
+
+local function activate_tap()
+ -- Match TLS Client Hello with SNI extension or TLS alerts.
+ local tap = Listener.new("tcp", "(tls.handshake.type==1 and tls.handshake.extensions_server_name) or tls.alert_message")
+
+ if gui_enabled() then
+ tw = TextWindow.new("TLS Alerts")
+ tw:set_atclose(function()
+ tap:remove()
+ tw = nil
+ end)
+ tap.draw = gui_draw
+ else
+ tap.draw = cli_draw
+ end
+
+ tap.packet = tap_packet
+ tap.reset = reset_stats
+ reset_stats()
+ if gui_enabled() then
+ retap_packets()
+ end
+end
+
+if gui_enabled() then
+ register_menu("TLS Alerts", activate_tap, MENU_TOOLS_UNSORTED)
+else
+ activate_tap()
+end
diff --git a/reordertcp.py b/reordertcp.py
new file mode 100755
index 0000000..0719fad
--- /dev/null
+++ b/reordertcp.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+# Reorder a pcap file with a single TCP stream. Convert with
+# 'tshark -r input.pcapng -w output.pcap -F pcap' if scapy crashes with
+# "struct.error: 'I' format requires 0 <= number <= 4294967295" in
+# _write_packet.
+
+import argparse
+import logging
+import time
+
+from scapy.all import *
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--linktype', type=int, default=1,
+ help='DLT (0 for Null/Loopback, 1 for Ethernet)')
+parser.add_argument('infile')
+parser.add_argument('outfile')
+
+
+def main():
+ args = parser.parse_args()
+ logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
+
+ t1 = time.monotonic()
+ packets = rdpcap(args.infile)
+ t2 = time.monotonic()
+ logging.info('Capture loaded in %.3f seconds', t2 - t1)
+
+ # Assume a single stream
+ clt = packets[0][TCP]
+ svr = packets[1][TCP]
+ assert clt.flags.S and not clt.flags.A
+ assert svr.flags.S and svr.flags.A
+ assert clt.sport == svr.dport
+ assert clt.dport == svr.sport
+ assert clt.dport != clt.sport
+
+ def seq_key(p):
+ t = p[TCP]
+ if t.sport == svr.sport:
+ return t.seq - svr.seq
+ elif t.dport == svr.sport:
+ return t.ack - svr.seq
+ else:
+ raise RuntimeError(f'Unexpected {t}')
+ packets.sort(key=seq_key)
+
+ t1 = time.monotonic()
+ wrpcap(args.outfile, packets, linktype=args.linktype)
+ t2 = time.monotonic()
+ logging.info('Capture written in %.3f seconds', t2 - t1)
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except KeyboardInterrupt:
+ pass
diff --git a/src/Makefile b/src/Makefile
index ea8e7b6..34120ac 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,6 +1,20 @@
-libsslkeylog.so: sslkeylog.c
- $(CC) $(CFLAGS) sslkeylog.c -shared -o $@ -fPIC -ldl
+UNAME_S := $(shell uname -s)
+ifeq ($(UNAME_S),Darwin)
+ LIBNAME := libsslkeylog.dylib
+ # Assumes default Homebrew installation prefix.
+ OPENSSL_PREFIX ?= /usr/local/opt/openssl@1.1
+ifneq ($(OPENSSL_PREFIX),)
+ CPPFLAGS ?= -I$(OPENSSL_PREFIX)/include
+ # Link to library to avoid having to set LD_LIBRARY_PATH at runtime.
+ LDFLAGS ?= -L$(OPENSSL_PREFIX)/lib -lssl
+endif
+else
+ LIBNAME := libsslkeylog.so
+endif
+
+$(LIBNAME): sslkeylog.c
+ $(CC) $(CPPFLAGS) $(CFLAGS) sslkeylog.c -shared -o $@ -fPIC -ldl $(LDFLAGS)
clean:
- $(RM) libsslkeylog.so
+ $(RM) $(LIBNAME)
diff --git a/src/sslkeylog.c b/src/sslkeylog.c
index 3706689..9d176c9 100644
--- a/src/sslkeylog.c
+++ b/src/sslkeylog.c
@@ -9,6 +9,13 @@
* Usage:
* cc sslkeylog.c -shared -o libsslkeylog.so -fPIC -ldl
* SSLKEYLOGFILE=premaster.txt LD_PRELOAD=./libsslkeylog.so openssl ...
+ *
+ * Usage for macOS:
+ * cc sslkeylog.c -shared -o libsslkeylog.dylib -fPIC -ldl \
+ * -I/usr/local/opt/openssl@1.1/include \
+ * -L/usr/local/opt/openssl@1.1/lib -lssl
+ * DYLD_INSERT_LIBRARIES=./libsslkeylog.dylib DYLD_FORCE_FLAT_NAMESPACE=1 \
+ * SSLKEYLOGFILE=premaster.txt /usr/local/opt/openssl@1.1/bin/openssl ...
*/
/*
@@ -39,9 +46,17 @@
#include <stdio.h>
#ifndef OPENSSL_SONAME
-/* fallback library if OpenSSL is not already loaded. Other values to try:
- * libssl.so.0.9.8 libssl.so.1.0.0 libssl.so.1.1 */
+/* fallback library if OpenSSL is not already loaded. */
+# ifdef __APPLE__
+/* libssl.dylib is a symlink, Homebrew installs:
+ * OpenSSL 1.0.2 /usr/local/opt/openssl/lib/libssl.1.0.0.dylib
+ * OpenSSL 1.1.1 /usr/local/opt/openssl@1.1/lib/libssl.1.1.dylib
+ */
+# define OPENSSL_SONAME "libssl.dylib"
+# else
+/* Other values to try: libssl.so.0.9.8 libssl.so.1.0.0 libssl.so.1.1 */
# define OPENSSL_SONAME "libssl.so"
+# endif
#endif
#define FIRSTLINE "# SSL key logfile generated by sslkeylog.c\n"
diff --git a/src/sslkeylog.sh b/src/sslkeylog.sh
index 0197302..38036f5 100755
--- a/src/sslkeylog.sh
+++ b/src/sslkeylog.sh
@@ -15,9 +15,54 @@ gdb() {
"$@"
}
-LD_PRELOAD=$(readlink -f "${BASH_SOURCE[0]%/*}")/libsslkeylog.so
-SSLKEYLOGFILE=${SSLKEYLOGFILE:-/dev/stderr}
-export LD_PRELOAD SSLKEYLOGFILE
+case "$OSTYPE" in
+darwin*)
+ # Unfortunately not all executables can be injected (e.g. /usr/bin/curl).
+ # See also man dyld
+ #
+ # "Note: If System Integrity Protection is enabled, these environment
+ # variables are ignored when executing binaries protected by System
+ # Integrity Protection."
+ #
+ # Note that DYLD_* env vars are *not* propagated though system binaries such
+ # as bash. To set an environment variable, use 'env' as in:
+ #
+ # ./sslkeylog.sh env DYLD_PRINT_OPTS=1 python3
+ #
+ # If the variable is picked up, it should show something like:
+ #
+ # opt[0] = "python3"
+ #
+ # If not visible, then interception is not possible until SIP is disabled.
+
+ export DYLD_INSERT_LIBRARIES=$(cd "${BASH_SOURCE[0]%/*}" && pwd)/libsslkeylog.dylib
+ export DYLD_FORCE_FLAT_NAMESPACE=1
+ # Expected output: dyld: loaded: <1A23FBC9-68C9-3808-88A5-C2D3A18C7DE1> .../wireshark-notes/src/libsslkeylog.dylib
+ #export DYLD_PRINT_LIBRARIES=1
+ # Expected output: dyld: lazy bind: openssl:0x105B21CE0 = libsslkeylog.dylib:_SSL_new, *0x105B21CE0 = 0x105B59660
+ #export DYLD_PRINT_BINDINGS
+
+ # Since DYLD is not propagated when using 'env', simulate it here.
+ # This is safer than using 'eval'.
+ if [[ ${BASH_SOURCE[0]} == $0 ]] && [[ "$1" == env ]]; then
+ shift
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ *=*)
+ export "$1"
+ shift
+ ;;
+ *)
+ break
+ esac
+ done
+ fi
+ ;;
+*)
+ export LD_PRELOAD=$(readlink -f "${BASH_SOURCE[0]%/*}")/libsslkeylog.so
+ ;;
+esac
+export SSLKEYLOGFILE=${SSLKEYLOGFILE:-/dev/stderr}
# Run the command (if not sourced)
[[ ${BASH_SOURCE[0]} != $0 ]] || \
diff --git a/sync-build.sh b/sync-build.sh
index bcd4c47..19da4e3 100755
--- a/sync-build.sh
+++ b/sync-build.sh
@@ -50,13 +50,6 @@ CXX=${CXX:-c++}
# "<optimized out>".
# -O1 -g -gdwarf-4 -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer
_default_flags=-fdiagnostics-color
-if $CC --version | grep -qE 'clang version ([89]|[1-9][0-9])'; then
- # Require Clang and at least LLD 8.0 to avoid broken binaries and crashes.
- # https://bugs.llvm.org/show_bug.cgi?id=37303
- _default_flags+=\ -fuse-ld=lld
-else
- _default_flags+=\ -fuse-ld=gold
-fi
# -fdebug-prefix-map is supported in GCC since 2007 (?), but only in Clang 3.8
# In GDB, use "dir /tmp/wireshark" to add the source directory anyway.
# -fmacro-prefix-map and -ffile-prefix-map were added in GCC 8. Hopefully it
@@ -123,13 +116,14 @@ if $force_cmake || [ ! -e $builddir/CMakeCache.txt ]; then
-DCMAKE_INSTALL_PREFIX=/tmp/wsroot \
-DENABLE_SMI=0 \
-DCMAKE_BUILD_TYPE=Debug \
- -DDISABLE_WERROR=1 \
+ -DENABLE_WERROR=0 \
-DENABLE_ASAN=1 \
-DENABLE_UBSAN=1 \
$remotesrcdir \
-DCMAKE_LIBRARY_PATH=$LIBDIR \
-DCMAKE_C_FLAGS=$(printf %q "$CFLAGS") \
-DCMAKE_CXX_FLAGS=$(printf %q "$CXXFLAGS") \
+ -DCMAKE_{EXE,SHARED,MODULE}_LINKER_FLAGS=-fuse-ld=lld \
-DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
$(printf ' %q' "${cmake_options[@]}")
fi &&