aboutsummaryrefslogtreecommitdiff
path: root/wireshark/sfeu24/quic_fingerprint.lua
blob: 3a75dd3b9af56f6a06f91a0c832e34bd76d3e7b2 (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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
-- Define the fields to be captured
local fields = {
    quic = Field.new("tls.quic.parameter.type"),    -- QUIC transport parameter type field
    tls = Field.new("tls.handshake.extension.type") -- TLS extension type field
}
local tps_values = {
    tp_max_idle_timeout                      = Field.new("tls.quic.parameter.max_idle_timeout"), -- 0x01
    tp_max_udp_payload_size                  = Field.new("tls.quic.parameter.max_udp_payload_size"), -- 0x03
    tp_initial_max_data                      = Field.new("tls.quic.parameter.initial_max_data"), -- 0x04
    tp_initial_max_stream_data_bidi_local    = Field.new("tls.quic.parameter.initial_max_stream_data_bidi_local"), -- 0x05
    tp_initial_max_stream_data_bidi_remote   = Field.new("tls.quic.parameter.initial_max_stream_data_bidi_remote"), -- 0x06
    tp_initial_max_stream_data_uni           = Field.new("tls.quic.parameter.initial_max_stream_data_uni"), -- 0x07
    tp_initial_max_streams_bidi              = Field.new("tls.quic.parameter.initial_max_streams_bidi"), -- 0x08
    tp_initial_max_streams_uni               = Field.new("tls.quic.parameter.initial_max_streams_uni"), -- 0x09
    tp_active_connection_id_limit            = Field.new("tls.quic.parameter.active_connection_id_limit"), -- 0x0e
    tp_max_datagram_frame_size               = Field.new("tls.quic.parameter.max_datagram_frame_size"), -- 0x20
}

-- Define the lookup tables for TLS extensions and transport parameters
local lookup_tls_extensions = {
    ["43"] = true, -- supported_versions
    ["51"] = true  -- key_share
}

local lookup_transport_parameters = {
    -- ["0x0"] = true, --  original_destination_connection_id	
    -- ["0x1"] = true, --	max_idle_timeout	
    -- ["0x2"] = true, --  stateless_reset_token	
    ["0x3"] = true, --	max_udp_payload_size	
    ["0x4"] = true, --	initial_max_data	
    -- ["0x5"] = true, --	initial_max_stream_data_bidi_local	
    ["0x6"] = true, --	initial_max_stream_data_bidi_remote	
    ["0x7"] = true, --	initial_max_stream_data_uni	
    ["0x8"] = true, --	initial_max_streams_bidi	
    -- ["0x9"] = true, --	initial_max_streams_uni	
    ["0xa"] = true, --	ack_delay_exponent	
    ["0xb"] = true, --	max_ack_delay	
    ["0xc"] = true, --	disable_active_migration	
    -- ["0xd"] = true, --	preferred_address	
    -- ["0xe"] = true, --	active_connection_id_limit	
    ["0xf"] = true, --	initial_source_connection_id	
    -- ["0x10"] = true, --	retry_source_connection_id	
}

-- Micro-db for known QUIC fingerprints
local known_fingerprints = {
    ["43_51-0x6_0x7_0x4_0x8_0x3_0xb_0xc_0xf"]     = "quic-go",
    ["51_43-0xf_0x7_0x4"]                         = "ngtcp2",
    ["43_51-0x6_0x7_0x4_0x8_0xa_0x3_0xf"]         = "mvfst",
    ["51_43-0x3_0x4_0x6_0x7_0x8_0xa_0xb_0xc_0xf"] = "quiche",
    ["43_51-0x3_0x4_0x6_0x7_0x8_0xa_0xb_0xf"]     = "kwik",
    ["51_43-0x4_0x8_0x3_0x6_0x7_0xb_0xf"]         = "picoquic",
    ["51_43-0x4_0x6_0x7_0x8_0xa_0xb_0xf"]         = "aioquic",
    ["43_51-0x3_0x4_0x6_0x7_0xa_0xb_0xf"]         = "msquic",
    ["43_51-0x3_0x4_0x6_0x7_0x8_0xc_0xf"]         = "xquic",
    ["51_43-0x4_0x7_0x8_0xf"]                     = "lsquic",
    ["43_51-0x3_0x4_0x6_0x7_0x8_0xf"]             = "quinn",
    ["43_51-0x4_0x6_0x7_0x8_0xf"]                 = "s2n-quic",
    ["43_51-0x3_0x4_0x6_0x7_0xc_0xf"]             = "go-x-net",
    ["43_51-0x6_0x7_0x4_0x8_0xa_0x3"]             = "mvfst(pre rfc)", --- mvfst draft-27
    ["43_51-0xf_0x6_0x7_0x4_0x3"]		  = "mvfst",
    ["51_43-0x3_0x4_0x6_0x7_0x8_0xa_0xb_0xf"]     = "tquic"
}

local known_fingerprints_sorted = {
    ["43_51-0x3_0x4_0x6_0x7_0x8_0xf"]     = "google-quiche",
    ["51_43-0x3_0x4_0x6_0x7_0x8_0xf"]     = "google-quiche",
    ["51_43-0x4_0x6_0x7_0x8_0xb_0xc_0xf"] = "neqo",
    ["51_43-0x4_0x6_0x7_0xf"]             = "applequic",
    ["43_51-0x4_0x6_0x7_0x8_0xf"]         = "applequic", --- seen with mask.icloud.com
}

local known_fingerprints_tp_values = {
    ["30000_M_25165824_12582912_1048576_1048576_16_16_8_1200"] = "Firefox",

    ["30000_1452_6291456_163840_163840_163840_2048_2048_5_M"] = "Generic Meta apps",
    ["60000_1500_6291456_163840_163840_163840_2048_2048_5_M"] = "Instagram app",
    ["60000_1280_6291456_163840_163840_163840_2048_2048_5_M"] = "Instagram app",
    ["30000_1280_6291456_163840_163840_163840_2048_2048_5_M"] =  "Instagram app",
    ["30000_1280_6291456_163840_262144_262144_M_100_7_M"] =  "Instagram app",
    ["30000_1252_6291456_163840_262144_262144_2048_100_2_M"] =  "Instagram app",
    ["30000_1252_1000000000_163840_1000000000_1000000000_2048_100_2_M"] =  "Instagram app",

    ["20000_1472_15728640_6291456_6291456_6291456_100_103_M_65536"] = "Snapchat app",
    ["240000_1472_15728640_6291456_6291456_6291456_100_103_M_65536"] = "Snapchat app",
    ["30000_1472_16384_16384_16384_16384_100_100_M_65536"] = "Snapchat app; audio/video call",

    ["120000_1472_15728640_6291456_6291456_6291456_100_103_M_65536"] = "Youtube app (android)",

    ["30000_1472_15728640_6291456_6291456_6291456_100_103_M_65536"] = "Generic Chrome-like",
    ["300000_1472_15728640_6291456_6291456_6291456_100_103_M_65536"] = "Android OS traffic",

    ["M_M_33554432_2097152_2097152_2097152_M_103_64_M"] = "Generic app on iOS",
    ["M_M_16777216_2097152_2097152_2097152_M_103_64_M"] = "Generic app on iOS",
    ["M_M_2097152_131072_131072_131072_M_103_64_M"] = "Generic app on iOS",
    ["M_M_1048576_131072_131072_131072_M_103_64_M"] = "Generic app on iOS",

    ["M_M_16777216_2097152_2097152_2097152_8_8_64_65535"] = "iCloud Private Relay",
    ["M_1472_16777216_2097152_2097152_2097152_8_8_64_65535"] = "iCloud Private Relay",
    ["30000_M_33554432_2097152_2097152_2097152_8_8_64_65535"] = "iCloud Private Relay",
    ["30000_M_16777216_2097152_2097152_2097152_8_8_64_65535"] = "iCloud Private Relay",
    ["30000_1472_16777216_2097152_2097152_2097152_8_8_64_65535"] = "iCloud Private Relay",

    ["60000_M_1000000_256000_256000_256000_1_100_7_M"] = "Temu app (iOS)",

    ["120000_1500_34359738368_16777216_16777216_16777216_1024_1024_8_M"] = "AliExpress app",

    ["M_1472_16777216_32768_32768_32768_M_M_4_M"] = "Windows SMB",
    ["M_1472_16777216_65536_65536_65536_M_M_4_M"] = "Windows SMB",
}

-- Create a new protocol for registering a post-dissector
local proto = Proto("quic_fingerprint", "QUIC FP")

-- Create a field for the fingerprint
local field_fingerprint_simple               = ProtoField.string("quic_fingerprint.simple", "QUIC Fingerprint (simple)")
local field_fingerprint_simple_sorted        = ProtoField.string("quic_fingerprint.simple.sorted", "QUIC Fingerprint (simple) Sorted")
local field_fingerprint_all                  = ProtoField.string("quic_fingerprint.all", "QUIC Fingerprint (all parameters)")
local field_fingerprint_all_sorted           = ProtoField.string("quic_fingerprint.all.sorted", "QUIC Fingerprint (all parameters) Sorted")
local field_guessed_library                  = ProtoField.string("quic_fingerprint.library", "QUIC Library")
local field_guessed_app                      = ProtoField.string("quic_fingerprint.app", "QUIC Application")
local field_fingerprint_values               = ProtoField.string("quic_fingerprint.values", "QUIC Fingerprint (values)") -- Only sorted version
proto.fields = {
    field_fingerprint_simple,
    field_fingerprint_simple_sorted,
    field_fingerprint_all,
    field_fingerprint_all_sorted,
    field_guessed_library,
    field_guessed_app,
    field_fingerprint_values,
}

local function is_grease(value)
    if (tonumber(value) - 27) % 31 == 0 then
        return true
    end
    return false
end

function dump(o)
   if type(o) == 'table' then
      local s = '{ '
      for k,v in pairs(o) do
         if type(k) ~= 'number' then k = '"'..k..'"' end
         s = s .. '['..k..'] = ' .. dump(v) .. ','
      end
      return s .. '} '
   else
      return tostring(o)
   end
end

-- The dissector function callback
function proto.dissector(tvb, pinfo, tree)
    local fingerprint = { {}, {} }
    local fingerprint_all = { {}, {} }
    for name, field in pairs(fields) do
        local values = { field() }
        if #values == 0 then return end
        for _, value in ipairs(values) do
            if name == "tls" then
                value = tostring(value)
                if lookup_tls_extensions[value] then
                    table.insert(fingerprint[1], value)
                    --table.insert(fingerprint_all[1], value)
                end
            elseif name == "quic" then
                value = string.format("0x%x", tostring(value))
                if lookup_transport_parameters[value] then
                    table.insert(fingerprint[2], value)
                end
		if is_grease(value) == false then
                    table.insert(fingerprint_all[2], value)
		end
            end
        end
    end
    if #fingerprint[1] == 0 or #fingerprint[2] == 0 then return end

    local fingerprint_values = {}
    local tp_value

    tps_value = tps_values.tp_max_idle_timeout()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))
    tps_value = tps_values.tp_max_udp_payload_size()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))
    tps_value = tps_values.tp_initial_max_data()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))
    tps_value = tps_values.tp_initial_max_stream_data_bidi_local()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))
    tps_value = tps_values.tp_initial_max_stream_data_bidi_remote()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))
    tps_value = tps_values.tp_initial_max_stream_data_uni()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))
    tps_value = tps_values.tp_initial_max_streams_bidi()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))
    tps_value = tps_values.tp_initial_max_streams_uni()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))
    tps_value = tps_values.tp_active_connection_id_limit()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))
    tps_value = tps_values.tp_max_datagram_frame_size()
    table.insert(fingerprint_values, tps_value == nil and "M" or tostring(tps_value))

    -- Create a string representation of the fingerprint
    local fingerprint_str = table.concat(fingerprint[1], "_") .. "-" .. table.concat(fingerprint[2], "_")
    --local fingerprint_all_str = table.concat(fingerprint_all[1], "_") .. "-" .. table.concat(fingerprint_all[2], "_")
    local fingerprint_all_str = table.concat(fingerprint_all[2], "_")

    -- Sort the transport parameters
    table.sort(fingerprint[2])
    local fingerprint_str_sorted = table.concat(fingerprint[1], "_") .. "-" .. table.concat(fingerprint[2], "_")
    
    table.sort(fingerprint_all[2])
    --local fingerprint_all_str_sorted = table.concat(fingerprint_all[1], "_") .. "-" .. table.concat(fingerprint_all[2], "_")
    local fingerprint_all_str_sorted = table.concat(fingerprint_all[2], "_")

    -- Guess the libraries
    local guesses = {}
    table.insert(guesses, known_fingerprints[fingerprint_str])
    table.insert(guesses, known_fingerprints_sorted[fingerprint_str_sorted])

    print(dump(fingerprint_values))
    local fingerprint_values_str = table.concat(fingerprint_values, "_")
    
    -- Guess the application
    local guessed_app = {}
    table.insert(guessed_app, known_fingerprints_tp_values[fingerprint_values_str])

    -- Add the fingerprint to the dissection tree
    local fingerprint_tree = tree:add(proto):set_generated()
    fingerprint_tree:add(field_fingerprint_simple, fingerprint_str):set_generated()
    fingerprint_tree:add(field_fingerprint_simple_sorted, fingerprint_str_sorted):set_generated()

    fingerprint_tree:add(field_guessed_library, #guesses > 0 and table.concat(guesses, ", ") or "Unknown"):set_generated()

    fingerprint_tree:add(field_fingerprint_all, fingerprint_all_str):set_generated()
    fingerprint_tree:add(field_fingerprint_all_sorted, fingerprint_all_str_sorted):set_generated()
    fingerprint_tree:add(field_guessed_app, #guessed_app > 0 and table.concat(guessed_app, ", ") or "Unknown"):set_generated()

    fingerprint_tree:add(field_fingerprint_values, fingerprint_values_str):set_generated()

end

-- Register the protocol as a post-dissector
register_postdissector(proto)