Skip to content

Commit

Permalink
Added support for decoding Lobby, Chat, and Zone recv packets
Browse files Browse the repository at this point in the history
Deucalion will partially parse the network data after it
is decompressed by the game client
  • Loading branch information
ff14wed committed Mar 9, 2023
1 parent 08596a7 commit b2642fc
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 130 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pelite = "0.10"
memchr = "2.5"
region = "3.0"
once_cell = "1.17"
binary-layout = "3.1.3"

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = [
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Deucalion-specific format:
struct DEUCALION_SEGMENT {
uint32_t source_actor;
uint32_t target_actor;
uint64_t timestamp; // milliseconds since UNIX epoch
FFXIVARR_IPC_HEADER ipc_header; // Includes reserved, type (opcode), serverId, etc.
uint8_t packet_data[];
}
Expand Down
41 changes: 34 additions & 7 deletions src/hook/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::fmt::Display;
use std::sync::Arc;

use anyhow::{format_err, Context, Result};
use thiserror::Error;

use tokio::sync::{mpsc, Mutex};

Expand All @@ -10,43 +12,68 @@ use pelite::pe::PeView;

use crate::rpc;

mod recvzonepacket;
mod packet;
mod recv;
mod waitgroup;

pub struct State {
rzp_hook: recvzonepacket::Hook,
recv_hook: recv::Hook,
wg: waitgroup::WaitGroup,
pub broadcast_rx: Arc<Mutex<mpsc::UnboundedReceiver<rpc::Payload>>>,
}

#[repr(u32)]
#[derive(Debug, Clone, Copy)]
pub(self) enum Channel {
Lobby,
Zone,
Chat,
}

impl Display for Channel {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
match *self {
Channel::Lobby => f.write_str("lobby"),
Channel::Zone => f.write_str("zone"),
Channel::Chat => f.write_str("chat"),
}
}
}

#[derive(Debug, Error)]
pub(self) enum HookError {
#[error("failed to set up {0} hook")]
SetupFailed(Channel),
}

impl State {
pub fn new() -> Result<State> {
let (broadcast_tx, broadcast_rx) = mpsc::unbounded_channel::<rpc::Payload>();

let wg = waitgroup::WaitGroup::new();
let hs = State {
rzp_hook: recvzonepacket::Hook::new(broadcast_tx.clone(), wg.clone())?,
recv_hook: recv::Hook::new(broadcast_tx.clone(), wg.clone())?,
wg,
broadcast_rx: Arc::new(Mutex::new(broadcast_rx)),
};
Ok(hs)
}

pub fn initialize_recv_zone_hook(&self, sig_str: String) -> Result<()> {
pub fn initialize_recv_hook(&self, sig_str: String) -> Result<()> {
let pat =
pattern::parse(&sig_str).context(format!("Invalid signature: \"{}\"", sig_str))?;
let sig: &[pattern::Atom] = &pat;
let handle_ffxiv = get_ffxiv_handle()?;
let pe_view = unsafe { PeView::module(handle_ffxiv) };

let recvzonepacket_rvas = find_pattern_matches("RecvZonePacket", sig, pe_view)
let decompresspacket_rvas = find_pattern_matches("recv::DecompressPacket", sig, pe_view)
.map_err(|e| format_err!("{}: {}", e, sig_str))?;

self.rzp_hook.setup(recvzonepacket_rvas[0])
self.recv_hook.setup(decompresspacket_rvas)
}

pub fn shutdown(&self) {
self.rzp_hook.shutdown();
self.recv_hook.shutdown();
// Wait for any hooks to finish what they're doing
self.wg.wait();
}
Expand Down
51 changes: 51 additions & 0 deletions src/hook/packet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use binary_layout::prelude::*;

define_layout!(frame_header, LittleEndian, {
magic: [u8; 16],
timestamp: u64,
size: u32,
connection_type: u16,
segment_count: u16,
unknown_20: u8,
compression: u8,
unknown_22: u16,
decompressed_length: u32,
});

define_layout!(segment_header, LittleEndian, {
size: u32,
source_actor: u32,
target_actor: u32,
segment_type: u16,
padding: u16,
});

define_layout!(segment, LittleEndian, {
header: segment_header::NestedView,
data: [u8],
});

define_layout!(ipc_header, LittleEndian, {
reserved: u16,
opcode: u16,
padding: u16,
server_id: u16,
timestamp: u32,
padding1: u32,
});

define_layout!(ipc_packet, LittleEndian, {
header: ipc_header::NestedView,
data: [u8],
});

define_layout!(deucalion_segment_header, LittleEndian, {
source_actor: u32,
target_actor: u32,
timestamp: u64,
});

define_layout!(deucalion_segment, LittleEndian, {
header: deucalion_segment_header::NestedView,
data: [u8],
});
224 changes: 224 additions & 0 deletions src/hook/recv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
use core::slice;
use std::mem;
use std::sync::Arc;

use anyhow::Result;

use tokio::sync::mpsc;

use once_cell::sync::OnceCell;

use retour;
use retour::static_detour;

use binary_layout::prelude::*;

use crate::rpc;

use crate::procloader::get_ffxiv_handle;

use super::packet;
use super::waitgroup;
use super::{Channel, HookError};

use log::info;

type HookedFunction = unsafe extern "system" fn(*const u8, *const u8, usize, usize, usize) -> usize;

static_detour! {
static DecompressPacketChat: unsafe extern "system" fn(*const u8, *const u8, usize, usize, usize) -> usize;
static DecompressPacketLobby: unsafe extern "system" fn(*const u8, *const u8, usize, usize, usize) -> usize;
static DecompressPacketZone: unsafe extern "system" fn(*const u8, *const u8, usize, usize, usize) -> usize;
}

#[derive(Clone)]
pub struct Hook {
data_tx: mpsc::UnboundedSender<rpc::Payload>,

chat_hook: Arc<OnceCell<&'static retour::StaticDetour<HookedFunction>>>,
lobby_hook: Arc<OnceCell<&'static retour::StaticDetour<HookedFunction>>>,
zone_hook: Arc<OnceCell<&'static retour::StaticDetour<HookedFunction>>>,

wg: waitgroup::WaitGroup,
}

impl Hook {
pub fn new(
data_tx: mpsc::UnboundedSender<rpc::Payload>,
wg: waitgroup::WaitGroup,
) -> Result<Hook> {
Ok(Hook {
data_tx,
chat_hook: Arc::new(OnceCell::new()),
lobby_hook: Arc::new(OnceCell::new()),
zone_hook: Arc::new(OnceCell::new()),
wg,
})
}

pub fn setup(&self, rvas: Vec<usize>) -> Result<()> {
let mut ptrs: Vec<*const u8> = Vec::new();
for rva in rvas {
ptrs.push(get_ffxiv_handle()?.wrapping_offset(rva as isize));
}

let self_clone = self.clone();
let chat_hook = unsafe {
let ptr_fn: HookedFunction = mem::transmute(ptrs[0] as *const ());
DecompressPacketChat.initialize(ptr_fn, move |a, b, c, d, e| {
self_clone.recv_packet(Channel::Chat, a, b, c, d, e)
})?
};
if let Err(_) = self.chat_hook.set(chat_hook) {
return Err(HookError::SetupFailed(Channel::Chat).into());
}

let self_clone = self.clone();
let lobby_hook = unsafe {
let ptr_fn: HookedFunction = mem::transmute(ptrs[1] as *const ());
DecompressPacketLobby.initialize(ptr_fn, move |a, b, c, d, e| {
self_clone.recv_packet(Channel::Lobby, a, b, c, d, e)
})?
};
if let Err(_) = self.lobby_hook.set(lobby_hook) {
return Err(HookError::SetupFailed(Channel::Lobby).into());
}

let self_clone = self.clone();
let zone_hook = unsafe {
let ptr_fn: HookedFunction = mem::transmute(ptrs[2] as *const ());
DecompressPacketZone.initialize(ptr_fn, move |a, b, c, d, e| {
self_clone.recv_packet(Channel::Zone, a, b, c, d, e)
})?
};
if let Err(_) = self.zone_hook.set(zone_hook) {
return Err(HookError::SetupFailed(Channel::Zone).into());
}

unsafe {
self.chat_hook.get_unchecked().enable()?;
self.lobby_hook.get_unchecked().enable()?;
self.zone_hook.get_unchecked().enable()?;
}
Ok(())
}

unsafe fn recv_packet(
&self,
channel: Channel,
a1: *const u8,
a2: *const u8,
a3: usize,
a4: usize,
a5: usize,
) -> usize {
let _guard = self.wg.add();

const INVALID_MSG: &str = "Hook function was called without a valid hook";
let hook = match channel {
Channel::Chat => self.chat_hook.clone(),
Channel::Lobby => self.lobby_hook.clone(),
Channel::Zone => self.zone_hook.clone(),
};
let ret = hook.get().expect(INVALID_MSG).call(a1, a2, a3, a4, a5);

let ptr_frame: *const u8 = *(a1.add(16) as *const usize) as *const u8;

if ptr_frame.is_null() {
return ret;
}

let frame_header_bytes = slice::from_raw_parts(ptr_frame, 40);
let frame_header = packet::frame_header::View::new(frame_header_bytes);

const ERR_PREFIX: &str = "Could not process packet";

let compression: u8 = frame_header.compression().read();
if compression != 0 {
info!(
"{}: packet is still compressed: {}",
ERR_PREFIX, compression
);
return ret;
}

let num_segments: u16 = frame_header.segment_count().read();
let frame_size: usize = frame_header.size().read() as usize;

let frame_header_size = packet::frame_header::SIZE.unwrap();

if frame_size > 0x10000 || frame_size < frame_header_size {
info!("{}: frame_size is invalid: {}", ERR_PREFIX, frame_size);
return ret;
}

let frame_data = slice::from_raw_parts(
ptr_frame.add(frame_header_size),
frame_size - frame_header_size,
);

let mut frame_data_offset: usize = 0;

let mut packets: Vec<Vec<u8>> = Vec::new();
for _ in 0..num_segments {
let segment_size = packet::segment_header::size::read(&frame_data[frame_data_offset..]);
let segment_header_size = packet::segment_header::SIZE.unwrap();

if segment_size > 0x10000 || (segment_size as usize) < segment_header_size {
info!("{}: segment_size is invalid: {}", ERR_PREFIX, segment_size);
return ret;
}

let segment = packet::segment::View::new(
&frame_data[frame_data_offset..frame_data_offset + segment_size as usize],
);
frame_data_offset += segment_size as usize;

// Capture only IPC segment type
if segment.header().segment_type().read() != 3 {
continue;
}
let segment_header = segment.header();
let deucalion_header_size = packet::deucalion_segment_header::SIZE.unwrap();
let payload_len = segment_size as usize - segment_header_size + deucalion_header_size;

let mut dst = Vec::<u8>::with_capacity(payload_len);
dst.set_len(payload_len);
let buf: &mut [u8] = dst.as_mut();
let mut deucalion_segment = packet::deucalion_segment::View::new(buf);
let mut dsh = deucalion_segment.header_mut();
dsh.source_actor_mut()
.write(segment_header.source_actor().read());
dsh.target_actor_mut()
.write(segment_header.target_actor().read());
dsh.timestamp_mut().write(frame_header.timestamp().read());

deucalion_segment.data_mut().copy_from_slice(segment.data());

packets.push(dst);
}

for packet in packets {
let _ = self.data_tx.send(rpc::Payload {
op: rpc::MessageOps::Recv,
ctx: channel as u32,
data: packet,
});
}
return ret;
}

pub fn shutdown(&self) {
unsafe {
if let Some(hook) = self.lobby_hook.get() {
let _ = hook.disable();
};
if let Some(hook) = self.chat_hook.get() {
let _ = hook.disable();
};
if let Some(hook) = self.zone_hook.get() {
let _ = hook.disable();
};
}
}
}
Loading

0 comments on commit b2642fc

Please sign in to comment.