From 882d9bc46d6da8a4134a66010f21b85d733353d4 Mon Sep 17 00:00:00 2001 From: Loek Le Blansch Date: Fri, 30 Aug 2024 17:25:18 +0200 Subject: dissect some of the reassembled pictochat content --- wireshark/main.lua | 2 + wireshark/pcmeta.lua | 186 +++++++++++++++++++++++++++++++++++++++++++ wireshark/pictochat.lua | 205 ++++++++++-------------------------------------- wireshark/util.lua | 6 ++ 4 files changed, 236 insertions(+), 163 deletions(-) create mode 100644 wireshark/pcmeta.lua diff --git a/wireshark/main.lua b/wireshark/main.lua index 91323da..ce48940 100644 --- a/wireshark/main.lua +++ b/wireshark/main.lua @@ -1,5 +1,7 @@ require "pictochat" +require "pcmeta" + require "ieee" require "txhdr" diff --git a/wireshark/pcmeta.lua b/wireshark/pcmeta.lua new file mode 100644 index 0000000..0541823 --- /dev/null +++ b/wireshark/pcmeta.lua @@ -0,0 +1,186 @@ +require "util" + +local p = Proto("pcmeta", "PictoChat Meta") + +local pictochat_dissector = Dissector.get("pictochat") + +function p.init() + local dt = DissectorTable.get("dslmp") + dt:add(GAMEID.PICTOCHAT, p) +end + +local MSG_TYPE = { + MSG_START = 1, + MSG_FRAG = 2, + + -- There doesn't seem to be an actual difference between type 4 and 5, even + -- the sequence number is shared between these message types + STATUS_A = 4, + STATUS_B = 5, +} + +local MSG_TYPE_MAP = { + [MSG_TYPE.MSG_START] = "msg start", + [MSG_TYPE.MSG_FRAG] = "msg fragment", + [MSG_TYPE.STATUS_A] = "status", -- constantly spammed, contains room users' addresses + [MSG_TYPE.STATUS_B] = "status", +} + +p.fields.unknown = ProtoField.uint16("pcmeta.unknown", "Unknown") + +local dissect_msg_type = { } + +p.fields.msg_start_len = ProtoField.uint8("pcmeta.msg_start.len", "Total length") +p.fields.msg_start_id = ProtoField.uint32("pcmeta.msg_start.id", "ID") +p.fields.msg_start_seq = ProtoField.uint16("pcmeta.msg_start.seq", "Sequence") +p.fields.msg_start_new = ProtoField.bool("pcmeta.msg_start.new", "New") +dissect_msg_type[MSG_TYPE.MSG_START] = function (buffer, pinfo, tree) + local subtree = tree:add(p, buffer(), string.format("PictoChat Meta Message start: %d bytes", buffer:len())) + + subtree:add_le(p.fields.unknown, buffer(0x00, 2)) + subtree:add_le(p.fields.unknown, buffer(0x02, 2)) -- usu. ffffh + subtree:add_le(p.fields.msg_start_len, buffer(0x04, 2)) + subtree:add_le(p.fields.unknown, buffer(0x06, 2)) -- usu. 0000h + subtree:add_le(p.fields.unknown, buffer(0x08, 2)) + subtree:add_le(p.fields.unknown, buffer(0x0a, 2)) -- usu. 0000h + subtree:add_le(p.fields.msg_start_id, buffer(0x0c, 4)) + local id = buffer(0x0c, 4):le_uint() + subtree:add_le(p.fields.msg_start_seq, buffer(0x10, 2)) + subtree:add_le(p.fields.new, buffer(0x12, 2)) + local new = buffer(0x12, 2):le_uint() ~= 0 + + if not pinfo.visited then + if new then + pc_global.mid = id + end + + pc_global.pid_mid_map[pinfo.number] = pc_global.mid + pc_global.msg[pc_global.mid] = ByteArray.new() + end +end + +p.fields.body_len = ProtoField.uint8("pcmeta.body.len", "Length") +p.fields.body_end = ProtoField.bool("pcmeta.body.end", "End") +p.fields.body_offset = ProtoField.uint16("pcmeta.body.offset", "Offset") +p.fields.body = ProtoField.bytes("pcmeta.body", "Data") +p.fields.body_seq = ProtoField.uint16("pcmeta.body.seq", "Sequence") +p.fields.body_new = ProtoField.bool("pcmeta.body.new", "New") +dissect_msg_type[MSG_TYPE.MSG_FRAG] = function (buffer, pinfo, tree) + local subtree = tree:add(p, buffer(), string.format("PictoChat Meta Message Fragment: %d bytes", buffer:len())) + + subtree:add_le(p.fields.unknown, buffer(0x00, 2)) + subtree:add_le(p.fields.body_len, buffer(0x02, 1)) + local body_len = buffer(0x02, 1):le_uint() + + subtree:add_le(p.fields.body_end, buffer(0x03, 1)) + -- This appears to be some kind of offset for indicating where to store the + -- current frame's data in a larger buffer. Messages sent in multiple parts + -- increment this value by 160 for each new original (p.fields.original == + -- True) message. + subtree:add_le(p.fields.body_offset, buffer(0x04, 2)) + local body_offset = buffer(0x04, 2):le_uint() + subtree:add_le(p.fields.unknown, buffer(0x06, 2)) -- usually 0 + buffer = buffer(0x08) + + -- This appears to be the actual message data (the drawing) sent as an + -- array of 8x8 tiles. + local body = buffer(0, body_len) + subtree:add(p.fields.body, body) + + buffer = buffer(body_len) + + subtree:add_le(p.fields.body_seq, buffer(0x00, 2)) + subtree:add_le(p.fields.body_new, buffer(0x02, 2)) + local new = buffer(0x02, 2):le_uint() ~= 0 + + if not pinfo.visited then + pc_global.pid_mid_map[pinfo.number] = pc_global.mid + + -- reassembly + if pc_global.msg[pc_global.mid]:len() == body_offset and new == true then + pc_global.msg[pc_global.mid]:append(body:bytes()) + end + end +end + +p.fields.status_id = ProtoField.uint32("pcmeta.status.id", "ID") +p.fields.status_addr = ProtoField.bytes("pcmeta.status.addr", "Address", base.COLON) +p.fields.status_seq = ProtoField.uint16("pcmeta.status.seq", "Sequence") +p.fields.status_new = ProtoField.bool("pcmeta.status.new", "New") +dissect_msg_type[MSG_TYPE.STATUS_A] = function (buffer, pinfo, tree) + local subtree = tree:add(p, buffer(), string.format("PictoChat Meta Status: %d bytes", buffer:len())) + + subtree:add_le(p.fields.status_id, buffer(0x00, 4)) + local id = buffer(0x00, 4):le_uint() + + buffer = buffer(0x04) + + for _ = 0, 15 do + add_addr_le(subtree, p.fields.status_addr, buffer(0, 6)) + buffer = buffer(6) + end + + subtree:add_le(p.fields.status_seq, buffer(0x00, 2)) + subtree:add_le(p.fields.status_new, buffer(0x02, 2)) + + pc_global.pid_mid_map[pinfo.number] = id +end +dissect_msg_type[MSG_TYPE.STATUS_B] = dissect_msg_type[MSG_TYPE.STATUS_A] + +p.fields.magic = ProtoField.bytes("pcmeta.magic", "Magic") +p.fields.new = ProtoField.bool("pcmeta.new", "New") +p.fields.type = ProtoField.uint8("pcmeta.type", "Content type", base.DEC, MSG_TYPE_MAP) +p.fields.len = ProtoField.uint16("pcmeta.len", "Payload length") +p.fields.data = ProtoField.bytes("pcmeta.data", "Payload") +function p.dissector(buffer, pinfo, tree) + -- check magic + if buffer(0x00, 2):uint() ~= GAMEID.PICTOCHAT then return 0 end + -- check length (avoid errors) + if buffer:len() < 6 then return 0 end + + pinfo.cols.protocol = p.name + local buffer_len = math.min(buffer:len(), 0x0a) + local subtree = tree:add(p, buffer(0, buffer_len), string.format("PictoChat Meta Header: %d bytes", buffer_len)) + + subtree:add_le(p.fields.magic, buffer(0x00, 2)) + subtree:add_le(p.fields.new, buffer(0x02, 2)) + subtree:add_le(p.fields.unknown, buffer(0x04, 2)) + local unknown = buffer(0x04, 2):le_uint() + if unknown == 0 then + -- no idea what this field means but if it is 0 then this is the last field + -- in the pictochat body, and we must return in order to prevent lua errors + pinfo.cols.info = "???" + return buffer_len + end + + subtree:add_le(p.fields.type, buffer(0x06, 2)) + local type = buffer(0x06, 2):le_uint() + local type_str = MSG_TYPE_MAP[type] + + if type_str ~= nil then + pinfo.cols.info = type_str + end + + subtree:add_le(p.fields.len, buffer(0x08, 2)) + local payload_length = buffer(0x08, 2):le_uint() + local payload = buffer(0x0a, payload_length) + + subtree:add(p.fields.data, payload) + + local subdissector = dissect_msg_type[type] + if subdissector ~= nil then + subdissector(payload, pinfo, tree) + end + + -- prefix info field with message ID + pinfo.cols.info = string.format("[%08x] %s", pc_global.pid_mid_map[pinfo.number] or 0, pinfo.cols.info) + + local reassembly = pc_global.msg[pc_global.pid_mid_map[pinfo.number]] + if reassembly ~= nil then + local tvb = ByteArray.tvb(reassembly, "Reassembly") + pictochat_dissector:call(tvb, pinfo, tree) + end + + return buffer_len +end + diff --git a/wireshark/pictochat.lua b/wireshark/pictochat.lua index 655f27e..507e486 100644 --- a/wireshark/pictochat.lua +++ b/wireshark/pictochat.lua @@ -2,190 +2,69 @@ require "util" local p = Proto("pictochat", "PictoChat") -function p.init() - local dt = DissectorTable.get("dslmp") - dt:add(GAMEID.PICTOCHAT, p) -end - local MSG_TYPE = { - MSG_START = 1, - MSG_BODY = 2, - STATUS_END = 4, - STATUS = 5, + USER_JOIN_A = 0, + USER_JOIN_B = 1, + MESSAGE = 2, } local MSG_TYPE_MAP = { - [MSG_TYPE.MSG_START] = "Message start", - [MSG_TYPE.MSG_BODY] = "Message body", - [MSG_TYPE.STATUS_END] = "Status (end)", - [MSG_TYPE.STATUS] = "Status", -- constantly spammed, contains room users' addresses + [MSG_TYPE.USER_JOIN_A] = "User join", + [MSG_TYPE.USER_JOIN_B] = "User join", + [MSG_TYPE.MESSAGE] = "Message", } p.fields.unknown = ProtoField.uint16("pictochat.unknown", "Unknown") local dissect_msg_type = { } -local state = { - pid_mid_map = {}, -- map packet id to pictochat message id (key = pid) - msg = {}, -- pictochat messages (key = mid) - mid = 0, -- current pictochat message id -} - -p.fields.msg_start_len = ProtoField.uint8("pictochat.msg_start.len", "Total length") -p.fields.msg_start_id = ProtoField.uint32("pictochat.msg_start.id", "ID") -p.fields.msg_start_seq = ProtoField.uint16("pictochat.msg_start.seq", "Sequence") -p.fields.msg_start_new = ProtoField.bool("pictochat.msg_start.new", "New") -dissect_msg_type[MSG_TYPE.MSG_START] = function (buffer, pinfo, tree) - local subtree = tree:add(p, buffer(), string.format("PictoChat Message start: %d bytes", buffer:len())) - - subtree:add_le(p.fields.unknown, buffer(0x00, 2)) - subtree:add_le(p.fields.unknown, buffer(0x02, 2)) -- usu. ffffh - subtree:add_le(p.fields.msg_start_len, buffer(0x04, 2)) - local len = buffer(0x04, 2):le_uint() - subtree:add_le(p.fields.unknown, buffer(0x06, 2)) -- usu. 0000h - subtree:add_le(p.fields.unknown, buffer(0x08, 2)) - subtree:add_le(p.fields.unknown, buffer(0x0a, 2)) -- usu. 0000h - subtree:add_le(p.fields.msg_start_id, buffer(0x0c, 4)) - local id = buffer(0x0c, 4):le_uint() - subtree:add_le(p.fields.msg_start_seq, buffer(0x10, 2)) - subtree:add_le(p.fields.new, buffer(0x12, 2)) - local new = buffer(0x12, 2):le_uint() ~= 0 - - if new then - state.mid = id - - if not pinfo.visited then - state.msg[state.mid] = { - buf = ByteArray.new(), - size = len, - } - end - end - - state.pid_mid_map[pinfo.number] = state.mid - - if pinfo.visited then - ByteArray.tvb(state.msg[state.pid_mid_map[pinfo.number]].buf, "Complete message") - end +p.fields.user_addr = ProtoField.bytes("pictochat.user.addr", "Address", base.COLON) +p.fields.user_name = ProtoField.string("pictochat.user.name", "Nickname") +p.fields.user_msg = ProtoField.string("pictochat.user.msg", "Message") +p.fields.user_color = ProtoField.uint16("pictochat.user.color", "Color", base.DEC, PROFILE_COLOR_MAP) +p.fields.user_bday_month = ProtoField.uint8("pictochat.user.bday_month", "Month") +p.fields.user_bday_day = ProtoField.uint8("pictochat.user.bday_day", "Day") +dissect_msg_type[MSG_TYPE.USER_JOIN_A] = function (buffer, pinfo, tree) + local user_name = buffer(0x06, 20):le_ustring() + add_addr_le(tree, p.fields.user_addr, buffer(0x00, 6)) + register_addr_le(buffer(0x00, 6):raw(), user_name) + tree:add(p.fields.user_name, buffer(0x06, 20), user_name) + tree:add(p.fields.user_msg, buffer(0x1a, 52), buffer(0x1a, 52):le_ustring()) + tree:add_le(p.fields.user_color, buffer(0x4e, 2)) + local bday_str = string.format("Birthday: %02d/%02d", buffer(0x50, 1):uint(), buffer(0x51, 1):uint()) + local bday = tree:add(buffer(0x50, 2), bday_str) + bday:add(p.fields.user_bday_month, buffer(0x50, 1)) + bday:add(p.fields.user_bday_day, buffer(0x51, 1)) + + pinfo.cols.info = string.format("%s, user join (%s)", pinfo.cols.info, user_name) end +dissect_msg_type[MSG_TYPE.USER_JOIN_B] = dissect_msg_type[MSG_TYPE.USER_JOIN_A] -p.fields.body_len = ProtoField.uint8("pictochat.body.len", "Length") -p.fields.body_end = ProtoField.bool("pictochat.body.end", "End") -p.fields.body_offset = ProtoField.uint16("pictochat.body.offset", "Offset") -p.fields.body = ProtoField.bytes("pictochat.body", "Data") -p.fields.body_seq = ProtoField.uint16("pictochat.body.seq", "Sequence") -p.fields.body_new = ProtoField.bool("pictochat.body.new", "New") -dissect_msg_type[MSG_TYPE.MSG_BODY] = function (buffer, pinfo, tree) - local subtree = tree:add(p, buffer(), string.format("PictoChat Message Body: %d bytes", buffer:len())) - - subtree:add_le(p.fields.unknown, buffer(0x00, 2)) - subtree:add_le(p.fields.body_len, buffer(0x02, 1)) - local body_len = buffer(0x02, 1):le_uint() - - subtree:add_le(p.fields.body_end, buffer(0x03, 1)) - -- This appears to be some kind of offset for indicating where to store the - -- current frame's data in a larger buffer. Messages sent in multiple parts - -- increment this value by 160 for each new original (p.fields.original == - -- True) message. - subtree:add_le(p.fields.body_offset, buffer(0x04, 2)) - local body_offset = buffer(0x04, 2):le_uint() - subtree:add_le(p.fields.unknown, buffer(0x06, 2)) -- usually 0 - buffer = buffer(0x08) - - -- This appears to be the actual message data (the drawing) sent as an - -- array of 8x8 tiles. - local body = buffer(0, body_len) - subtree:add(p.fields.body, body) - - buffer = buffer(body_len) - - subtree:add_le(p.fields.body_seq, buffer(0x00, 2)) - subtree:add_le(p.fields.body_new, buffer(0x02, 2)) - local new = buffer(0x02, 2):le_uint() ~= 0 - - -- reassembly - if pinfo.visited == false and state.msg[state.mid].buf:len() == body_offset and new == true then - state.msg[state.mid].buf:append(body:bytes()) - end - - state.pid_mid_map[pinfo.number] = state.mid - - if pinfo.visited then - ByteArray.tvb(state.msg[state.pid_mid_map[pinfo.number]].buf, "Complete message") - end -end - -p.fields.status_id = ProtoField.uint32("pictochat.status.id", "ID") -p.fields.status_addr = ProtoField.bytes("pictochat.status.addr", "Address", base.COLON) -p.fields.status_seq = ProtoField.uint16("pictochat.status.seq", "Sequence") -p.fields.status_new = ProtoField.bool("pictochat.status.new", "New") -dissect_msg_type[MSG_TYPE.STATUS] = function (buffer, pinfo, tree) - local subtree = tree:add(p, buffer(), string.format("PictoChat Status: %d bytes", buffer:len())) - - subtree:add_le(p.fields.status_id, buffer(0x00, 4)) - local id = buffer(0x00, 4):le_uint() - - buffer = buffer(0x04) - - for _ = 0, 15 do - add_addr_le(subtree, p.fields.status_addr, buffer(0, 6)) - buffer = buffer(6) - end - - subtree:add_le(p.fields.status_seq, buffer(0x00, 2)) - subtree:add_le(p.fields.status_new, buffer(0x02, 2)) - - state.pid_mid_map[pinfo.number] = id +p.fields.msg_addr = ProtoField.bytes("pictochat.msg.addr", "Address", base.COLON) +p.fields.msg_data = ProtoField.bytes("pictochat.msg.data", "Drawing data") +dissect_msg_type[MSG_TYPE.MESSAGE] = function (buffer, pinfo, tree) + add_addr_le(tree, p.fields.msg_addr, buffer(0x00, 6)) end -dissect_msg_type[MSG_TYPE.STATUS_END] = dissect_msg_type[MSG_TYPE.STATUS] +dissect_msg_type[MSG_TYPE.USER_JOIN_B] = dissect_msg_type[MSG_TYPE.USER_JOIN_A] -p.fields.magic = ProtoField.bytes("pictochat.magic", "Magic") -p.fields.new = ProtoField.bool("pictochat.new", "New") -p.fields.flags = ProtoField.uint16("pictochat.flags", "Flags", base.HEX, nil, 0xffff) -p.fields.msg_type = ProtoField.uint8("pictochat.msg_type", "Message type", base.DEC, MSG_TYPE_MAP) -p.fields.len = ProtoField.uint16("pictochat.len", "Payload length") -p.fields.data = ProtoField.bytes("pictochat.data", "Payload") +p.fields.type = ProtoField.uint8("pictochat.type", "Type", base.DEC, MSG_TYPE_MAP) function p.dissector(buffer, pinfo, tree) - -- check magic - if buffer(0x00, 2):uint() ~= GAMEID.PICTOCHAT then return 0 end - -- check length (avoid errors) - if buffer:len() < 6 then return 0 end - pinfo.cols.protocol = p.name - local buffer_len = math.min(buffer:len(), 0x0a) - local subtree = tree:add(p, buffer(0, buffer_len), string.format("PictoChat Header: %d bytes", buffer_len)) + local buffer_len = buffer:len() + local mid = pc_global.pid_mid_map[pinfo.number] + local type = buffer(0x01, 1):le_uint() + local type_str = MSG_TYPE_MAP[type] or "" + local subtree = tree:add(p, buffer(), string.format("%s %s (mid=%08x): %d bytes", p.description, type_str, mid, buffer_len)) - subtree:add_le(p.fields.magic, buffer(0x00, 2)) - subtree:add_le(p.fields.new, buffer(0x02, 2)) - subtree:add_le(p.fields.flags, buffer(0x04, 2)) - local flags = buffer(0x04, 2):le_uint() - if flags == 0 then - pinfo.cols.info = "???" - return buffer_len - end + subtree:add_le(p.fields.unknown, buffer(0x00, 1)) + subtree:add_le(p.fields.type, buffer(0x01, 1)) + buffer = buffer(2) - subtree:add_le(p.fields.msg_type, buffer(0x06, 2)) - local msg_type = buffer(0x06, 2):le_uint() - local msg_type_str = MSG_TYPE_MAP[msg_type] - - if msg_type_str ~= nil then - pinfo.cols.info = msg_type_str - end - - subtree:add_le(p.fields.len, buffer(0x08, 2)) - local payload_length = buffer(0x08, 2):le_uint() - local payload = buffer(0x0a, payload_length) - - subtree:add(p.fields.data, payload) - - local subdissector = dissect_msg_type[msg_type] + local subdissector = dissect_msg_type[type] if subdissector ~= nil then - subdissector(payload, pinfo, tree) + subdissector(buffer, pinfo, subtree) end - -- prefix info field with message ID - pinfo.cols.info = string.format("[%08x] %s", state.pid_mid_map[pinfo.number] or 0, pinfo.cols.info) - return buffer_len end diff --git a/wireshark/util.lua b/wireshark/util.lua index 6051351..7c9a16d 100644 --- a/wireshark/util.lua +++ b/wireshark/util.lua @@ -1,3 +1,9 @@ +pc_global = { + pid_mid_map = {}, -- map packet id to pictochat message id (key = pid) + msg = {}, -- pictochat messages (key = mid) + mid = 0, -- current pictochat message id +} + function bits(idx, len) len = len or 1 return ((2^len)-1) * 2^idx -- cgit v1.2.3