From 2a643308faf4262b938f9c32ab49fd56a95f04a0 Mon Sep 17 00:00:00 2001 From: Loek Le Blansch Date: Sat, 31 Aug 2024 14:00:02 +0200 Subject: more reassembled data dissection + notes --- docs/notes.md | 36 ++++++++++++++-------- wireshark/main.lua | 2 +- wireshark/pcmeta.lua | 12 ++++---- wireshark/pcmsg.lua | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ wireshark/pictochat.lua | 70 ------------------------------------------- wireshark/txhdr.lua | 8 ++--- wireshark/util.lua | 3 +- 7 files changed, 115 insertions(+), 95 deletions(-) create mode 100644 wireshark/pcmsg.lua delete mode 100644 wireshark/pictochat.lua diff --git a/docs/notes.md b/docs/notes.md index e3d1659..c4a72ab 100644 --- a/docs/notes.md +++ b/docs/notes.md @@ -96,14 +96,14 @@ source: which appears to be some kind of 16-bit encoding of the username set on the emulator used to capture these packets (this is likely UCS-2 and not UTF-16 as suggested by [masscat-nifi] which specifies UCS-2). +- WEP is not used for local multiplayer. This was verified after using + Wireshark to dissassemble the IEEE802.11 header present in all captured + frames. None of the captured frames had the WEP encrypt flag set. - The messages are not sent as single packets. The Ni-Fi protocol appears to set up a constant stream, and messages are sent across multiple frames. -- A full height filled-in message results in 64 packets (with Wireshark filter - `nifi.type.enum == 1 && pictochat && frame[0x1a] == 0x02 && frame.len == - 246`) - PictoChat does not appear to send messages when you are in a chat room by - yourself, so local multiplayer / Ni-Fi emulation is required for capturing - message content + yourself, so local multiplayer emulation is required for capturing message + content ### Message sizing/cropping @@ -306,18 +306,31 @@ messages from the system that joined later) - - +## Reassembly + +The pictochat protocol itself also seems to be made up of several layers, one +of which splits large messages over multiple frames. After writing more lua +code to reassemble these chunks, the following observations were made: + +- There is a fixed 0x24 (36) byte header before all drawings +- The size of the reassembled messages for pictochat drawings exactly follows + the equation $f(x) = 36 = 2048x$ where $x$ is the number of rows the message + shows up as. The row count of the message does not appear to be stored + anywhere. +- The first 8 bytes of reassembled messages (drawings, room join) consists of a + constant 0x03, a message type indicator (1 byte) and the address of the + author/subject (6 bytes). +- The remaining 28 bytes of the reassembled message header are constant for + drawings. These bytes appear to be some kind of padding as changing them + appears to have no effect and does not show up in the drawing. + ## Unsure/notes - Is the endianness of the DS properly emulated? -- Is the Ni-Fi magic value also present in physical frames or is this something - MelonDS did? -- The DS implemented WEP(?) encryption for connecting to home network - routers/APs, but is this encryption also used when the WiFi module is used in - local multiplayer mode? Does this even matter inside the emulator? +- Where is a message's destination (pictochat room) defined? ## TODO: -- message reassembly field? (how does pictochat know which part a message index is) - are message-resends required or can the packets be dropped? - user identifier / login / announcement procedure? - actual message format @@ -326,5 +339,4 @@ messages from the system that joined later) * palette color indices (pixels are 1 nibble) - what types of pictochat packets are there? (i.e. how are room join/leave events broadcast?) -- `pictochat.msg_type in {10, 24, 86}` message reassembly in dissector diff --git a/wireshark/main.lua b/wireshark/main.lua index ce48940..908864c 100644 --- a/wireshark/main.lua +++ b/wireshark/main.lua @@ -1,4 +1,4 @@ -require "pictochat" +require "pcmsg" require "pcmeta" diff --git a/wireshark/pcmeta.lua b/wireshark/pcmeta.lua index 0541823..5d35ab4 100644 --- a/wireshark/pcmeta.lua +++ b/wireshark/pcmeta.lua @@ -2,7 +2,7 @@ require "util" local p = Proto("pcmeta", "PictoChat Meta") -local pictochat_dissector = Dissector.get("pictochat") +local message_dissector = Dissector.get("pcmsg") function p.init() local dt = DissectorTable.get("dslmp") @@ -173,12 +173,14 @@ function p.dissector(buffer, 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 mid = pc_global.pid_mid_map[pinfo.number] or 0 + pinfo.cols.info = string.format("[%08x] %s", mid, pinfo.cols.info) - local reassembly = pc_global.msg[pc_global.pid_mid_map[pinfo.number]] + -- add reassembly to current frame if current frame is part of the complete message + local reassembly = pc_global.msg[mid] if reassembly ~= nil then - local tvb = ByteArray.tvb(reassembly, "Reassembly") - pictochat_dissector:call(tvb, pinfo, tree) + local tvb = ByteArray.tvb(reassembly, string.format("msg %08x", mid)) + message_dissector:call(tvb, pinfo, tree) end return buffer_len diff --git a/wireshark/pcmsg.lua b/wireshark/pcmsg.lua new file mode 100644 index 0000000..e3c946d --- /dev/null +++ b/wireshark/pcmsg.lua @@ -0,0 +1,79 @@ +require "util" + +local p = Proto("pcmsg", "PictoChat Message") + +local MSG_TYPE = { + USER_JOIN_A = 0, + USER_JOIN_B = 1, + IMAGE = 2, +} + +local MSG_TYPE_MAP = { + [MSG_TYPE.USER_JOIN_A] = "user join", + [MSG_TYPE.USER_JOIN_B] = "user join", + [MSG_TYPE.IMAGE] = "image", +} + +p.fields.unknown = ProtoField.uint16("pcmsg.unknown", "Unknown") +p.fields.padding = ProtoField.bytes("pcmsg.padding", "Padding") + +local dissect_msg_type = { } + +p.fields.user_addr = ProtoField.bytes("pcmsg.user.addr", "Address", base.COLON) +p.fields.user_name = ProtoField.string("pcmsg.user.name", "Nickname") +p.fields.user_msg = ProtoField.string("pcmsg.user.msg", "Message") +p.fields.user_color = ProtoField.uint16("pcmsg.user.color", "Color", base.DEC, PROFILE_COLOR_MAP) +p.fields.user_bday_month = ProtoField.uint8("pcmsg.user.bday_month", "Month") +p.fields.user_bday_day = ProtoField.uint8("pcmsg.user.bday_day", "Day") +dissect_msg_type[MSG_TYPE.USER_JOIN_A] = function (buffer, pinfo, tree) + add_addr_le(tree, p.fields.user_addr, buffer(0x02, 6)) + local user_name = buffer(0x08, 20):le_ustring() + tree:add(p.fields.user_name, buffer(0x08, 20), user_name) + tree:add(p.fields.user_msg, buffer(0x1c, 52), buffer(0x1c, 52):le_ustring()) + tree:add_le(p.fields.user_color, buffer(0x50, 2)) + local bday_str = string.format("Birthday: %02d/%02d", buffer(0x52, 1):uint(), buffer(0x53, 1):uint()) + local bday = tree:add(buffer(0x52, 2), bday_str) + bday:add(p.fields.user_bday_month, buffer(0x52, 1)) + bday:add(p.fields.user_bday_day, buffer(0x53, 1)) + + register_addr_le(buffer(0x02, 6):raw(), user_name) + + 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.img_addr = ProtoField.bytes("pcmsg.img.addr", "Address", base.COLON) +p.fields.img_data = ProtoField.bytes("pcmsg.img.data", "Image data") +dissect_msg_type[MSG_TYPE.IMAGE] = function (buffer, pinfo, tree) + add_addr_le(tree, p.fields.img_addr, buffer(0x02, 6)) + tree:add(p.fields.padding, buffer(0x08, 0x1c)) + tree:add(p.fields.img_data, buffer(0x24)) + + local user = get_addr_label(fix_addr_endianness(buffer(0x02, 6):raw())) + local rows = (buffer:len() - 0x24) / 0x800 + pinfo.cols.info = string.format("%s %d row drawing by %s", pinfo.cols.info, rows, user) +end +dissect_msg_type[MSG_TYPE.USER_JOIN_B] = dissect_msg_type[MSG_TYPE.USER_JOIN_A] + +p.fields.type = ProtoField.uint8("pcmsg.type", "Type", base.DEC, MSG_TYPE_MAP) +function p.dissector(buffer, pinfo, tree) + 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 %08x: %s, %d bytes", p.description, mid, type_str, buffer_len)) + + pinfo.cols.protocol = p.name + pinfo.cols.info = string.format("[%08x]", mid) + + subtree:add_le(p.fields.unknown, buffer(0x00, 1)) + subtree:add_le(p.fields.type, buffer(0x01, 1)) + + local subdissector = dissect_msg_type[type] + if subdissector ~= nil then + subdissector(buffer, pinfo, subtree) + end + + return buffer_len +end + diff --git a/wireshark/pictochat.lua b/wireshark/pictochat.lua deleted file mode 100644 index 507e486..0000000 --- a/wireshark/pictochat.lua +++ /dev/null @@ -1,70 +0,0 @@ -require "util" - -local p = Proto("pictochat", "PictoChat") - -local MSG_TYPE = { - USER_JOIN_A = 0, - USER_JOIN_B = 1, - MESSAGE = 2, -} - -local MSG_TYPE_MAP = { - [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 = { } - -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.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.USER_JOIN_B] = dissect_msg_type[MSG_TYPE.USER_JOIN_A] - -p.fields.type = ProtoField.uint8("pictochat.type", "Type", base.DEC, MSG_TYPE_MAP) -function p.dissector(buffer, pinfo, tree) - pinfo.cols.protocol = p.name - 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.unknown, buffer(0x00, 1)) - subtree:add_le(p.fields.type, buffer(0x01, 1)) - buffer = buffer(2) - - local subdissector = dissect_msg_type[type] - if subdissector ~= nil then - subdissector(buffer, pinfo, subtree) - end - - return buffer_len -end - diff --git a/wireshark/txhdr.lua b/wireshark/txhdr.lua index e901d7d..f70f983 100644 --- a/wireshark/txhdr.lua +++ b/wireshark/txhdr.lua @@ -11,10 +11,7 @@ p.fields.status = ProtoField.uint16("txhdr.status", "Status", base.DEC, { [0x05] = "Failed", }, 0x00ff) p.fields.new = ProtoField.bool("txhdr.new", "New frame") -p.fields.rate = ProtoField.uint8("txhdr.rate", "Transfer rate", base.HEX, { - [0x0a] = "1 Mbit/s", - [0x14] = "2 Mbit/s", -}) +p.fields.rate = ProtoField.uint8("txhdr.rate", "Transfer rate", base.HEX) p.fields.channel = ProtoField.uint8("txhdr.channel", "802.11 channel") p.fields.length = ProtoField.uint16("txhdr.len", "Remaining message length") p.fields.data = ProtoField.bytes("txhdr.data", "Remaining message") @@ -33,7 +30,8 @@ function p.dissector(buffer, pinfo, tree) subtree:add_le(p.fields.unknown, buffer(0x04, 1)) subtree:add_le(p.fields.unknown, buffer(0x05, 1)) subtree:add_le(p.fields.unknown, buffer(0x06, 2)) - subtree:add_le(p.fields.rate, buffer(0x08, 1)) + local rate_field = subtree:add_le(p.fields.rate, buffer(0x08, 1)) + rate_field:append_text(string.format(" (%1.1f Mbit/s)", buffer(0x08, 1):uint() / 10)) subtree:add_le(p.fields.channel, buffer(0x09, 1)) subtree:add_le(p.fields.length, buffer(0x0a, 2)) local length = buffer(0x0a, 2):le_uint() diff --git a/wireshark/util.lua b/wireshark/util.lua index 7c9a16d..1022b6b 100644 --- a/wireshark/util.lua +++ b/wireshark/util.lua @@ -24,7 +24,6 @@ end room_user_addrs = { } DS_SYSTEM_ADDRS = { ["\x00\x00\x00\x00\x00\x00"] = "Null / empty", - ["\x00\x09\xbf\x11\x22\x33"] = "Default firmware MAC", ["\x03\x09\xbf\x00\x00\x00"] = "Multiplayer CMD", ["\x03\x09\xbf\x00\x00\x10"] = "Multiplayer Reply", ["\x03\x09\xbf\x00\x00\x03"] = "Multiplayer ACK", @@ -40,7 +39,7 @@ function get_addr_label(addr) label = room_user_addrs[addr] if label ~= nil then - return string.format("User: %s", label) + return string.format("user %s", label) end return nil -- cgit v1.2.3