From 1634d546d3e941701fdbab211dfa376f334339f1 Mon Sep 17 00:00:00 2001 From: lonkaars Date: Thu, 2 May 2024 16:58:35 +0200 Subject: WIP messy pictochat protocol dissector --- .gitmodules | 3 + assets/pictochat-msg-corrupt-black-lork2.png | Bin 0 -> 2894 bytes assets/ws-announce-vs.png | Bin 0 -> 191656 bytes docs/notes.md | 91 ++++++++++++++++++++++----- experiments/draw/fill | 28 +++++++++ experiments/draw/lib.sh | 48 ++++++++++++++ experiments/draw/sequence | 31 +++++++++ experiments/pixel-sequence/draw | 59 ----------------- melonDS | 1 + wireshark/nifi.lua | 22 +++---- wireshark/pictochat.lua | 49 +++++++++++++-- 11 files changed, 241 insertions(+), 91 deletions(-) create mode 100644 .gitmodules create mode 100644 assets/pictochat-msg-corrupt-black-lork2.png create mode 100644 assets/ws-announce-vs.png create mode 100755 experiments/draw/fill create mode 100644 experiments/draw/lib.sh create mode 100755 experiments/draw/sequence delete mode 100755 experiments/pixel-sequence/draw create mode 160000 melonDS 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 Binary files /dev/null and b/assets/pictochat-msg-corrupt-black-lork2.png differ diff --git a/assets/ws-announce-vs.png b/assets/ws-announce-vs.png new file mode 100644 index 0000000..30820d1 Binary files /dev/null and b/assets/ws-announce-vs.png 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: 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: 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: |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)| -|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)| +|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| + + ### 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 index 0000000..d529048 --- /dev/null +++ b/melonDS @@ -0,0 +1 @@ +Subproject commit d529048e9ba286af275d9cefc0ebae9800c34f76 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) -- cgit v1.2.3