From 1184af99d3788931ee691f5922c55d9ea06b3003 Mon Sep 17 00:00:00 2001 From: xavo95 Date: Fri, 16 May 2025 01:31:53 +0700 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 218 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 17 ++++ src/lib.rs | 110 ++++++++++++++++++++++++ src/opts.rs | 27 ++++++ src/replacer.rs | 82 ++++++++++++++++++ src/utils.rs | 11 +++ 7 files changed, 466 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/opts.rs create mode 100644 src/replacer.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c41cc9e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..23b6342 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,218 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "curly-injector" +version = "0.1.0" +dependencies = [ + "ilhook", + "interceptor-rs", + "regex", +] + +[[package]] +name = "iced-x86" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "ilhook" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7900295ca4c0c37e0325ddbf4b2b78cecabdb36d75d02712c31fb7d777e658f6" +dependencies = [ + "bitflags", + "iced-x86", + "lazy_static", + "libc", + "regex", + "thiserror", + "windows-sys", +] + +[[package]] +name = "interceptor-rs" +version = "0.1.0" +source = "git+https://git.xeondev.com/ReversedRoomsMisc/interceptor-rs.git#418aef083cc6768201f0512fdbdca5c03aa4f787" +dependencies = [ + "ilhook", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d21825b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "curly-injector" +version = "0.1.0" +edition = "2024" + +[features] + +[dependencies] +ilhook = "2.1.1" +interceptor-rs = { git = "https://git.xeondev.com/ReversedRoomsMisc/interceptor-rs.git" } +regex = "1.11.1" + +[profile.release] +strip = true # Automatically strip symbols from the binary. +lto = true # Link-time optimization. +opt-level = 3 # Optimization level 3. +codegen-units = 1 # Maximum size reduction optimizations. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cf617f8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,110 @@ +mod opts; + +pub mod replacer; +pub mod utils; + +use ilhook::x64::Registers; +use replacer::{AbstractReplacer, Replacer}; +use std::ffi::{c_char, CStr, CString}; +use std::sync::OnceLock; + +type CurlEasySetStr = fn(handle: usize, tag: u64, value: *const c_char); + +#[derive(Debug)] +pub struct CurlConfig { + pub handle_rcx_relative_offset: u64, + pub url_handle_relative_offset: usize, + pub http_headers_handle_relative_offset: Option, + pub curl_easy_setopt: usize, + pub curl_easy_perform: usize, +} + +#[derive(Debug)] +pub struct ProxyConfig { + pub proxy_url: &'static str, + pub skip_url_replace: bool, +} + +#[derive(Debug)] +pub struct CurlUserData { + pub set_opt_ptr: CurlEasySetStr, + pub curl_config: &'static CurlConfig, + pub replacer: Vec, + pub proxy: Option, +} + +pub fn hook_curl( + interceptor: &mut interceptor_rs::Interceptor, + base_addr: usize, + config: &'static CurlConfig, + replacer: Vec, + proxy_config: Option, + lock: &OnceLock, +) { + let set_opt_ptr = base_addr + config.curl_easy_setopt; + let set_opt_transmute = unsafe { std::mem::transmute::(set_opt_ptr) }; + lock.set(CurlUserData { + set_opt_ptr: set_opt_transmute, + curl_config: config, + replacer, + proxy: proxy_config, + }).unwrap(); + interceptor + .attach( + base_addr + config.curl_easy_perform, + on_curl_easy_perform, + Some(lock as *const _ as usize), + ) + .unwrap(); +} + +unsafe extern "win64" fn on_curl_easy_perform(reg: *mut Registers, user_data: usize) { + let data = unsafe { + (*(user_data as *mut OnceLock)).get_mut().unwrap() + }; + + let curl_handle = if data.curl_config.handle_rcx_relative_offset == 0 { + // If relative offset is 0, it means that rcx already contains the handle. + unsafe { (*reg).rcx as usize } + } else { + // Otherwise it means it comes from another struct, so we need to dereference it. + unsafe { *(((*reg).rcx + data.curl_config.handle_rcx_relative_offset) as *const usize) } + }; + let url_ptr = + unsafe { *((curl_handle + data.curl_config.url_handle_relative_offset) as *const usize) }; + let url = unsafe { CStr::from_ptr(url_ptr as *const i8) } + .to_str() + .unwrap(); + println!("[curl_easy_perform] Original URL: {url}"); + + if let Some(proxy) = &data.proxy { + println!( + "[curl_easy_perform] Setting proxy for url: {}, skip url replace: {}", + proxy.proxy_url, proxy.skip_url_replace + ); + let url = CString::new(proxy.proxy_url).unwrap(); + (data.set_opt_ptr)(curl_handle, opts::CURL_OPT_PROXY, url.as_ptr()); + if proxy.skip_url_replace { + return; + } + } + + for replacer in &mut data.replacer { + if let Ok(result) = replacer.replace(url) { + // URL Replacement + println!("[curl_easy_perform] Replacement URL: {result}"); + let url = CString::new(result.as_str()).unwrap(); + (data.set_opt_ptr)(curl_handle, opts::CURL_OPT_URL, url.as_ptr()); + + // if let Some(offset) = data.curl_config.http_headers_handle_relative_offset { + // let headers_ptr = unsafe { *(((*reg).rcx + offset) as *const usize) }; + // println!("[curl_easy_perform] Headers URL: {headers_ptr}"); + // println!("[curl_easy_perform] Headers URL: {}", (*reg).rcx + offset); + // // let headers = unsafe { std::slice::from_raw_parts(*(headers_ptr as *const usize) as *const u8, 1000) }; + // // println!("Headers: {headers:02X?}"); + // } + return; + }; + } + println!("No valid url match found"); +} diff --git a/src/opts.rs b/src/opts.rs new file mode 100644 index 0000000..4dd2ec1 --- /dev/null +++ b/src/opts.rs @@ -0,0 +1,27 @@ +#![allow(unused)] + +const CURL_OPT_TYPE_LONG: u64 = 0; +const CURL_OPT_TYPE_OBJECT_POINT: u64 = 10000; +const CURL_OPT_TYPE_FUNCTION_POINT: u64 = 20000; +const CURL_OPT_TYPE_OFF_T: u64 = 30000; +const CURL_OPT_TYPE_BLOB: u64 = 40000; + +const CURL_OPT_TYPE_STRING_POINT: u64 = CURL_OPT_TYPE_OBJECT_POINT; +const CURL_OPT_TYPE_S_LIST_POINT: u64 = CURL_OPT_TYPE_OBJECT_POINT; +const CURL_OPT_TYPE_CB_POINT: u64 = CURL_OPT_TYPE_OBJECT_POINT; +const CURL_OPT_TYPE_VALUES: u64 = CURL_OPT_TYPE_LONG; + +#[macro_export] +macro_rules! curl_opt { + ($name:ident, $typ:ident, $value:literal) => { + pub(crate) const $name: u64 = $typ + $value; + }; +} + +curl_opt!(CURL_OPT_URL, CURL_OPT_TYPE_STRING_POINT, 2); +curl_opt!(CURL_OPT_PROXY, CURL_OPT_TYPE_STRING_POINT, 4); +curl_opt!(CURL_OPT_HTTP_HEADER, CURL_OPT_TYPE_S_LIST_POINT, 23); +curl_opt!(CURL_OPT_SSL_VERIFY_PEER, CURL_OPT_TYPE_LONG, 64); +curl_opt!(CURL_OPT_CA_INFO, CURL_OPT_TYPE_STRING_POINT, 65); +curl_opt!(CURL_OPT_SSL_VERIFY_HOST, CURL_OPT_TYPE_LONG, 81); +curl_opt!(CURL_OPT_CA_PATH, CURL_OPT_TYPE_STRING_POINT, 97); \ No newline at end of file diff --git a/src/replacer.rs b/src/replacer.rs new file mode 100644 index 0000000..cf9e66b --- /dev/null +++ b/src/replacer.rs @@ -0,0 +1,82 @@ +pub trait Replacer { + fn replace(&mut self, original: &str) -> Result; +} + +#[derive(Debug, Clone)] +pub enum AbstractReplacer { + GenericReplacer(GenericReplacer), + GenericCdnReplacer(GenericCdnReplacer), +} + +#[derive(Debug, Clone)] +pub struct GenericReplacer { + pub regex: regex::Regex, + pub replacement: String, + pub force_http: bool, +} + +#[derive(Debug, Clone)] +pub struct GenericCdnReplacer { + pub regex: regex::Regex, + pub replacement: Vec, + pub force_http: bool, + pub url_index: u8, +} + +impl Replacer for AbstractReplacer { + fn replace(&mut self, original: &str) -> Result { + match self { + AbstractReplacer::GenericReplacer(replacer) => replacer.replace(original), + AbstractReplacer::GenericCdnReplacer(replacer) => replacer.replace(original) + } + } +} + +impl Replacer for GenericReplacer { + fn replace(&mut self, original: &str) -> Result { + // Prepare output array + let mut results: Vec = vec![]; + // Perform the capture over input + for (_, [scheme, path]) in self.regex.captures_iter(original).map(|c| c.extract()) { + results.push(format!( + "{}://{}/{}", + if self.force_http { "http" } else { scheme }, + self.replacement, + path + )); + } + // We are supposed to only parse one entry from text + if 1 == results.len() { + Ok(results.remove(0)) + } else if results.is_empty() { + Err("No valid url match found so returning original url".to_string()) + } else { + Err(format!("Invalid number of entries parsed, expected 1, obtained {:?}", results.len())) + } + } +} + +impl Replacer for GenericCdnReplacer { + fn replace(&mut self, original: &str) -> Result { + // Prepare output array + let mut results: Vec = vec![]; + // Perform the capture over input + for (_, [scheme, path]) in self.regex.captures_iter(original).map(|c| c.extract()) { + results.push(format!( + "{}://{}/{}", + if self.force_http { "http" } else { scheme }, + self.replacement[self.url_index as usize], + path + )); + } + // We are supposed to only parse one entry from text + if 1 == results.len() { + self.url_index = (self.url_index + 1) % self.replacement.len() as u8; + Ok(results.remove(0)) + } else if results.is_empty() { + Err("No valid url match found".to_string()) + } else { + Err(format!("Invalid number of entries parsed, expected 1, obtained {:?}", results.len())) + } + } +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..6b89c75 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,11 @@ +use std::env::VarError; + +pub trait EnvBoolMapper { + fn map_env_bool(self, default: bool) -> bool; +} + +impl EnvBoolMapper for Result { + fn map_env_bool(self, default: bool) -> bool { + self.map(|val| matches!(val.as_str(), "y")).unwrap_or(default) + } +}