Big update

This repository is no longer meant for just radarflow, thus it will be renamed.
I have split the SDK from radarflow, allowing for simpler use with new projects.
Other than that, radarflow is functionally the same.

- Fixed bug in radarflow where the entity loop didn't include the last entity.
This commit is contained in:
Janek
2023-12-30 18:07:55 +01:00
parent 462cfddfef
commit 45bba35a71
85 changed files with 6982 additions and 1036 deletions

40
radarflow/Cargo.toml Normal file
View File

@@ -0,0 +1,40 @@
[package]
name = "radarflow"
version = "0.2.0"
authors = ["Janek S <development@superyu.xyz>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
csflow = { path = "../csflow" }
# cli
clap = { version = "4.3.19", features = ["derive", "string"] }
# tokio
tokio = { version = "1.29.1", features = ["full"] }
tokio-timerfd = "0.2.0"
# serde
serde = { version = "1.0.181", features = ["derive"] }
serde_json = "1.0.104"
# networking
axum = { version = "0.6.20", features = ["ws"] }
tower-http = { version = "0.4.3", features = ["fs"] }
tower = "0.4.13"
local-ip-address = "0.5.4"
# other
# error handling
anyhow = "1.0.77"
# logging
log = "0.4.19"
simple_logger = "4.2.0"
[build-dependencies]
vergen = { version = "8.0.0", features = ["build", "cargo", "git", "gitcl", "rustc", "si"] }

44
radarflow/README.md Normal file
View File

@@ -0,0 +1,44 @@
# radarflow
A Web radar for CS2 using [memflow](https://github.com/memflow/memflow)
## How can I run this?
There is two ways to run this, first way is using a KVM/QEMU setup to target a running VM to read memory out of it. The second way is using pcileech hardware, like a PCIe Screamer.
> [!NOTE]
> The pcileech method is untested. However, I have ordered hardware, and will test soon.
### The KVM/QEMU method
First, you need to set up a virtual machine on linux using qemu.
How to set up a VM on linux is way out of scope for this. You can find plenty of information online on how to do it.
Before you begin, install the necessary memflow plugins using memflowup from the *stable 0.2.0 channel!*
Clone the repo on your vm host:
`git clone https://github.com/superyu1337/radarflow2.git`
Run radarflow:
`cargo run --release`
For an overview of CLI commands, run this:
`cargo run --release -- --help`
### The pcileech method
> [!WARNING]
> The pcileech method is untested.
Install your pcileech hardware in your target pc. On your attacking pc, install the necessary memflow plugins using memflowup from the *stable 0.2.0 channel!*
Clone the repo on your attacking pc:
`git clone https://github.com/superyu1337/radarflow2.git`
Run radarflow:
`cargo run --release`
For an overview of CLI commands, run this:
`cargo run --release -- --help`
## Detection Status
VAC: ✅ (Undetected)
FaceIt: ❓ (Unknown, could work with proper spoofing on pcileech method)
ESEA: ❓ (Unknown, could work with proper spoofing on pcileech method)

14
radarflow/build.rs Normal file
View File

@@ -0,0 +1,14 @@
use std::error::Error;
use vergen::EmitBuilder;
fn main() -> Result<(), Box<dyn Error>> {
EmitBuilder::builder()
.git_sha(true)
.git_commit_date()
.cargo_debug()
.cargo_target_triple()
.rustc_semver()
.rustc_llvm_version()
.emit()?;
Ok(())
}

114
radarflow/src/cli.rs Normal file
View File

@@ -0,0 +1,114 @@
use std::path::PathBuf;
use clap::{Parser, ValueEnum};
use csflow::{Connector, memflow::Inventory};
const PORT_RANGE: std::ops::RangeInclusive<usize> = 8000..=65535;
const POLL_RANGE: std::ops::RangeInclusive<usize> = 1..=1000;
#[derive(Parser)]
#[command(author, version = version(), about, long_about = None)]
pub struct Cli {
/// Specifies the connector type for DMA
#[clap(value_enum, short, long, ignore_case = true, default_value_t = Connector::Qemu)]
pub connector: Connector,
/// Name of the Pcileech device
#[clap(long, default_value_t = String::from("FPGA"))]
pub pcileech_device: String,
/// Port number for the Webserver to run on
#[arg(short, long, default_value_t = 8000, value_parser = port_in_range)]
pub port: u16,
/// Path to the directory served by the Webserver
#[arg(short, long, default_value = "./webradar", value_parser = valid_path)]
pub web_path: PathBuf,
/// Polling frequency in times per second for the DMA thread
#[arg(short = 'r', long, default_value_t = 60, value_parser = poll_in_range)]
pub poll_rate: u16,
/// Verbosity level for logging to the console
#[arg(value_enum, long, short, ignore_case = true, default_value_t = Loglevel::Warn)]
pub loglevel: Loglevel,
}
fn version() -> String {
let pkg_ver = env!("CARGO_PKG_VERSION");
let git_hash = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown");
let commit_date = option_env!("VERGEN_GIT_COMMIT_DATE").unwrap_or("unknown");
let avail_cons = {
let inventory = Inventory::scan();
inventory.available_connectors().join(", ")
};
format!(" {pkg_ver} (rev {git_hash})\nCommit Date: {commit_date}\nAvailable Connectors: {avail_cons}")
}
fn port_in_range(s: &str) -> Result<u16, String> {
let port: usize = s
.parse()
.map_err(|_| format!("`{s}` isn't a port number"))?;
if PORT_RANGE.contains(&port) {
Ok(port as u16)
} else {
Err(format!(
"port not in range {}-{}",
PORT_RANGE.start(),
PORT_RANGE.end()
))
}
}
fn valid_path(s: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(s);
if !path.exists() {
return Err("Path does not exist".to_string())
}
if !path.is_dir() {
return Err("Path is not a directory".to_string())
}
Ok(path)
}
fn poll_in_range(s: &str) -> Result<u16, String> {
let port: usize = s
.parse()
.map_err(|_| format!("`{s}` isn't a valid number"))?;
if POLL_RANGE.contains(&port) {
Ok(port as u16)
} else {
Err(format!(
"not in range {}-{}",
POLL_RANGE.start(),
POLL_RANGE.end()
))
}
}
/// Wrapper because log::LevelFilter doesn't implement ValueEnum
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
pub enum Loglevel {
Error,
#[default]
Warn,
Info,
Debug,
Trace,
}
impl From<Loglevel> for log::LevelFilter {
fn from(val: Loglevel) -> Self {
match val {
Loglevel::Error => log::LevelFilter::Error,
Loglevel::Warn => log::LevelFilter::Warn,
Loglevel::Info => log::LevelFilter::Info,
Loglevel::Debug => log::LevelFilter::Debug,
Loglevel::Trace => log::LevelFilter::Trace,
}
}
}

68
radarflow/src/comms.rs Normal file
View File

@@ -0,0 +1,68 @@
use csflow::{enums::PlayerType, structs::Vec3};
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayerData {
pos: Vec3,
yaw: f32,
#[serde(rename = "playerType")]
player_type: PlayerType,
#[serde(rename = "hasBomb")]
has_bomb: bool
}
impl PlayerData {
pub fn new(pos: Vec3, yaw: f32, player_type: PlayerType, has_bomb: bool) -> PlayerData {
PlayerData { pos, yaw, player_type, has_bomb }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BombData {
pos: Vec3,
#[serde(rename = "isPlanted")]
is_planted: bool
}
#[allow(dead_code)]
impl BombData {
pub fn new(pos: Vec3, is_planted: bool) -> BombData {
BombData { pos, is_planted }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EntityData {
Player(PlayerData),
Bomb(BombData)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RadarData {
ingame: bool,
#[serde(rename = "mapName")]
map_name: String,
#[serde(rename(serialize = "entityData"))]
player_data: Vec<EntityData>,
//#[serde(rename(serialize = "localYaw"))]
//local_yaw: f32,
}
impl RadarData {
pub fn new(ingame: bool, map_name: String, player_data: Vec<EntityData>) -> RadarData {
RadarData { ingame, map_name, player_data }
}
/// Returns empty RadarData, it's also the same data that gets sent to clients when not ingame
pub fn empty() -> RadarData {
RadarData {
ingame: false,
map_name: String::new(),
player_data: Vec::new(),
}
}
}

View File

@@ -0,0 +1,85 @@
use csflow::{memflow::Address, enums::PlayerType};
#[derive(Clone, Copy)]
pub enum CachedEntity {
Bomb {ptr: Address},
Player {ptr: Address, player_type: PlayerType},
}
pub struct Cache {
timestamp: std::time::Instant,
entity_cache: Vec<CachedEntity>,
map_name: String,
entity_list: Address,
}
impl Cache {
pub fn is_valid(&self) -> bool {
if self.timestamp.elapsed() > std::time::Duration::from_millis(250) {
return false;
}
true
}
pub fn new_invalid() -> Cache {
Cache {
timestamp: std::time::Instant::now().checked_sub(std::time::Duration::from_millis(500)).unwrap(),
entity_cache: Vec::new(),
map_name: String::new(),
entity_list: Address::null(),
}
}
pub fn entity_cache(&mut self) -> Vec<CachedEntity> {
self.entity_cache.clone()
}
pub fn map_name(&self) -> String {
self.map_name.clone()
}
pub fn entity_list(&self) -> Address {
self.entity_list
}
}
pub struct CacheBuilder {
entity_cache: Option<Vec<CachedEntity>>,
map_name: Option<String>,
entity_list: Option<Address>
}
impl CacheBuilder {
pub fn new() -> CacheBuilder {
CacheBuilder {
entity_cache: None,
map_name: None,
entity_list: None,
}
}
pub fn entity_cache(mut self, entity_cache: Vec<CachedEntity>) -> CacheBuilder {
self.entity_cache = Some(entity_cache);
self
}
pub fn map_name(mut self, map_name: String) -> CacheBuilder {
self.map_name = Some(map_name);
self
}
pub fn entity_list(mut self, entity_list: Address) -> CacheBuilder {
self.entity_list = Some(entity_list);
self
}
pub fn build(self) -> anyhow::Result<Cache> {
Ok(Cache {
timestamp: std::time::Instant::now(),
entity_cache: self.entity_cache.ok_or(anyhow::anyhow!("entity_cache not set on builder"))?,
map_name: self.map_name.ok_or(anyhow::anyhow!("map_name not set on builder"))?,
entity_list: self.entity_list.ok_or(anyhow::anyhow!("entity_list not set on builder"))?,
})
}
}

202
radarflow/src/dma/mod.rs Normal file
View File

@@ -0,0 +1,202 @@
use std::sync::Arc;
use csflow::{CheatCtx, Connector, memflow::Process, traits::{MemoryClass, BaseEntity}, enums::PlayerType, structs::{CBaseEntity, CPlayerController}};
use tokio::{sync::RwLock, time::{Duration, Instant}};
use crate::{comms::{RadarData, EntityData, BombData, PlayerData}, dma::cache::CacheBuilder};
use self::cache::Cache;
mod cache;
const SECOND_AS_NANO: u64 = 1000*1000*1000;
static ONCE: std::sync::Once = std::sync::Once::new();
pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, data_lock: Arc<RwLock<RadarData>>) -> anyhow::Result<()> {
let mut ctx = CheatCtx::setup(connector, pcileech_device)?;
println!("---------------------------------------------------");
println!("Found cs2.exe at {:X}", ctx.process.info().address);
println!("Found engine module at cs2.exe+{:X}", ctx.engine_module.base);
println!("Found client module at cs2.exe+{:X}", ctx.client_module.base);
println!("---------------------------------------------------");
// Avoid printing warnings and other stuff before the initial prints are complete
tokio::time::sleep(Duration::from_millis(500)).await;
// For poll rate timing
let should_time = poll_rate != 0;
let target_interval = Duration::from_nanos(SECOND_AS_NANO / poll_rate as u64);
let mut last_iteration_time = Instant::now();
let mut missmatch_count = 0;
let mut cache = Cache::new_invalid();
loop {
if ctx.process.state().is_dead() {
break;
}
if !cache.is_valid() {
let mut cached_entities = Vec::new();
let globals = ctx.get_globals()?;
let highest_index = ctx.highest_entity_index()?;
let map_name = ctx.map_name(globals)?;
let entity_list = ctx.get_entity_list()?;
let local = ctx.get_local()?;
if local.get_pawn(&mut ctx, entity_list)?.is_some() {
cached_entities.push(cache::CachedEntity::Player {
ptr: local.ptr(),
player_type: PlayerType::Local
});
for idx in 1..=highest_index {
if let Some(entity) = CBaseEntity::from_index(&mut ctx, entity_list, idx)? {
let class_name = entity.class_name(&mut ctx)?;
match class_name.as_str() {
"weapon_c4" => {
cached_entities.push(cache::CachedEntity::Bomb {
ptr: entity.ptr()
})
},
"cs_player_controller" => {
let controller = entity.to_player_controller();
let player_type = {
match controller.get_player_type(&mut ctx, &local)? {
Some(t) => {
if t == PlayerType::Spectator { continue } else { t }
},
None => { continue },
}
};
cached_entities.push(cache::CachedEntity::Player {
ptr: entity.ptr(),
player_type,
})
}
_ => {}
}
}
}
}
cache = CacheBuilder::new()
.entity_cache(cached_entities)
.entity_list(entity_list)
.map_name(map_name)
.build()?;
log::debug!("Rebuilt cache.");
}
if ctx.network_is_ingame()? {
let mut radar_data = Vec::with_capacity(64);
if ctx.is_bomb_planted()? {
let bomb = ctx.get_plantedc4()?;
let bomb_pos = bomb.pos(&mut ctx)?;
radar_data.push(
EntityData::Bomb(BombData::new(
bomb_pos,
true
))
);
}
for cached_data in cache.entity_cache() {
match cached_data {
cache::CachedEntity::Bomb { ptr } => {
if ctx.is_bomb_dropped()? {
let bomb_entity = CBaseEntity::new(ptr);
let pos = bomb_entity.pos(&mut ctx)?;
radar_data.push(
EntityData::Bomb(
BombData::new(
pos,
false
)
)
);
}
},
cache::CachedEntity::Player { ptr, player_type } => {
let controller = CPlayerController::new(ptr);
if let Some(pawn) = controller.get_pawn(&mut ctx, cache.entity_list())? {
if pawn.is_alive(&mut ctx)? {
let pos = pawn.pos(&mut ctx)?;
let yaw = pawn.angles(&mut ctx)?.y;
let has_bomb = pawn.has_c4(&mut ctx, cache.entity_list())?;
radar_data.push(
EntityData::Player(
PlayerData::new(
pos,
yaw,
player_type,
has_bomb
)
)
);
}
}
},
}
}
let mut data = data_lock.write().await;
if cache.map_name() == "<empty>" || cache.map_name().is_empty() {
*data = RadarData::empty()
} else {
*data = RadarData::new(true, cache.map_name(), radar_data)
}
} else {
let mut data = data_lock.write().await;
*data = RadarData::empty()
}
if should_time {
let elapsed = last_iteration_time.elapsed();
let remaining = match target_interval.checked_sub(elapsed) {
Some(t) => t,
None => {
if missmatch_count >= 25 {
ONCE.call_once(|| {
log::warn!("Remaining time till target interval was negative more than 25 times");
log::warn!("You should decrease your poll rate.");
log::warn!("elapsed: {}ns", elapsed.as_nanos());
log::warn!("target: {}ns", target_interval.as_nanos());
});
} else {
missmatch_count += 1;
}
Duration::from_nanos(0)
},
};
#[cfg(target_os = "linux")]
tokio_timerfd::sleep(remaining).await?;
#[cfg(not(target_os = "linux"))]
tokio::time::sleep(remaining).await;
log::info!("poll rate: {:.2}Hz", SECOND_AS_NANO as f64 / last_iteration_time.elapsed().as_nanos() as f64);
log::trace!("elapsed: {}ns", elapsed.as_nanos());
log::trace!("target: {}ns", target_interval.as_nanos());
log::trace!("missmatch count: {}", missmatch_count);
last_iteration_time = Instant::now();
}
}
Ok(())
}

57
radarflow/src/main.rs Normal file
View File

@@ -0,0 +1,57 @@
use std::sync::Arc;
use clap::Parser;
use cli::Cli;
use comms::RadarData;
use tokio::sync::RwLock;
mod comms;
mod cli;
mod dma;
mod websocket;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
simple_logger::SimpleLogger::new()
.with_level(cli.loglevel.into())
.init()
.expect("Initializing logger");
let rwlock = Arc::new(
RwLock::new(
RadarData::empty()
)
);
let rwlock_clone = rwlock.clone();
let dma_handle = tokio::spawn(async move {
if let Err(err) = dma::run(cli.connector, cli.pcileech_device, cli.poll_rate, rwlock_clone).await {
log::error!("Error in dma thread: [{}]", err.to_string());
}
});
tokio::spawn(async move {
let future = websocket::run(cli.web_path, cli.port, rwlock);
if let Ok(my_local_ip) = local_ip_address::local_ip() {
let address = format!("http://{}:{}", my_local_ip, cli.port);
println!("Launched webserver at {}", address);
} else {
let address = format!("http://0.0.0.0:{}", cli.port);
println!("launched webserver at! {}", address);
}
if let Err(err) = future.await {
log::error!("Error in websocket server: [{}]", err.to_string());
}
});
if let Err(err) = dma_handle.await {
log::error!("Error when waiting for dma thread: {}", err.to_string());
}
Ok(())
}

View File

@@ -0,0 +1,66 @@
use std::{sync::Arc, path::PathBuf};
use axum::{
extract::{ws::{WebSocketUpgrade, WebSocket, Message}, State},
response::Response,
routing::get,
Router,
};
use tokio::sync::RwLock;
use tower_http::services::ServeDir;
use crate::comms::RadarData;
#[derive(Clone)]
struct AppState {
data_lock: Arc<RwLock<RadarData>>
}
async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response {
ws.on_upgrade(|socket| handle_socket(socket, state))
}
async fn handle_socket(mut socket: WebSocket, state: AppState) {
while let Some(msg) = socket.recv().await {
if let Ok(msg) = msg {
if msg == Message::Text("requestInfo".to_string()) {
let data = state.data_lock.read().await;
let str = {
match serde_json::to_string(&*data) {
Ok(json) => json,
Err(e) => {
log::error!("Could not serialize data into json: {}", e.to_string());
log::error!("Sending \"error\" instead");
"error".to_string()
},
}
};
if socket.send(Message::Text(str)).await.is_err() {
// client disconnected
return;
}
}
} else {
// client disconnected
return;
}
}
}
pub async fn run(path: PathBuf, port: u16, data_lock: Arc<RwLock<RadarData>>) -> anyhow::Result<()> {
let app = Router::new()
.nest_service("/", ServeDir::new(path))
.route("/ws", get(ws_handler))
.with_state(AppState { data_lock });
let address = format!("0.0.0.0:{}", port);
axum::Server::bind(&address.parse()?)
.serve(app.into_make_service())
.await?;
Ok(())
}