From 8565eba0f32ec5944928870523feb7720ef22afc Mon Sep 17 00:00:00 2001 From: xavo95 Date: Thu, 2 Jan 2025 23:52:10 +0100 Subject: [PATCH] Moved tools from private repo to public --- .gitignore | 3 + Cargo.lock | 254 ++++++++++++++++++++++++++++++++++ Cargo.toml | 30 ++++ README.md | 28 ++++ aes-key-finder/Cargo.toml | 9 ++ aes-key-finder/src/lib.rs | 110 +++++++++++++++ cursor/Cargo.toml | 8 ++ cursor/src/lib.rs | 141 +++++++++++++++++++ offset-finder/Cargo.toml | 16 +++ offset-finder/src/json/mod.rs | 32 +++++ offset-finder/src/lib.rs | 155 +++++++++++++++++++++ pe-utils/Cargo.toml | 8 ++ pe-utils/src/lib.rs | 130 +++++++++++++++++ restorer/Cargo.toml | 11 ++ restorer/src/lib.rs | 99 +++++++++++++ 15 files changed, 1034 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 aes-key-finder/Cargo.toml create mode 100644 aes-key-finder/src/lib.rs create mode 100644 cursor/Cargo.toml create mode 100644 cursor/src/lib.rs create mode 100644 offset-finder/Cargo.toml create mode 100644 offset-finder/src/json/mod.rs create mode 100644 offset-finder/src/lib.rs create mode 100644 pe-utils/Cargo.toml create mode 100644 pe-utils/src/lib.rs create mode 100644 restorer/Cargo.toml create mode 100644 restorer/src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02e822c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +.idea +*.exe \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3d375d0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,254 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aes-key-finder" +version = "0.1.0" +dependencies = [ + "goblin", + "offset-finder", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "cursor" +version = "0.1.0" +dependencies = [ + "thiserror 2.0.9", + "widestring", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "goblin" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ab3f32d1d77146981dea5d6b1e8fe31eedcb7013e5e00d6ccd1259a4b4d923" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "offset-finder" +version = "0.1.0" +dependencies = [ + "goblin", + "log", + "patternscanner", + "pe-utils", + "serde", + "thiserror 2.0.9", +] + +[[package]] +name = "patternscanner" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231e020a85ebd759abfe6da0220754bfee066eb5b6e4970a673c09a96da50033" +dependencies = [ + "rayon", + "thiserror 1.0.69", +] + +[[package]] +name = "pe-utils" +version = "0.1.0" +dependencies = [ + "goblin", + "thiserror 2.0.9", +] + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "restorer" +version = "0.1.0" +dependencies = [ + "goblin", + "log", + "pe-utils", + "thiserror 2.0.9", +] + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" +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 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", +] + +[[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 = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7e85566 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[workspace] +resolver = "2" +members = [ + "aes-key-finder", + "cursor", + "offset-finder", + "pe-utils", + "restorer" +] + +[workspace.package] +version = "0.1.0" +edition = "2021" + +[workspace.dependencies] +goblin = "0.9.2" +log = "0.4.22" +patternscanner = "0.5.0" +serde = { version = "1.0.217", features = ["derive"] } +thiserror = "2.0.9" +widestring = "1.1.0" + +offset-finder = { path = "offset-finder" } +pe-utils = { path = "pe-utils" } + +[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/README.md b/README.md new file mode 100644 index 0000000..eb13de8 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Reverse Assembling Program Engineering(RAPE) Toolkit + +This repository contains a subset of my private tools for Reverse Engineering, a new project(codename: Symphonic) will +make an appearance soon. As such, all required dependencies are made public ahead of time. + +Also on the past 6 months I received questions on: How do you find this? How do you get AES keys? How do you fix the +dump? So I hope this helps people wanting to learn to + +## Quick tool summary + +- AES Key Finder + - Should be self-explanatory but, basically after parsing a PE file you can pass image base, sections, and + data(raw binary) and the filter(this tool includes Restricted and Relax filters, but you can add more) + - To get the 3 first params, please refer to PE Utils down below +- Cursor + - An implementation for a cursor Read + Write. Rust already has one, but I needed something like string and + wide string parsing, so I created my own +- Offset Finder + - This library allows to find patterns in executables + - Allows to find either exact or partial matches by leveraging wildcards(??) + - Also has options for silent reporting(skip_print_offset) or allow multiple matches + - Leveraging PE Utils it returns both in file and RVA of the pattern found +- PE Utils + - Subset of functions to work with PE files. Not very valuable alone, but it allows to omit repetitive code for the + rest of projects +- Restorer + - Allows to go from a memory dump(Frida, and others(Including private dumpers)) to a file that has section table + fixed for more analysis with other tools \ No newline at end of file diff --git a/aes-key-finder/Cargo.toml b/aes-key-finder/Cargo.toml new file mode 100644 index 0000000..9e36200 --- /dev/null +++ b/aes-key-finder/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "aes-key-finder" +version.workspace = true +edition.workspace = true + +[dependencies] +goblin.workspace = true + +offset-finder.workspace = true \ No newline at end of file diff --git a/aes-key-finder/src/lib.rs b/aes-key-finder/src/lib.rs new file mode 100644 index 0000000..938a0df --- /dev/null +++ b/aes-key-finder/src/lib.rs @@ -0,0 +1,110 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::OnceLock; + +use goblin::pe::section_table::SectionTable; + +// TODO: Check for more false positives +const FALSE_POSITIVES: [[u8; 32]; 2] = [ + [0x6F, 0x16, 0x80, 0x73, 0xB9, 0xB2, 0x14, 0x49, 0xD7, 0x42, 0x24, 0x17, 0x00, 0x06, 0x8A, 0xDA, 0xBC, 0x30, 0x6F, 0xA9, 0xAA, 0x38, 0x31, 0x16, 0x4D, 0xEE, 0x8D, 0xE3, 0x4E, 0x0E, 0xFB, 0xB0], + [0x67, 0xE6, 0x09, 0x6A, 0x85, 0xAE, 0x67, 0xBB, 0x72, 0xF3, 0x6E, 0x3C, 0x3A, 0xF5, 0x4F, 0xA5, 0x7F, 0x52, 0x0E, 0x51, 0x8C, 0x68, 0x05, 0x9B, 0xAB, 0xD9, 0x83, 0x1F, 0x19, 0xCD, 0xE0, 0x5B] +]; + +struct Filter { + offsets: HashMap, + locator: offset_finder::OffsetLocator<'static>, +} + +static RESTRICTED_FILTER: OnceLock = OnceLock::new(); +static RELAXED_FILTER: OnceLock = OnceLock::new(); + +fn get_restricted_filter() -> &'static Filter { + RESTRICTED_FILTER.get_or_init(|| { + let mut offsets: HashMap = HashMap::new(); + offsets.insert(0, &[2, 9, 16, 23, 30, 37, 44, 51]); + offsets.insert(1, &[3, 10, 17, 24, 35, 42, 49, 56]); + offsets.insert(2, &[3, 14, 25, 32, 44, 51, 58, 65]); + offsets.insert(3, &[3, 10, 21, 28, 35, 42, 49, 56]); + offsets.insert(4, &[3, 10, 21, 28, 35, 42, 49, 56]); + Filter { + offsets, + locator: offset_finder::OffsetLocator { + name: "AES", + partial_match: vec![ + "c7 01 ?? ?? ?? ?? c7 41 04 ?? ?? ?? ?? c7 41 08 ?? ?? ?? ?? c7 41 0c ?? ?? ?? ?? c7 41 10 ?? ?? ?? ?? c7 41 14 ?? ?? ?? ?? c7 41 18 ?? ?? ?? ?? c7 41 1c ?? ?? ?? ?? c3", + "c7 45 d0 ?? ?? ?? ?? c7 45 d4 ?? ?? ?? ?? c7 45 d8 ?? ?? ?? ?? c7 45 dc ?? ?? ?? ?? 0f ?? ?? ?? c7 45 e0 ?? ?? ?? ?? c7 45 e4 ?? ?? ?? ?? c7 45 e8 ?? ?? ?? ?? c7 45 ec ?? ?? ?? ?? 0f", + "c7 45 d0 ?? ?? ?? ?? ?? ?? ?? ?? c7 45 d4 ?? ?? ?? ?? ?? ?? ?? ?? c7 45 d8 ?? ?? ?? ?? c7 45 dc ?? ?? ?? ?? ?? ?? ?? ?? ?? c7 45 e0 ?? ?? ?? ?? c7 45 e4 ?? ?? ?? ?? c7 45 e8 ?? ?? ?? ?? c7 45 ec ?? ?? ?? ??", + "c7 45 d0 ?? ?? ?? ?? c7 45 d4 ?? ?? ?? ?? ?? ?? ?? ?? c7 45 d8 ?? ?? ?? ?? c7 45 dc ?? ?? ?? ?? c7 45 e0 ?? ?? ?? ?? c7 45 e4 ?? ?? ?? ?? c7 45 e8 ?? ?? ?? ?? c7 45 ec ?? ?? ?? ??", + "c7 45 ?? ?? ?? ?? ?? c7 45 ?? ?? ?? ?? ?? ?? ?? ?? ?? c7 45 ?? ?? ?? ?? ?? c7 45 ?? ?? ?? ?? ?? c7 45 ?? ?? ?? ?? ?? c7 45 ?? ?? ?? ?? ?? c7 45 ?? ?? ?? ?? ?? c7 45 ?? ?? ?? ?? ??", + ], + full_match: "", + skip_offset_print: false, + allow_multiple_matches: true, + }, + } + }) +} + +fn get_relaxed_filter() -> &'static Filter { + RELAXED_FILTER.get_or_init(|| { + let mut offsets: HashMap = HashMap::new(); + offsets.insert(0, &[3, 10, 17, 24, 35, 42, 49, 56]); + offsets.insert(1, &[2, 9, 16, 23, 30, 37, 44, 51]); + offsets.insert(2, &[3, 10, 21, 28, 35, 42, 49, 56]); + Filter { + offsets, + locator: offset_finder::OffsetLocator { + name: "AES", + partial_match: vec![ + "c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ??", + "c7 ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ??", + "c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ?? c7 ?? ?? ?? ?? ?? ??", + ], + full_match: "", + skip_offset_print: false, + allow_multiple_matches: true, + }, + } + }) +} + +pub fn dump_aes_key_restricted(image_base: usize, + sections: &[SectionTable], + data: &[u8]) -> Result>, offset_finder::Error> { + dump_aes_key_internal(image_base, sections, data, get_restricted_filter()) +} + +pub fn dump_aes_key(image_base: usize, + sections: &[SectionTable], + data: &[u8]) -> Result>, offset_finder::Error> { + dump_aes_key_internal(image_base, sections, data, get_relaxed_filter()) +} + +fn dump_aes_key_internal(image_base: usize, + sections: &[SectionTable], + data: &[u8], + filter: &Filter) -> Result>, offset_finder::Error> { + let results = filter.locator.find_all_partial_only(image_base, sections, data)?; + // Probabilistic allocation, 50% or more will be false positives, so preallocate (n / 2) + 1 + let mut output: HashSet> = HashSet::with_capacity((results.len() / 2) + 1); + for outer in results { + let offset = *filter.offsets.get(&outer.0).unwrap(); + for inner in outer.1 { + let mut key = Vec::with_capacity(32); + for tmp in offset { + let tmp = inner.0 + (*tmp as usize); + key.extend_from_slice(&data[tmp..tmp + 4]); + } + let mut should_add = true; + for false_positive in FALSE_POSITIVES { + if false_positive.eq(&key[0..32]) { + should_add = false; + break; + } + } + if should_add { + output.insert(key); + } + } + } + Ok(output) +} \ No newline at end of file diff --git a/cursor/Cargo.toml b/cursor/Cargo.toml new file mode 100644 index 0000000..8ab3fbf --- /dev/null +++ b/cursor/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cursor" +version.workspace = true +edition.workspace = true + +[dependencies] +thiserror.workspace = true +widestring.workspace = true \ No newline at end of file diff --git a/cursor/src/lib.rs b/cursor/src/lib.rs new file mode 100644 index 0000000..94ed412 --- /dev/null +++ b/cursor/src/lib.rs @@ -0,0 +1,141 @@ +use std::io::{Read, Seek, Write}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("IO Error: {0}")] + Io(#[from] std::io::Error), + #[error("Nul error: {0}")] + WideStringNul(#[from] widestring::error::NulError), + #[error("ContainsNul error: {0}")] + ContainsNul(#[from] widestring::error::ContainsNul), + #[error("Utf16 conversion error: {0}")] + Utf16(#[from] widestring::error::Utf16Error), + #[error("FromVecWithNul conversion error: {0}")] + FromVecWithNul(#[from] std::ffi::FromVecWithNulError), + #[error("IntoString conversion error: {0}")] + IntoString(#[from] std::ffi::IntoStringError), + #[error("Nul error: {0}")] + FfiNul(#[from] std::ffi::NulError), +} + +pub trait Reader { + fn read_wide_string(&mut self) -> Result; + fn read_string(&mut self) -> Result; + fn read_u8(&mut self) -> Result; + fn read_u16_le(&mut self) -> Result; + fn read_u32_le(&mut self) -> Result; + fn read_u64_le(&mut self) -> Result; + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error>; +} + +pub trait Writer { + fn write_wide_string>(&mut self, value: T) -> Result; + fn write_string>(&mut self, value: T) -> Result; + fn write_u8(&mut self, value: u8) -> Result; + fn write_u16_le(&mut self, value: u16) -> Result; + fn write_u32_le(&mut self, value: u32) -> Result; + fn write_u64_le(&mut self, value: u64) -> Result; + fn write_all(&mut self, buf: &[u8]) -> Result; +} + +pub struct Cursor { + inner: T, +} + +impl Cursor { + pub fn new(inner: T) -> Self { + Self { inner } + } +} + +impl Reader for Cursor { + fn read_wide_string(&mut self) -> Result { + let mut output: Vec = Vec::with_capacity(1024); + let mut character = self.read_u16_le()?; + while character != 0 { + output.push(character); + character = self.read_u16_le()?; + } + // Push last null + output.push(character); + Ok(widestring::U16CStr::from_slice(&output)?.to_string()?) + } + + fn read_string(&mut self) -> Result { + let mut output: Vec = Vec::with_capacity(1024); + let mut character = self.read_u8()?; + while character != 0 { + output.push(character); + character = self.read_u8()?; + } + // Push last null + output.push(character); + Ok(std::ffi::CString::from_vec_with_nul(output)?.into_string()?) + } + + fn read_u8(&mut self) -> Result { + let mut result = [0u8; 1]; + self.inner.read_exact(&mut result)?; + Ok(u8::from_le_bytes(result)) + } + + fn read_u16_le(&mut self) -> Result { + let mut result = [0u8; 2]; + self.inner.read_exact(&mut result)?; + Ok(u16::from_le_bytes(result)) + } + + fn read_u32_le(&mut self) -> Result { + let mut result = [0u8; 4]; + self.inner.read_exact(&mut result)?; + Ok(u32::from_le_bytes(result)) + } + + fn read_u64_le(&mut self) -> Result { + let mut result = [0u8; 8]; + self.inner.read_exact(&mut result)?; + Ok(u64::from_le_bytes(result)) + } + + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> { + Ok(self.inner.read_exact(buf)?) + } +} + +impl Writer for Cursor { + fn write_wide_string>(&mut self, value: S) -> Result { + let wide_string = widestring::U16CString::from_str(value.as_ref())?; + let data = wide_string.into_vec_with_nul(); + let mut output = Vec::with_capacity(data.len() * 2); + for element in data { + output.extend_from_slice(&element.to_le_bytes()); + } + self.write_all(&output) + } + + fn write_string>(&mut self, value: S) -> Result { + let string = std::ffi::CString::new(value.as_ref())?; + self.write_all(string.as_bytes_with_nul()) + } + + fn write_u8(&mut self, value: u8) -> Result { + self.write_all(&[value]) + } + + fn write_u16_le(&mut self, value: u16) -> Result { + self.write_all(&value.to_le_bytes()) + } + + fn write_u32_le(&mut self, value: u32) -> Result { + self.write_all(&value.to_le_bytes()) + } + + fn write_u64_le(&mut self, value: u64) -> Result { + self.write_all(&value.to_le_bytes()) + } + + fn write_all(&mut self, buf: &[u8]) -> Result { + self.inner.write_all(buf)?; + Ok(self.inner.stream_position()? as usize) + } +} \ No newline at end of file diff --git a/offset-finder/Cargo.toml b/offset-finder/Cargo.toml new file mode 100644 index 0000000..a9c89ae --- /dev/null +++ b/offset-finder/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "offset-finder" +version.workspace = true +edition.workspace = true + +[features] +json_input = ["dep:serde"] + +[dependencies] +goblin.workspace = true +log.workspace = true +patternscanner.workspace = true +serde = { workspace = true, optional = true } +thiserror.workspace = true + +pe-utils.workspace = true \ No newline at end of file diff --git a/offset-finder/src/json/mod.rs b/offset-finder/src/json/mod.rs new file mode 100644 index 0000000..30ce077 --- /dev/null +++ b/offset-finder/src/json/mod.rs @@ -0,0 +1,32 @@ +#![cfg(feature = "json_input")] + +use serde::{Deserialize, Serialize}; + +use crate::OffsetLocator; + +#[derive(Serialize, Deserialize)] +pub struct OffsetLocatorJson { + pub name: String, + pub partial_match: Vec, + pub full_match: String, + #[serde(default)] + pub skip_offset_print: bool, + #[serde(default)] + pub allow_multiple_matches: bool, +} + +impl<'a> Into> for &'a OffsetLocatorJson { + fn into(self) -> OffsetLocator<'a> { + let partials = self.partial_match.iter() + .map(|pattern| pattern.as_str()) + .collect::>(); + + OffsetLocator { + name: &self.name, + partial_match: partials, + full_match: &self.full_match, + skip_offset_print: self.skip_offset_print, + allow_multiple_matches: self.allow_multiple_matches, + } + } +} \ No newline at end of file diff --git a/offset-finder/src/lib.rs b/offset-finder/src/lib.rs new file mode 100644 index 0000000..d13a7c7 --- /dev/null +++ b/offset-finder/src/lib.rs @@ -0,0 +1,155 @@ +use std::collections::HashMap; + +use goblin::pe::section_table::SectionTable; +use log::{debug, warn}; +use patternscanner::PatternScannerBuilder; + +#[cfg(feature = "json_input")] +pub mod json; + +pub struct OffsetLocator<'a> { + pub name: &'a str, + pub partial_match: Vec<&'a str>, + pub full_match: &'a str, + pub skip_offset_print: bool, + pub allow_multiple_matches: bool, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Offset for: {0} not found")] + NotFound(String), + #[error("Too many matches found for: {0}")] + TooManyMatches(String), + #[error("PE Utils: {0}")] + PeUtils(#[from] pe_utils::Error), + #[error("Toml Error: {0}")] + PatternScanner(#[from] patternscanner::PatternScannerError), +} + +#[inline] +fn find_pattern(image_base: usize, + sections: &[SectionTable], + data: &[u8], + pattern: &str, + name: &str, + allow_multiple_matches: bool) -> Result<(usize, usize), Error> { + let items = find_all_pattern(image_base, sections, data, pattern, name)?; + if items.len() == 1 { + return Ok((items[0].0, items[0].1)); + } + + if allow_multiple_matches { + return Ok(items[0]); + } + for item in items { + warn!("Possible candidate for: {}, 0x{:02X?}", name, item.1); + } + Err(Error::TooManyMatches(name.to_string())) +} + +#[inline] +fn find_all_pattern(image_base: usize, + sections: &[SectionTable], + data: &[u8], + pattern: &str, + name: &str) -> Result, Error> { + let items = PatternScannerBuilder::builder() + .with_bytes(data) + .build() + .scan_all(pattern)?; + + if items.is_empty() { + return Err(Error::NotFound(name.to_string())); + } + + let mut output: Vec<(usize, usize)> = Vec::with_capacity(items.len()); + for item in items { + output.push((item, pe_utils::resolve_symbol(image_base, sections, item)?)); + } + Ok(output) +} + +#[inline] +fn find_patterns(image_base: usize, + sections: &[SectionTable], + data: &[u8], + patterns: &[&str], + name: &str, + allow_multiple_matches: bool) -> Result<(usize, usize), Error> { + for pattern in patterns { + let result = match find_pattern(image_base, sections, data, pattern, name, allow_multiple_matches) { + Ok(result) => Ok(result), + Err(Error::NotFound(_)) => continue, + Err(err) => Err(err) + }?; + debug!("Partial pattern match with: {}", pattern); + return Ok(result); + } + Err(Error::NotFound(name.to_string())) +} + +#[inline] +fn find_all_patterns(image_base: usize, + sections: &[SectionTable], + data: &[u8], + patterns: &[&str], + name: &str) -> Result>, Error> { + let mut output = HashMap::new(); + for i in 0..patterns.len() { + let result = match find_all_pattern(image_base, sections, data, &patterns[i], name) { + Ok(result) => Ok(result), + Err(Error::NotFound(_)) => continue, + Err(err) => Err(err) + }?; + debug!("Partial pattern match with: {}", &patterns[i]); + output.insert(i, result); + } + match output.is_empty() { + true => Err(Error::NotFound(name.to_string())), + false => Ok(output) + } +} + +impl<'a> OffsetLocator<'a> { + pub fn find_offset(&self, + image_base: usize, + sections: &[SectionTable], + executable: &[u8]) -> Result<(usize, usize, bool), Error> { + match find_pattern( + image_base, + sections, + executable, + self.full_match, + self.name, + self.allow_multiple_matches, + ) { + Ok(result) => Ok((result.0, result.1, true)), + Err(Error::NotFound(_)) => { + let result = find_patterns( + image_base, + sections, + executable, + &self.partial_match, + self.name, + self.allow_multiple_matches, + )?; + Ok((result.0, result.1, false)) + } + Err(err) => Err(err) + } + } + + pub fn find_all_partial_only(&self, + image_base: usize, + sections: &[SectionTable], + executable: &[u8]) -> Result>, Error> { + find_all_patterns( + image_base, + sections, + executable, + &self.partial_match, + self.name, + ) + } +} \ No newline at end of file diff --git a/pe-utils/Cargo.toml b/pe-utils/Cargo.toml new file mode 100644 index 0000000..520b378 --- /dev/null +++ b/pe-utils/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "pe-utils" +version.workspace = true +edition.workspace = true + +[dependencies] +goblin.workspace = true +thiserror.workspace = true diff --git a/pe-utils/src/lib.rs b/pe-utils/src/lib.rs new file mode 100644 index 0000000..e7aa33c --- /dev/null +++ b/pe-utils/src/lib.rs @@ -0,0 +1,130 @@ +use goblin::container; +use goblin::pe::{import, options}; +use goblin::pe::header::Header; +use goblin::pe::import::ImportData; +use goblin::pe::optional_header::OptionalHeader; +use goblin::pe::section_table::SectionTable; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Goblin Error: {0}")] + Goblin(#[from] goblin::error::Error), + #[error("Optional header missing")] + NoOptionalHeader, + #[error("Offset: {0} not found in any section")] + NotInSection(usize), +} + +pub fn take_hint_bytes(bytes: &[u8]) -> Option<&[u8; 16]> { + bytes + .get(0..16) + .and_then(|hint_bytes_slice| hint_bytes_slice.try_into().ok()) +} + +pub trait MemAlignedAddress { + fn get_mem_aligned_address(address: T, alignment: T) -> T; +} + +impl MemAlignedAddress for u32 { + fn get_mem_aligned_address(address: Self, alignment: Self) -> Self { + let remainder = address % alignment; + if remainder != 0 { + return address + alignment - remainder; + } + address + } +} + +impl MemAlignedAddress for u64 { + fn get_mem_aligned_address(address: Self, alignment: Self) -> Self { + let remainder = address % alignment; + if remainder != 0 { + return address + alignment - remainder; + } + address + } +} + +pub fn parse_headers(dump: &[u8]) -> Result { + let result = if let Some(hint_bytes) = take_hint_bytes(dump) { + match goblin::peek_bytes(hint_bytes)? { + goblin::Hint::PE => Ok(Header::parse(dump)?), + _ => Err(goblin::error::Error::Malformed( + "We were expecting a PE and it's not a PE".to_string()) + ) + } + } else { + Err(goblin::error::Error::Malformed("Object is too small.".to_string())) + }?; + Ok(result) +} + +pub fn get_optional_headers(header: &Header) -> Result { + match header.optional_header { + None => Err(Error::NoOptionalHeader), + Some(optional_header) => Ok(optional_header) + } +} + +pub fn get_sections(header: &Header, dump: &[u8]) -> Result, Error> { + let optional_header_offset = header.dos_header.pe_pointer as usize + + goblin::pe::header::SIZEOF_PE_MAGIC + + goblin::pe::header::SIZEOF_COFF_HEADER; + let offset = + &mut (optional_header_offset + header.coff_header.size_of_optional_header as usize); + Ok(header.coff_header.sections(dump, offset)?) +} + +pub fn resolve_symbol(image_base: usize, + sections: &[SectionTable], + addr: usize) -> Result { + for section in sections { + if (addr > section.pointer_to_raw_data as usize) && + (addr < (section.pointer_to_raw_data + section.size_of_raw_data) as usize) { + return Ok(image_base + (section.virtual_address - section.pointer_to_raw_data) as usize + addr); + } + } + Err(Error::NotInSection(addr)) +} + +#[deprecated(since = "0.1.0", note = "Actually not deprecated, but its not finished, so do not use")] +pub fn get_imports<'a>(bytes: &'a [u8], optional_header: &OptionalHeader, sections: &[SectionTable]) -> Result>, Error> { + let opts = &options::ParseOptions::default(); + let file_alignment = optional_header.windows_fields.file_alignment; + let is_64 = optional_header.container()? == container::Container::Big; + let mut imports = vec![]; + let mut import_data = None; + if let Some(&import_table) = optional_header.data_directories.get_import_table() { + let id = if is_64 { + ImportData::parse_with_opts::( + bytes, + import_table, + §ions, + file_alignment, + opts, + )? + } else { + ImportData::parse_with_opts::( + bytes, + import_table, + §ions, + file_alignment, + opts, + )? + }; + if is_64 { + imports = import::Import::parse::(bytes, &id, §ions)? + } else { + imports = import::Import::parse::(bytes, &id, §ions)? + } + let mut libraries = id + .import_data + .iter() + .map(|data| data.name) + .collect::>(); + libraries.sort(); + libraries.dedup(); + import_data = Some(id); + } + Ok(import_data) +} \ No newline at end of file diff --git a/restorer/Cargo.toml b/restorer/Cargo.toml new file mode 100644 index 0000000..e55cfaa --- /dev/null +++ b/restorer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "restorer" +version.workspace = true +edition.workspace = true + +[dependencies] +goblin.workspace = true +log.workspace = true +thiserror.workspace = true + +pe-utils.workspace = true \ No newline at end of file diff --git a/restorer/src/lib.rs b/restorer/src/lib.rs new file mode 100644 index 0000000..da89667 --- /dev/null +++ b/restorer/src/lib.rs @@ -0,0 +1,99 @@ +use std::io::Write; + +use goblin::pe::optional_header::OptionalHeader; +use goblin::pe::section_table::SectionTable; +use log::{info, trace}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("PE Utils Error: {0}")] + PEUtils(#[from] pe_utils::Error), + #[error("IO Error: {0}")] + Io(#[from] std::io::Error), +} + +pub fn restore_from_ptr, B: AsRef>(name: A, + module_base: usize, + restored_filename: Option) -> Result, Error> { + let data = unsafe { std::slice::from_raw_parts(module_base as *const u8, 0x1000) }; + + let header = pe_utils::parse_headers(data)?; + trace!("{:#?}", header); + let optional_headers = pe_utils::get_optional_headers(&header)?; + let sections = pe_utils::get_sections(&header, data)?; + + let mut vaddr_end: u32 = 0; + for section in §ions { + let virtual_end = section.virtual_address + section.size_of_raw_data; + if virtual_end > vaddr_end { + vaddr_end = virtual_end; + } + } + let data = unsafe { + std::slice::from_raw_parts(module_base as *const u8, vaddr_end as usize) + }; + restore_raw(name, data, optional_headers, §ions, restored_filename) +} + +pub fn restore_from_dump, B: AsRef>(name: A, + dump: &[u8], + restored_filename: Option) -> Result, Error> { + let header = pe_utils::parse_headers(dump)?; + trace!("{:#?}", header); + let optional_headers = pe_utils::get_optional_headers(&header)?; + let sections = pe_utils::get_sections(&header, dump)?; + restore_raw(name, dump, optional_headers, §ions, restored_filename) +} + +pub fn restore_raw, B: AsRef>(name: A, + dump: &[u8], + optional_headers: OptionalHeader, + sections: &[SectionTable], + restored_filename: Option) -> Result, Error> { + let mut output = vec![0; dump.len()]; + output[0..optional_headers.windows_fields.size_of_headers as usize] + .copy_from_slice(&dump[0..optional_headers.windows_fields.size_of_headers as usize]); + + let mut eof: u32 = 0; + for section in sections { + let phys_end = section.pointer_to_raw_data + section.size_of_raw_data; + let virtual_end = section.virtual_address + section.size_of_raw_data; + let virtual_end_aligned = >::get_mem_aligned_address( + section.virtual_address + section.virtual_size, + optional_headers.windows_fields.section_alignment, + ); + + trace!( + "Section name: {}\nPhys ptr: 0x{:02X?}\nPhys size: 0x{:02X?}\nPhys End: 0x{:02X?}\n\ + Virtual ptr: 0x{:02X?}\nVirtual size: 0x{:02X?}\nVirtual End: 0x{:02X?}\n\ + Virtual End Aligned: 0x{:02X?}", + String::from_utf8_lossy(§ion.name), + section.pointer_to_raw_data, + section.size_of_raw_data, + phys_end, + section.virtual_address, + section.virtual_size, + virtual_end, + virtual_end_aligned + ); + + output[section.pointer_to_raw_data as usize..phys_end as usize] + .copy_from_slice(&dump[section.virtual_address as usize..virtual_end as usize]); + + if phys_end > eof { + eof = phys_end; + } + } + + match restored_filename { + None => info!("Since no restored_filename was provided, the restored output will not be saved to a file"), + Some(filename) => { + let mut data_file = std::fs::File::create(filename.as_ref())?; + data_file.write_all(&output[0..eof as usize])?; + info!("Restored executable saved to: {}", filename.as_ref()); + } + } + + info!("Executable {} restored successfully", name.as_ref()); + Ok(output) +} \ No newline at end of file