summaryrefslogtreecommitdiff
path: root/lua/tls-alerts.lua
blob: be79dc965be07350a5fd643b75bef2796a6366be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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