Skip to content

Instantly share code, notes, and snippets.

@adamnew123456
Created March 13, 2021 21:20
Show Gist options
  • Save adamnew123456/e717eac8976e0e2bb1f7ed2b849635be to your computer and use it in GitHub Desktop.
Save adamnew123456/e717eac8976e0e2bb1f7ed2b849635be to your computer and use it in GitHub Desktop.
A Wireshark dissector for the SANE network protocol
--[[
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
--]]
--[[
SANE Network Protocol Dissector
-------------------------------
This is a protocol dissector for Wireshark which is capable of dissecting
the network traffic of the SANE daemon. The network protocol is documented
by the SANE project itself, but note that you may want to have a copy of
the libsane header files handy for reference in case you encounter any
structs that are not included in the protocol docs:
http://www.sane-project.org/html/doc015.html
https://github.com/chadjoan/libsane/blob/master/c/sane.h
Currently only the control protocol is supported. The image protocol is
primarily just blobs of pixel data, so decoding it is less interesting
unless you want to capture images.
--]]
-- The types of messages the client can send to the server. All communication is
-- initiated by the client in the SANE protocol.
local rpc_enum = {
[0] = "SANE_NET_INIT",
[1] = "SANE_NET_GET_DEVICES",
[2] = "SANE_NET_OPEN",
[3] = "SANE_NET_CLOSE",
[4] = "SANE_NET_GET_OPTION_DESCRIPTORS",
[5] = "SANE_NET_CONTROL_OPTION",
[6] = "SANE_NET_GET_PARAMETERS",
[7] = "SANE_NET_START",
[8] = "SANE_NET_CANCEL",
[9] = "SANE_NET_AUTHORIZE",
[10] = "SANE_NET_EXIT",
}
-- Status codes returned by the server
local status_enum = {
[0] = "SANE_STATUS_GOOD",
[1] = "SANE_STATUS_UNSUPPORTED",
[2] = "SANE_STATUS_CANCELLED",
[3] = "SANE_STATUS_DEVICE_BUSY",
[4] = "SANE_STATUS_INVAL",
[5] = "SANE_STATUS_EOF",
[6] = "SANE_STATUS_JAMMED",
[7] = "SANE_STATUS_NO_DOCS",
[8] = "SANE_STATUS_COVER_OPEN",
[9] = "SANE_STATUS_IO_ERROR",
[10] = "SANE_STATUS_NO_MEM",
[11] = "SANE_STATUS_ACCESS_DENIED",
}
-- The types used for property values
local value_type_enum = {
[0] = "SANE_TYPE_BOOL",
[1] = "SANE_TYPE_INT",
[2] = "SANE_TYPE_FIXED",
[3] = "SANE_TYPE_STRING",
[4] = "SANE_TYPE_BUTTON",
[5] = "SANE_TYPE_GROUP",
}
-- The units supported for property values
local value_unit_enum = {
[0] = "SANE_UNIT_NONE",
[1] = "SANE_UNIT_PIXEL",
[2] = "SANE_UNIT_BIT",
[3] = "SANE_UNIT_MM",
[4] = "SANE_UNIT_DPI",
[5] = "SANE_UNIT_PERCENT",
[6] = "SANE_UNIT_MICROSECOND",
}
-- Constraints that determine the values that a property is allowed to have
local value_constraint_enum = {
[0] = "SANE_CONSTRAINT_NONE",
[1] = "SANE_CONSTRAINT_RANGE",
[2] = "SANE_CONSTRAINT_WORD_LIST",
[3] = "SANE_CONSTRAINT_STRING_LIST",
}
-- Flag used to determine the preferred endianness of the server with respect
-- to pixel data
local endian_enum = {
[0x1234] = "Little-Endian",
[0x4321] = "Big-Endian",
}
-- The layout of data used when retrieving scanned images
local format_enum = {
[0] = "SANE_FRAME_GRAY",
[1] = "SANE_FRAME_RGB",
[2] = "SANE_FRAME_RED",
[3] = "SANE_FRAME_GREEN",
[4] = "SANE_FRAME_BLUE",
}
-- What the client is requesting the server do to a property
local action_enum = {
[0] = "SANE_ACTION_GET_VALUE",
[1] = "SANE_ACTION_SET_VALUE",
[2] = "SANE_ACTION_SET_AUTO",
}
local proto_saned = Proto("saned", "SANE Daemon");
local rpc_base = ProtoField.uint8("saned.rpc", "RPC Codes", base.DEC, rpc_enum)
local resource = ProtoField.stringz("saned.resource", "Scanner Resource", base.ASCII)
local version_code = ProtoField.uint32("saned.version", "Version Code", base.DEC)
local handle = ProtoField.uint32("saned.handle", "Handle", base.DEC)
local value = ProtoField.uint32("saned.value.type", "Option Value Type", base.DEC, value_type_enum)
local value_bool = ProtoField.uint32("saned.value.bool", "Boolean Option Value", base.DEC)
local value_int = ProtoField.uint32("saned.value.int", "Integer Option Value", base.DEC)
local value_fixed = ProtoField.uint32("saned.value.fixed", "Fixed Decimal Option Value", base.DEC)
local value_string = ProtoField.stringz("saned.value.string", "String Option Value", base.ASCII)
local req_username = ProtoField.stringz("saned.username", "Username", base.ASCII)
local req_password = ProtoField.stringz("saned.password", "Password", base.ASCII)
local req_device_name = ProtoField.stringz("saned.device_name", "Device Name", base.ASCII)
local req_option = ProtoField.uint32("saned.option", "Option Number", base.DEC)
local req_action = ProtoField.uint32("saned.action", "Action", base.DEC, action_enum)
local resp_status = ProtoField.uint32("saned.status", "Status", base.DEC, status)
local resp_dummy = ProtoField.uint32("saned.dummy", "Dummy Protocol Synchronization Value", base.DEC)
local resp_device_name = ProtoField.stringz("saned.device.name", "Device Name", base.ASCII)
local resp_device_vendor = ProtoField.stringz("saned.device.vendor", "Device Vendor", base.ASCII)
local resp_device_model = ProtoField.stringz("saned.device.model", "Device Model", base.ASCII)
local resp_device_type = ProtoField.stringz("saned.device.type", "Device Type", base.ASCII)
local resp_opt_name = ProtoField.stringz("saned.opt.name", "Option Name", base.ASCII)
local resp_opt_title = ProtoField.stringz("saned.opt.title", "Option Title", base.ASCII)
local resp_opt_desc = ProtoField.stringz("saned.opt.desc", "Option Description", base.ASCII)
local resp_opt_valuetype = ProtoField.uint32("saned.opt.valuetype", "Option Value Type", base.DEC, value_type_enum)
local resp_opt_valueunit = ProtoField.uint32("saned.opt.valueunit", "Option Value Unit", base.DEC, value_unit_enum)
local resp_opt_valuesize = ProtoField.uint32("saned.opt.valuesize", "Option Value Size", base.DEC)
local resp_opt_valuecap = ProtoField.uint32("saned.opt.cap", "Option Capability Flags", base.DEC)
local resp_opt_cap_soft = ProtoField.uint32("saned.opt.cap.soft", "Software Selectable", base.DEC, {}, 1)
local resp_opt_cap_hard = ProtoField.uint32("saned.opt.cap.hard", "Hardware Selectable", base.DEC, {}, 2)
local resp_opt_cap_detect = ProtoField.uint32("saned.opt.cap.detect", "Software Detectable", base.DEC, {}, 4)
local resp_opt_cap_emulated = ProtoField.uint32("saned.opt.cap.emulated", "Emulated By SANE", base.DEC, {}, 8)
local resp_opt_cap_automatic = ProtoField.uint32("saned.opt.cap.automatic", "Device Can Pick Value", base.DEC, {}, 16)
local resp_opt_cap_inactive = ProtoField.uint32("saned.opt.cap.inactive", "Not Active", base.DEC, {}, 32)
local resp_opt_cap_advanced = ProtoField.uint32("saned.opt.cap.advanced", "Advanced", base.DEC, {}, 64)
local resp_opt_valuecons = ProtoField.uint32("saned.opt.cons", "Option Constraint Flags", base.DEC, value_constraint_enum)
local resp_opt_cons_strings = ProtoField.stringz("saned.opt.cons.strings", "Text Constraint Value", base.STRING)
local resp_opt_cons_words = ProtoField.uint32("saned.opt.cons.words", "Integer Constraint Value", base.DEC)
local resp_opt_cons_rangemin = ProtoField.uint32("saned.opt.cons.range.min", "Range Constraint Min", base.DEC)
local resp_opt_cons_rangemax = ProtoField.uint32("saned.opt.cons.range.max", "Range Constraint Max", base.DEC)
local resp_opt_cons_rangequant = ProtoField.uint32("saned.opt.cons.range.quant", "Range Quantization", base.DEC)
local resp_param_format = ProtoField.uint32("saned.param.format", "Frame Format", base.DEC, format_enum)
local resp_param_last_frame = ProtoField.uint32("saned.param.is_last_frame", "Range Quantization", base.DEC)
local resp_param_bytes_per_line = ProtoField.uint32("saned.param.bytes_per_line", "Bytes Per Line", base.DEC)
local resp_param_pixels_per_line = ProtoField.uint32("saned.param.pixels_per_line", "Pixels Per Line", base.DEC)
local resp_param_lines = ProtoField.uint32("saned.param.lines", "Total Lines", base.DEC)
local resp_param_depth = ProtoField.uint32("saned.param.depth", "Depth", base.DEC)
local resp_port = ProtoField.uint32("saned.port", "Data Port", base.DEC)
local resp_byte_order = ProtoField.uint32("saned.byte_order", "Byte Order", base.DEC, endian_enum)
local resp_info = ProtoField.uint32("saned.info", "Info Flags", base.DEC)
local resp_info_inexact = ProtoField.uint32("saned.info.inexact", "Rounded Value", base.DEC, {}, 1)
local resp_info_reload_opt = ProtoField.uint32("saned.info.reload_opt", "Options changed", base.DEC, {}, 2)
local resp_info_reload_par = ProtoField.uint32("saned.info.reload_par", "Parameters changed", base.DEC, {}, 4)
proto_saned.fields = {
rpc_base,
resource,
version_code,
handle,
value,
value_bool,
value_int,
value_fixed,
value_string,
req_username,
req_password,
req_device_name,
req_option,
req_action,
resp_status,
resp_dummy,
resp_device_name,
resp_device_vendor,
resp_device_model,
resp_device_type,
resp_opt_name,
resp_opt_title,
resp_opt_desc,
resp_opt_valuetype,
resp_opt_valueunit,
resp_opt_valuesize,
resp_opt_valuecap,
resp_opt_cap_soft,
resp_opt_cap_hard,
resp_opt_cap_detect,
resp_opt_cap_emulated,
resp_opt_cap_automatic,
resp_opt_cap_inactive,
resp_opt_cap_advanced,
resp_opt_valuecons,
resp_opt_cons_strings,
resp_opt_cons_words,
resp_opt_cons_rangemin,
resp_opt_cons_rangemax,
resp_opt_cons_rangequant,
resp_param_format,
resp_param_last_frame,
resp_param_bytes_per_line,
resp_param_pixels_per_line,
resp_param_lines,
resp_param_depth,
resp_port,
resp_byte_order,
resp_info,
resp_info_inexact,
resp_info_reload_opt,
resp_info_reload_par,
}
-- Captures a slice of the buffer representing a string value. String values are
-- prefixed by their length encoded as a 4-byte word
function read_string(buf, offset)
local length = buf(offset, 4):uint()
if length == 0 then
return nil, offset + 4
else
return buf(offset + 4, length), offset + 4 + length
end
end
-- Reads the value of a 4-byte word
function read_uint(buf, offset)
return buf(offset, 4):uint(), offset + 4
end
function dissect_request(rpc, buf, pkg, subtree)
local str -- Used by various readers
local offset = 4
if rpc == 0 then -- SANE_NET_INIT
subtree:add(version_code, buf(offset, 4))
offset = offset + 4
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(req_username, str) end
elseif rpc == 1 then -- SANE_NET_GET_DEVICES
-- Void request. These seem to contain some dummy value but it's always zero
elseif rpc == 2 then -- SANE_NET_OPEN
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(req_device_name, str) end
elseif rpc == 3 or rpc == 4 or rpc == 6 or rpc == 7 or rpc == 8 then
-- SANE_NET_CLOSE
-- SANE_NET_GET_OPTION_DESCRIPTORS
-- SANE_NET_GET_PARAMETERS
-- SANE_NET_START
-- SANE_NET_CANCEL
subtree:add(handle, buf(offset, 4))
offset = offset + 4
elseif rpc == 5 then -- SANE_NET_CONTROL_OPTION
subtree:add(handle, buf(offset, 4))
offset = offset + 4
subtree:add(req_option, buf(offset, 4))
offset = offset + 4
subtree:add(req_action, buf(offset, 4))
offset = offset + 4
subtree:add(value, buf(offset, 4))
local value_type
value_type, offset = read_uint(buf, offset)
local value_size
value_size, offset = read_uint(buf, offset)
-- All of these are encoded as pointers to values, except for strings
-- which are not wrapped in this way since they're already pointers
local value_null
if value_type == 0 then -- SANE_TYPE_BOOL
value_null, offset = read_uint(buf, offset)
if value_null == 1 then subtree:add(value_bool, buf(offset, 4)) end
elseif value_type == 1 then -- SANE_TYPE_INT
value_null, offset = read_uint(buf, offset)
if value_null == 1 then subtree:add(value_int, buf(offset, 4)) end
elseif value_type == 2 then -- SANE_TYPE_FIXED
value_null, offset = read_uint(buf, offset)
if value_null == 1 then subtree:add(value_fixed, buf(offset, 4)) end
elseif value_type == 3 then -- SANE_TYPE_STRING
str = read_string(buf, offset)
if str ~= nil then subtree:add(value_string, str) end
else
offset = offset + 8 -- Include both null toggle and empty value
end
elseif rpc == 9 then -- SANE_NET_AUTHORIZE
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(resource, str) end
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(req_username, str) end
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(req_password, str) end
end
end
function dissect_response(rpc, buf, pkg, subtree)
local str -- Used by various readers
local status
local offset = 0
if rpc == 0 then -- SANE_NET_INIT
subtree:add(resp_status, buf(offset, 4))
status, offset = read_uint(buf, offset)
if status == 0 then -- SANE_STATUS_GOOD
subtree:add(version_code, buf(offset, 4))
end
elseif rpc == 1 then -- SANE_NET_GET_DEVICES
subtree:add(resp_status, buf(offset, 4))
status, offset = read_uint(buf, offset)
if status == 0 then -- SANE_STATUS_GOOD
offset = offset + 4 -- We don't care about the device array length, it's NULL terminated
while true do
local device_null
device_null, offset = read_uint(buf, offset)
if device_null == 1 then
break
end
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(resp_device_name, str) end
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(resp_device_vendor, str) end
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(resp_device_model, str) end
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(resp_device_type, str) end
end
end
elseif rpc == 2 then -- SANE_NET_OPEN
subtree:add(resp_status, buf(offset, 4))
status, offset = read_uint(buf, offset)
if status == 0 then -- SANE_STATUS_GOOD
subtree:add(handle, buf(offset, 4))
offset = offset + 4
str = read_string(buf, offset)
if str ~= nil then subtree:add(resource, str) end
end
elseif rpc == 3 or rpc == 8 or rpc == 9 then
-- SANE_NET_CLOSE
-- SANE_NET_CANCEL
-- SANE_NET_AUTHORIZE
subtree:add(resp_dummy, buf(offset, 4))
elseif rpc == 4 then -- SANE_NET_OPTION_GET_DESCRIPTORS
local desc_count
desc_count, offset = read_uint(buf, offset)
while desc_count > 0 do
local desc_null
desc_null, offset = read_uint(buf, offset)
if desc_null == 0 then
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(resp_opt_name, str) end
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(resp_opt_title, str) end
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(resp_opt_desc, str) end
subtree:add(resp_opt_valuetype, buf(offset, 4))
offset = offset + 4
subtree:add(resp_opt_valueunit, buf(offset, 4))
offset = offset + 4
subtree:add(resp_opt_valuesize, buf(offset, 4))
offset = offset + 4
subtree:add(resp_opt_valuecap, buf(offset, 4))
subtree:add(resp_opt_cap_soft, buf(offset, 4))
subtree:add(resp_opt_cap_hard, buf(offset, 4))
subtree:add(resp_opt_cap_detect, buf(offset, 4))
subtree:add(resp_opt_cap_emulated, buf(offset, 4))
subtree:add(resp_opt_cap_automatic, buf(offset, 4))
subtree:add(resp_opt_cap_inactive, buf(offset, 4))
subtree:add(resp_opt_cap_advanced, buf(offset, 4))
offset = offset + 4
local constraint_type
subtree:add(resp_opt_valuecons, buf(offset, 4))
constraint_type, offset = read_uint(buf, offset)
if constraint_type == 1 then -- SANE_CONSTRAINT_RANGE
offset = offset + 4 -- Skip redundant NULL tag, these always have values
subtree:add(resp_opt_cons_rangemin, buf(offset, 4))
offset = offset + 4
subtree:add(resp_opt_cons_rangemax, buf(offset, 4))
offset = offset + 4
subtree:add(resp_opt_cons_rangequant, buf(offset, 4))
offset = offset + 4
elseif constraint_type == 2 then -- SANE_CONSTRAINT_WORD_LIST
local word_count
word_count, offset = read_uint(buf, offset)
word_count = word_count - 1
offset = offset + 4 -- Skip length value included in the data
while word_count > 0 do
subtree:add(resp_opt_cons_words, buf(offset, 4))
offset = offset + 4
word_count = word_count - 1
end
elseif constraint_type == 3 then -- SANE_CONSTRAINT_STRING_LIST
local str_count
str_count, offset = read_uint(buf, offset)
while str_count > 0 do
str, offset = read_string(buf, offset)
if str ~= nil then subtree:add(resp_opt_cons_strings, str) end
str_count = str_count - 1
end
end
end
desc_count = desc_count - 1
end
elseif rpc == 5 then -- SANE_NET_CONTROL_OPTION
subtree:add(resp_status, buf(offset, 4))
status, offset = read_uint(buf, offset)
if status == 0 then -- SANE_STATUS_GOOD
subtree:add(resp_info, buf(offset, 4))
subtree:add(resp_info_inexact, buf(offset, 4))
subtree:add(resp_info_reload_opt, buf(offset, 4))
subtree:add(resp_info_reload_par, buf(offset, 4))
offset = offset + 4
subtree:add(value, buf(offset, 4))
local value_type
value_type, offset = read_uint(buf, offset)
local value_size
value_size, offset = read_uint(buf, offset)
-- All of these are encoded as pointers to values, except for strings
-- which are not wrapped in this way since they're already pointers
local value_null
if value_type == 0 then -- SANE_TYPE_BOOL
value_null, offset = read_uint(buf, offset)
if value_null == 1 then subtree:add(value_bool, buf(offset, 4)) end
elseif value_type == 1 then -- SANE_TYPE_INT
value_null, offset = read_uint(buf, offset)
if value_null == 1 then subtree:add(value_int, buf(offset, 4)) end
elseif value_type == 2 then -- SANE_TYPE_FIXED
value_null, offset = read_uint(buf, offset)
if value_null == 1 then subtree:add(value_fixed, buf(offset, 4)) end
elseif value_type == 3 then -- SANE_TYPE_STRING
str = read_string(buf, offset)
if str ~= nil then subtree:add(value_string, str) end
else
offset = offset + 8 -- Include both null toggle and empty value
end
local resource_null
resource_null, offset = read_uint(buf, offset)
if resource_null == 0 then
str = read_string(buf, offset)
if str ~= nil then subtree:add(resource, str) end
end
end
elseif rpc == 6 then -- SANE_NET_GET_PARAMETERS
subtree:add(resp_status, buf(offset, 4))
status, offset = read_uint(buf, offset)
if status == 0 then -- SANE_STATUS_GOOD
subtree:add(resp_param_format, buf(offset, 4))
offset = offset + 4
subtree:add(resp_param_last_frame, buf(offset, 4))
offset = offset + 4
subtree:add(resp_param_bytes_per_line, buf(offset, 4))
offset = offset + 4
subtree:add(resp_param_pixels_per_line, buf(offset, 4))
offset = offset + 4
subtree:add(resp_param_lines, buf(offset, 4))
offset = offset + 4
subtree:add(resp_param_depth, buf(offset, 4))
offset = offset + 4
end
elseif rpc == 7 then -- SANE_NET_START
subtree:add(resp_status, buf(offset, 4))
status, offset = read_uint(buf, offset)
if status == 0 then -- SANE_STATUS_GOOD
subtree:add(resp_port, buf(offset, 4))
offset = offset + 4
subtree:add(resp_byte_order, buf(offset, 4))
offset = offset + 4
str = read_string(buf, offset)
if str ~= nil then subtree:add(resource, str) end
end
end
end
--[[
HACK!
Wireshark seems to do an initial pass over the packet data with the dissector,
and then re-invokes it later when it wants to display the packet data. During
the initial pass we have to associate each response with its request since the
response packets don't include an RPC tag.
This *will* break if you try it on a network where more than one machine is
accessing a SANE server. You could store the last RPC value in its own table
indexed by (src_ip, dest_ip, src_port, dest_port), I just didn't here.
--]]
local rpc_cache = {}
last_rpc = nil
function proto_saned.dissector(buf, pkt, tree)
local subtree = tree:add(proto_saned, buf)
if pkt.dst_port == 6566 then
subtree:add(rpc_base, buf(0, 4))
proto_type = buf(0, 4):uint()
last_rpc = proto_type
dissect_request(proto_type, buf, pkt, subtree)
elseif not pkt.visited then
-- Initial pass saves the discovered RPC value so that we can run the
-- dissector out of order later
rpc_cache[pkt.number] = last_rpc
dissect_response(proto_type, buf, pkt, subtree)
else
-- The user clicked on a packet
dissect_response(rpc_cache[pkt.number], buf, pkt, subtree)
end
end
-- ?
local wtap_encap_table = DissectorTable.get("wtap_encap")
wtap_encap_table:add(wtap.USER0, proto_saned)
-- Register the protocol to a specific port
local tcp_encap_table = DissectorTable.get("tcp.port")
tcp_encap_table:add(6566, proto_saned)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment