add Rust ScrapHacks prototype and network sniffer/parser
This commit is contained in:
parent
58407ecc9f
commit
63962c95cc
27 changed files with 5008 additions and 0 deletions
2
tools/remaster/scrap_net/.gitignore
vendored
Normal file
2
tools/remaster/scrap_net/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/.history
|
1015
tools/remaster/scrap_net/Cargo.lock
generated
Normal file
1015
tools/remaster/scrap_net/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
28
tools/remaster/scrap_net/Cargo.toml
Normal file
28
tools/remaster/scrap_net/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "scrap_net"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Daniel Seiller <earthnuker@gmail.com>"]
|
||||
description = "Scrapland Remastered network sniffer, proxy (and soon hopefully parser)"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
chacha20 = { version = "0.9", features = ["std"] }
|
||||
poly1305 = { version = "0.8", features = ["std"] }
|
||||
rhexdump = "0.1"
|
||||
tokio = { version = "1.21", features = ["full"] }
|
||||
clap = {version = "4.0", features = ["derive"]}
|
||||
rand = "0.8"
|
||||
dialoguer = "0.10"
|
||||
binrw = "0.11"
|
||||
modular-bitfield = "0.11"
|
||||
hex = "0.4"
|
||||
lazy_static = "1.4.0"
|
||||
rustyline-async = "0.3"
|
||||
futures-util = "0.3.24"
|
||||
itertools = "0.10.5"
|
||||
anyhow = "1.0.68"
|
||||
|
||||
[profile.release]
|
||||
lto="fat"
|
||||
opt-level = 3
|
22
tools/remaster/scrap_net/get_app.py
Normal file
22
tools/remaster/scrap_net/get_app.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from distutils.command.install_data import install_data
|
||||
import winreg as reg
|
||||
import vdf
|
||||
from pathlib import Path
|
||||
import pefile
|
||||
app_id="897610"
|
||||
try:
|
||||
key = reg.OpenKey(reg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Valve\\Steam")
|
||||
except FileNotFoundError:
|
||||
key = reg.OpenKey(reg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Wow6432Node\\Valve\\Steam")
|
||||
path=Path(reg.QueryValueEx(key,"InstallPath")[0])
|
||||
libraryfolders=vdf.load((path/"steamapps"/"libraryfolders.vdf").open("r"))['libraryfolders']
|
||||
for folder in libraryfolders.values():
|
||||
path=Path(folder['path'])
|
||||
if app_id in folder['apps']:
|
||||
install_dir = vdf.load((path/"steamapps"/f"appmanifest_{app_id}.acf").open("r"))['AppState']['installdir']
|
||||
install_dir=path/"steamapps"/"common"/install_dir
|
||||
for file in install_dir.glob("**/*.exe"):
|
||||
pe = pefile.PE(file, fast_load=True)
|
||||
entry = pe.OPTIONAL_HEADER.AddressOfEntryPoint
|
||||
if pe.get_dword_at_rva(entry) == 0xE8:
|
||||
print(file)
|
93
tools/remaster/scrap_net/src/hex_ii.rs
Normal file
93
tools/remaster/scrap_net/src/hex_ii.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use itertools::Itertools;
|
||||
use std::fmt::Display;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum HexII {
|
||||
Ascii(char),
|
||||
Byte(u8),
|
||||
Null,
|
||||
Full,
|
||||
Eof,
|
||||
}
|
||||
|
||||
impl From<&u8> for HexII {
|
||||
fn from(v: &u8) -> Self {
|
||||
match v {
|
||||
0x00 => Self::Null,
|
||||
0xFF => Self::Full,
|
||||
c if c.is_ascii_graphic() => Self::Ascii(*c as char),
|
||||
v => Self::Byte(*v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HexII {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
HexII::Ascii(v) => write!(f, ".{}", v)?,
|
||||
HexII::Byte(v) => write!(f, "{:02x}", v)?,
|
||||
HexII::Null => write!(f, " ")?,
|
||||
HexII::Full => write!(f, "##")?,
|
||||
HexII::Eof => write!(f, " ]")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct HexIILine(Vec<HexII>);
|
||||
|
||||
impl Display for HexIILine {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for (i, v) in self.0.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
write!(f, "{}", v)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for HexIILine {
|
||||
fn from(l: &[u8]) -> Self {
|
||||
Self(l.iter().map(HexII::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for HexIILine {
|
||||
type Target = Vec<HexII>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for HexIILine {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hex_ii_dump<T: Iterator<Item = u8>>(data: T, base_offset: usize, total: usize) {
|
||||
const CHUNK_SIZE: usize = 0x10;
|
||||
let mut num_digits = (std::mem::size_of_val(&total) * 8) - (total.leading_zeros() as usize);
|
||||
if (num_digits % 8) != 0 {
|
||||
num_digits += 8 - (num_digits % 8)
|
||||
}
|
||||
num_digits >>= 2;
|
||||
for (mut offset, line) in data.chunks(CHUNK_SIZE).into_iter().enumerate() {
|
||||
offset += base_offset;
|
||||
let mut line = HexIILine::from(line.collect::<Vec<_>>().as_slice());
|
||||
if line.len() < CHUNK_SIZE {
|
||||
line.push(HexII::Eof);
|
||||
}
|
||||
while line.len() < CHUNK_SIZE {
|
||||
line.push(HexII::Null);
|
||||
}
|
||||
if line.iter().all(|v| v == &HexII::Null) {
|
||||
continue;
|
||||
}
|
||||
let offset = format!("{:digits$x}", offset * CHUNK_SIZE, digits = num_digits);
|
||||
println!("{} | {:<16} |", offset, line);
|
||||
}
|
||||
}
|
640
tools/remaster/scrap_net/src/main.rs
Normal file
640
tools/remaster/scrap_net/src/main.rs
Normal file
|
@ -0,0 +1,640 @@
|
|||
use anyhow::{bail, ensure, Result};
|
||||
use binrw::BinReaderExt;
|
||||
use binrw::{BinRead, NullString};
|
||||
use chacha20::cipher::KeyInit;
|
||||
use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
|
||||
use chacha20::ChaCha20;
|
||||
use clap::Parser;
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use dialoguer::Select;
|
||||
use futures_util::FutureExt;
|
||||
use poly1305::Poly1305;
|
||||
use rand::{thread_rng, Rng};
|
||||
use rhexdump::hexdump;
|
||||
use rustyline_async::{Readline, ReadlineError, SharedWriter};
|
||||
use std::collections::BTreeMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use std::io::Cursor;
|
||||
use std::io::Write;
|
||||
use std::iter;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time;
|
||||
|
||||
mod hex_ii;
|
||||
mod parser;
|
||||
|
||||
const KEY: &[u8; 32] = b"\x02\x04\x06\x08\x0a\x0c\x0e\x10\x12\x14\x16\x18\x1a\x1c\x1e\x20\x22\x24\x26\x28\x2a\x2c\x2e\x30\x32\x34\x36\x38\x3a\x3c\x3e\x40";
|
||||
const INFO_PACKET: &[u8] = b"\x7f\x01\x00\x00\x07";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ServerFlags {
|
||||
dedicated: bool,
|
||||
force_vehicle: bool,
|
||||
_rest: u8,
|
||||
}
|
||||
|
||||
impl Display for ServerFlags {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let force_vehicle = if self.force_vehicle { "F" } else { " " };
|
||||
let dedicated = if self.dedicated { "D" } else { " " };
|
||||
write!(f, "{}{}", force_vehicle, dedicated)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for ServerFlags {
|
||||
fn from(v: u8) -> Self {
|
||||
ServerFlags {
|
||||
dedicated: v & 0b1 != 0,
|
||||
force_vehicle: v & 0b10 != 0,
|
||||
_rest: (v & 0b11111100) >> 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, BinRead)]
|
||||
#[br(little, magic = b"\xba\xce", import(rtt: Duration, addr: SocketAddr))]
|
||||
pub struct Server {
|
||||
#[br(calc=addr)]
|
||||
addr: SocketAddr,
|
||||
#[br(calc=rtt)]
|
||||
rtt: Duration,
|
||||
#[br(map = |v: (u8,u8)| format!("{}.{}",v.0,v.1))]
|
||||
version: String,
|
||||
port: u16,
|
||||
max_players: u16,
|
||||
cur_players: u16,
|
||||
#[br(map = u8::into)]
|
||||
flags: ServerFlags,
|
||||
#[br(pad_size_to(0x20), map = |s :NullString| s.to_string())]
|
||||
name: String,
|
||||
#[br(pad_size_to(0x10), map = |s :NullString| s.to_string())]
|
||||
mode: String,
|
||||
#[br(pad_size_to(0x20), map = |s :NullString| s.to_string())]
|
||||
map: String,
|
||||
_pad: u8,
|
||||
}
|
||||
|
||||
fn pad_copy(d: &[u8], l: usize) -> Vec<u8> {
|
||||
let diff = d.len() % l;
|
||||
if diff != 0 {
|
||||
d.iter()
|
||||
.copied()
|
||||
.chain(iter::repeat(0).take(l - diff))
|
||||
.collect()
|
||||
} else {
|
||||
d.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
fn pad(d: &mut Vec<u8>, l: usize) {
|
||||
let diff = d.len() % l;
|
||||
if diff != 0 {
|
||||
d.extend(iter::repeat(0).take(l - diff))
|
||||
}
|
||||
}
|
||||
|
||||
struct Packet {
|
||||
nonce: Vec<u8>,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
fn encrypt(data: &[u8]) -> Packet {
|
||||
let mut data: Vec<u8> = data.to_vec();
|
||||
let mut rng = thread_rng();
|
||||
let mut nonce = vec![0u8; 12];
|
||||
rng.fill(nonce.as_mut_slice());
|
||||
let mut cipher = ChaCha20::new(KEY.into(), nonce.as_slice().into());
|
||||
cipher.seek(KEY.len() + 32);
|
||||
cipher.apply_keystream(&mut data);
|
||||
Packet { nonce, data }
|
||||
}
|
||||
|
||||
fn get_tag(&self) -> Vec<u8> {
|
||||
let mut sign_data = vec![];
|
||||
sign_data.extend(pad_copy(&self.nonce, 16).iter());
|
||||
sign_data.extend(pad_copy(&self.data, 16).iter());
|
||||
sign_data.extend((self.nonce.len() as u64).to_le_bytes().iter());
|
||||
sign_data.extend((self.data.len() as u64).to_le_bytes().iter());
|
||||
let mut cipher = ChaCha20::new(KEY.into(), self.nonce.as_slice().into());
|
||||
let mut poly_key = *KEY;
|
||||
cipher.apply_keystream(&mut poly_key);
|
||||
let signer = Poly1305::new(&poly_key.into());
|
||||
signer.compute_unpadded(&sign_data).into_iter().collect()
|
||||
}
|
||||
|
||||
fn bytes(&self) -> Vec<u8> {
|
||||
let mut data = vec![];
|
||||
data.extend(pad_copy(&self.nonce, 16).iter());
|
||||
data.extend(pad_copy(&self.data, 16).iter());
|
||||
data.extend((self.nonce.len() as u64).to_le_bytes().iter());
|
||||
data.extend((self.data.len() as u64).to_le_bytes().iter());
|
||||
data.extend(self.get_tag().iter());
|
||||
data
|
||||
}
|
||||
|
||||
fn decrypt(&self) -> Result<Vec<u8>> {
|
||||
let mut data = self.data.clone();
|
||||
let mut sign_data = data.clone();
|
||||
pad(&mut sign_data, 16);
|
||||
let mut nonce = self.nonce.clone();
|
||||
pad(&mut nonce, 16);
|
||||
let sign_data = nonce
|
||||
.iter()
|
||||
.chain(sign_data.iter())
|
||||
.chain((self.nonce.len() as u64).to_le_bytes().iter())
|
||||
.chain((self.data.len() as u64).to_le_bytes().iter())
|
||||
.copied()
|
||||
.collect::<Vec<u8>>();
|
||||
let mut poly_key = *KEY;
|
||||
let mut cipher = ChaCha20::new(KEY.into(), self.nonce.as_slice().into());
|
||||
cipher.apply_keystream(&mut poly_key);
|
||||
let signer = Poly1305::new(&poly_key.into());
|
||||
let signature: Vec<u8> = signer.compute_unpadded(&sign_data).into_iter().collect();
|
||||
|
||||
if signature != self.get_tag() {
|
||||
bail!("Invalid signature!");
|
||||
};
|
||||
cipher.seek(poly_key.len() + 32);
|
||||
cipher.apply_keystream(&mut data);
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Packet {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(data: &[u8]) -> Result<Self> {
|
||||
let (mut nonce, data) = data.split_at(16);
|
||||
let (mut data, tag) = data.split_at(data.len() - 16);
|
||||
let nonce_len = u64::from_le_bytes(data[data.len() - 16..][..8].try_into()?) as usize;
|
||||
let data_len = u64::from_le_bytes(data[data.len() - 8..].try_into()?) as usize;
|
||||
data = &data[..data_len];
|
||||
nonce = &nonce[..nonce_len];
|
||||
let pkt = Packet {
|
||||
nonce: nonce.into(),
|
||||
data: data.into(),
|
||||
};
|
||||
if pkt.get_tag() != tag {
|
||||
bail!("Invalid signature!");
|
||||
}
|
||||
Ok(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ServerEntry {
|
||||
Alive(Server),
|
||||
Dead { addr: SocketAddr, reason: String },
|
||||
}
|
||||
|
||||
impl Display for ServerEntry {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ServerEntry::Alive(srv) => write!(
|
||||
f,
|
||||
"[{}] {} ({} {}/{} Players on {}) version {} [{}] RTT: {:?}",
|
||||
srv.addr,
|
||||
srv.name,
|
||||
srv.mode,
|
||||
srv.cur_players,
|
||||
srv.max_players,
|
||||
srv.map,
|
||||
srv.version,
|
||||
srv.flags,
|
||||
srv.rtt
|
||||
),
|
||||
ServerEntry::Dead { addr, reason } => write!(f, "[{}] (error: {})", addr, reason),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt(data: &[u8]) -> Vec<u8> {
|
||||
Packet::encrypt(data).bytes()
|
||||
}
|
||||
|
||||
fn decrypt(data: &[u8]) -> Result<Vec<u8>> {
|
||||
Packet::try_from(data)?.decrypt()
|
||||
}
|
||||
|
||||
async fn recv_from_timeout(
|
||||
sock: &UdpSocket,
|
||||
buf: &mut [u8],
|
||||
timeout: f64,
|
||||
) -> Result<(usize, SocketAddr)> {
|
||||
Ok(time::timeout(Duration::from_secs_f64(timeout), sock.recv_from(buf)).await??)
|
||||
}
|
||||
|
||||
async fn query_server<'a>(addr: SocketAddr) -> Result<Server> {
|
||||
let mut buf = [0; 32 * 1024];
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
socket.connect(addr).await?;
|
||||
let msg = encrypt(INFO_PACKET);
|
||||
let t_start = Instant::now();
|
||||
socket.send(&msg).await?;
|
||||
let size = recv_from_timeout(&socket, &mut buf, 5.0).await?.0;
|
||||
let rtt = t_start.elapsed();
|
||||
let data = decrypt(&buf[..size])?;
|
||||
if !data.starts_with(&[0xba, 0xce]) {
|
||||
// Server Info
|
||||
bail!("Invalid response");
|
||||
}
|
||||
let mut cur = Cursor::new(&data);
|
||||
let info: Server = cur.read_le_args((rtt, addr))?;
|
||||
if info.port != addr.port() {
|
||||
eprint!("[WARN] Port differs for {}: {}", addr, info.port);
|
||||
}
|
||||
if cur.position() != (data.len() as u64) {
|
||||
bail!("Leftover data");
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
async fn get_servers(master_addr: &str) -> Result<(Duration, Vec<ServerEntry>)> {
|
||||
let master_addr: SocketAddr = master_addr.to_socket_addrs()?.next().unwrap();
|
||||
let mut rtt = std::time::Duration::from_secs_f32(0.0);
|
||||
let mut servers = vec![];
|
||||
let mut buf = [0; 32 * 1024];
|
||||
let master = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
master.connect(master_addr).await?;
|
||||
for n in 0..(256 / 32) {
|
||||
let data = format!("Brw={},{}\0", n * 32, (n + 1) * 32);
|
||||
let data = &encrypt(data.as_bytes());
|
||||
let t_start = Instant::now();
|
||||
master.send(data).await?;
|
||||
let size = master.recv(&mut buf).await?;
|
||||
rtt += t_start.elapsed();
|
||||
let data = decrypt(&buf[..size])?;
|
||||
if data.starts_with(b"\0\0\0\0}") {
|
||||
for chunk in data[5..].chunks(6) {
|
||||
if chunk.iter().all(|v| *v == 0) {
|
||||
break;
|
||||
}
|
||||
let port = u16::from_le_bytes(chunk[chunk.len() - 2..].try_into()?);
|
||||
let addr = SocketAddr::from(([chunk[0], chunk[1], chunk[2], chunk[3]], port));
|
||||
let server = match query_server(addr).await {
|
||||
Ok(server) => ServerEntry::Alive(server),
|
||||
Err(err) => ServerEntry::Dead {
|
||||
addr,
|
||||
reason: err.to_string(),
|
||||
},
|
||||
};
|
||||
servers.push(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
rtt = Duration::from_secs_f64(rtt.as_secs_f64() / ((256 / 32) as f64));
|
||||
Ok((rtt, servers))
|
||||
}
|
||||
|
||||
fn indent_hexdump(data: &[u8], indentation: usize, label: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let indent = " ".repeat(indentation);
|
||||
out.push_str(&indent);
|
||||
out.push_str(label);
|
||||
out.push('\n');
|
||||
for line in rhexdump::hexdump(data).lines() {
|
||||
out.push_str(&indent);
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
out.trim_end().to_owned()
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct State {
|
||||
client: BTreeMap<usize, BTreeMap<u8, usize>>,
|
||||
server: BTreeMap<usize, BTreeMap<u8, usize>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn update_client(&mut self, data: &[u8]) {
|
||||
data.iter().enumerate().for_each(|(pos, b)| {
|
||||
*self.client.entry(pos).or_default().entry(*b).or_default() += 1;
|
||||
});
|
||||
}
|
||||
fn update_server(&mut self, data: &[u8]) {
|
||||
data.iter().enumerate().for_each(|(pos, b)| {
|
||||
*self.server.entry(pos).or_default().entry(*b).or_default() += 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
enum Direction {
|
||||
Client,
|
||||
Server,
|
||||
Both,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum CmdResult {
|
||||
Exit,
|
||||
Packet {
|
||||
data: Vec<u8>,
|
||||
direction: Direction,
|
||||
},
|
||||
Fuzz {
|
||||
direction: Direction,
|
||||
start: usize,
|
||||
end: usize,
|
||||
chance: (u32, u32),
|
||||
},
|
||||
NoFuzz,
|
||||
Log(bool),
|
||||
}
|
||||
|
||||
async fn handle_line(
|
||||
line: &str,
|
||||
state: &State,
|
||||
stdout: &mut SharedWriter,
|
||||
) -> Result<Option<CmdResult>> {
|
||||
use CmdResult::*;
|
||||
let cmd: Vec<&str> = line.trim().split_ascii_whitespace().collect();
|
||||
match cmd[..] {
|
||||
["log", "off"] => Ok(Some(Log(false))),
|
||||
["log", "on"] => Ok(Some(Log(true))),
|
||||
["state", pos] => {
|
||||
let pos = pos.parse()?;
|
||||
writeln!(stdout, "Client: {:?}", state.client.get(&pos))?;
|
||||
writeln!(stdout, "Server: {:?}", state.server.get(&pos))?;
|
||||
Ok(None)
|
||||
}
|
||||
[dir @ ("client" | "server"), ref args @ ..] => {
|
||||
let mut data: Vec<u8> = vec![];
|
||||
for args in args.iter() {
|
||||
let args = hex::decode(args)?;
|
||||
data.extend(args);
|
||||
}
|
||||
Ok(Some(CmdResult::Packet {
|
||||
data,
|
||||
direction: match dir {
|
||||
"client" => Direction::Client,
|
||||
"server" => Direction::Server,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
}))
|
||||
}
|
||||
["fuzz", dir @ ("client" | "server" | "both"), start, end, chance_num, chance_den] => {
|
||||
let direction = match dir {
|
||||
"client" => Direction::Client,
|
||||
"server" => Direction::Server,
|
||||
"both" => Direction::Both,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let start = start.parse()?;
|
||||
let end = end.parse()?;
|
||||
if start > end {
|
||||
bail!("Fuzz start>end");
|
||||
}
|
||||
let res = CmdResult::Fuzz {
|
||||
direction,
|
||||
start,
|
||||
end,
|
||||
chance: (chance_num.parse()?, chance_den.parse()?),
|
||||
};
|
||||
Ok(Some(res))
|
||||
}
|
||||
["fuzz", "off"] => Ok(Some(CmdResult::NoFuzz)),
|
||||
["exit"] => Ok(Some(CmdResult::Exit)),
|
||||
[""] => Ok(None),
|
||||
_ => bail!("Unknown command: {:?}", line),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_proxy(
|
||||
remote_addr: &SocketAddr,
|
||||
local_addr: &SocketAddr,
|
||||
logfile: &Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
let mut print_log = false;
|
||||
let mut state = State::default();
|
||||
let mut logfile = match logfile {
|
||||
Some(path) => Some(std::fs::File::create(path)?),
|
||||
None => None,
|
||||
};
|
||||
let mut fuzz = None;
|
||||
let mut rng = thread_rng();
|
||||
let mut client_addr: Option<SocketAddr> = None;
|
||||
let local = UdpSocket::bind(local_addr).await?;
|
||||
let remote = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
remote.connect(remote_addr).await?;
|
||||
let mut local_buf = vec![0; 32 * 1024];
|
||||
let mut remote_buf = vec![0; 32 * 1024];
|
||||
println!("Proxy listening on {}", local_addr);
|
||||
let (mut rl, mut stdout) = Readline::new(format!("{}> ", remote_addr)).unwrap();
|
||||
loop {
|
||||
tokio::select! {
|
||||
line = rl.readline().fuse() => {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
let line=line.trim();
|
||||
rl.add_history_entry(line.to_owned());
|
||||
match handle_line(line, &state, &mut stdout).await {
|
||||
Ok(Some(result)) => {
|
||||
match result {
|
||||
CmdResult::Packet{data,direction} => {
|
||||
let data=encrypt(&data);
|
||||
match direction {
|
||||
Direction::Client => {
|
||||
if client_addr.is_some() {
|
||||
local
|
||||
.send_to(&data, client_addr.unwrap())
|
||||
.await?;
|
||||
} else {
|
||||
writeln!(stdout,"Error: No client address")?;
|
||||
}
|
||||
},
|
||||
Direction::Server => {
|
||||
remote.send(&data).await?;
|
||||
}
|
||||
Direction::Both => unreachable!()
|
||||
};
|
||||
}
|
||||
CmdResult::Log(log) => {
|
||||
print_log=log;
|
||||
}
|
||||
CmdResult::Exit => break Ok(()),
|
||||
CmdResult::NoFuzz => {
|
||||
fuzz=None;
|
||||
}
|
||||
CmdResult::Fuzz { .. } => {
|
||||
fuzz=Some(result)
|
||||
},
|
||||
}
|
||||
},
|
||||
Ok(None) => (),
|
||||
Err(msg) => {
|
||||
writeln!(stdout, "Error: {}", msg)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(ReadlineError::Eof) =>{ writeln!(stdout, "Exiting...")?; break Ok(()) },
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
writeln!(stdout, "^C")?;
|
||||
break Ok(());
|
||||
},
|
||||
Err(err) => {
|
||||
writeln!(stdout, "Received err: {:?}", err)?;
|
||||
writeln!(stdout, "Exiting...")?;
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
local_res = local.recv_from(&mut local_buf) => {
|
||||
let (size, addr) = local_res?;
|
||||
client_addr.get_or_insert(addr);
|
||||
let mut data = Packet::try_from(&local_buf[..size])?.decrypt()?;
|
||||
state.update_client(&data);
|
||||
if print_log {
|
||||
writeln!(stdout,"{}", indent_hexdump(&data, 0, &format!("OUT: {}", addr)))?;
|
||||
}
|
||||
if let Some(lf) = logfile.as_mut() {
|
||||
writeln!(lf, ">{:?} {} {}", addr, data.len(), hex::encode(&data))?;
|
||||
};
|
||||
if let Some(CmdResult::Fuzz{direction,start,end,chance}) = fuzz {
|
||||
if (direction==Direction::Server || direction==Direction::Both) && rng.gen_ratio(chance.0,chance.1) {
|
||||
rng.fill(&mut data[start..end]);
|
||||
}
|
||||
}
|
||||
remote.send(&encrypt(&data)).await?;
|
||||
}
|
||||
remote_res = remote.recv_from(&mut remote_buf) => {
|
||||
let (size, addr) = remote_res?;
|
||||
let mut data = Packet::try_from(&remote_buf[..size])?.decrypt()?;
|
||||
state.update_server(&data);
|
||||
if print_log {
|
||||
writeln!(stdout,"\r{}", indent_hexdump(&data, 5, &format!("IN: {}", addr)))?;
|
||||
}
|
||||
if let Some(lf) = logfile.as_mut() {
|
||||
writeln!(lf, "<{:?} {} {}", addr, data.len(), hex::encode(&data))?;
|
||||
};
|
||||
if client_addr.is_some() {
|
||||
if let Some(CmdResult::Fuzz{direction,start,end,chance}) = &fuzz {
|
||||
if (*direction==Direction::Client || *direction==Direction::Both) && rng.gen_ratio(chance.0,chance.1) {
|
||||
rng.fill(&mut data[*start..*end]);
|
||||
}
|
||||
}
|
||||
local
|
||||
.send_to(&encrypt(&data), client_addr.unwrap())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_master_cmd(sock: &UdpSocket, cmd: &str) -> Result<Vec<u8>> {
|
||||
let mut buf = [0; 32 * 1024];
|
||||
let mut data: Vec<u8> = cmd.as_bytes().to_vec();
|
||||
data.push(0);
|
||||
let data = &encrypt(&data);
|
||||
sock.send(data).await?;
|
||||
let size = recv_from_timeout(sock, &mut buf, 5.0).await?.0;
|
||||
decrypt(&buf[..size])
|
||||
}
|
||||
|
||||
async fn run_master_shell(master_addr: &str) -> Result<()> {
|
||||
let master = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
master.connect(master_addr).await?;
|
||||
let (mut rl, mut stdout) = Readline::new(format!("{}> ", master_addr)).unwrap();
|
||||
loop {
|
||||
tokio::select! {
|
||||
line = rl.readline().fuse() => {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
let line=line.trim();
|
||||
rl.add_history_entry(line.to_owned());
|
||||
writeln!(stdout,"[CMD] {line}")?;
|
||||
match send_master_cmd(&master,line).await {
|
||||
Ok(data) => writeln!(stdout,"{}",hexdump(&data))?,
|
||||
Err(e) => writeln!(stdout,"Error: {e}")?
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Eof) =>{ writeln!(stdout, "Exiting...")?; break Ok(()) },
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
writeln!(stdout, "^C")?;
|
||||
break Ok(());
|
||||
},
|
||||
Err(err) => {
|
||||
writeln!(stdout, "Receive error: {err}")?;
|
||||
break Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Server to connect to (if unspecified will query the master server)
|
||||
server: Option<SocketAddr>,
|
||||
/// Only list servers without starting proxy
|
||||
#[clap(short, long, action)]
|
||||
list: bool,
|
||||
/// Local Address to bind to
|
||||
#[clap(short,long, default_value_t = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 28086))]
|
||||
addr: SocketAddr,
|
||||
/// Master server to query for running games
|
||||
#[clap(short, long, default_value = "scrapland.mercurysteam.com:5000")]
|
||||
master: String,
|
||||
/// Path of file to log decrypted packets to
|
||||
#[clap(short = 'f', long)]
|
||||
logfile: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
if args.list && args.server.is_some() {
|
||||
let addr = args.server.unwrap();
|
||||
let server = match query_server(addr).await {
|
||||
Ok(server) => ServerEntry::Alive(server),
|
||||
Err(msg) => ServerEntry::Dead {
|
||||
addr,
|
||||
reason: msg.to_string(),
|
||||
},
|
||||
};
|
||||
println!("{}", server);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(server) = args.server {
|
||||
run_proxy(&server, &args.addr, &args.logfile).await?;
|
||||
return Ok(());
|
||||
}
|
||||
loop {
|
||||
let (rtt, servers) = get_servers(&args.master).await?;
|
||||
println!("Master RTT: {:?}", rtt);
|
||||
if args.list {
|
||||
for server in servers {
|
||||
println!("{}", server);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let selection = Select::with_theme(&ColorfulTheme::default())
|
||||
.items(&servers)
|
||||
.with_prompt("Select server (press Esc to drop into master server command shell)")
|
||||
.interact_opt()?
|
||||
.map(|v| &servers[v]);
|
||||
match selection {
|
||||
Some(ServerEntry::Dead { addr, reason }) => {
|
||||
eprintln!("{:?} returned an error: {}", addr, reason)
|
||||
}
|
||||
Some(ServerEntry::Alive(srv)) => {
|
||||
return run_proxy(&srv.addr, &args.addr, &args.logfile).await;
|
||||
}
|
||||
None => {
|
||||
return run_master_shell(&args.master).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
151
tools/remaster/scrap_net/src/parser.rs
Normal file
151
tools/remaster/scrap_net/src/parser.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
|
||||
use crate::hex_ii::hex_ii_dump;
|
||||
use crate::ServerFlags;
|
||||
use binrw::BinReaderExt;
|
||||
use binrw::{binread, BinRead, NullString};
|
||||
|
||||
/*
|
||||
00000000: 7f 4c 00 00 06 ba ce 01 01 06 63 61 63 6f 74 61 | .L........cacota
|
||||
00000010: 10 5b 42 4a 5a 5d 20 45 61 72 74 68 6e 75 6b 65 | .[BJZ].Earthnuke
|
||||
00000020: 72 06 53 50 6f 6c 69 31 37 00 08 50 5f 50 6f 6c | r.SPoli17..P_Pol
|
||||
00000030: 69 63 65 06 4d 50 4f 4c 49 31 00 00 00 0d 30 2c | ice.MPOLI1....0,
|
||||
00000040: 30 2c 30 2c 31 2c 30 2c 30 2c 31 00 00 00 00 | 0,0,1,0,0,1....
|
||||
|
||||
00000000: 7f 49 00 00 06 ba ce 01 01 06 63 61 63 6f 74 61 | .I........cacota
|
||||
00000010: 0e 55 6e 6e 61 6d 65 64 20 50 6c 61 79 65 72 07 | .Unnamed.Player.
|
||||
00000020: 53 42 65 74 74 79 31 50 00 07 50 5f 42 65 74 74 | SBetty1P..P_Bett
|
||||
00000030: 79 07 4d 42 65 74 74 79 31 00 00 00 0b 31 2c 31 | y.MBetty1....1,1
|
||||
00000040: 2c 30 2c 31 2c 33 2c 30 00 00 00 00 | ,0,1,3,0....
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, BinRead)]
|
||||
#[br(big)]
|
||||
#[br(magic = b"\xba\xce")]
|
||||
struct ServerInfoJoin {
|
||||
#[br(map = |v: (u8,u8)| format!("{}.{}",v.0,v.1))]
|
||||
version: String,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
player_id: u32,
|
||||
num_vals: u32,
|
||||
pos: [f32; 3],
|
||||
player_index: u32,
|
||||
rtt: u32,
|
||||
}
|
||||
|
||||
#[binread]
|
||||
#[br(big)]
|
||||
#[derive(Debug, Clone)]
|
||||
enum PacketData {
|
||||
#[br(magic = b"\x7f")]
|
||||
PlayerJoin {
|
||||
data_len: u8,
|
||||
_1: u8,
|
||||
cur_players: u8,
|
||||
max_players: u8,
|
||||
info: ServerInfoJoin,
|
||||
#[br(temp)]
|
||||
pw_len: u8,
|
||||
#[br(count = pw_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
|
||||
password: String,
|
||||
#[br(temp)]
|
||||
player_name_len: u8,
|
||||
#[br(count = player_name_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
|
||||
player_name: String,
|
||||
#[br(temp)]
|
||||
ship_model_len: u8,
|
||||
#[br(count = ship_model_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
|
||||
ship_model: String,
|
||||
#[br(little)]
|
||||
max_health: u16,
|
||||
#[br(temp)]
|
||||
pilot_model_len: u8,
|
||||
#[br(count = pilot_model_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
|
||||
pilot_model: String,
|
||||
#[br(temp)]
|
||||
engine_model_r_len: u8,
|
||||
#[br(count = engine_model_r_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
|
||||
engine_model_r: String,
|
||||
#[br(temp)]
|
||||
engine_model_l_len: u8,
|
||||
#[br(count = engine_model_r_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
|
||||
engine_model_l: String,
|
||||
_2: u16,
|
||||
#[br(temp)]
|
||||
loadout_len: u8,
|
||||
#[br(count = loadout_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
|
||||
loadout: String,
|
||||
team_number: u16,
|
||||
padding: [u8; 2],
|
||||
},
|
||||
#[br(magic = b"\x80\x15")]
|
||||
MapInfo {
|
||||
#[br(temp)]
|
||||
map_len: u32,
|
||||
#[br(count = map_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
|
||||
map: String,
|
||||
#[br(temp)]
|
||||
mode_len: u8,
|
||||
#[br(count = mode_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
|
||||
mode: String,
|
||||
_2: u16,
|
||||
item_count: u8,
|
||||
// _3: u32,
|
||||
// #[br(count = item_count)]
|
||||
// items: Vec<[u8;0x11]>
|
||||
},
|
||||
#[br(magic = b"\xba\xce")]
|
||||
ServerInfo {
|
||||
#[br(map = |v: (u8,u8)| format!("{}.{}",v.1,v.0))]
|
||||
version: String,
|
||||
port: u16,
|
||||
max_players: u16,
|
||||
cur_players: u16,
|
||||
#[br(map = u8::into)]
|
||||
flags: ServerFlags,
|
||||
#[br(pad_size_to(0x20), map=|s: NullString| s.to_string())]
|
||||
name: String,
|
||||
#[br(pad_size_to(0x10), map=|s: NullString| s.to_string())]
|
||||
mode: String,
|
||||
#[br(pad_size_to(0x20), map=|s: NullString| s.to_string())]
|
||||
map: String,
|
||||
_pad: u8,
|
||||
},
|
||||
}
|
||||
|
||||
fn parse(data: &[u8]) -> Result<(PacketData, Vec<u8>), Box<dyn Error>> {
|
||||
use std::io::Cursor;
|
||||
let mut rdr = Cursor::new(data);
|
||||
let pkt: PacketData = rdr.read_le()?;
|
||||
let rest = data[rdr.position() as usize..].to_vec();
|
||||
println!("{}", rhexdump::hexdump(data));
|
||||
Ok((pkt, rest))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
let log = include_str!("../test_.log").lines();
|
||||
let mut hm = HashMap::new();
|
||||
for line in log {
|
||||
let data = line.split_ascii_whitespace().nth(1).unwrap();
|
||||
let data = hex::decode(data).unwrap();
|
||||
*hm.entry(data[0..1].to_vec()).or_insert(0usize) += 1;
|
||||
match parse(&data) {
|
||||
Ok((pkt, rest)) => {
|
||||
println!("{:#x?}", pkt);
|
||||
}
|
||||
Err(e) => (),
|
||||
}
|
||||
}
|
||||
let mut hm: Vec<(_, _)> = hm.iter().collect();
|
||||
hm.sort_by_key(|(_, v)| *v);
|
||||
for (k, v) in hm {
|
||||
let k = k.iter().map(|v| format!("{:02x}", v)).collect::<String>();
|
||||
println!("{} {}", k, v);
|
||||
}
|
||||
// println!("{:#x?}",parse("8015000000094c6576656c732f465a08466c616748756e7400000100000000000000000000000000000000000004105feb0006003e1125f3bc1300000019007e9dfa0404d5f9003f00000000000000000000"));
|
||||
// println!("{:#x?}",parse("8015000000094c6576656c732f465a08466c616748756e7400002000000000000000000000000000000000000004105feb0006003e1125f3bc1300000019007e9dfa0404d5f9003f000000000000000000001f020b0376a8e2475b6e5b467c1e99461e020903982d14c5ec79cb45b2ee96471d020e03b29dbc46caa433464a28a0c71c020603aa80514658b8ab458db025c71b020803ce492f4658b8ab4514d320c71a02070344532f4658b8ab4587cf16c7190205031b3a0d4658b8ab459eaf25c7180206030ac34c4669e1fd469891ca47170208032e8c2a4669e1fd465500cd4716020703a4952a4669e1fd461b02d247150205037b7c084669e1fd460f92ca4714020603da6b7ec714aa3746b77c5a4713020803c87c83c714aa3746305a5f47120207039a7b83c714aa3746bd5d694711020503bfbe87c714aa3746a67d5a4710020803c5c719474ad5d445a7b3d2c60f0206037c5522474ad5d4459a6edcc60e02070323ca19474ad5d4458dacbec60d020503d84311474ad5d445bb6cdcc60c020603a9b16b47d52d974602dd15470b020803f2236347d52d97467bba1a470a02070350266347d52d974608be24470902050305a05a47d52d9746f1dd1547080206031f4066c6384b9c46955bd345070208037e3b84c6384b9c466147fa4506020703c33684c6384b9c46e431254605020503574395c6384b9c461063d34504020603ba349bc77a60294640f387c103020803957b9fc77a602946658f994402020703677a9fc77a60294680006d45010205038cbda3c77a602946807880c1"));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue