1---------------------------------------- 2-- script-name: dns_dissector.lua 3-- 4-- author: Hadriel Kaplan <hadrielk at yahoo dot com> 5-- Copyright (c) 2014, Hadriel Kaplan 6-- This code is in the Public Domain, or the BSD (3 clause) license if Public Domain does not apply 7-- in your country. 8-- 9-- Version: 2.1 10-- 11-- Changes since 2.0: 12-- * fixed a bug with default settings 13-- * added ability for command-line to overide defaults 14-- 15-- Changes since 1.0: 16-- * made it use the new ProtoExpert class model for expert info 17-- * add a protocol column with the proto name 18-- * added heuristic dissector support 19-- * added preferences settings 20-- * removed byteArray2String(), and uses the new ByteArray:raw() method instead 21-- 22-- BACKGROUND: 23-- This is an example Lua script for a protocol dissector. The purpose of this script is two-fold: 24-- * To provide a reference tutorial for others writing Wireshark dissectors in Lua 25-- * To test various functions being called in various ways, so this script can be used in the test-suites 26-- I've tried to meet both of those goals, but it wasn't easy. No doubt some folks will wonder why some 27-- functions are called some way, or differently than previous invocations of the same function. I'm trying to 28-- to show both that it can be done numerous ways, but also I'm trying to test those numerous ways, and my more 29-- immediate need is for test coverage rather than tutorial guide. (the Lua API is sorely lacking in test scripts) 30-- 31-- OVERVIEW: 32-- This script creates an elementary dissector for DNS. It's neither comprehensive nor error-free with regards 33-- to the DNS protocol. That's OK. The goal isn't to fully dissect DNS properly - Wireshark already has a good 34-- DNS dissector built-in. We don't need another one. We also have other example Lua scripts, but I don't think 35-- they do a good job of explaining things, and the nice thing about this one is getting capture files to 36-- run it against is trivial. (plus I uploaded one) 37-- 38-- HOW TO RUN THIS SCRIPT: 39-- Wireshark and Tshark support multiple ways of loading Lua scripts: through a dofile() call in init.lua, 40-- through the file being in either the global or personal plugins directories, or via the command line. 41-- See the Wireshark User's Guide chapter on Lua (https://www.wireshark.org/docs/wsdg_html_chunked/wsluarm_modules.html). 42-- Once the script is loaded, it creates a new protocol named "MyDNS" (or "MYDNS" in some places). If you have 43-- a capture file with DNS packets in it, simply select one in the Packet List pane, right-click on it, and 44-- select "Decode As ...", and then in the dialog box that shows up scroll down the list of protocols to one 45-- called "MYDNS", select that and click the "ok" or "apply" button. Voila`, you're now decoding DNS packets 46-- using the simplistic dissector in this script. Another way is to download the capture file made for 47-- this script, and open that - since the DNS packets in it use UDP port 65333 (instead of the default 53), 48-- and since the MyDNS protocol in this script has been set to automatically decode UDP port 65333, it will 49-- automagically do it without doing "Decode As ...". 50-- 51---------------------------------------- 52-- do not modify this table 53local debug_level = { 54 DISABLED = 0, 55 LEVEL_1 = 1, 56 LEVEL_2 = 2 57} 58 59-- set this DEBUG to debug_level.LEVEL_1 to enable printing debug_level info 60-- set it to debug_level.LEVEL_2 to enable really verbose printing 61-- note: this will be overridden by user's preference settings 62local DEBUG = debug_level.LEVEL_1 63 64local default_settings = 65{ 66 debug_level = DEBUG, 67 port = 65333, 68 heur_enabled = true, 69 heur_regmode = 1, 70} 71 72-- for testing purposes, we want to be able to pass in changes to the defaults 73-- from the command line; because you can't set lua preferences from the command 74-- line using the '-o' switch (the preferences don't exist until this script is 75-- loaded, so the command line thinks they're invalid preferences being set) 76-- so we pass them in as command arguments insetad, and handle it here: 77local args={...} -- get passed-in args 78if args and #args > 0 then 79 for _, arg in ipairs(args) do 80 local name, value = arg:match("(.+)=(.+)") 81 if name and value then 82 if tonumber(value) then 83 value = tonumber(value) 84 elseif value == "true" or value == "TRUE" then 85 value = true 86 elseif value == "false" or value == "FALSE" then 87 value = false 88 elseif value == "DISABLED" then 89 value = debug_level.DISABLED 90 elseif value == "LEVEL_1" then 91 value = debug_level.LEVEL_1 92 elseif value == "LEVEL_2" then 93 value = debug_level.LEVEL_2 94 else 95 error("invalid commandline argument value") 96 end 97 else 98 error("invalid commandline argument syntax") 99 end 100 101 default_settings[name] = value 102 end 103end 104 105local dprint = function() end 106local dprint2 = function() end 107local function reset_debug_level() 108 if default_settings.debug_level > debug_level.DISABLED then 109 dprint = function(...) 110 print(table.concat({"Lua:", ...}," ")) 111 end 112 113 if default_settings.debug_level > debug_level.LEVEL_1 then 114 dprint2 = dprint 115 end 116 end 117end 118-- call it now 119reset_debug_level() 120 121dprint2("Wireshark version = ", get_version()) 122dprint2("Lua version = ", _VERSION) 123 124---------------------------------------- 125-- Unfortunately, the older Wireshark/Tshark versions have bugs, and part of the point 126-- of this script is to test those bugs are now fixed. So we need to check the version 127-- end error out if it's too old. 128local major, minor, micro = get_version():match("(%d+)%.(%d+)%.(%d+)") 129if major and tonumber(major) <= 1 and ((tonumber(minor) <= 10) or (tonumber(minor) == 11 and tonumber(micro) < 3)) then 130 error( "Sorry, but your Wireshark/Tshark version ("..get_version()..") is too old for this script!\n".. 131 "This script needs Wireshark/Tshark version 1.11.3 or higher.\n" ) 132end 133 134-- more sanity checking 135-- verify we have the ProtoExpert class in wireshark, as that's the newest thing this file uses 136assert(ProtoExpert.new, "Wireshark does not have the ProtoExpert class, so it's too old - get the latest 1.11.3 or higher") 137 138---------------------------------------- 139 140 141---------------------------------------- 142-- creates a Proto object, but doesn't register it yet 143local dns = Proto("mydns","MyDNS Protocol") 144 145---------------------------------------- 146-- multiple ways to do the same thing: create a protocol field (but not register it yet) 147-- the abbreviation should always have "<myproto>." before the specific abbreviation, to avoid collisions 148local pf_trasaction_id = ProtoField.new ("Transaction ID", "mydns.trans_id", ftypes.UINT16) 149local pf_flags = ProtoField.new ("Flags", "mydns.flags", ftypes.UINT16, nil, base.HEX) 150local pf_num_questions = ProtoField.uint16("mydns.num_questions", "Number of Questions") 151local pf_num_answers = ProtoField.uint16("mydns.num_answers", "Number of Answer RRs") 152local pf_num_authority_rr = ProtoField.uint16("mydns.num_authority_rr", "Number of Authority RRs") 153local pf_num_additional_rr = ProtoField.uint16("mydns.num_additional_rr", "Number of Additional RRs") 154 155-- within the flags field, we want to parse/show the bits separately 156-- note the "base" argument becomes the size of the bitmask'ed field when ftypes.BOOLEAN is used 157-- the "mask" argument is which bits we want to use for this field (e.g., base=16 and mask=0x8000 means we want the top bit of a 16-bit field) 158-- again the following shows different ways of doing the same thing basically 159local pf_flag_response = ProtoField.new ("Response", "mydns.flags.response", ftypes.BOOLEAN, {"this is a response","this is a query"}, 16, 0x8000, "is the message a response?") 160local pf_flag_opcode = ProtoField.new ("Opcode", "mydns.flags.opcode", ftypes.UINT16, nil, base.DEC, 0x7800, "operation code") 161local pf_flag_authoritative = ProtoField.new ("Authoritative", "mydns.flags.authoritative", ftypes.BOOLEAN, nil, 16, 0x0400, "is the response authoritative?") 162local pf_flag_truncated = ProtoField.bool ("mydns.flags.truncated", "Truncated", 16, nil, 0x0200, "is the message truncated?") 163local pf_flag_recursion_desired = ProtoField.bool ("mydns.flags.recursion_desired", "Recursion desired", 16, {"yes","no"}, 0x0100, "do the query recursivley?") 164local pf_flag_recursion_available = ProtoField.bool ("mydns.flags.recursion_available", "Recursion available", 16, nil, 0x0080, "does the server support recursion?") 165local pf_flag_z = ProtoField.uint16("mydns.flags.z", "World War Z - Reserved for future use", base.HEX, nil, 0x0040, "when is it the future?") 166local pf_flag_authenticated = ProtoField.bool ("mydns.flags.authenticated", "Authenticated", 16, {"yes","no"}, 0x0020, "did the server DNSSEC authenticate?") 167local pf_flag_checking_disabled = ProtoField.bool ("mydns.flags.checking_disabled", "Checking disabled", 16, nil, 0x0010) 168 169-- no, these aren't all the DNS response codes - this is just an example 170local rcodes = { 171 [0] = "No Error", 172 [1] = "Format Error", 173 [2] = "Server Failure", 174 [3] = "Non-Existent Domain", 175 [9] = "Server Not Authoritative for zone" 176} 177-- the above rcodes table is used in this next ProtoField 178local pf_flag_rcode = ProtoField.uint16("mydns.flags.rcode", "Response code", base.DEC, rcodes, 0x000F) 179local pf_query = ProtoField.new("Query", "mydns.query", ftypes.BYTES) 180local pf_query_name = ProtoField.new("Name", "mydns.query.name", ftypes.STRING) 181local pf_query_name_len = ProtoField.new("Name Length", "mydns.query.name.len", ftypes.UINT8) 182local pf_query_label_count = ProtoField.new("Label Count", "mydns.query.label.count", ftypes.UINT8) 183local rrtypes = { [1] = "A (IPv4 host address)", [2] = "NS (authoritative name server)", [28] = "AAAA (for geeks only)" } 184local pf_query_type = ProtoField.uint16("mydns.query.type", "Type", base.DEC, rrtypes) 185-- again, not all class types are listed here 186local classes = { 187 [0] = "Reserved", 188 [1] = "IN (Internet)", 189 [2] = "The 1%", 190 [5] = "First class", 191 [6] = "Business class", 192 [65535] = "Cattle class" 193} 194local pf_query_class = ProtoField.uint16("mydns.query.class", "Class", base.DEC, classes, nil, "keep it classy folks") 195 196---------------------------------------- 197-- this actually registers the ProtoFields above, into our new Protocol 198-- in a real script I wouldn't do it this way; I'd build a table of fields programmatically 199-- and then set dns.fields to it, so as to avoid forgetting a field 200dns.fields = { pf_trasaction_id, pf_flags, 201 pf_num_questions, pf_num_answers, pf_num_authority_rr, pf_num_additional_rr, 202 pf_flag_response, pf_flag_opcode, pf_flag_authoritative, 203 pf_flag_truncated, pf_flag_recursion_desired, pf_flag_recursion_available, 204 pf_flag_z, pf_flag_authenticated, pf_flag_checking_disabled, pf_flag_rcode, 205 pf_query, pf_query_name, pf_query_name_len, pf_query_label_count, pf_query_type, pf_query_class } 206 207---------------------------------------- 208-- create some expert info fields (this is new functionality in 1.11.3) 209-- Expert info fields are very similar to proto fields: they're tied to our protocol, 210-- they're created in a similar way, and registered by setting a 'experts' field to 211-- a table of them just as proto fields were put into the 'dns.fields' above 212-- The old way of creating expert info was to just add it to the tree, but that 213-- didn't let the expert info be filterable in wireshark, whereas this way does 214local ef_query = ProtoExpert.new("mydns.query.expert", "DNS query message", 215 expert.group.REQUEST_CODE, expert.severity.CHAT) 216local ef_response = ProtoExpert.new("mydns.response.expert", "DNS response message", 217 expert.group.RESPONSE_CODE, expert.severity.CHAT) 218local ef_ultimate = ProtoExpert.new("mydns.response.ultimate.expert", "DNS answer to life, the universe, and everything", 219 expert.group.COMMENTS_GROUP, expert.severity.NOTE) 220-- some error expert info's 221local ef_too_short = ProtoExpert.new("mydns.too_short.expert", "DNS message too short", 222 expert.group.MALFORMED, expert.severity.ERROR) 223local ef_bad_query = ProtoExpert.new("mydns.query.missing.expert", "DNS query missing or malformed", 224 expert.group.MALFORMED, expert.severity.WARN) 225 226-- register them 227dns.experts = { ef_query, ef_too_short, ef_bad_query, ef_response, ef_ultimate } 228 229---------------------------------------- 230-- we don't just want to display our protocol's fields, we want to access the value of some of them too! 231-- There are several ways to do that. One is to just parse the buffer contents in Lua code to find 232-- the values. But since ProtoFields actually do the parsing for us, and can be retrieved using Field 233-- objects, it's kinda cool to do it that way. So let's create some Fields to extract the values. 234-- The following creates the Field objects, but they're not 'registered' until after this script is loaded. 235-- Also, these lines can't be before the 'dns.fields = ...' line above, because the Field.new() here is 236-- referencing fields we're creating, and they're not "created" until that line above. 237-- Furthermore, you cannot put these 'Field.new()' lines inside the dissector function. 238-- Before Wireshark version 1.11, you couldn't even do this concept (of using fields you just created). 239local questions_field = Field.new("mydns.num_questions") 240local query_type_field = Field.new("mydns.query.type") 241local query_class_field = Field.new("mydns.query.class") 242local response_field = Field.new("mydns.flags.response") 243 244-- here's a little helper function to access the response_field value later. 245-- Like any Field retrieval, you can't retrieve a field's value until its value has been 246-- set, which won't happen until we actually use our ProtoFields in TreeItem:add() calls. 247-- So this isResponse() function can't be used until after the pf_flag_response ProtoField 248-- has been used inside the dissector. 249-- Note that calling the Field object returns a FieldInfo object, and calling that 250-- returns the value of the field - in this case a boolean true/false, since we set the 251-- "mydns.flags.response" ProtoField to ftype.BOOLEAN way earlier when we created the 252-- pf_flag_response ProtoField. Clear as mud? 253-- 254-- A shorter version of this function would be: 255-- local function isResponse() return response_field()() end 256-- but I though the below is easier to understand. 257local function isResponse() 258 local response_fieldinfo = response_field() 259 return response_fieldinfo() 260end 261 262-------------------------------------------------------------------------------- 263-- preferences handling stuff 264-------------------------------------------------------------------------------- 265 266-- a "enum" table for our enum pref, as required by Pref.enum() 267-- having the "index" number makes ZERO sense, and is completely illogical 268-- but it's what the code has expected it to be for a long time. Ugh. 269local debug_pref_enum = { 270 { 1, "Disabled", debug_level.DISABLED }, 271 { 2, "Level 1", debug_level.LEVEL_1 }, 272 { 3, "Level 2", debug_level.LEVEL_2 }, 273} 274 275dns.prefs.debug = Pref.enum("Debug", default_settings.debug_level, 276 "The debug printing level", debug_pref_enum) 277 278dns.prefs.port = Pref.uint("Port number", default_settings.port, 279 "The UDP port number for MyDNS") 280 281dns.prefs.heur = Pref.bool("Heuristic enabled", default_settings.heur_enabled, 282 "Whether heuristic dissection is enabled or not") 283 284---------------------------------------- 285-- a function for handling prefs being changed 286function dns.prefs_changed() 287 dprint2("prefs_changed called") 288 289 default_settings.debug_level = dns.prefs.debug 290 reset_debug_level() 291 292 default_settings.heur_enabled = dns.prefs.heur 293 294 if default_settings.port ~= dns.prefs.port then 295 -- remove old one, if not 0 296 if default_settings.port ~= 0 then 297 dprint2("removing MyDNS from port",default_settings.port) 298 DissectorTable.get("udp.port"):remove(default_settings.port, dns) 299 end 300 -- set our new default 301 default_settings.port = dns.prefs.port 302 -- add new one, if not 0 303 if default_settings.port ~= 0 then 304 dprint2("adding MyDNS to port",default_settings.port) 305 DissectorTable.get("udp.port"):add(default_settings.port, dns) 306 end 307 end 308 309end 310 311dprint2("MyDNS Prefs registered") 312 313 314---------------------------------------- 315---- some constants for later use ---- 316-- the DNS header size 317local DNS_HDR_LEN = 12 318 319-- the smallest possible DNS query field size 320-- has to be at least a label length octet, label character, label null terminator, 2-bytes type and 2-bytes class 321local MIN_QUERY_LEN = 7 322 323---------------------------------------- 324-- some forward "declarations" of helper functions we use in the dissector 325-- I don't usually use this trick, but it'll help reading/grok'ing this script I think 326-- if we don't focus on them. 327local getQueryName 328 329 330---------------------------------------- 331-- The following creates the callback function for the dissector. 332-- It's the same as doing "dns.dissector = function (tvbuf,pkt,root)" 333-- The 'tvbuf' is a Tvb object, 'pktinfo' is a Pinfo object, and 'root' is a TreeItem object. 334-- Whenever Wireshark dissects a packet that our Proto is hooked into, it will call 335-- this function and pass it these arguments for the packet it's dissecting. 336function dns.dissector(tvbuf,pktinfo,root) 337 dprint2("dns.dissector called") 338 339 -- set the protocol column to show our protocol name 340 pktinfo.cols.protocol:set("MYDNS") 341 342 -- We want to check that the packet size is rational during dissection, so let's get the length of the 343 -- packet buffer (Tvb). 344 -- Because DNS has no additional payload data other than itself, and it rides on UDP without padding, 345 -- we can use tvb:len() or tvb:reported_len() here; but I prefer tvb:reported_length_remaining() as it's safer. 346 local pktlen = tvbuf:reported_length_remaining() 347 348 -- We start by adding our protocol to the dissection display tree. 349 -- A call to tree:add() returns the child created, so we can add more "under" it using that return value. 350 -- The second argument is how much of the buffer/packet this added tree item covers/represents - in this 351 -- case (DNS protocol) that's the remainder of the packet. 352 local tree = root:add(dns, tvbuf:range(0,pktlen)) 353 354 -- now let's check it's not too short 355 if pktlen < DNS_HDR_LEN then 356 -- since we're going to add this protocol to a specific UDP port, we're going to 357 -- assume packets in this port are our protocol, so the packet being too short is an error 358 -- the old way: tree:add_expert_info(PI_MALFORMED, PI_ERROR, "packet too short") 359 -- the correct way now: 360 tree:add_proto_expert_info(ef_too_short) 361 dprint("packet length",pktlen,"too short") 362 return 363 end 364 365 -- Now let's add our transaction id under our dns protocol tree we just created. 366 -- The transaction id starts at offset 0, for 2 bytes length. 367 tree:add(pf_trasaction_id, tvbuf:range(0,2)) 368 369 -- We'd like to put the transaction id number in the GUI row for this packet, in its 370 -- INFO column/cell. First we need the transaction id value, though. Since we just 371 -- dissected it with the previous code line, we could now get it using a Field's 372 -- FieldInfo extractor, but instead we'll get it directly from the TvbRange just 373 -- to show how to do that. We'll use Field/FieldInfo extractors later on... 374 local transid = tvbuf:range(0,2):uint() 375 pktinfo.cols.info:set("(".. transid ..")") 376 377 -- now let's add the flags, which are all in the packet bytes at offset 2 of length 2 378 -- instead of calling this again and again, let's just use a variable 379 local flagrange = tvbuf:range(2,2) 380 381 -- for our flags field, we want a sub-tree 382 local flag_tree = tree:add(pf_flags, flagrange) 383 -- I'm indenting this for clarity, because it's adding to the flag's child-tree 384 385 -- let's add the type of message (query vs. response) 386 local query_flag_tree = flag_tree:add(pf_flag_response, flagrange) 387 388 -- let's also add an expert info about it 389 if isResponse() then 390 query_flag_tree:add_proto_expert_info(ef_response, "It's a response!") 391 if transid == 42 then 392 tree:add_tvb_expert_info(ef_ultimate, tvbuf:range(0,2)) 393 end 394 else 395 query_flag_tree:add_proto_expert_info(ef_query) 396 end 397 398 -- we now know if it's a response or query, so let's put that in the 399 -- GUI packet row, in the INFO column cell 400 -- this line of code uses a Lua trick for doing something similar to 401 -- the C/C++ 'test ? true : false' shorthand 402 pktinfo.cols.info:prepend(isResponse() and "Response " or "Query ") 403 404 flag_tree:add(pf_flag_opcode, flagrange) 405 406 if isResponse() then 407 flag_tree:add(pf_flag_authoritative, flagrange) 408 end 409 410 flag_tree:add(pf_flag_truncated, flagrange) 411 412 if isResponse() then 413 flag_tree:add(pf_flag_recursion_available, flagrange) 414 else 415 flag_tree:add(pf_flag_recursion_desired, flagrange) 416 end 417 418 flag_tree:add(pf_flag_z, flagrange) 419 420 if isResponse() then 421 flag_tree:add(pf_flag_authenticated, flagrange) 422 flag_tree:add(pf_flag_rcode, flagrange) 423 end 424 425 flag_tree:add(pf_flag_checking_disabled, flagrange) 426 427 -- now add more to the main mydns tree 428 tree:add(pf_num_questions, tvbuf:range(4,2)) 429 tree:add(pf_num_answers, tvbuf:range(6,2)) 430 -- another way to get a TvbRange is just to call the Tvb like this 431 tree:add(pf_num_authority_rr, tvbuf(8,2)) 432 -- or if we're crazy, we can create a sub-TvbRange, from a sub-TvbRange of the TvbRange 433 tree:add(pf_num_additional_rr, tvbuf:range(10,2):range()()) 434 435 local num_queries = questions_field()() 436 local pos = DNS_HDR_LEN 437 438 if num_queries > 0 then 439 -- let's create a sub-tree, using a plain text description (not a field from the packet) 440 local queries_tree = tree:add("Queries") 441 442 local pktlen_remaining = pktlen - pos 443 444 while num_queries > 0 and pktlen_remaining > 0 do 445 if pktlen_remaining < MIN_QUERY_LEN then 446 -- old way: queries_tree:add_expert_info(PI_MALFORMED, PI_ERROR, "query field missing or too short") 447 queries_tree:add_proto_expert_info(ef_bad_query) 448 return 449 end 450 451 -- we don't know how long this query field in total is, so we have to parse it first before 452 -- adding it to the tree, because we want to identify the correct bytes it covers 453 local label_count, name, name_len = getQueryName(tvbuf:range(pos,pktlen_remaining)) 454 if not label_count then 455 q_tree:add_expert_info(PI_MALFORMED, PI_ERROR, name) 456 return 457 end 458 459 -- now add the first query to the 'Queries' child tree we just created 460 -- we're going to change the string generated by this later, after we figure out the subsequent fields. 461 -- the whole query field is the query name field length we just got, plus the 20 byte type and 2-byte class 462 local q_tree = queries_tree:add(pf_query, tvbuf:range(pos, name_len + 4)) 463 464 q_tree:add(pf_query_name, tvbuf:range(pos, name_len), name) 465 pos = pos + name_len 466 467 pktinfo.cols.info:append(" "..name) 468 469 -- the following tree items are generated by us, not encoded in the packet per se, so mark them as such 470 q_tree:add(pf_query_name_len, name_len):set_generated() 471 q_tree:add(pf_query_label_count, label_count):set_generated() 472 473 q_tree:add(pf_query_type, tvbuf:range(pos, 2)) 474 q_tree:add(pf_query_class, tvbuf:range(pos + 2, 2)) 475 pos = pos + 4 476 477 -- now change the query text 478 q_tree:set_text(name..": type "..query_type_field().display ..", class "..query_class_field().display) 479 480 pktlen_remaining = pktlen_remaining - (name_len + 4) 481 num_queries = num_queries - 1 482 end -- end of while loop 483 484 if num_queries > 0 then 485 -- we didn't process them all 486 queries_tree:add_expert_info(PI_MALFORMED, PI_ERROR, num_queries .. " query field(s) missing") 487 return 488 end 489 end 490 491 dprint2("dns.dissector returning",pos) 492 493 -- tell wireshark how much of tvbuff we dissected 494 return pos 495end 496 497---------------------------------------- 498-- we want to have our protocol dissection invoked for a specific UDP port, 499-- so get the udp dissector table and add our protocol to it 500DissectorTable.get("udp.port"):add(default_settings.port, dns) 501 502---------------------------------------- 503-- we also want to add the heuristic dissector, for any UDP protocol 504-- first we need a heuristic dissection function 505-- this is that function - when wireshark invokes this, it will pass in the same 506-- things it passes in to the "dissector" function, but we only want to actually 507-- dissect it if it's for us, and we need to return true if it's for us, or else false 508-- figuring out if it's for us or not is not easy 509-- we need to try as hard as possible, or else we'll think it's for us when it's 510-- not and block other heuristic dissectors from getting their chance 511-- 512-- in practice, you'd never set a dissector like this to be heuristic, because there 513-- just isn't enough information to safely detect if it's DNS or not 514-- but I'm doing it to show how it would be done 515-- 516-- Note: this heuristic stuff is new in 1.11.3 517local function heur_dissect_dns(tvbuf,pktinfo,root) 518 dprint2("heur_dissect_dns called") 519 520 -- if our preferences tell us not to do this, return false 521 if not default_settings.heur_enabled then 522 return false 523 end 524 525 if tvbuf:len() < DNS_HDR_LEN then 526 dprint("heur_dissect_dns: tvb shorter than DNS_HDR_LEN of:",DNS_HDR_LEN) 527 return false 528 end 529 530 local tvbr = tvbuf:range(0,DNS_HDR_LEN) 531 532 -- the first 2 bytes are transaction id, which can be anything so no point in checking those 533 -- the next 2 bytes contain flags, a couple of which have some values we can check against 534 535 -- the opcode has to be 0, 1, 2, 4 or 5 536 -- the opcode field starts at bit offset 17 (in C-indexing), for 4 bits in length 537 local check = tvbr:bitfield(17,4) 538 if check == 3 or check > 5 then 539 dprint("heur_dissect_dns: invalid opcode:",check) 540 return false 541 end 542 543 -- the rcode has to be 0-10, 16-22 (we're ignoring private use rcodes here) 544 -- the rcode field starts at bit offset 28 (in C-indexing), for 4 bits in length 545 check = tvbr:bitfield(28,4) 546 if check > 22 or (check > 10 and check < 16) then 547 dprint("heur_dissect_dns: invalid rcode:",check) 548 return false 549 end 550 551 dprint2("heur_dissect_dns checking questions/answers") 552 553 -- now let's verify the number of questions/answers are reasonable 554 check = tvbr:range(4,2):uint() -- num questions 555 if check > 100 then return false end 556 check = tvbr:range(6,2):uint() -- num answers 557 if check > 100 then return false end 558 check = tvbr:range(8,2):uint() -- num authority 559 if check > 100 then return false end 560 check = tvbr:range(10,2):uint() -- num additional 561 if check > 100 then return false end 562 563 dprint2("heur_dissect_dns: everything looks good calling the real dissector") 564 565 -- don't do this line in your script - I'm just doing it so our test-suite can 566 -- verify this script 567 root:add("Heuristic dissector used"):set_generated() 568 569 -- ok, looks like it's ours, so go dissect it 570 -- note: calling the dissector directly like this is new in 1.11.3 571 -- also note that calling a Dissector object, as this does, means we don't 572 -- get back the return value of the dissector function we created previously 573 -- so it might be better to just call the function directly instead of doing 574 -- this, but this script is used for testing and this tests the call() function 575 dns.dissector(tvbuf,pktinfo,root) 576 577 -- since this is over a transport protocol, such as UDP, we can set the 578 -- conversation to make it sticky for our dissector, so that all future 579 -- packets to/from the same address:port pair will just call our dissector 580 -- function directly instead of this heuristic function 581 -- this is a new attribute of pinfo in 1.11.3 582 pktinfo.conversation = dns 583 584 return true 585end 586 587-- now register that heuristic dissector into the udp heuristic list 588if default_settings.heur_regmode == 1 then 589 -- this is the "normal" way to register a heuristic: using a lua function 590 dns:register_heuristic("udp",heur_dissect_dns) 591elseif default_settings.heur_regmode == 2 then 592 -- this is to test the fix for bug 10695: 593 dns:register_heuristic("udp",dns.dissector) 594elseif default_settings.heur_regmode == 3 then 595 -- and this too is to test the fix for bug 10695: 596 dns:register_heuristic("udp", function (...) return dns.dissector(...); end ) 597end 598 599-- We're done! 600-- our protocol (Proto) gets automatically registered after this script finishes loading 601---------------------------------------- 602 603---------------------------------------- 604-- DNS query names are not just null-terminated strings; they're actually a sequence of 605-- 'labels', with a length octet before each one. So "foobar.com" is actually the 606-- string "\06foobar\03com\00". We could create a ProtoField for label_length and label_name 607-- or whatever, but since this is an example script I'll show how to do it in raw code. 608-- This function is given the TvbRange object from the dissector() function, and needs to 609-- parse it. 610-- On success, it returns three things: the number of labels, the name string, and how 611-- many bytes it covered of the buffer (which is always 2 more than the name length in this case). 612-- On failure, it returns nil and the error message. 613getQueryName = function (tvbr) 614 local label_count = 0 615 local name = "" 616 617 local len_remaining = tvbr:len() 618 if len_remaining < 2 then 619 -- it's too short 620 return nil, "invalid name" 621 end 622 623 local barray = tvbr:bytes() -- gets a ByteArray of the TvbRange 624 local pos = 0 -- unlike Lua, ByteArray uses 0-based indexing 625 626 -- get the first octet/label-length 627 local label_len = barray:get_index(pos) 628 if label_len == 0 then 629 return nil, "invalid initial label length of 0" 630 end 631 632 while label_len > 0 do 633 if label_len >= len_remaining then 634 return nil, "invalid label length of "..label_len 635 end 636 pos = pos + 1 -- move past label length octet 637 -- append the label and a dot to name string 638 -- note: this uses the new method of ByteArray:raw(), added in 1.11.3 639 name = name .. barray:raw(pos, label_len) .. "." 640 len_remaining = len_remaining - (label_len + 1) -- subtract label and its length octet 641 label_count = label_count + 1 642 pos = pos + label_len -- move past label 643 label_len = barray:get_index(pos) 644 end 645 646 -- we appended an extra dot, so get rid of it 647 name = name:sub(1, -2) 648 649 return label_count, name, name:len() + 2 650end 651