aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLoek Le Blansch <loek@pipeframe.xyz>2024-08-31 14:00:02 +0200
committerLoek Le Blansch <loek@pipeframe.xyz>2024-08-31 14:00:02 +0200
commit2a643308faf4262b938f9c32ab49fd56a95f04a0 (patch)
tree7f62cea2492948cb461d677baf45e09c145ec662
parent882d9bc46d6da8a4134a66010f21b85d733353d4 (diff)
more reassembled data dissection + notes
-rw-r--r--docs/notes.md36
-rw-r--r--wireshark/main.lua2
-rw-r--r--wireshark/pcmeta.lua12
-rw-r--r--wireshark/pcmsg.lua79
-rw-r--r--wireshark/pictochat.lua70
-rw-r--r--wireshark/txhdr.lua8
-rw-r--r--wireshark/util.lua3
7 files changed, 115 insertions, 95 deletions
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: <https://git.pipeframe.xyz/fork/melonDS>
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)
- <https://www.problemkaputt.de/gbatek.htm#dswifiieee80211frames>
- <https://www.problemkaputt.de/gbatek.htm#dswifinintendobeacons>
+## 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