Skip to content

Instantly share code, notes, and snippets.

@djanatyn
Last active October 15, 2022 18:10
Show Gist options
  • Save djanatyn/fa57f7d578907b778e8c2f0d9fffe56a to your computer and use it in GitHub Desktop.
Save djanatyn/fa57f7d578907b778e8c2f0d9fffe56a to your computer and use it in GitHub Desktop.
decoding timestamps in final fantasy 14 packets

I'm capturing packets from Final Fantasy 14 and trying to decode some information!

I used ss to see which TCP socket my ffxiv_dx11.exe process was talking to (204.2.229.99:55024)

❯ sudo ss -Otnp | grep ffxiv
ESTAB      0      0                                 192.168.1.232:55060                             204.2.229.99:55024 users:(("ffxiv_dx11.exe",pid=1022957,fd=360),("wineserver",pid=1022873,fd=529))
ESTAB      0      0                                 192.168.1.232:55058                             204.2.229.99:55024 users:(("ffxiv_dx11.exe",pid=1022957,fd=361),("wineserver",pid=1022873,fd=528))

I used tshark to capture some packets:

sudo tshark -f 'tcp port 55024 and ip host 204.2.229.99' -w /tmp/ffxiv-packets.pcap -x

We can look at the data in the packets, using -T fields output for tshark with -e data as the field selected. That gives a UTF-8 hex string as output, so we read this string with xxd -r and then pass the bits back to hexdump -C:

for packet in $(tshark -r /tmp/ffxiv-packets.pcap -Y 'ip.dst == 192.168.1.232' -T fields -e data); do
  xxd -r -p <<< "$packet" | hexdump -C
  echo
done
00000000  52 52 a0 41 ff 5d 46 e2  7f 2a 64 4d 7b 99 c4 75  |RR.A.]F..*dM{..u|
00000010  10 05 8b c9 83 01 00 00  88 00 00 00 00 00 02 00  |................|
00000020  01 00 00 00 00 00 00 00  30 00 00 00 25 82 10 40  |........0...%..@|
00000030  3a f5 93 10 03 00 00 00  14 00 9f 03 00 00 16 02  |:...............|
00000040  ea 06 46 63 00 00 00 00  25 03 42 00 14 7f 32 70  |..Fc....%.B...2p|
00000050  27 8a 73 5a dc 7f 00 00  30 00 00 00 17 82 10 40  |'.sZ....0......@|
00000060  3a f5 93 10 03 00 00 00  14 00 9f 03 00 00 16 02  |:...............|
00000070  ea 06 46 63 00 00 00 00  d2 6e 82 00 14 7f 1b 79  |..Fc.....n.....y|
00000080  3d 88 ee 58 dc 7f 00 00                           |=..X....|
00000088

00000000  52 52 a0 41 ff 5d 46 e2  7f 2a 64 4d 7b 99 c4 75  |RR.A.]F..*dM{..u|
00000010  68 05 8b c9 83 01 00 00  5a 00 00 00 00 00 01 00  |h.......Z.......|
00000020  01 02 00 00 40 00 00 00  3f ff fc 00 35 b6 36 82  |....@...?...5.6.|
00000030  de 15 36 2f a9 9e ff d0  10 e0 c1 4b 95 2f f2 0e  |..6/.......K./..|
00000040  62 98 f7 1c d1 99 d0 00  28 9f fd 80 94 0e 6f 18  |b.......(.....o.|
00000050  11 1f 5f 70 a5 50 b9 3d  97 21                    |.._p.P.=.!|
0000005a
...

To get some information on how packets are structured, we can read ravahn/machina on GitHub. In FFXIVBundleDecoder.cs, we see a check for a magic number in the header, 0x41a05252:

if (header.magic0 != 0x41a05252)

And our packets start with 52 52 a0 41 - that's 0x41a05252 in little endian! We can use xxd's -e flag:

Switch to little-endian hexdump. This option treats byte groups as words in little-endian byte order. The default grouping of 4 bytes may be changed using -g.

00000000: 41a05252 e2465dff 4d642a7f 75c4997b  RR.A.]F..*dM{..u
00000010: c98b0510 00000183 00000088 00020000  ................
00000020: 00000001 00000000 00000030 40108225  ........0...%..@
00000030: 1093f53a 00000003 039f0014 02160000  :...............
00000040: 634606ea 00000000 00420325 70327f14  ..Fc....%.B...2p
00000050: 5a738a27 00007fdc 00000030 40108217  '.sZ....0......@
00000060: 1093f53a 00000003 039f0014 02160000  :...............
00000070: 634606ea 00000000 00826ed2 791b7f14  ..Fc.....n.....y
00000080: 58ee883d 00007fdc                    =..X....

So there's a specification of some data that comes after the header in Server__BundleHeader.cs:

    [StructLayout(LayoutKind.Explicit)]
    public struct Server_BundleHeader
    {
        [FieldOffset(0)]
        public uint magic0;
        [FieldOffset(4)]
        public uint magic1;
        [FieldOffset(8)]
        public uint magic2;
        [FieldOffset(12)]
        public uint magic3;
        [FieldOffset(16)]
        private readonly ulong _epoch;

Sounds like there's a timestamp (private readonly ulong _epoch) 16 bytes in ([FieldOffset(16)]). What's the current time, in hex?

❯ fend "$(date +%s) in hex"
634aae36

Okay, let's see if these packets have a time around then, by seeking forward 16 bytes (-s $((16 * 4))) and printing 4 bytes (-l 4):

for packet in $(tshark -r /tmp/ffxiv-packets.pcap -Y 'ip.dst == 192.168.1.232' -T fields -e data); do
  xxd -r -p <<< "$packet" | xxd -e -s $((16 * 4)) -l 4
done | head
00000040: 634606ea                             ..Fc
00000040: 1cf79862                             b...
00000040: 634606eb                             ..Fc
00000040: 634606eb                             ..Fc
00000040: 634606eb                             ..Fc
00000040: 0cf76a63                             cj..
00000040: 634606eb                             ..Fc
00000040: 0cf76a63                             cj..
00000040: 634606eb                             ..Fc
00000040: fcf63c64                             d<..

Nice! We can even interpret these times:

for packet in $(tshark -r /tmp/ffxiv-packets.pcap -Y 'ip.dst == 192.168.1.232' -T fields -e data); do
  timestamp=$(xxd -r -p <<< "$packet" | xxd -e -s $((16 * 4)) -l 4 | cut -d ' ' -f 2)
  plaintext="$(date -d "@$(fend "0x${timestamp} in dec")")"; echo "${timestamp} -> ${plaintext}"
done | head
634606ea -> Tue Oct 11 08:14:34 PM EDT 2022
1cf79862 -> Sun May 26 04:47:30 PM EDT 1985
634606eb -> Tue Oct 11 08:14:35 PM EDT 2022
634606eb -> Tue Oct 11 08:14:35 PM EDT 2022
634606eb -> Tue Oct 11 08:14:35 PM EDT 2022
0cf76a63 -> Mon Nov 22 03:06:59 PM EST 1976
634606eb -> Tue Oct 11 08:14:35 PM EDT 2022
0cf76a63 -> Mon Nov 22 03:06:59 PM EST 1976
634606eb -> Tue Oct 11 08:14:35 PM EDT 2022
fcf63c64 -> Fri Jun 27 03:42:28 AM EDT 2104

Looks like not all of these are valid dates, but many of them look close enough :)

zzzzzzz okay i'm going back to playing the game


I got curious and tried out pcap in Rust:

use std::io::Write;

const CAPTURE_FILTER: &str = "tcp port 55024 and ip host 204.2.229.99";

fn main() {
    let mut cap = pcap::Capture::from_device("any")
        .expect("failed to find device")
        .immediate_mode(true)
        .open()
        .expect("failed to open device");

    cap.filter(CAPTURE_FILTER, true)
        .expect("failed to apply filter");

    let packet = cap.next_packet().expect("failed to get packet");

    dbg!(packet.header);

    std::io::stdout()
        .write_all(packet.data)
        .expect("failed to write");
}
❯ cargo build && sudo ./target/debug/ffxiv-lifestream | xxd
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
[src/main.rs:17] packet.header = PacketHeader { ts: 1665856980.727584, caplen: 155, len: 155 }
00000000: 0000 0001 0006 e8fc aff8 8f20 0000 0800  ........... ....
00000010: 4500 008b 4765 4000 3706 8811 cc02 e563  E...Ge@.7......c
00000020: c0a8 01e8 d6f0 8022 7661 02be 927d 8468  ......."va...}.h
00000030: 8018 06af 89a2 0000 0101 080a 338f 3245  ............3.2E
00000040: fbad cce1 5252 a041 ff5d 46e2 7f2a 644d  ....RR.A.]F..*dM
00000050: 7b99 c475 d846 d0dc 8301 0000 5700 0000  {..u.F......W...
00000060: 0000 0100 0102 0000 4000 0000 3fff fc00  ........@...?...
00000070: 0c21 0cec 9430 0cd8 8a6e ffd0 10e0 c14b  .!...0...n.....K
00000080: 9530 196c aa51 29b5 e159 d000 289f 7c83  .0.l.Q)..Y..(.|.
00000090: d4d9 cf47 a0a5 a880 e42b 3e              ...G.....+>

In this case the payload (starting with 5252 a041) starts out at offset 0x44. Hmm, is the data preceding that the captured TCP header?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment