From 26fa9563e3c4da275df25017e4011860bc1a6d49 Mon Sep 17 00:00:00 2001 From: samyyc <3356207189@qq.com> Date: Sat, 8 Aug 2026 17:48:55 +0800 Subject: [PATCH] Add JSON string dump binding --- Cargo.lock | 26 +++++ README.md | 5 +- headers/s2binlib_static.h | 6 ++ s2binlib/Cargo.toml | 1 + s2binlib/src/s2binlib.rs | 105 +++++++++++++++++++-- s2binlib_binding/src/c_bindings.rs | 32 ++++++- s2binlib_binding/src/compat/s2binlib003.rs | 19 ++++ 7 files changed, 181 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20ac7c6..c9e439d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "lazy_static" version = "1.5.0" @@ -213,6 +219,7 @@ dependencies = [ "object", "region", "serde", + "serde_json", "winapi", "windows", ] @@ -263,6 +270,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -496,3 +516,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/README.md b/README.md index 88eff57..fe7fa34 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ int main() { void* vtable_addr; s2binlib_find_vtable("server", "CBaseEntity", &vtable_addr); + // Dump every printable ASCII string and its RVA to a JSON file + s2binlib_dump_strings_to_json("server", "server_strings.json"); + // Free after use, this will only release the file bytes in memory // Dumped xref and other information will still be cached s2binlib_unload_all_binaries(); @@ -139,4 +142,4 @@ Or use the `CreateInterface` function for dynamic loading: // windows example S2CreateInterfaceFn createInterface = (S2CreateInterfaceFn)GetProcAddress(hDll, "S2BinLib_CreateInterface"); auto s2binlib = createInterface(S2BINLIB_INTERFACE_NAME); -``` \ No newline at end of file +``` diff --git a/headers/s2binlib_static.h b/headers/s2binlib_static.h index 0d86bc5..fe704c6 100644 --- a/headers/s2binlib_static.h +++ b/headers/s2binlib_static.h @@ -254,6 +254,12 @@ int s2binlib_find_string_rva(const char* binary_name, const char* string, void** /// @return 0 on success, negative error code on failure int s2binlib_find_string(const char* binary_name, const char* string, void** result); +/// Dump all printable ASCII strings and their relative virtual addresses to a JSON file +/// @param binary_name Name of the binary to scan +/// @param output_path UTF-8 path of the JSON file to create or overwrite +/// @return 0 on success, negative error code on failure +int s2binlib_dump_strings_to_json(const char* binary_name, const char* output_path); + // ============================================================================ // Module Base Address Functions // ============================================================================ diff --git a/s2binlib/Cargo.toml b/s2binlib/Cargo.toml index 6ac91f3..be7cb56 100644 --- a/s2binlib/Cargo.toml +++ b/s2binlib/Cargo.toml @@ -16,6 +16,7 @@ cpp_demangle = "0.4" msvc-demangler = "0.10" serde = { version = "1.0", optional = true, features = ["derive"] } hashbrown = "0.16.0" +serde_json = "1.0" [target.'cfg(target_os = "windows")'.dependencies] diff --git a/s2binlib/src/s2binlib.rs b/s2binlib/src/s2binlib.rs index fd13ab8..4bf495c 100644 --- a/s2binlib/src/s2binlib.rs +++ b/s2binlib/src/s2binlib.rs @@ -20,10 +20,14 @@ use anyhow::{Result, bail}; use hashbrown::HashMap; use iced_x86::{Code, Decoder, DecoderOptions, Instruction, OpKind, Register}; -use object::{ - Object, ObjectSection, ObjectSymbol, SectionKind, read::pe::ImageOptionalHeader, +use object::{Object, ObjectSection, ObjectSymbol, SectionKind, read::pe::ImageOptionalHeader}; +use std::{ + cell::Cell, + collections::BTreeMap, + fs::{self, File}, + io::{BufWriter, Write}, + path::{Path, PathBuf}, }; -use std::{cell::Cell, fs, path::PathBuf}; use crate::{ VTableInfo, find_pattern_simd, is_executable, @@ -58,7 +62,7 @@ pub struct S2BinLib<'a> { pub(crate) custom_binary_paths_linux: HashMap, pub(crate) vtables: HashMap>, pub(crate) name_to_vtables: HashMap, - /// Cached ASCII strings: binary_name -> (string_rva -> string) + /// Cached ASCII strings: binary_name -> (string -> string_rva) pub(crate) strings_cache: HashMap>, pub(crate) calls_targets_cache: HashMap>, } @@ -79,6 +83,22 @@ fn read_int64(data: &[u8], offset: u64) -> i64 { rvalue } +fn write_strings_to_json>( + strings: &HashMap, + output_path: P, +) -> Result<()> { + let ordered_strings: BTreeMap<&str, u64> = strings + .iter() + .map(|(string, rva)| (string.as_str(), *rva)) + .collect(); + + let mut output = BufWriter::new(File::create(output_path)?); + serde_json::to_writer_pretty(&mut output, &ordered_strings)?; + output.flush()?; + + Ok(()) +} + impl<'a> S2BinLib<'a> { fn get_os_name(&self) -> String { match self.os.as_str() { @@ -1106,6 +1126,24 @@ impl<'a> S2BinLib<'a> { Ok(()) } + /// Dump all printable ASCII strings and their RVAs to a JSON file. + /// + /// The binary is scanned before every write so the output always reflects + /// the currently loaded binary. JSON object keys are sorted to make dumps + /// deterministic across runs. + pub fn dump_strings_to_json>( + &mut self, + binary_name: &str, + output_path: P, + ) -> Result<()> { + self.dump_strings(binary_name)?; + + let strings = self + .get_strings(binary_name) + .ok_or_else(|| anyhow::anyhow!("Strings were not cached."))?; + write_strings_to_json(strings, output_path) + } + pub fn get_strings(&self, binary_name: &str) -> Option<&HashMap> { self.strings_cache.get(binary_name) } @@ -1522,12 +1560,18 @@ impl<'a> S2BinLib<'a> { } pub fn find_func_start_rva(&self, binary_name: &str, include_rva: u64) -> Result { - Ok(std::cmp::max( - self.find_xref_func_start_rva(binary_name, include_rva)?, - self.find_vfunc_start_rva(binary_name, include_rva) - .unwrap() - .2, - )) + let xref_start = self.find_xref_func_start_rva(binary_name, include_rva)?; + let vfunc_start = self + .find_vfunc_start_rva(binary_name, include_rva) + .map(|(_, _, rva)| rva) + .unwrap_or_default(); + let func_start = std::cmp::max(xref_start, vfunc_start); + + if func_start == 0 { + bail!("No function found."); + } + + Ok(func_start) } pub fn find_func_start(&self, binary_name: &str, include_rva: u64) -> Result { @@ -1612,3 +1656,44 @@ impl<'a> S2BinLib<'a> { ) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strings_json_is_sorted_and_contains_rvas() { + let mut strings = HashMap::new(); + strings.insert("zeta".to_string(), 0x200); + strings.insert("alpha".to_string(), 0x100); + let output_path = + std::env::temp_dir().join(format!("s2binlib-strings-{}.json", std::process::id())); + + write_strings_to_json(&strings, &output_path).unwrap(); + + let output = fs::read_to_string(&output_path).unwrap(); + let parsed: BTreeMap = serde_json::from_str(&output).unwrap(); + assert_eq!(parsed.get("alpha"), Some(&0x100)); + assert_eq!(parsed.get("zeta"), Some(&0x200)); + assert!(output.find("alpha").unwrap() < output.find("zeta").unwrap()); + + fs::remove_file(output_path).unwrap(); + } + + #[test] + fn find_func_start_rva_falls_back_to_xref_without_vtable_match() { + let mut library = S2BinLib::new(".", "csgo", "windows"); + library + .calls_targets_cache + .insert("server".to_string(), vec![0x100]); + + assert_eq!(library.find_func_start_rva("server", 0x200).unwrap(), 0x100); + } + + #[test] + fn find_func_start_rva_returns_error_without_candidates() { + let library = S2BinLib::new(".", "csgo", "windows"); + + assert!(library.find_func_start_rva("server", 0x200).is_err()); + } +} diff --git a/s2binlib_binding/src/c_bindings.rs b/s2binlib_binding/src/c_bindings.rs index 9900b3a..4e3b01a 100644 --- a/s2binlib_binding/src/c_bindings.rs +++ b/s2binlib_binding/src/c_bindings.rs @@ -17,7 +17,7 @@ ***********************************************************************************/ use crate::compat::s2binlib003::{PatternScanCallback, S2BinLib003}; -use std::ffi::{c_char, c_void}; +use std::ffi::{CStr, c_char, c_void}; use std::sync::Mutex; // Safety: S2BinLib003 contains a raw pointer to a vtable, but the vtable is static @@ -370,6 +370,34 @@ wrap_method_mut!( result: *mut *mut c_void ); +/// Dump all printable ASCII strings and their RVAs to a JSON file. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn s2binlib_dump_strings_to_json( + binary_name: *const c_char, + output_path: *const c_char, +) -> i32 { + if binary_name.is_null() || output_path.is_null() { + return -2; + } + + let binary_name = unsafe { + match CStr::from_ptr(binary_name).to_str() { + Ok(value) => value, + Err(_) => return -2, + } + }; + let output_path = unsafe { + match CStr::from_ptr(output_path).to_str() { + Ok(value) => value, + Err(_) => return -2, + } + }; + + with_global_instance!(|instance: &mut S2BinLib003| { + instance.dump_strings_to_json(binary_name, output_path) + }) +} + // ============================================================================ // Module Base Address Functions // ============================================================================ @@ -693,4 +721,4 @@ wrap_method_mut!( func_rva: u64, result_out: *mut c_char, result_out_size: usize -); \ No newline at end of file +); diff --git a/s2binlib_binding/src/compat/s2binlib003.rs b/s2binlib_binding/src/compat/s2binlib003.rs index 25c74c2..cea79cf 100644 --- a/s2binlib_binding/src/compat/s2binlib003.rs +++ b/s2binlib_binding/src/compat/s2binlib003.rs @@ -108,6 +108,25 @@ impl S2BinLib003 { s2binlib: None, } } + + pub(crate) fn dump_strings_to_json(&mut self, binary_name: &str, output_path: &str) -> i32 { + let Some(s2binlib) = self.s2binlib.as_mut() else { + c_debug!("Error -1: S2BinLib003 is not initialized"); + return -1; + }; + + if !s2binlib.is_binary_loaded(binary_name) { + s2binlib.load_binary(binary_name); + } + + match s2binlib.dump_strings_to_json(binary_name, output_path) { + Ok(()) => 0, + Err(error) => { + c_debug!("Error -4: Failed to dump strings to JSON: {}", error); + -4 + } + } + } } #[repr(C)]