aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlonkaars <loek@pipeframe.xyz>2024-05-02 16:58:35 +0200
committerlonkaars <loek@pipeframe.xyz>2024-05-02 16:58:35 +0200
commit1634d546d3e941701fdbab211dfa376f334339f1 (patch)
tree16aaba7f0d0b529fb6793809e1c05b87770c9b87
parent7c402c347b46f908eefefb6a957bf92100061951 (diff)
WIP messy pictochat protocol dissector
-rw-r--r--.gitmodules3
-rw-r--r--assets/pictochat-msg-corrupt-black-lork2.pngbin0 -> 2894 bytes
-rw-r--r--assets/ws-announce-vs.pngbin0 -> 191656 bytes
-rw-r--r--docs/notes.md91
-rwxr-xr-xexperiments/draw/fill28
-rw-r--r--experiments/draw/lib.sh48
-rwxr-xr-xexperiments/draw/sequence31
-rwxr-xr-xexperiments/pixel-sequence/draw59
m---------melonDS0
-rw-r--r--wireshark/nifi.lua22
-rw-r--r--wireshark/pictochat.lua49
11 files changed, 240 insertions, 91 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..c492c2d
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "melonDS"]
+ path = melonDS
+ url = git@pipeframe.xyz:fork/melonDS
diff --git a/assets/pictochat-msg-corrupt-black-lork2.png b/assets/pictochat-msg-corrupt-black-lork2.png
new file mode 100644
index 0000000..dfd948e
--- /dev/null
+++ b/assets/pictochat-msg-corrupt-black-lork2.png
Binary files differ
diff --git a/assets/ws-announce-vs.png b/assets/ws-announce-vs.png
new file mode 100644
index 0000000..30820d1
--- /dev/null
+++ b/assets/ws-announce-vs.png
Binary files differ
diff --git a/docs/notes.md b/docs/notes.md
index 10c2608..6e515f9 100644
--- a/docs/notes.md
+++ b/docs/notes.md
@@ -93,10 +93,13 @@ source: <https://git.pipeframe.xyz/fork/melonDS>
the string `lork` is visible as plain text in the hexdump (offset 0x0056),
which appears to be some kind of 16-bit encoding of the username set on the
emulator used to capture these packets
-- The messages are not sent as single packets. The nifi protocol appears to 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 / nifi emulation is required for capturing
+ yourself, so local multiplayer / Ni-Fi emulation is required for capturing
message content
### Message sizing/cropping
@@ -136,12 +139,36 @@ source: <https://git.pipeframe.xyz/fork/melonDS>
message picture.
- The drawable area (including obstructed top-left corner) is 228x80 pixels
+Observed package counts (no resends) for messages of different sizes:
+|draw area|display height|packet#|
+|-|-|-|
+|top-left pixel of row 1 only|1|13|
+|row 1|1|13|
+|row 2|2|25|
+|row 3|2|25|
+|rows 1-2|2|25|
+|rows 1-3|3|38|
+|rows 4-5|3|38|
+|rows 2-3|3|38|
+|rows 1-4|4|51|
+|rows 1-5|5|64|
+|rows 2-5|5|64|
+
+Notable:
+- Messages that draw below line 1 while keeping line 1 empty can't be cropped
+ to utilize line 1 as the username label is in the way. The captured packet
+ counts suggest that the content of line 1 is still sent (although empty).
+- The messages are cropped twice:
+ * (destructive) *before* sending, as suggested by the packet counts
+ * (non-destructive or display only) when displayed in the message log on
+ the top screen
+
### Message content
![](../assets/pictochat-msg-pattern.png)
![](../assets/ws-msg-pattern.png)
-- All messages with interesting content have NIFI header type 1 (CMD).
+- All messages with interesting content have Ni-Fi header type 1 (CMD).
- PictoChat messages appear to be sent over frames of length 0xf6 (246)
regardless of actual message size.
- All frames appear to be sent exactly 5 times. 'New' frames have a value of
@@ -168,20 +195,28 @@ source: <https://git.pipeframe.xyz/fork/melonDS>
|offset|type|description|
|-|-|-|
-|0x0000|`u32`|NIFI: Magic (0x4e494649)|
-|0x0004|`u32`|NIFI: SenderID (melonDS InstanceID)|
-|0x0008|`u32`|NIFI: Type|
-|0x000c|`u32`|NIFI: Length (after NIFI header)|
-|0x0010|`u64`|NIFI: Timestamp|
+|0x00|`u32`|Ni-Fi: Magic (0x4e494649)|
+|0x04|`u32`|Ni-Fi: SenderID (melonDS InstanceID)|
+|0x08|`u32`|Ni-Fi: Type|
+|0x0c|`u32`|Ni-Fi: Length (after Ni-Fi header)|
+|0x10|`u64`|Ni-Fi: Timestamp|
|
-|0x0018|`u16`|PictoChat: 0|
-|0x001a|`u16`|PictoChat: (Resend???) (2=New, 0=Resend)|<!-- I assume u16 because the next byte is always 0x00 -->
-|0x0028|`u8[6]`|PictoChat: multiplayer CMD MAC (melonDS Wifi::MPCmdMAC)|
-|0x002e|`u8[6]`|PictoChat: sender MAC|
-|0x0034|`u8[6]`|PictoChat: sender MAC (again)|
-|0x004d|`u8[0xa0]`|PictoChat: Message data (encoding unknown)|
-|0x00ed|`u16`|PictoChat: (random???)|
-|0x00f0|`u8[6]`|PictoChat: (mac/id???)|
+|0x18|`u16`|PictoChat: Message type??? (0)|
+|0x1a|`u16`|PictoChat: Resend (2=New, 0=Resend)|<!-- I assume u16 because the next byte is always 0x00 -->
+|0x22|`u16`|PictoChat: Length (offset 36 of complete packet length)|
+|0x28|`u8[6]`|PictoChat: multiplayer CMD MAC (melonDS Wifi::MPCmdMAC)|
+|0x2e|`u8[6]`|PictoChat: sender MAC|
+|0x34|`u8[6]`|PictoChat: sender MAC (again)|
+|0x3a|`u16`|Unknown: counter|
+|0x4a|`u16`|PictoChat: Message content offset|
+|0x4e|`u8[0xa0]`|PictoChat: Message data (in 8x8 tiles where each byte represents two pixels as nibbles)|
+|0xee|`u16`|PictoChat: packet sequence number|
+|0xf0|`u16`|PictoChat: copy of 0x1a (Resend)|
+|0xf2|`u32`|Unknown: constant 0x93ffb8b6|
+
+<!--
+TODO: 0x4e(PictoChat msg data offset) - 0x18(Ni-Fi header length) == 0x36
+-->
### Fiddling
@@ -200,13 +235,35 @@ This shows a few important details:
- The ordering of pixels in the messages is not reading order
- Message content is not checked or validated in any way
+## Room host
+
+The system that initially joins an empty room appears to become the room host.
+This system ends up never sending messages of Ni-Fi type 2 (Reply), while the
+packets from other systems after joining an existing room are sent as Ni-Fi
+type 2 exclusively.
+
+![](../assets/ws-announce-vs.png)
+
+(Bottom Wireshark window is the room host, top window shows type != 2 Ni-Fi
+messages from the system that joined later)
+
## Unsure/notes
- Is the endianness of the DS properly emulated?
-- Is the NIFI magic value also present in physical frames or is this something
+- 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?
+## 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
+ * resolution
+ * pixel ordering
+ * palette color indices (pixels are 1 nibble)
+
diff --git a/experiments/draw/fill b/experiments/draw/fill
new file mode 100755
index 0000000..c0278f3
--- /dev/null
+++ b/experiments/draw/fill
@@ -0,0 +1,28 @@
+#!/bin/sh
+cd "$(dirname "$0")"
+. ./lib.sh
+
+msg_clear
+pen
+big
+
+bottom_screen 24 34
+
+xdotool mousedown 1
+sleep 0.02
+xdotool mousemove_relative -- 0 -18
+sleep 0.02
+
+for line in $(seq 22) ; do
+ xdotool mousemove_relative -- 230 0
+ sleep 0.02
+ xdotool mousemove_relative -- 0 2
+ sleep 0.02
+ xdotool mousemove_relative -- -230 0
+ sleep 0.02
+ xdotool mousemove_relative -- 0 2
+ sleep 0.02
+done
+
+xdotool mouseup 1
+
diff --git a/experiments/draw/lib.sh b/experiments/draw/lib.sh
new file mode 100644
index 0000000..90e5689
--- /dev/null
+++ b/experiments/draw/lib.sh
@@ -0,0 +1,48 @@
+WINDOW="$(xdotool search --onlyvisible --maxdepth 2 --class melonDS | head -n1)"
+[ -z "$WINDOW" ] && exit 1
+
+eval "$(xdotool getwindowgeometry --shell "$WINDOW")"
+
+sleep 0.5
+
+bottom_screen() {
+ xdotool mousemove $X $Y
+ # skip menu bar
+ xdotool mousemove_relative 0 20
+ # skip top screen
+ xdotool mousemove_relative 0 192
+
+ xdotool mousemove_relative -- $1 $2
+}
+
+tap() {
+ xdotool mousedown 1
+ sleep 0.02
+ xdotool mouseup 1
+ sleep 0.02
+}
+
+eraser() {
+ bottom_screen 8 60
+ tap
+}
+
+pen() {
+ bottom_screen 8 44
+ tap
+}
+
+small() {
+ bottom_screen 8 92
+ tap
+}
+
+big() {
+ bottom_screen 8 76
+ tap
+}
+
+msg_clear() {
+ bottom_screen 240 170
+ tap
+}
diff --git a/experiments/draw/sequence b/experiments/draw/sequence
new file mode 100755
index 0000000..6000fed
--- /dev/null
+++ b/experiments/draw/sequence
@@ -0,0 +1,31 @@
+#!/bin/sh
+cd "$(dirname "$0")"
+. ./lib.sh
+
+pattern='
+10101010
+1100110011001100
+11110000111100001111000011110000
+1111111100000000111111110000000011111111000000001111111100000000
+'
+pattern="$(echo "$pattern" | tr -d '\n')"
+length="$(echo "$pattern" | wc -c)"
+pattern="$(echo "$pattern" | sed 's/./\0 /g')"
+
+msg_clear
+pen
+small
+
+# message top left
+bottom_screen 82 18
+
+for pixel in $pattern ; do
+ # shift mouse 1 pixel right
+ xdotool mousemove_relative 1 0
+
+ # skip 0's in $pattern
+ [ $pixel -ne 1 ] && continue
+
+ tap
+done
+
diff --git a/experiments/pixel-sequence/draw b/experiments/pixel-sequence/draw
deleted file mode 100755
index 3561865..0000000
--- a/experiments/pixel-sequence/draw
+++ /dev/null
@@ -1,59 +0,0 @@
-#!/bin/sh
-
-WINDOW="$(xdotool search --onlyvisible --maxdepth 2 --class melonDS | head -n1)"
-[ -z "$WINDOW" ] && exit 1
-
-eval "$(xdotool getwindowgeometry --shell "$WINDOW")"
-
-sleep 0.5
-
-bottom_screen() {
- xdotool mousemove $X $Y
- # skip menu bar
- xdotool mousemove_relative 0 20
- # skip top screen
- xdotool mousemove_relative 0 192
-
- xdotool mousemove_relative -- $1 $2
-}
-tap() {
- xdotool mousedown 1
- sleep 0.02
- xdotool mouseup 1
- sleep 0.02
-}
-
-pattern='
-10101010
-1100110011001100
-11110000111100001111000011110000
-1111111100000000111111110000000011111111000000001111111100000000
-'
-pattern="$(echo "$pattern" | tr -d '\n')"
-length="$(echo "$pattern" | wc -c)"
-pattern="$(echo "$pattern" | sed 's/./\0 /g')"
-
-# clear
-bottom_screen 240 170
-tap
-
-# pen
-bottom_screen 8 44
-tap
-# small
-bottom_screen 8 92
-tap
-
-# message top left
-bottom_screen 82 18
-
-for pixel in $pattern ; do
- # shift mouse 1 pixel right
- xdotool mousemove_relative 1 0
-
- # skip 0's in $pattern
- [ $pixel -ne 1 ] && continue
-
- tap
-done
-
diff --git a/melonDS b/melonDS
new file mode 160000
+Subproject d529048e9ba286af275d9cefc0ebae9800c34f7
diff --git a/wireshark/nifi.lua b/wireshark/nifi.lua
index 2bc96a6..d98324b 100644
--- a/wireshark/nifi.lua
+++ b/wireshark/nifi.lua
@@ -1,4 +1,4 @@
-local nifi = Proto("nifi", "Nintendo DS ni-fi")
+local nifi = Proto("nifi", "Nintendo DS Ni-Fi")
nifi.fields.magic = ProtoField.uint32("nifi.magic", "Magic", base.HEX)
nifi.fields.senderid = ProtoField.int32("nifi.senderid", "SenderID", base.DEC)
nifi.fields.type = ProtoField.new("Type", "nifi.type", ftypes.UINT32)
@@ -17,23 +17,23 @@ local nifi_type_enum_field = Field.new("nifi.type.enum")
function nifi.dissector(buffer, pinfo, tree)
-- check magic ("NIFI")
- if buffer(0, 4):uint() ~= 0x4e494649 then return end
- local nifi_tree = tree:add(nifi, buffer(0, 24), "Ni-Fi data")
- nifi_tree:add(nifi.fields.magic, buffer(0, 4))
+ if buffer(0x00, 4):uint() ~= 0x4e494649 then return end
+ local nifi_tree = tree:add(nifi, buffer(0, 0x18), "Ni-Fi Header: 24 bytes")
+ nifi_tree:add(nifi.fields.magic, buffer(0x00, 4))
- nifi_tree:add_le(nifi.fields.senderid, buffer(4, 4))
+ nifi_tree:add_le(nifi.fields.senderid, buffer(0x04, 4))
- local nifi_type_tree = nifi_tree:add_le(nifi.fields.type, buffer(8, 4))
- nifi_type_tree:add_le(nifi.fields.type_enum, buffer(8, 2))
- nifi_type_tree:add_le(nifi.fields.type_aid, buffer(10, 2))
+ local nifi_type_tree = nifi_tree:add_le(nifi.fields.type, buffer(0x08, 4))
+ nifi_type_tree:add_le(nifi.fields.type_enum, buffer(0x08, 2))
+ nifi_type_tree:add_le(nifi.fields.type_aid, buffer(0x0a, 2))
- nifi_tree:add_le(nifi.fields.length, buffer(12, 4))
- nifi_tree:add_le(nifi.fields.timestamp, buffer(16, 8))
+ nifi_tree:add_le(nifi.fields.length, buffer(0x0c, 4))
+ nifi_tree:add_le(nifi.fields.timestamp, buffer(0x10, 8))
pinfo.cols.protocol = nifi.name
pinfo.cols.src = nifi_senderid_field().display
pinfo.cols.info = "type:" .. nifi_type_enum_field().display
- return 24
+ return 0x18
end
diff --git a/wireshark/pictochat.lua b/wireshark/pictochat.lua
index 5eb8089..4927a9b 100644
--- a/wireshark/pictochat.lua
+++ b/wireshark/pictochat.lua
@@ -1,14 +1,55 @@
local pc = Proto("pictochat", "Nintendo DS PictoChat")
+pc.fields.msg_type = ProtoField.uint16("pictochat.msg_type", "Frame type", base.DEC, {
+ [0] = "Message",
+ [1] = "???",
+})
+pc.fields.resend = ProtoField.uint16("pictochat.resend", "Resend", base.DEC, {
+ [0] = "Resend",
+ [2] = "Original",
+})
+-- TODO: 6 bytes unknown
+pc.fields.length = ProtoField.uint16("pictochat.length", "Message length")
+-- TODO: 4 bytes unknown
+pc.fields.mp_sender = ProtoField.ether("pictochat.mp_sender", "Multiplayer sender MAC")
+pc.fields.sender = ProtoField.ether("pictochat.sender", "Sender MAC")
+pc.fields.unknown_counter = ProtoField.uint16("pictochat.unknown_counter", "Unknown counter")
+-- TODO: 14 bytes unknown
+pc.fields.content_offset = ProtoField.uint16("pictochat.content_offset", "Content offset")
+pc.fields.content = ProtoField.bytes("pictochat.content", "Content")
+pc.fields.sequence = ProtoField.uint16("pictochat.sequence", "Packet sequence")
+pc.fields.unknown_constant = ProtoField.bytes("pictochat.unknown_constant", "unknown_constant")
+
+local nifi_length_field = Field.new("nifi.length")
+local pc_msg_type_field = Field.new("pictochat.msg_type")
+local pc_resend_field = Field.new("pictochat.resend")
+local pc_sender_field = Field.new("pictochat.sender")
function pc.dissector(buffer, pinfo, tree)
+ local header_length = nifi_length_field()()
+ if header_length == 0 then return end
+
+ buffer = buffer(0x18) -- skip the Ni-Fi header
+
+ local pc_tree = tree:add(pc, buffer(), "PictoChat: " .. header_length .. " bytes")
- local pc_tree = tree:add(pc, buffer(), "PictoChat Message")
+ pc_tree:add_le(pc.fields.msg_type, buffer(0x00, 2))
+ pc_tree:add_le(pc.fields.resend, buffer(0x02, 2))
+ pc_tree:add_le(pc.fields.length, buffer(0x0a, 2))
+ pc_tree:add_le(pc.fields.mp_sender, buffer(0x10, 6))
+ pc_tree:add_le(pc.fields.sender, buffer(0x16, 6))
+ pc_tree:add_le(pc.fields.sender, buffer(0x1c, 6)) -- copy
+ pc_tree:add_le(pc.fields.unknown_counter, buffer(0x22, 2))
+ pc_tree:add_le(pc.fields.content_offset, buffer(0x32, 2))
+ pc_tree:add(pc.fields.content, buffer(0x36, 0xa0))
+ pc_tree:add_le(pc.fields.sequence, buffer(0xd6, 2))
+ pc_tree:add_le(pc.fields.resend, buffer(0xd8, 2)) -- copy
+ pc_tree:add(pc.fields.unknown_constant, buffer(0xda, 4))
pinfo.cols.protocol = pc.name
+ pinfo.cols.src = tostring(pc_sender_field())
+ pinfo.cols.info = pc_msg_type_field().display .. ", " .. pc_resend_field().display
end
--- no worky
-local nifi = DissectorTable.get("nifi.length")
-nifi:add('>0', pc)
+register_postdissector(pc)