diff options
-rw-r--r-- | wireshark/sfeu24/quic_fingerprint.lua | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/wireshark/sfeu24/quic_fingerprint.lua b/wireshark/sfeu24/quic_fingerprint.lua new file mode 100644 index 000000000..3a75dd3b9 --- /dev/null +++ b/wireshark/sfeu24/quic_fingerprint.lua @@ -0,0 +1,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) |