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"));
|
||||||
|
}
|
2
tools/remaster/scraphacks_rs/.cargo/config.toml
Normal file
2
tools/remaster/scraphacks_rs/.cargo/config.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
target = "i686-pc-windows-msvc"
|
1624
tools/remaster/scraphacks_rs/Cargo.lock
generated
Normal file
1624
tools/remaster/scraphacks_rs/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
45
tools/remaster/scraphacks_rs/Cargo.toml
Normal file
45
tools/remaster/scraphacks_rs/Cargo.toml
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
[package]
|
||||||
|
name = "scraphacks_rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[profile.release]
|
||||||
|
debug = 0
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["dylib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
console=[]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.68"
|
||||||
|
comma = "1.0.0"
|
||||||
|
custom-print = "0.1.0"
|
||||||
|
derivative = "2.2.0"
|
||||||
|
detour3 = "0.1.0"
|
||||||
|
discord-sdk = "0.3.2"
|
||||||
|
futures = "0.3.25"
|
||||||
|
hex = "0.4.3"
|
||||||
|
iced-x86 = "1.18.0"
|
||||||
|
mlua = { version = "0.8.7", features = ["luajit", "vendored", "macros", "serialize", "mlua_derive"] }
|
||||||
|
nom = "7.1.3"
|
||||||
|
nom-greedyerror = "0.5.0"
|
||||||
|
nom-supreme = "0.8.0"
|
||||||
|
nom_locate = "4.1.0"
|
||||||
|
num-traits = "0.2.15"
|
||||||
|
once_cell = "1.17.0"
|
||||||
|
parse_int = "0.6.0"
|
||||||
|
pelite = "0.10.0"
|
||||||
|
region = "3.0.0"
|
||||||
|
rhexdump = "0.1.1"
|
||||||
|
rustc-hash = "1.1.0"
|
||||||
|
shadow-rs = "0.21.0"
|
||||||
|
struct_layout = "0.1.0"
|
||||||
|
tokio = "1.24.2"
|
||||||
|
viable = "0.2.0"
|
||||||
|
winsafe = { version = "0.0.15", features = ["kernel", "user", "dshow"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
shadow-rs = "0.21.0"
|
11
tools/remaster/scraphacks_rs/Pipfile
Normal file
11
tools/remaster/scraphacks_rs/Pipfile
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.9"
|
242
tools/remaster/scraphacks_rs/Save0.sav.json
Normal file
242
tools/remaster/scraphacks_rs/Save0.sav.json
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
{
|
||||||
|
"id": "Outskirts - 07:41:13",
|
||||||
|
"title": "Scrapland savegame",
|
||||||
|
"data": {
|
||||||
|
"CTFFinishPlayerLoosesTextCad": "Mission_CTFAgainstBankers_RemoteMessage3",
|
||||||
|
"CTFFinishPlayerWinsTextCad": "Mission_CTFAgainstBankers_RemoteMessage2",
|
||||||
|
"CTFStartTextCad": "Mission_CTFAgainstBankers_RemoteMessage1",
|
||||||
|
"CTFOnDeathSpawnTime": "5.0",
|
||||||
|
"CTFFriendProfile": "BankersCTFFriends",
|
||||||
|
"CTFEnemyProfile": "BankersCTFEnemies",
|
||||||
|
"CTFFriendHead": "Functionary",
|
||||||
|
"CTFFriendType": "Functionary",
|
||||||
|
"CTFEnemyType": "BankDirector",
|
||||||
|
"CTFNumFriends": "5",
|
||||||
|
"CTFNumEnemies": "5",
|
||||||
|
"CTFFlags": "5",
|
||||||
|
"CombatFriendProfile": "ArrangeBankersCombatFriends",
|
||||||
|
"CombatFriendType": "Police",
|
||||||
|
"CombatNumFriends": "4",
|
||||||
|
"CombatNumEnemies": "5",
|
||||||
|
"PlayerWinsCombat": "1",
|
||||||
|
"OnVictory": "import SaveGame; SaveGame.QWayPoint();import MissionsFuncs; MissionsFuncs.SetNextMission(\"Mission_BackFromMortalRace\", \"GamblinDen\");Scrap.SetSaveVar(\"Map\", \"Levels/GamblinDen\");import SaveGame; SaveGame.QLoad(\"Levels/GamblinDen\");Scrap.SetSaveVar('MapPress_BureauExists','1')",
|
||||||
|
"OnAbort": "import MissionsFuncs; MissionsFuncs.EndOfSuperDeal('SuperDeal_Faliure_SystemMessage1')",
|
||||||
|
"CombatFinishPlayerLoosesTextCad": "SuperDeal_First_RemoteMessage3",
|
||||||
|
"CombatFinishPlayerWinsTextCad": "SuperDeal_First_RemoteMessage2",
|
||||||
|
"CombatStartTextCad": "SuperDeal_First_RemoteMessage1",
|
||||||
|
"CombatEnemyProfile": "SuperDealFirstElite",
|
||||||
|
"CombatEnemyTypeHead": "CrazyGambler",
|
||||||
|
"CombatEnemyType": "BankDirector",
|
||||||
|
"CombatDeaths": "5",
|
||||||
|
"SuperDealType": "",
|
||||||
|
"IgorFirstContactMissionState": "TalkToMercenaries",
|
||||||
|
"Stats.MadHunter": "10",
|
||||||
|
"Stats.Nurse.Dazed": "2",
|
||||||
|
"Stats.BankMaster": "6758",
|
||||||
|
"Bank.Circuit.36": "0",
|
||||||
|
"Bank.Circuit.35": "0",
|
||||||
|
"Bank.Circuit.34": "0",
|
||||||
|
"Bank.Circuit.33": "0",
|
||||||
|
"Bank.Circuit.32": "0",
|
||||||
|
"Bank.Circuit.31": "0",
|
||||||
|
"Bank.Circuit.30": "0",
|
||||||
|
"Bank.Circuit.29": "0",
|
||||||
|
"Bank.Circuit.28": "0",
|
||||||
|
"Bank.Circuit.27": "0",
|
||||||
|
"Bank.Circuit.26": "0",
|
||||||
|
"Bank.Circuit.25": "0",
|
||||||
|
"Bank.Circuit.24": "0",
|
||||||
|
"Bank.Circuit.23": "0",
|
||||||
|
"Bank.Circuit.22": "0",
|
||||||
|
"Bank.Circuit.21": "0",
|
||||||
|
"Bank.Circuit.20": "0",
|
||||||
|
"Bank.Circuit.19": "0",
|
||||||
|
"Bank.Circuit.18": "0",
|
||||||
|
"Bank.Circuit.17": "0",
|
||||||
|
"Bank.Circuit.16": "0",
|
||||||
|
"Bank.Circuit.15": "0",
|
||||||
|
"Bank.Circuit.14": "0",
|
||||||
|
"Bank.Circuit.13": "0",
|
||||||
|
"Bank.Circuit.12": "0",
|
||||||
|
"Bank.Circuit.11": "0",
|
||||||
|
"Bank.Circuit.10": "0",
|
||||||
|
"Bank.Circuit.9": "0",
|
||||||
|
"Bank.Circuit.8": "0",
|
||||||
|
"Bank.Circuit.7": "0",
|
||||||
|
"Bank.Circuit.6": "0",
|
||||||
|
"Bank.Circuit.5": "0",
|
||||||
|
"Bank.Circuit.4": "0",
|
||||||
|
"Bank.Circuit.3": "0",
|
||||||
|
"Bank.Circuit.2": "0",
|
||||||
|
"Bank.Circuit.1": "0",
|
||||||
|
"Bank.Circuit.0": "0",
|
||||||
|
"Stats.Mosquito": "116",
|
||||||
|
"PoliceBossAtTownHall": "0",
|
||||||
|
"Stats.Parking": "18",
|
||||||
|
"Police.FicusDeath": "1",
|
||||||
|
"CostumeAtPolice": "0",
|
||||||
|
"Hangar.HangarShip10": "SLifeBoat<-<-<-<-<-0,0,0,0,0,0,1<-30<-0,0,0,0,0,0",
|
||||||
|
"Hangar.iHangarShips": "8",
|
||||||
|
"AutoSaveGameOnLoad": "0",
|
||||||
|
"GameAct": "3rdMurder",
|
||||||
|
"BankDebt": "0",
|
||||||
|
"Map": "Levels/Outskirts",
|
||||||
|
"Mission.Library": "Mission_DestroyBadDebtors",
|
||||||
|
"EnergyBarActive": "1",
|
||||||
|
"SpecialActionActive": "2",
|
||||||
|
"CrazyWing.Status": "1",
|
||||||
|
"Conversor.ActiveConversors": "1",
|
||||||
|
"Hangar.iHangarShip": "0",
|
||||||
|
"Player.NumLives": "100",
|
||||||
|
"Hangar.shipsToEditList": "['SPoli1', 'SPoli2', 'SPoli3', 'SPoli4', 'SPoli5', 'SPoliBoss1', 'SMerc1', 'SMerc2', 'SMerc3', 'SMayor1', 'SBanker1', 'SBankMaster1', 'SBishop1', 'SArchbishop1', 'SFunc1', 'SBerto1', 'SBetty1', 'SHump1', 'SBoss1', 'SPoli4']",
|
||||||
|
"Hangar.availableEnginesList": "['MPOLI4', 'MPOLI5', 'MPOLIBOSS1', 'MPOLI2', 'MBERTO1', 'MBETTY1', 'MPOLI1', 'MMERC1', 'MMERC2', 'MPOLI3', 'MMAYOR1', 'MFUNC1', 'MBANKER1', 'MBANKMASTER1', 'MBISHOP1', 'MARCHBISHOP1', 'MHUMP1', 'MBOSS1', 'MMERC3']",
|
||||||
|
"Hangar.availableWeaponsList": "['Vulcan', 'Devastator', 'Swarm', 'Inferno', 'Tesla', 'ATPC', 'Swarm', 'Devastator']",
|
||||||
|
"Hangar.availableUpgradesList": "[\"VulcanUpgrade1\", \"VulcanUpgrade2\", \"DevastatorUpgrade1\", \"DevastatorUpgrade2\", \"SwarmUpgrade1\", \"SwarmUpgrade2\", \"InfernoUpgrade1\", \"InfernoUpgrade2\", \"TeslaUpgrade1\", \"TeslaUpgrade2\", \"ATPCUpgrade1\", \"ATPCUpgrade2\"]",
|
||||||
|
"JackInTheBox.Active": "1",
|
||||||
|
"Parked.Police": "['SPOLI1', 'SPOLI2', 'SPOLI3', 'SPOLI4']",
|
||||||
|
"Parked.Mercs": "['SMERC1', 'SMERC2']",
|
||||||
|
"Parked.TownHall": "['SFUNC1']",
|
||||||
|
"Parked.Bank": "['SBANKER1']",
|
||||||
|
"Parked.Press": "['SBERTO1', 'SBETTY1', 'SHUMP1']",
|
||||||
|
"Parked.Temple": "['SBISHOP1']",
|
||||||
|
"PoliceBlueprints.Ships": "['SPoli2', 'SPoli3', 'SPoli5', 'SPoliBoss1']",
|
||||||
|
"PoliceBlueprints.Engines": "['MPoli2', 'MPoli3', 'MPoli4', 'MPoli5', 'MPoliBoss1']",
|
||||||
|
"PressBlueprints.Ships": "['SBerto1', 'SBetty1', 'SHump1']",
|
||||||
|
"PressBlueprints.Engines": "['MBerto1', 'MBetty1', 'MHump1']",
|
||||||
|
"MayorAtGambinDen": "0",
|
||||||
|
"PolicesAtGambinDen": "1",
|
||||||
|
"PoliceBossAtPolice": "0",
|
||||||
|
"CrazyGamblerAtGambinDen": "1",
|
||||||
|
"FunctionaryTwinsAtGambinDen": "1",
|
||||||
|
"BankersAtGambinDen": "0",
|
||||||
|
"BishopsAtGambinDen": "0",
|
||||||
|
"GameSkill": "0",
|
||||||
|
"CreateBertos": "0",
|
||||||
|
"MercsHelpDtritus": "0",
|
||||||
|
"RobotsControlledByBoss": "0",
|
||||||
|
"Player.InfiniteLives": "0",
|
||||||
|
"PrevMap": "Levels/GamblinDen",
|
||||||
|
"Spawn": "DM_Player_Spawn_GamblinDen",
|
||||||
|
"Char": "Dtritus",
|
||||||
|
"ComeFrom": "DoorElevator",
|
||||||
|
"AlarmActive": "0",
|
||||||
|
"AlarmStatus": "0.0",
|
||||||
|
"Money": "2147483647",
|
||||||
|
"Challenge": "1",
|
||||||
|
"Challenge.Type": "",
|
||||||
|
"Challenge.Foe": "",
|
||||||
|
"Challenge.NumEnemies": "",
|
||||||
|
"Challenge.Money": "0",
|
||||||
|
"Revenge.Type": "",
|
||||||
|
"Revenge.Foe": "",
|
||||||
|
"Revenge.NumEnemies": "",
|
||||||
|
"Revenge.LastWinner": "",
|
||||||
|
"Mission.Map": "Outskirts",
|
||||||
|
"BadReputation": "3",
|
||||||
|
"GameplayTime": "27673.5857997",
|
||||||
|
"LonelyMercInGamblinDen": "0",
|
||||||
|
"LonelyMercLairActive": "1",
|
||||||
|
"LonelyMercDataActive": "0",
|
||||||
|
"ComSatsMissionsMapsFinished": "[]",
|
||||||
|
"Conversor.AvailableChars": "['Police', 'Nurse', 'BankDirector', 'Desktop', 'Sentinel', 'Gear', 'Bishop', 'Messenger', 'Functionary', 'Betty', 'Berto', 'BankMaster', 'Dtritus']",
|
||||||
|
"Batteries": "5",
|
||||||
|
"AcBatteries": "2",
|
||||||
|
"PrimaryMissionDesc": "Ich muss den Bankdirektor besuchen. Er treibt gerade ausstehende Kreditzahlungen mit seinem Kampfraumschiff ein. Ich sehe mal, ob ich ihm helfen kann.",
|
||||||
|
"SecondaryMissionDesc": "",
|
||||||
|
"TakePhotoMsg": "0",
|
||||||
|
"Race.Num": "3",
|
||||||
|
"Race.FirstTime": "0",
|
||||||
|
"Race.Profile": "Pilot",
|
||||||
|
"Combat.FirstTime": "1",
|
||||||
|
"Combat.Profile": "Rookie",
|
||||||
|
"Traffic.AcShips": "[\"SPoli6\", \"SMerc1\",\"SMerc2\",\"SBanker1\"]",
|
||||||
|
"IsSecondMission": "0",
|
||||||
|
"CrazyDeal.1.Var": "Stats.Dtritus",
|
||||||
|
"CrazyDeal.1.Tgt": "5",
|
||||||
|
"CrazyDeal.2.Var": "Stats.Parking",
|
||||||
|
"CrazyDeal.2.Tgt": "3",
|
||||||
|
"CrazyDeal.3.Var": "Stats.Battery",
|
||||||
|
"CrazyDeal.3.Tgt": "5",
|
||||||
|
"SuperDeal.Map": "FZ",
|
||||||
|
"SuperDeal.Library": "SuperDeal_Second",
|
||||||
|
"SuperDeal.Type": "MortalRace",
|
||||||
|
"CrazyWing.List": "['Sentinel', 'Betty', 'CrazyGambler', 'Functionary', 'Bishop']",
|
||||||
|
"Journalist.Humphrey_Defaut": "NoPlace",
|
||||||
|
"Journalist.Betty_Defaut": "NoPlace",
|
||||||
|
"Journalist.Berto_Defaut": "Press",
|
||||||
|
"Conversor.FirstConversion": "0",
|
||||||
|
"Conversor.FirstPossession": "0",
|
||||||
|
"WindowsError": "0",
|
||||||
|
"Orbit.Decontaminated": "yes",
|
||||||
|
"MercFriends.MercenaryA_Smartie": "0",
|
||||||
|
"MercFriends.MercenaryC_Brutus": "0",
|
||||||
|
"MercFriends.MercenaryB_Dumber": "0",
|
||||||
|
"StdShipAIProfile": "Elite",
|
||||||
|
"usrShip.Ammo00": "1000.0",
|
||||||
|
"usrShip.Ammo01": "500.0",
|
||||||
|
"usrShip.Ammo02": "1500.0",
|
||||||
|
"usrShip.AcWeap": "6",
|
||||||
|
"Parking.Desolate": "0",
|
||||||
|
"Hangar.HangarShip1": "SBoss1<-MBOSS1<-MBOSS1<-<-<-15,15,15,15,15,15,1<-187<-1,8,6,9,11,3",
|
||||||
|
"Hangar.HangarShip2": "",
|
||||||
|
"Hangar.HangarShip3": "",
|
||||||
|
"Hangar.HangarShip4": "",
|
||||||
|
"Hangar.HangarShip5": "",
|
||||||
|
"Hangar.HangarShip6": "",
|
||||||
|
"Hangar.HangarShip7": "",
|
||||||
|
"Hangar.HangarShip8": "",
|
||||||
|
"Hangar.HangarShip9": "",
|
||||||
|
"Hangar.HangarShipAux": "SLifeBoat<-<-<-<-<-0,0,0,0,0,0,1<-50<-0,0,0,0,0,0",
|
||||||
|
"Hangar.DestroyedShips": "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]",
|
||||||
|
"NewBluePrintAvaliable": "11",
|
||||||
|
"DebugSave": "1",
|
||||||
|
"OutMusicRelax": "141",
|
||||||
|
"MayorAtTownHall": "1",
|
||||||
|
"GamblinMusic": "1",
|
||||||
|
"OutMusicAction": "122",
|
||||||
|
"Stats.Traffic": "767",
|
||||||
|
"Stats.OutPolice": "110",
|
||||||
|
"MapPress_HiAlarm": "0",
|
||||||
|
"MapPress_BureauExists": "1",
|
||||||
|
"Stats.PossessBerto": "1",
|
||||||
|
"Stats.ConvertIntoDtritus": "1",
|
||||||
|
"Stats.WinHumphreyRace": "1",
|
||||||
|
"Stats.Possession": "57",
|
||||||
|
"Stats.Betty": "49",
|
||||||
|
"Stats.Killer": "119",
|
||||||
|
"Stats.Jump": "1",
|
||||||
|
"Stats.Jump.Police": "1",
|
||||||
|
"Stats.Bishop": "10",
|
||||||
|
"Stats.Battery": "0",
|
||||||
|
"Stats.Dtritus": "0",
|
||||||
|
"Stats.Race.Press": "1",
|
||||||
|
"Stats.TotalRaces.Press": "1",
|
||||||
|
"BankMasterAtBank": "1",
|
||||||
|
"DM_ExtraLife_00": "-60",
|
||||||
|
"DM_ExtraLife_01": "-60",
|
||||||
|
"DM_ExtraLife_02": "-60",
|
||||||
|
"DM_ExtraLife_03": "-60",
|
||||||
|
"DM_ExtraLife_04": "16294.7919922",
|
||||||
|
"Stats.TempleLife": "4",
|
||||||
|
"Mission_CatchTrurl_Data": "[]",
|
||||||
|
"Mission_CatchTrurl_MapsPos": "[]",
|
||||||
|
"Mission_CatchTrurl_NumMapsDropped": "0",
|
||||||
|
"Mission_CatchTrurl_NumMapsTaken": "0",
|
||||||
|
"GDB.BishopsMsg": "1",
|
||||||
|
"Stats.GDB": "6",
|
||||||
|
"LonelyMercActive": "0",
|
||||||
|
"Stats.Messenger": "2",
|
||||||
|
"MortalRaceRace": "[((71611.59375, 18231.6992188, 93232.796875), 422.337219238), ((45388.4140625, 14599.3476563, 79622.6640625), 400.984222412), ((25194.9804688, 18783.4863281, 59759.296875), 404.390136719), ((-433.664245605, 26340.1289063, 34561.0898438), 409.718261719), ((-38229.3671875, 26457.5292969, 679.068054199), 449.472442627), ((-107464.132813, 19331.875, 3288.328125), 528.452758789), ((-113911.117188, 14331.4462891, 40812.9414063), 558.054199219), ((-102532.132813, 11236.1474609, 75072.375), 630.567077637), ((-58177.6289063, 6282.20654297, 74209.3515625), 673.615478516), ((-24157.5449219, 7054.30419922, 43223.1679688), 630.510681152), ((33550.1445313, 15480.2402344, 41122.5820313), -55.4565696716), ((56201.6054688, 15587.5126953, 24649.8496094), 23.7488441467), ((26343.9511719, 22077.8789063, -32317.0292969), 90.5086135864), ((-13835.4755859, 26276.8730469, -31975.1582031), 145.932754517), ((-29244.3652344, 26745.4667969, -2544.81738281), -892.995666504), ((-23808.9570313, 27246.9980469, 32018.1816406), -819.383483887), ((21584.3066406, 29452.4667969, 41221.6171875), -822.313781738), ((54026.796875, 24611.7421875, 42694.0898438), -802.188171387), ((95732.015625, 16516.8085938, 36323.546875), -872.699890137), ((113450.46875, 12325.5195313, 77796.75), -969.003662109)]",
|
||||||
|
"MortalRaceWaypoints": "20",
|
||||||
|
"MortalRaceRacers": "['ArchBishop', 'BankDirector', 'Functionary', 'MercenaryA', 'MercenaryB']",
|
||||||
|
"MortalRaceRacersProfile": "MortalRace",
|
||||||
|
"MortalRaceFightingFreezeControlAim": "4",
|
||||||
|
"MortalRaceRespawnTime": "5.0",
|
||||||
|
"MortalRaceStartTextCad": "Mission_WinMortalRace_RemoteMessage1",
|
||||||
|
"MortalRaceFinishPlayerLoosesTextCad": "Mission_WinMortalRace_RemoteMessage2",
|
||||||
|
"MortalRaceFinishPlayerLoosesTextFoe": "Messenger",
|
||||||
|
"MortalRaceFinishPlayerWinsTextCad": "Mission_WinMortalRace_RemoteMessage3",
|
||||||
|
"MortalRaceFinishPlayerWinsTextFoe": "Messenger",
|
||||||
|
"MortalRaceAutoRestart": "1"
|
||||||
|
}
|
||||||
|
}
|
3
tools/remaster/scraphacks_rs/build.rs
Normal file
3
tools/remaster/scraphacks_rs/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() -> shadow_rs::SdResult<()> {
|
||||||
|
shadow_rs::new()
|
||||||
|
}
|
4
tools/remaster/scraphacks_rs/build.sh
Normal file
4
tools/remaster/scraphacks_rs/build.sh
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
impo
|
||||||
|
cargo b -r
|
||||||
|
cp D:/devel/Git_Repos/Scrapland-RE/tools/remaster/scraphacks_rs/target/i686-pc-windows-msvc/release/scraphacks_rs.dll E:/Games/Steam/steamapps/common/Scrapland/lib/ScrapHack.pyd
|
||||||
|
# x32dbg E:/Games/Steam/steamapps/common/Scrapland/Bin/Scrap.unpacked.exe "-debug:10 -console"
|
16
tools/remaster/scraphacks_rs/notes.md
Normal file
16
tools/remaster/scraphacks_rs/notes.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Notes
|
||||||
|
|
||||||
|
## Snippets
|
||||||
|
|
||||||
|
Map name: `Scrap.GetLangStr("Station_" + Scrap.GetLevelPath()[7:])`
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
`steam://run/897610/`
|
||||||
|
|
||||||
|
## Signatures
|
||||||
|
|
||||||
|
- World pointer: `a3 *{'} e8 ? ? ? ? 6a 00 68 *"World initialized"`
|
||||||
|
- print: `6a0068 *{"Scrap engine"} 6a?e8 $'`
|
||||||
|
- console handler: `68 *{"import Viewer"} e8 $'`
|
||||||
|
- DirectX Device: ``
|
34
tools/remaster/scraphacks_rs/run.py
Normal file
34
tools/remaster/scraphacks_rs/run.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import subprocess as SP
|
||||||
|
import shutil as sh
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import psutil
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
os.environ['DISCORD_INSTANCE_ID']='1'
|
||||||
|
SP.check_call(["cargo","b","-r"])
|
||||||
|
info=[json.loads(line) for line in SP.check_output(["cargo","b", "-r" ,"-q","--message-format=json"]).splitlines()]
|
||||||
|
dll_path=None
|
||||||
|
for line in info:
|
||||||
|
if line.get('reason')=="compiler-artifact" and ("dylib" in line.get("target",{}).get("crate_types",[])):
|
||||||
|
dll_path=Path(line['filenames'][0])
|
||||||
|
|
||||||
|
sh.copy(dll_path,"E:/Games/Steam/steamapps/common/Scrapland/lib/ScrapHack.pyd")
|
||||||
|
|
||||||
|
if "--run" not in sys.argv[1:]:
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
os.startfile("steam://run/897610/")
|
||||||
|
pid=None
|
||||||
|
while pid is None:
|
||||||
|
for proc in psutil.process_iter():
|
||||||
|
try:
|
||||||
|
if proc.name()=="Scrap.exe":
|
||||||
|
pid=proc.pid
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
print(f"PID: {pid:x}")
|
||||||
|
if "--dbg" in sys.argv[1:]:
|
||||||
|
SP.run(["x32dbg","-p",str(pid)])
|
||||||
|
# cp D:/devel/Git_Repos/Scrapland-RE/tools/remaster/scraphacks_rs/target/i686-pc-windows-msvc/release/scraphacks_rs.dll E:/Games/Steam/steamapps/common/Scrapland/lib/ScrapHack.pyd
|
||||||
|
# x32dbg E:/Games/Steam/steamapps/common/Scrapland/Bin/Scrap.unpacked.exe "-debug:10 -console"
|
16
tools/remaster/scraphacks_rs/scrap.hpp
Normal file
16
tools/remaster/scraphacks_rs/scrap.hpp
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#include <stdint.h>
|
||||||
|
struct HashTable {
|
||||||
|
uint32_t num_slots;
|
||||||
|
struct HashTableEntry **chains;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HashTableEntry {
|
||||||
|
void *data;
|
||||||
|
const char *name;
|
||||||
|
HashTableEntry *next;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct World {
|
||||||
|
void** VMT;
|
||||||
|
HashTable *entities;
|
||||||
|
};
|
7
tools/remaster/scraphacks_rs/src/config.rs
Normal file
7
tools/remaster/scraphacks_rs/src/config.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
enum FilePatch {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
file_patches: FxHashMap<PathBuf,FilePatch>
|
||||||
|
}
|
94
tools/remaster/scraphacks_rs/src/discord.rs
Normal file
94
tools/remaster/scraphacks_rs/src/discord.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
use std::{num::NonZeroU32, thread::JoinHandle, time::SystemTime};
|
||||||
|
|
||||||
|
use crate::{cdbg, ceprintln, cprint, cprintln};
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use discord_sdk::{
|
||||||
|
activity::{ActivityBuilder, Assets, PartyPrivacy, Secrets},
|
||||||
|
registration::{register_app, Application, LaunchCommand},
|
||||||
|
wheel::Wheel,
|
||||||
|
Discord, DiscordApp, Subscriptions,
|
||||||
|
};
|
||||||
|
const APP_ID: discord_sdk::AppId = 1066820570097930342;
|
||||||
|
const STEAM_APP_ID: u32 = 897610;
|
||||||
|
pub struct Client {
|
||||||
|
pub discord: discord_sdk::Discord,
|
||||||
|
pub user: discord_sdk::user::User,
|
||||||
|
pub wheel: discord_sdk::wheel::Wheel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn run() -> Result<JoinHandle<Result<()>>> {
|
||||||
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?;
|
||||||
|
register_app(Application {
|
||||||
|
id: APP_ID,
|
||||||
|
name: Some("Scrapland Remastered".to_owned()),
|
||||||
|
command: LaunchCommand::Steam(STEAM_APP_ID),
|
||||||
|
})?;
|
||||||
|
Ok(std::thread::spawn(move || rt.block_on(Self::run_async())))
|
||||||
|
}
|
||||||
|
async fn run_async() -> Result<()> {
|
||||||
|
let (wheel, handler) = Wheel::new(Box::new(|err| {
|
||||||
|
ceprintln!("Encountered an error: {}", err);
|
||||||
|
}));
|
||||||
|
let mut user = wheel.user();
|
||||||
|
let discord = Discord::new(
|
||||||
|
DiscordApp::PlainId(APP_ID),
|
||||||
|
Subscriptions::ACTIVITY,
|
||||||
|
Box::new(handler),
|
||||||
|
)?;
|
||||||
|
user.0.changed().await?;
|
||||||
|
let user = match &*user.0.borrow() {
|
||||||
|
discord_sdk::wheel::UserState::Connected(user) => user.clone(),
|
||||||
|
discord_sdk::wheel::UserState::Disconnected(err) => {
|
||||||
|
ceprintln!("Failed to connect to Discord: {err}");
|
||||||
|
bail!("{}", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let uid = user.id;
|
||||||
|
cprintln!(
|
||||||
|
"Logged in as: {user}#{discriminator}",
|
||||||
|
user = user.username,
|
||||||
|
discriminator = user
|
||||||
|
.discriminator
|
||||||
|
.map(|d| d.to_string())
|
||||||
|
.unwrap_or_else(|| "????".to_owned())
|
||||||
|
);
|
||||||
|
let mut activity = ActivityBuilder::new()
|
||||||
|
.state("Testing")
|
||||||
|
.assets(Assets::default().large("scrap_logo", Some("Testing")))
|
||||||
|
.timestamps(Some(SystemTime::now()), Option::<SystemTime>::None)
|
||||||
|
.details("Testing ScrapHack");
|
||||||
|
if false {
|
||||||
|
// (SCRAP.is_server()||SCRAP.is_client())
|
||||||
|
let players = 1;
|
||||||
|
let capacity = 32;
|
||||||
|
activity = activity
|
||||||
|
.instance(true)
|
||||||
|
.party(
|
||||||
|
"Testt",
|
||||||
|
NonZeroU32::new(players),
|
||||||
|
NonZeroU32::new(capacity),
|
||||||
|
if false {
|
||||||
|
PartyPrivacy::Private
|
||||||
|
} else {
|
||||||
|
PartyPrivacy::Public
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.secrets(Secrets {
|
||||||
|
r#match: Some("MATCH".to_owned()), // Use server_ip+port
|
||||||
|
join: Some("JOIN".to_owned()), // Use server_ip+port
|
||||||
|
spectate: Some("SPECTATE".to_owned()), // Use server_ip+port
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
discord.update_activity(activity).await?;
|
||||||
|
loop {
|
||||||
|
if let Ok(req) = wheel.activity().0.try_recv() {
|
||||||
|
cprintln!("Got Join request: {req:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
96
tools/remaster/scraphacks_rs/src/lib.rs
Normal file
96
tools/remaster/scraphacks_rs/src/lib.rs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
#![feature(abi_thiscall)]
|
||||||
|
#![feature(c_variadic)]
|
||||||
|
mod discord;
|
||||||
|
mod lua;
|
||||||
|
mod mem;
|
||||||
|
mod parser;
|
||||||
|
mod scrap;
|
||||||
|
use std::ffi::{c_char, c_void, CString};
|
||||||
|
use anyhow::Result;
|
||||||
|
use crate::mem::search;
|
||||||
|
use crate::scrap::SCRAP;
|
||||||
|
use shadow_rs::shadow;
|
||||||
|
use winsafe::{co::{MB, CS, WS}, prelude::*, HWND, WNDCLASSEX, RegisterClassEx, WString};
|
||||||
|
|
||||||
|
shadow!(build);
|
||||||
|
|
||||||
|
custom_print::define_macros!({cprint, cprintln, cdbg}, fmt, |value: &str| {crate::scrap::SCRAP.print(value)});
|
||||||
|
custom_print::define_macros!({ceprint, ceprintln}, fmt, |value: &str| {crate::scrap::SCRAP.print_c(0x800000,value)});
|
||||||
|
|
||||||
|
#[allow(clippy::single_component_path_imports)]
|
||||||
|
pub(crate) use {cdbg, cprint, cprintln};
|
||||||
|
#[warn(clippy::single_component_path_imports)]
|
||||||
|
pub(crate) use {ceprint, ceprintln};
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct PyMethodDef {
|
||||||
|
name: *const c_char,
|
||||||
|
func: *const (*const c_void, *const c_void),
|
||||||
|
ml_flags: i32,
|
||||||
|
doc: *const c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct PyModuleDef {
|
||||||
|
name: *const c_char,
|
||||||
|
methods: *const PyMethodDef,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_py_mod() {
|
||||||
|
let py_init_module: fn(
|
||||||
|
*const c_char, // name
|
||||||
|
*const PyMethodDef, // methods
|
||||||
|
*const c_char, // doc
|
||||||
|
*const (), // passthrough
|
||||||
|
i32, // module_api_version
|
||||||
|
) -> *const () =
|
||||||
|
unsafe { std::mem::transmute(search("68 *{\"Scrap\" 00} e8 ${'}", 1, None).unwrap_or_default()) };
|
||||||
|
let name = CString::new("ScrapHack").unwrap_or_default();
|
||||||
|
let desc = CString::new("ScrapHack Rust version").unwrap_or_default();
|
||||||
|
let methods: &[PyMethodDef] = &[PyMethodDef {
|
||||||
|
name: 0 as _,
|
||||||
|
func: 0 as _,
|
||||||
|
ml_flags: 0,
|
||||||
|
doc: 0 as _,
|
||||||
|
}];
|
||||||
|
assert!(
|
||||||
|
!py_init_module(name.as_ptr(), methods.as_ptr(), desc.as_ptr(), 0 as _, 1007).is_null()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn initScrapHack() {
|
||||||
|
#[cfg(feature = "console")]
|
||||||
|
unsafe {
|
||||||
|
AllocConsole();
|
||||||
|
}
|
||||||
|
std::panic::set_hook(Box::new(|info| {
|
||||||
|
ceprintln!("ScrapHacks: {info}");
|
||||||
|
HWND::DESKTOP
|
||||||
|
.MessageBox(&format!("{info}"), "ScrapHacks error", MB::ICONERROR)
|
||||||
|
.unwrap();
|
||||||
|
std::process::exit(1);
|
||||||
|
}));
|
||||||
|
init_py_mod();
|
||||||
|
print_version_info();
|
||||||
|
cprintln!("{SCRAP:#x?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn DllMain(_inst: isize, _reason: u32, _: *const u8) -> u32 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_version_info() {
|
||||||
|
cprintln!(
|
||||||
|
"{} v{} ({} {}), built for {} by {}.",
|
||||||
|
build::PROJECT_NAME,
|
||||||
|
build::PKG_VERSION,
|
||||||
|
build::SHORT_COMMIT,
|
||||||
|
build::BUILD_TIME,
|
||||||
|
build::BUILD_TARGET,
|
||||||
|
build::RUST_VERSION
|
||||||
|
);
|
||||||
|
}
|
204
tools/remaster/scraphacks_rs/src/lua.rs
Normal file
204
tools/remaster/scraphacks_rs/src/lua.rs
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cprintln,
|
||||||
|
mem::{get_module, get_modules},
|
||||||
|
parser::Cmd,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use detour3::GenericDetour;
|
||||||
|
use mlua::{prelude::*, Variadic};
|
||||||
|
use pelite::pattern;
|
||||||
|
use pelite::pe32::{Pe, PeObject};
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
use winsafe::{prelude::*, HINSTANCE};
|
||||||
|
|
||||||
|
struct Ptr(*const ());
|
||||||
|
|
||||||
|
impl LuaUserData for Ptr {
|
||||||
|
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
|
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, _: ()| {
|
||||||
|
Ok(format!("{:p}", this.0))
|
||||||
|
});
|
||||||
|
methods.add_method("read", |_, this, (size,): (usize,)| {
|
||||||
|
let addr = this.0 as u32;
|
||||||
|
let ptr = this.0 as *const u8;
|
||||||
|
let info = region::query(ptr).map_err(mlua::Error::external)?;
|
||||||
|
let end = info.as_ptr_range::<()>().end as u32;
|
||||||
|
let size = ((end - addr) as usize).min(size);
|
||||||
|
if !info.is_readable() {
|
||||||
|
return Err(LuaError::external(anyhow!("No read permission on page")));
|
||||||
|
}
|
||||||
|
let data = unsafe { std::slice::from_raw_parts(ptr, size) };
|
||||||
|
Ok(data.to_vec())
|
||||||
|
});
|
||||||
|
methods.add_method("write", |_, this, data: Vec<u8>| {
|
||||||
|
let data = data.as_slice();
|
||||||
|
let addr = this.0 as *const u8;
|
||||||
|
unsafe {
|
||||||
|
let handle = region::protect_with_handle(
|
||||||
|
addr,
|
||||||
|
data.len(),
|
||||||
|
region::Protection::READ_WRITE_EXECUTE,
|
||||||
|
)
|
||||||
|
.map_err(mlua::Error::external)?;
|
||||||
|
std::ptr::copy(data.as_ptr(), addr as *mut u8, data.len());
|
||||||
|
drop(handle);
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
// methods.add_method("hook", |_, this, func: LuaFunction| -> LuaResult<()> {
|
||||||
|
// let addr = this.0;
|
||||||
|
// cprintln!("Hook: {func:?} @ {addr:p}");
|
||||||
|
// let dt = unsafe { GenericDetour::<extern "thiscall" fn(*const (), (u32,u32,u32)) -> u32>::new(std::mem::transmute(addr), hook_func) }.unwrap();
|
||||||
|
// Err(LuaError::external(anyhow!("TODO: hook")))
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extern "thiscall" fn hook_func(this: *const (), args: (u32,u32,u32)) -> u32 {
|
||||||
|
// return 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub(crate) fn init() -> Result<Lua> {
|
||||||
|
let lua = unsafe { Lua::unsafe_new() };
|
||||||
|
{
|
||||||
|
let globals = lua.globals();
|
||||||
|
globals.set("scan", lua.create_function(lua_scan)?)?;
|
||||||
|
globals.set("print", lua.create_function(lua_print)?)?;
|
||||||
|
globals.set("hook", lua.create_function(lua_hook)?)?;
|
||||||
|
globals.set("imports", lua.create_function(lua_imports)?)?;
|
||||||
|
globals.set(
|
||||||
|
"ptr",
|
||||||
|
lua.create_function(|_, addr: u32| Ok(Ptr(addr as _)))?,
|
||||||
|
)?;
|
||||||
|
globals.set(
|
||||||
|
"ptr",
|
||||||
|
lua.create_function(lua_alloc)?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(lua)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lua_val_to_string(val: &LuaValue) -> LuaResult<String> {
|
||||||
|
Ok(match val {
|
||||||
|
LuaNil => "Nil".to_owned(),
|
||||||
|
LuaValue::Boolean(b) => format!("{b}"),
|
||||||
|
LuaValue::LightUserData(u) => format!("{u:?}"),
|
||||||
|
LuaValue::Integer(i) => format!("{i}"),
|
||||||
|
LuaValue::Number(n) => format!("{n}"),
|
||||||
|
LuaValue::String(s) => (s.to_str()?).to_string(),
|
||||||
|
LuaValue::Table(t) => {
|
||||||
|
let mut vals = vec![];
|
||||||
|
for res in t.clone().pairs() {
|
||||||
|
let (k, v): (LuaValue, LuaValue) = res?;
|
||||||
|
vals.push(format!(
|
||||||
|
"{k} = {v}",
|
||||||
|
k = lua_val_to_string(&k)?,
|
||||||
|
v = lua_val_to_string(&v)?
|
||||||
|
));
|
||||||
|
}
|
||||||
|
format!("{{{vals}}}", vals = vals.join(", "))
|
||||||
|
}
|
||||||
|
LuaValue::Function(f) => format!("{f:?}"),
|
||||||
|
LuaValue::Thread(t) => format!("{t:?}"),
|
||||||
|
LuaValue::UserData(u) => format!("{u:?}"),
|
||||||
|
LuaValue::Error(e) => format!("{e:?}"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lua_alloc(lua: &Lua, size: usize) -> LuaResult<Ptr> {
|
||||||
|
let data = vec![0u8;size].into_boxed_slice();
|
||||||
|
Ok(Ptr(Box::leak(data).as_ptr() as _))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lua_hook(lua: &Lua, (addr, func): (u32, LuaFunction)) -> LuaResult<()> {
|
||||||
|
cprintln!("Hook: {func:?} @ {addr:08x}");
|
||||||
|
Err(LuaError::external(anyhow!("TODO: hook")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lua_imports(lua: &Lua, (): ()) -> LuaResult<()> {
|
||||||
|
Err(LuaError::external(anyhow!("TODO: imports")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lua_print(lua: &Lua, args: Variadic<LuaValue>) -> LuaResult<()> {
|
||||||
|
let msg: Vec<String> = args
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| lua_val_to_string(&v))
|
||||||
|
.collect::<LuaResult<Vec<String>>>()?;
|
||||||
|
cprintln!("{}", msg.join(" "));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ScanMode {
|
||||||
|
MainModule,
|
||||||
|
Modules(Vec<String>),
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromLua<'_> for ScanMode {
|
||||||
|
fn from_lua<'lua>(lua_value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
|
||||||
|
match &lua_value {
|
||||||
|
LuaValue::Nil => Ok(ScanMode::MainModule),
|
||||||
|
LuaValue::Boolean(true) => Ok(ScanMode::All),
|
||||||
|
LuaValue::Table(t) => Ok(ScanMode::Modules(FromLua::from_lua(lua_value, lua)?)),
|
||||||
|
_ => Err(LuaError::FromLuaConversionError {
|
||||||
|
from: lua_value.type_name(),
|
||||||
|
to: "scan_mode",
|
||||||
|
message: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lua_scan(lua: &Lua, (pattern, scan_mode): (String, ScanMode)) -> LuaResult<LuaTable> {
|
||||||
|
let pat = pattern::parse(&pattern).map_err(mlua::Error::external)?;
|
||||||
|
let mut ret = FxHashMap::default();
|
||||||
|
let modules = match scan_mode {
|
||||||
|
ScanMode::MainModule => vec![get_module(None).map_err(mlua::Error::external)?],
|
||||||
|
ScanMode::Modules(modules) => modules
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| get_module(Some(m)))
|
||||||
|
.collect::<Result<_>>()
|
||||||
|
.map_err(mlua::Error::external)?,
|
||||||
|
ScanMode::All => get_modules().map_err(mlua::Error::external)?,
|
||||||
|
};
|
||||||
|
'outer: for module in modules {
|
||||||
|
let regions = region::query_range(module.image().as_ptr(), module.image().len())
|
||||||
|
.map_err(mlua::Error::external)?;
|
||||||
|
for region in regions {
|
||||||
|
let Ok(region)=region else {
|
||||||
|
continue 'outer;
|
||||||
|
};
|
||||||
|
if !region.is_readable() {
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let h_module = unsafe { HINSTANCE::from_ptr(module.image().as_ptr() as _) };
|
||||||
|
let module_name = PathBuf::from(
|
||||||
|
h_module
|
||||||
|
.GetModuleFileName()
|
||||||
|
.map_err(mlua::Error::external)?,
|
||||||
|
)
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_owned();
|
||||||
|
if let Ok(res) = crate::mem::scan(&pat, &module) {
|
||||||
|
if !res.is_empty() {
|
||||||
|
let res: Vec<Vec<Ptr>> = res
|
||||||
|
.into_iter()
|
||||||
|
.map(|res| res.into_iter().map(|a| Ptr(a as _)).collect())
|
||||||
|
.collect();
|
||||||
|
ret.insert(module_name, res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
lua.create_table_from(ret.into_iter())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn exec(chunk: &str) -> Result<()> {
|
||||||
|
Ok(init()?.load(chunk).set_name("ScrapLua")?.exec()?)
|
||||||
|
}
|
94
tools/remaster/scraphacks_rs/src/mem.rs
Normal file
94
tools/remaster/scraphacks_rs/src/mem.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use anyhow::bail;
|
||||||
|
use anyhow::Result;
|
||||||
|
use pelite::pattern::parse;
|
||||||
|
use pelite::pattern::save_len;
|
||||||
|
use pelite::pattern::Atom;
|
||||||
|
use pelite::pe32::{Pe, PeView};
|
||||||
|
use winsafe::co::TH32CS;
|
||||||
|
use winsafe::prelude::*;
|
||||||
|
use winsafe::HINSTANCE;
|
||||||
|
use winsafe::HPROCESSLIST;
|
||||||
|
pub(crate) struct Pattern(Vec<Atom>, usize);
|
||||||
|
|
||||||
|
impl Pattern {
|
||||||
|
pub(crate) fn set_index(mut self, idx: usize) -> Self {
|
||||||
|
self.1 = idx;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Pattern {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self(parse(s)?, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pattern {
|
||||||
|
pub(crate) fn scan(&self, module: Option<String>) -> Result<u32> {
|
||||||
|
let pe = get_module(module)?;
|
||||||
|
let scan = pe.scanner();
|
||||||
|
let mut save = vec![0u32; save_len(&self.0)];
|
||||||
|
if !scan.finds(&self.0, 0..u32::MAX, &mut save) {
|
||||||
|
bail!("Pattern not found");
|
||||||
|
}
|
||||||
|
save.get(self.1)
|
||||||
|
.ok_or_else(|| anyhow!("Result index out of range"))
|
||||||
|
.and_then(|r| pe.rva_to_va(*r).map_err(|e| e.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_modules() -> Result<Vec<PeView<'static>>> {
|
||||||
|
let mut res = vec![];
|
||||||
|
let pid = std::process::id();
|
||||||
|
let mut h_snap = HPROCESSLIST::CreateToolhelp32Snapshot(TH32CS::SNAPMODULE, Some(pid))?;
|
||||||
|
for module in h_snap.iter_modules() {
|
||||||
|
res.push(unsafe { PeView::module(module?.hModule.as_ptr() as *const u8) });
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_module(module: Option<String>) -> Result<PeView<'static>> {
|
||||||
|
let hmodule = HINSTANCE::GetModuleHandle(module.as_deref())?;
|
||||||
|
Ok(unsafe { PeView::module(hmodule.as_ptr() as *const u8) })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn scan(pat: &[Atom], pe: &PeView) -> Result<Vec<Vec<u32>>> {
|
||||||
|
let mut ret = vec![];
|
||||||
|
let scan = pe.scanner();
|
||||||
|
let mut m = scan.matches(pat, 0..u32::MAX);
|
||||||
|
let mut save = vec![0u32; save_len(pat)];
|
||||||
|
while m.next(&mut save) {
|
||||||
|
ret.push(
|
||||||
|
save.iter()
|
||||||
|
.map(|rva| pe.rva_to_va(*rva).map_err(|e| e.into()))
|
||||||
|
.collect::<Result<Vec<u32>>>()?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn search(pat: &str, idx: usize, module: Option<String>) -> Result<u32> {
|
||||||
|
pat.parse::<Pattern>()?.set_index(idx).scan(module)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addr_info(addr: u32) -> Result<()> {
|
||||||
|
let pid = std::process::id();
|
||||||
|
let mut h_snap = HPROCESSLIST::CreateToolhelp32Snapshot(TH32CS::SNAPMODULE, Some(pid))?;
|
||||||
|
for module in h_snap.iter_modules() {
|
||||||
|
let module = module?;
|
||||||
|
let module_name = module.szModule();
|
||||||
|
if module_name.to_lowercase() == "kernel32.dll" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mod_range =
|
||||||
|
unsafe { module.modBaseAddr..module.modBaseAddr.offset(module.modBaseSize as isize) };
|
||||||
|
println!("{module_name}: {mod_range:?}");
|
||||||
|
// let module = unsafe { PeView::module(module.modBaseAddr as *const u8) };
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
177
tools/remaster/scraphacks_rs/src/parser.rs
Normal file
177
tools/remaster/scraphacks_rs/src/parser.rs
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
// use crate::{cdbg, ceprintln, cprint, cprintln};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use nom::branch::alt;
|
||||||
|
use nom::bytes::complete::{take_till, take_while1};
|
||||||
|
use nom::character::complete::{digit1, hex_digit1};
|
||||||
|
use nom::character::streaming::char;
|
||||||
|
use nom::combinator::{eof, opt, rest};
|
||||||
|
use nom::sequence::{separated_pair, tuple};
|
||||||
|
use nom::{IResult, Parser};
|
||||||
|
use nom_locate::LocatedSpan;
|
||||||
|
use nom_supreme::error::ErrorTree;
|
||||||
|
use nom_supreme::final_parser::final_parser;
|
||||||
|
use nom_supreme::tag::complete::{tag, tag_no_case};
|
||||||
|
use nom_supreme::ParserExt;
|
||||||
|
use pelite::pattern::{self, Atom};
|
||||||
|
|
||||||
|
type Span<'a> = LocatedSpan<&'a str>;
|
||||||
|
|
||||||
|
type ParseResult<'a, 'b, T> = IResult<Span<'a>, T, ErrorTree<Span<'b>>>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Cmd {
|
||||||
|
Imports,
|
||||||
|
Read(u32, usize),
|
||||||
|
ReadPE(u32, usize),
|
||||||
|
Write(u32, Vec<u8>),
|
||||||
|
Disams(u32, usize),
|
||||||
|
Info(Option<u32>),
|
||||||
|
Script(PathBuf),
|
||||||
|
Unload,
|
||||||
|
ScanModule(Vec<Atom>, Option<String>),
|
||||||
|
Lua(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Cmd {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match parse(s) {
|
||||||
|
Ok(cmd) => Ok(cmd),
|
||||||
|
Err(err) => Err(anyhow!("{}", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ws(input: Span) -> ParseResult<()> {
|
||||||
|
take_while1(|c: char| c.is_whitespace())
|
||||||
|
.value(())
|
||||||
|
.context("Whitepace")
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Test
|
||||||
|
|
||||||
|
fn hex_bytes(input: Span) -> ParseResult<Vec<u8>> {
|
||||||
|
hex_digit1
|
||||||
|
.map_res_cut(hex::decode)
|
||||||
|
.context("Hex string")
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn num(input: Span) -> ParseResult<usize> {
|
||||||
|
digit1
|
||||||
|
.map_res_cut(|n: Span| parse_int::parse(&n))
|
||||||
|
.context("Number")
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address(input: Span) -> ParseResult<u32> {
|
||||||
|
tag_no_case("0x")
|
||||||
|
.precedes(hex_digit1)
|
||||||
|
.recognize()
|
||||||
|
.map_res_cut(|addr: Span| parse_int::parse::<u32>(&addr))
|
||||||
|
.context("Memory address")
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_read_pe(input: Span) -> ParseResult<Cmd> {
|
||||||
|
tag("read_pe")
|
||||||
|
.precedes(ws)
|
||||||
|
.precedes(separated_pair(address, ws, num.opt()))
|
||||||
|
.map(|(addr, size)| Cmd::ReadPE(addr, size.unwrap_or(0x100)))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_read(input: Span) -> ParseResult<Cmd> {
|
||||||
|
tag("read")
|
||||||
|
.precedes(ws)
|
||||||
|
.precedes(separated_pair(address, ws, num.opt()))
|
||||||
|
.map(|(addr, size)| Cmd::Read(addr, size.unwrap_or(0x100)))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_disasm(input: Span) -> ParseResult<Cmd> {
|
||||||
|
tag("disasm")
|
||||||
|
.precedes(ws)
|
||||||
|
.precedes(separated_pair(address, ws, num.opt()))
|
||||||
|
.map(|(addr, size)| Cmd::Disams(addr, size.unwrap_or(50)))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_write(input: Span) -> ParseResult<Cmd> {
|
||||||
|
tag("write")
|
||||||
|
.precedes(ws)
|
||||||
|
.precedes(separated_pair(address, ws, hex_bytes))
|
||||||
|
.map(|(addr, data)| Cmd::Write(addr, data))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_info(input: Span) -> ParseResult<Cmd> {
|
||||||
|
tag("info")
|
||||||
|
.precedes(eof)
|
||||||
|
.value(Cmd::Info(None))
|
||||||
|
.or(tag("info")
|
||||||
|
.precedes(ws)
|
||||||
|
.precedes(address)
|
||||||
|
.map(|addr| Cmd::Info(Some(addr))))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_scan(input: Span) -> ParseResult<Cmd> {
|
||||||
|
let (input, _) = tag("scan").parse(input)?;
|
||||||
|
let (input, module) =
|
||||||
|
opt(tuple((char(':'), take_till(|c: char| c.is_whitespace())))).parse(input)?;
|
||||||
|
let module = module.map(|(_, module)| module.fragment().to_string());
|
||||||
|
let (input, _) = ws.parse(input)?;
|
||||||
|
let (input, pattern) = rest
|
||||||
|
.map_res(|pat: Span| pattern::parse(&pat))
|
||||||
|
.parse(input)?;
|
||||||
|
Ok((input, Cmd::ScanModule(pattern, module)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_unload(input: Span) -> ParseResult<Cmd> {
|
||||||
|
tag("unload").value(Cmd::Unload).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_imports(input: Span) -> ParseResult<Cmd> {
|
||||||
|
tag("imports").value(Cmd::Imports).parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_lua(input: Span) -> ParseResult<Cmd> {
|
||||||
|
tag("lua")
|
||||||
|
.precedes(ws)
|
||||||
|
.precedes(rest)
|
||||||
|
.map(|s| Cmd::Lua(s.fragment().to_string()))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_script(input: Span) -> ParseResult<Cmd> {
|
||||||
|
tag("script")
|
||||||
|
.precedes(ws)
|
||||||
|
.precedes(rest)
|
||||||
|
.map(|s| Cmd::Script(PathBuf::from(s.fragment())))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(input: &str) -> Result<Cmd, ErrorTree<Span<'_>>> {
|
||||||
|
final_parser(
|
||||||
|
alt((
|
||||||
|
parse_imports,
|
||||||
|
parse_unload,
|
||||||
|
parse_scan,
|
||||||
|
parse_info,
|
||||||
|
parse_write,
|
||||||
|
parse_read,
|
||||||
|
parse_read_pe,
|
||||||
|
parse_script,
|
||||||
|
parse_disasm,
|
||||||
|
parse_lua,
|
||||||
|
))
|
||||||
|
.context("command"),
|
||||||
|
)(Span::new(input))
|
||||||
|
}
|
381
tools/remaster/scraphacks_rs/src/scrap.rs
Normal file
381
tools/remaster/scraphacks_rs/src/scrap.rs
Normal file
|
@ -0,0 +1,381 @@
|
||||||
|
use crate::{
|
||||||
|
cdbg, ceprintln, cprint, cprintln, lua,
|
||||||
|
mem::{get_module, scan, search},
|
||||||
|
parser::Cmd, discord,
|
||||||
|
};
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use derivative::Derivative;
|
||||||
|
use detour3::GenericDetour;
|
||||||
|
use futures::executor::block_on;
|
||||||
|
use iced_x86::{Decoder, DecoderOptions, Formatter, Instruction, NasmFormatter};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use pelite::{pe::PeView, pe32::Pe};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
ffi::{c_char, CStr, CString},
|
||||||
|
fmt::Debug,
|
||||||
|
ptr,
|
||||||
|
thread::JoinHandle,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use winsafe::HINSTANCE;
|
||||||
|
use winsafe::{co::TH32CS, prelude::*, HPROCESSLIST};
|
||||||
|
|
||||||
|
const POINTER_SIZE: usize = std::mem::size_of::<*const ()>();
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct VirtualMethodTable(*const *const ());
|
||||||
|
|
||||||
|
impl Debug for VirtualMethodTable {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut methods = vec![];
|
||||||
|
for idx in 0.. {
|
||||||
|
let ptr = self.get::<()>(idx);
|
||||||
|
if ptr.is_null()
|
||||||
|
|| !region::query(ptr)
|
||||||
|
.map(|r| r.is_executable())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
methods.push(ptr);
|
||||||
|
}
|
||||||
|
f.debug_tuple("VMT").field(&methods).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualMethodTable {
|
||||||
|
fn get<T>(&self, offset: usize) -> *const T {
|
||||||
|
unsafe { self.0.add(POINTER_SIZE * offset).read() as *const T }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative)]
|
||||||
|
#[derivative(Debug)]
|
||||||
|
pub struct Scrap {
|
||||||
|
print: extern "C" fn(u32, *const c_char, u8),
|
||||||
|
console_detour: GenericDetour<extern "C" fn(*const c_char)>,
|
||||||
|
world: WorldPointer,
|
||||||
|
discord_thread_handle: JoinHandle<Result<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Entity {
|
||||||
|
vmt: VirtualMethodTable,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct HashTableEntry<T> {
|
||||||
|
data: *const T,
|
||||||
|
name: *const c_char,
|
||||||
|
next: *const Self,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct HashTable<T> {
|
||||||
|
num_slots: u32,
|
||||||
|
chains: *const *const HashTableEntry<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read<T>(ptr: *const T) -> Option<T> {
|
||||||
|
(!ptr.is_null()).then(|| unsafe { ptr.read() })
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: std::fmt::Debug> std::fmt::Debug for HashTable<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut entries: HashMap<String, Option<T>> = HashMap::default();
|
||||||
|
for offset in 0..self.num_slots {
|
||||||
|
let offset = offset as _;
|
||||||
|
// let chain=vec![];
|
||||||
|
let mut chain_ptr = unsafe { self.chains.offset(offset).read() };
|
||||||
|
while !chain_ptr.is_null() {
|
||||||
|
let entry = unsafe { chain_ptr.read() };
|
||||||
|
let data = try_read(entry.data);
|
||||||
|
let key = unsafe { CStr::from_ptr(entry.name) }
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_owned();
|
||||||
|
chain_ptr = entry.next;
|
||||||
|
entries.insert(key, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.debug_struct(&format!("HashTable @ {self:p} "))
|
||||||
|
.field("num_slots", &self.num_slots)
|
||||||
|
.field("entries", &entries)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct World {
|
||||||
|
vmt: VirtualMethodTable,
|
||||||
|
entities: HashTable<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for World {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("World")
|
||||||
|
.field("vmt", &self.vmt)
|
||||||
|
.field("entities", &self.entities)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WorldPointer(u32);
|
||||||
|
|
||||||
|
impl std::fmt::Debug for WorldPointer {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let ptr = self.ptr();
|
||||||
|
let world = unsafe { ptr.read() };
|
||||||
|
f.debug_tuple(&format!("WorldPointer @ {ptr:p} "))
|
||||||
|
.field(&world)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorldPointer {
|
||||||
|
fn ptr(&self) -> *const World {
|
||||||
|
let ptr = self.0 as *const *const World;
|
||||||
|
unsafe { ptr.read() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_hashtable(&self) {
|
||||||
|
// let ents = unsafe { self.ptr().read().entities.read() };
|
||||||
|
// cprintln!("Ents: {ents:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) static SCRAP: Lazy<Scrap> =
|
||||||
|
Lazy::new(|| Scrap::init().expect("Failed to initialize Scrap data structure"));
|
||||||
|
|
||||||
|
impl Scrap {
|
||||||
|
const PRINT_PATTERN: &str = r#"6a0068 *{"Scrap engine"} 6a?e8 $'"#;
|
||||||
|
const PY_EXEC: &str = r#"68 *{"import Viewer"} e8 $'"#;
|
||||||
|
const WORLD_PATTERN: &str = r#"8b 0d *{'} 68 *"CTFFriends""#;
|
||||||
|
fn init() -> Result<Self> {
|
||||||
|
let scrap = unsafe {
|
||||||
|
Self {
|
||||||
|
world: WorldPointer(search(Self::WORLD_PATTERN, 1, None)? as _),
|
||||||
|
print: std::mem::transmute(search(Self::PRINT_PATTERN, 1, None)?),
|
||||||
|
console_detour: GenericDetour::<extern "C" fn(*const c_char)>::new(
|
||||||
|
std::mem::transmute(search(Self::PY_EXEC, 1, None)?),
|
||||||
|
Self::console_input,
|
||||||
|
)?,
|
||||||
|
discord_thread_handle: discord::Client::run()?,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsafe { scrap.console_detour.enable()? }
|
||||||
|
Ok(scrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn console_input(orig_line: *const c_char) {
|
||||||
|
let line = unsafe { CStr::from_ptr(orig_line) }.to_str();
|
||||||
|
let Ok(line) = line else {
|
||||||
|
return SCRAP.console_detour.call(orig_line);
|
||||||
|
};
|
||||||
|
if let Some(cmd) = line.strip_prefix('$') {
|
||||||
|
let res = cmd.parse().and_then(|cmd: Cmd| cmd.exec());
|
||||||
|
if let Err(err) = res {
|
||||||
|
ceprintln!("Error: {err}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
SCRAP.console_detour.call(orig_line)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn println(&self, msg: &str) {
|
||||||
|
self.println_c(0x008000, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(&self, msg: &str) {
|
||||||
|
self.print_c(0x008000, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_c(&self, col: u32, msg: &str) {
|
||||||
|
let col = (col & 0xffffff).swap_bytes() >> 8; // 0xRRGGBB -> 0xBBGGRR
|
||||||
|
let msg = CString::new(msg.to_string()).unwrap();
|
||||||
|
(self.print)(col, msg.as_ptr(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn println_c(&self, col: u32, msg: &str) {
|
||||||
|
let msg = msg.to_owned() + "\n";
|
||||||
|
self.print_c(col, &msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd {
|
||||||
|
pub(crate) fn exec(&self) -> Result<()> {
|
||||||
|
let pe = get_module(None)?;
|
||||||
|
match self {
|
||||||
|
Cmd::Imports => {
|
||||||
|
for import in pe.imports()? {
|
||||||
|
let iat = import.iat()?;
|
||||||
|
let int = import.int()?;
|
||||||
|
for (func, imp) in iat.zip(int) {
|
||||||
|
let imp = imp?;
|
||||||
|
cprintln!(
|
||||||
|
"{addr:p}: {name} {imp:?}",
|
||||||
|
name = import.dll_name()?,
|
||||||
|
addr = func
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cmd::Read(addr, size) => {
|
||||||
|
let ptr = *addr as *const u8;
|
||||||
|
let info = region::query(ptr)?;
|
||||||
|
let end = info.as_ptr_range::<()>().end as u32;
|
||||||
|
let size = ((end - addr) as usize).min(*size);
|
||||||
|
if !info.is_readable() {
|
||||||
|
bail!("No read permission on page");
|
||||||
|
}
|
||||||
|
let data = unsafe { std::slice::from_raw_parts(ptr, size) };
|
||||||
|
cprintln!("{}", &rhexdump::hexdump_offset(data, *addr));
|
||||||
|
}
|
||||||
|
Cmd::Disams(addr, size) => {
|
||||||
|
let ptr = *addr as *const u8;
|
||||||
|
let info = region::query(ptr)?;
|
||||||
|
let end = info.as_ptr_range::<()>().end as u32;
|
||||||
|
let size = ((end - addr) as usize).min(*size);
|
||||||
|
if !info.is_readable() {
|
||||||
|
bail!("No read permission on page");
|
||||||
|
}
|
||||||
|
let data = unsafe { std::slice::from_raw_parts(ptr, size) };
|
||||||
|
let mut decoder = Decoder::with_ip(32, data, *addr as u64, DecoderOptions::NONE);
|
||||||
|
let mut instruction = Instruction::default();
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut formatter = NasmFormatter::new();
|
||||||
|
while decoder.can_decode() {
|
||||||
|
decoder.decode_out(&mut instruction);
|
||||||
|
output.clear();
|
||||||
|
formatter.format(&instruction, &mut output);
|
||||||
|
cprint!("{:016X} ", instruction.ip());
|
||||||
|
let start_index = (instruction.ip() - (*addr as u64)) as usize;
|
||||||
|
let instr_bytes = &data[start_index..start_index + instruction.len()];
|
||||||
|
for b in instr_bytes.iter() {
|
||||||
|
cprint!("{:02X}", b);
|
||||||
|
}
|
||||||
|
cprintln!(" {}", output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cmd::Write(addr, data) => {
|
||||||
|
let data = data.as_slice();
|
||||||
|
let addr = *addr as *const u8;
|
||||||
|
unsafe {
|
||||||
|
let handle = region::protect_with_handle(
|
||||||
|
addr,
|
||||||
|
data.len(),
|
||||||
|
region::Protection::READ_WRITE_EXECUTE,
|
||||||
|
)?;
|
||||||
|
std::ptr::copy(data.as_ptr(), addr as *mut u8, data.len());
|
||||||
|
drop(handle);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Cmd::ReadPE(addr, size) => {
|
||||||
|
if !region::query(*addr as *const ())?.is_readable() {
|
||||||
|
bail!("No read permission for 0x{addr:x}");
|
||||||
|
}
|
||||||
|
let data = pe.read_bytes(*addr)?;
|
||||||
|
cprintln!("{}", &rhexdump::hexdump_offset(&data[..*size], *addr));
|
||||||
|
}
|
||||||
|
Cmd::Info(None) => {
|
||||||
|
let regions = region::query_range(ptr::null::<()>(), usize::MAX)?;
|
||||||
|
for region in regions.flatten() {
|
||||||
|
cprintln!(
|
||||||
|
"{:?}: {}",
|
||||||
|
region.as_ptr_range::<*const ()>(),
|
||||||
|
region.protection()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cmd::Info(Some(addr)) => {
|
||||||
|
let info = region::query(*addr as *const ())?;
|
||||||
|
cprintln!(
|
||||||
|
"{:?}: {}",
|
||||||
|
info.as_ptr_range::<*const ()>(),
|
||||||
|
info.protection()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Cmd::ScanModule(pat, module) => {
|
||||||
|
cprintln!("{:?}", pat);
|
||||||
|
let mut total_hits = 0;
|
||||||
|
let mut modules = vec![];
|
||||||
|
let is_wildcard = matches!(module.as_deref(), Some("*"));
|
||||||
|
if is_wildcard {
|
||||||
|
let pid = std::process::id();
|
||||||
|
let mut h_snap =
|
||||||
|
HPROCESSLIST::CreateToolhelp32Snapshot(TH32CS::SNAPMODULE, Some(pid))?;
|
||||||
|
for module in h_snap.iter_modules() {
|
||||||
|
let module = module?;
|
||||||
|
let module_name = module.szModule();
|
||||||
|
let module_addr = module.hModule.as_ptr() as *const u8;
|
||||||
|
let module = region::query_range(module_addr, module.modBaseSize as usize)?
|
||||||
|
.all(|m| m.ok().map(|m| m.is_readable()).unwrap_or(false))
|
||||||
|
.then(|| unsafe { PeView::module(module_addr) });
|
||||||
|
if let Some(module) = module {
|
||||||
|
modules.push((module_name, module));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let module = HINSTANCE::GetModuleHandle(module.as_deref())?;
|
||||||
|
let module_name = module.GetModuleFileName()?;
|
||||||
|
let module_addr = module.as_ptr() as *const u8;
|
||||||
|
let module = region::query(module_addr)
|
||||||
|
.map(|m| m.is_readable())
|
||||||
|
.unwrap_or(false)
|
||||||
|
.then(|| unsafe { PeView::module(module_addr) });
|
||||||
|
if let Some(module) = module {
|
||||||
|
modules.push((module_name, module));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for (module_name, pe) in modules {
|
||||||
|
let res = scan(pat, &pe)?;
|
||||||
|
if res.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
total_hits += res.len();
|
||||||
|
cprintln!("Module: {module_name}");
|
||||||
|
let sections = pe.section_headers();
|
||||||
|
for hit in &res {
|
||||||
|
for (idx, addr) in hit.iter().enumerate() {
|
||||||
|
let mut section_name = String::from("<invalid address>");
|
||||||
|
if let Ok(section_rva) = pe.va_to_rva(*addr) {
|
||||||
|
if let Some(section) = sections.by_rva(section_rva) {
|
||||||
|
section_name = match section.name() {
|
||||||
|
Ok(name) => name.to_string(),
|
||||||
|
Err(name_bytes) => format!("{name_bytes:?}"),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
section_name = String::from("<invalid section>");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Ok(region) = region::query(addr) {
|
||||||
|
cprintln!(
|
||||||
|
"\t{}: {:?} {} [{}] {:p}",
|
||||||
|
idx,
|
||||||
|
region.as_ptr_range::<()>(),
|
||||||
|
region.protection(),
|
||||||
|
section_name,
|
||||||
|
addr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cprintln!("Results: {total_hits}");
|
||||||
|
}
|
||||||
|
Cmd::Lua(code) => {
|
||||||
|
lua::exec(code)?;
|
||||||
|
}
|
||||||
|
Cmd::Script(path) => {
|
||||||
|
for line in std::fs::read_to_string(path)?.lines() {
|
||||||
|
line.parse().and_then(|cmd: Cmd| cmd.exec())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => bail!("Not implemented: {other:?}"),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
1
tools/remaster/scraphacks_rs/target/.rustc_info.json
Normal file
1
tools/remaster/scraphacks_rs/target/.rustc_info.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"rustc_fingerprint":18143952876974389501,"outputs":{"16636649553340150347":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\Earthnuker\\scoop\\persist\\rustup-msvc\\.rustup\\toolchains\\nightly-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\nfeature=\"cargo-clippy\"\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"32\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.70.0-nightly (9df3a39fb 2023-04-11)\nbinary: rustc\ncommit-hash: 9df3a39fb30575d808e70800f9fad5362aac57a2\ncommit-date: 2023-04-11\nhost: x86_64-pc-windows-msvc\nrelease: 1.70.0-nightly\nLLVM version: 16.0.2\n","stderr":""},"1185988223601034215":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\Earthnuker\\scoop\\persist\\rustup-msvc\\.rustup\\toolchains\\nightly-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\nfeature=\"cargo-clippy\"\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"pc\"\nwindows\n","stderr":""}},"successes":{}}
|
3
tools/remaster/scraphacks_rs/target/CACHEDIR.TAG
Normal file
3
tools/remaster/scraphacks_rs/target/CACHEDIR.TAG
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Signature: 8a477f597d28d172789f06886806bc55
|
||||||
|
# This file is a cache directory tag created by cargo.
|
||||||
|
# For information about cache directory tags see https://bford.info/cachedir/
|
|
@ -0,0 +1,3 @@
|
||||||
|
Signature: 8a477f597d28d172789f06886806bc55
|
||||||
|
# This file is a cache directory tag created by cargo.
|
||||||
|
# For information about cache directory tags see https://bford.info/cachedir/
|
Loading…
Reference in a new issue