diff options
-rwxr-xr-x | crafted-pkt/tls-handshake-fragments.py | 10 | ||||
-rw-r--r-- | doc/wireshark-dissection-and-reassembly.md | 134 | ||||
-rwxr-xr-x | exportpdu.py | 45 | ||||
-rwxr-xr-x | extcap/ssh-tcpdump | 4 | ||||
-rw-r--r-- | lua/doh-get.lua | 54 | ||||
-rw-r--r-- | lua/file-zip.lua | 32 | ||||
-rw-r--r-- | lua/tls-alerts.lua | 136 | ||||
-rwxr-xr-x | reordertcp.py | 58 | ||||
-rw-r--r-- | src/Makefile | 20 | ||||
-rw-r--r-- | src/sslkeylog.c | 19 | ||||
-rwxr-xr-x | src/sslkeylog.sh | 51 | ||||
-rwxr-xr-x | sync-build.sh | 10 |
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 && |