├── Cargo.toml ├── src ├── main.rs └── lib.rs ├── .gitignore ├── LICENSE ├── README.md └── .github └── workflows └── rust-ci.yml /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustdsplit" 3 | version = "1.0.0" 4 | authors = ["epi "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | structopt = "0.3" 9 | env_logger = "0.7.1" 10 | colored = "1.9" 11 | regex = "1" 12 | 13 | [profile.release] 14 | opt-level = 'z' # optimize for size 15 | lto = true 16 | codegen-units = 1 17 | panic = 'abort' 18 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use std::process; 3 | use structopt::StructOpt; 4 | 5 | use rustdsplit::{run, Cli}; 6 | 7 | fn main() { 8 | let args = Cli::from_args(); 9 | 10 | env_logger::init(); 11 | 12 | if let Err(e) = run(&args) { 13 | eprintln!("{} Error encountered: {}", "[!]".bright_red(), e); 14 | process::exit(1); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .idea/ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 epi052 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rustdsplit 2 | 3 | ![CI Pipeline](https://github.com/epi052/rustdsplit/workflows/CI%20Pipeline/badge.svg) 4 | 5 | # What's it For? 6 | 7 | > Several years ago, the tool "DSplit" was released by class101 which was used to demonstrate how some AV signatures could be bypassed by finding and modifying one byte within the binary. Unfortunately, the original file (and source code?) is no longer available for download by the author. 8 | > 9 | > http://obscuresecurity.blogspot.com/2012/12/finding-simple-av-signatures-with.html 10 | 11 | During OSCE's AV bypass module, I recalled learning about the method described in the linked post and using DSplit to bypass signature based AV detection. I wanted to play around with it using the OSCE labs. I proceeded to search for DSplit and came to the same conclusion as the above author, what **can** be found looks rather janky. 12 | 13 | At the same time I was playing with the Rust programming language. I decided it would be a fun learning exercise to re-implement DSplit using Rust. This project is the result. 14 | 15 | # What's it Do? 16 | 17 | This tool takes a binary (or really any file) and splits it in half. It also adds a 12-byte signature to each half that stores the original file's offsets (main takeaway is that files can be named w/e and still be split again). 18 | 19 | # How do I Use it? 20 | 21 | The typical work flow is as follows: 22 | 23 | 1. Take original binary that AV flags as malicious 24 | 2. Split it in half 25 | 3. Scan both halves 26 | 4. Find the half that still gets flagged by the AV 27 | 5. Repeat steps 2-4 until you know the byte-offset of the signature 28 | 6. Change it 29 | 30 | Due to the fact that only half of the file is (re)split, the number of total splits is quite small (we effectively perform a binary search on the file, which is O(log n) in the worst case). 31 | 32 | # Small Demo 33 | 34 | [![asciicast](https://asciinema.org/a/351311.svg)](https://asciinema.org/a/351311) 35 | 36 | # Downloads 37 | 38 | There are pre-built binaries for the following systems: 39 | 40 | - [Linux x86](https://github.com/epi052/rustdsplit/releases/latest/download/x86-linux-rustdsplit.zip) 41 | - [Linux x86_64](https://github.com/epi052/rustdsplit/releases/latest/download/x86_64-linux-rustdsplit.zip) 42 | - [MacOS x86_64](https://github.com/epi052/rustdsplit/releases/latest/download/x86_64-macos-rustdsplit.zip) 43 | - [Windows x86](https://github.com/epi052/rustdsplit/releases/latest/download/x86-windows-rustdsplit.exe.zip) 44 | - [Windows x86_64](https://github.com/epi052/rustdsplit/releases/latest/download/x86_64-windows-rustdsplit.exe.zip) 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/rust-ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Pipeline 2 | 3 | on: [push] 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rust fmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | 50 | clippy: 51 | name: Clippy 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D clippy::all -D clippy::pedantic -D warnings 65 | 66 | build: 67 | runs-on: ubuntu-latest 68 | steps: 69 | - uses: actions/checkout@v2 70 | - uses: actions-rs/toolchain@v1 71 | with: 72 | toolchain: stable 73 | target: x86_64-unknown-linux-musl 74 | override: true 75 | - uses: actions-rs/cargo@v1 76 | with: 77 | use-cross: true 78 | command: build 79 | args: --release --target=x86_64-unknown-linux-musl 80 | 81 | build-all: 82 | runs-on: ${{ matrix.os }} 83 | if: github.ref == 'refs/heads/master' 84 | strategy: 85 | matrix: 86 | type: [ubuntu-x64, ubuntu-x86, macos, windows-x64, windows-x86] 87 | include: 88 | - type: ubuntu-x64 89 | os: ubuntu-latest 90 | target: x86_64-unknown-linux-musl 91 | name: x86_64-linux-rustdsplit 92 | path: target/x86_64-unknown-linux-musl/release/rustdsplit 93 | - type: ubuntu-x86 94 | os: ubuntu-latest 95 | target: i686-unknown-linux-musl 96 | name: x86-linux-rustdsplit 97 | path: target/i686-unknown-linux-musl/release/rustdsplit 98 | - type: macos 99 | os: macos-latest 100 | target: x86_64-apple-darwin 101 | name: x86_64-macos-rustdsplit 102 | path: target/x86_64-apple-darwin/release/rustdsplit 103 | - type: windows-x64 104 | os: windows-latest 105 | target: x86_64-pc-windows-msvc 106 | name: x86_64-windows-rustdsplit.exe 107 | path: target\x86_64-pc-windows-msvc\release\rustdsplit.exe 108 | - type: windows-x86 109 | os: windows-latest 110 | target: i686-pc-windows-msvc 111 | name: x86-windows-rustdsplit.exe 112 | path: target\i686-pc-windows-msvc\release\rustdsplit.exe 113 | steps: 114 | - uses: actions/checkout@v2 115 | - uses: actions-rs/toolchain@v1 116 | with: 117 | toolchain: stable 118 | target: ${{ matrix.target }} 119 | override: true 120 | - uses: actions-rs/cargo@v1 121 | with: 122 | use-cross: true 123 | command: build 124 | args: --release --target=${{ matrix.target }} 125 | - uses: actions/upload-artifact@v2 126 | with: 127 | name: ${{ matrix.name }} 128 | path: ${{ matrix.path }} 129 | 130 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::default::Default; 3 | use std::{error, fmt, fs, path, process}; 4 | 5 | use colored::Colorize; 6 | use regex::Regex; 7 | use structopt::StructOpt; 8 | 9 | #[derive(Debug, StructOpt)] 10 | pub struct Cli { 11 | /// path to the file to split in half 12 | #[structopt(parse(from_os_str))] 13 | pub infile: path::PathBuf, 14 | } 15 | 16 | #[derive(Default)] 17 | struct SplitFile { 18 | path: path::PathBuf, 19 | bytes: Vec, 20 | size: usize, 21 | middle: usize, 22 | left_sig: Signature, 23 | right_sig: Signature, 24 | } 25 | 26 | enum SplitHalf { 27 | Left, 28 | Right, 29 | } 30 | 31 | #[derive(Default)] 32 | struct Signature { 33 | start_offset: u32, 34 | end_offset: u32, 35 | } 36 | 37 | impl Signature { 38 | const LEN: usize = 12; 39 | const TAG: [u8; 4] = [0x65, 0x70, 0x69, 0x3a]; // epi: 40 | 41 | /// Simple helper to return a String showing the start and end offsets as a range 42 | fn range_str(&self) -> String { 43 | format!("{}-{}", self.start_offset, self.end_offset) 44 | } 45 | 46 | /// Returns 12 byte representation of a Signature; `TAG + START_OFFSET + END_OFFSET` 47 | fn as_bytes(&self) -> [u8; 12] { 48 | let mut ret: [u8; 12] = [0; 12]; 49 | 50 | let concatenated = &[ 51 | Signature::TAG, 52 | u32::to_le_bytes(self.start_offset), 53 | u32::to_le_bytes(self.end_offset), 54 | ] 55 | .concat(); 56 | 57 | for (i, ch) in concatenated.iter().enumerate() { 58 | ret[i] = *ch; 59 | } 60 | 61 | ret 62 | } 63 | } 64 | 65 | impl fmt::Display for Signature { 66 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 67 | write!( 68 | f, 69 | "\n[-] {{\n[-]\t{}: {}\n[-]\t{}: {}\n[-] }}", 70 | "start".bright_yellow(), 71 | self.start_offset, 72 | "end".bright_yellow(), 73 | self.end_offset 74 | ) 75 | } 76 | } 77 | 78 | impl SplitFile { 79 | fn from_path(path: &path::PathBuf) -> SplitFile { 80 | let bytes = match fs::read(path) { 81 | Ok(d) => d, 82 | Err(e) => { 83 | eprintln!("[!] Could not open {}: {}", path.as_path().display(), e); 84 | process::exit(5); 85 | } 86 | }; 87 | 88 | let path = path.to_path_buf(); 89 | let size = bytes.len(); 90 | let middle = size / 2; 91 | let left_sig = Signature::default(); 92 | let right_sig = Signature::default(); 93 | 94 | SplitFile { 95 | path, 96 | bytes, 97 | size, 98 | middle, 99 | left_sig, 100 | right_sig, 101 | } 102 | } 103 | 104 | fn data(&self, half: &SplitHalf) -> &[u8] { 105 | match half { 106 | SplitHalf::Left => &self.bytes[0..self.middle], 107 | SplitHalf::Right => &self.bytes[self.middle..], 108 | } 109 | } 110 | 111 | fn left_sig_mut(&mut self) -> &mut Signature { 112 | &mut self.left_sig 113 | } 114 | 115 | fn right_sig_mut(&mut self) -> &mut Signature { 116 | &mut self.right_sig 117 | } 118 | 119 | fn signature(&self, half: &SplitHalf) -> &Signature { 120 | match half { 121 | SplitHalf::Left => &self.left_sig, 122 | SplitHalf::Right => &self.right_sig, 123 | } 124 | } 125 | 126 | fn set_signature(&mut self, start: u32, end: u32, half: &SplitHalf) { 127 | let to_set = Signature { 128 | start_offset: start, 129 | end_offset: end, 130 | }; 131 | 132 | match half { 133 | SplitHalf::Left => { 134 | *self.left_sig_mut() = to_set; 135 | } 136 | SplitHalf::Right => { 137 | *self.right_sig_mut() = to_set; 138 | } 139 | } 140 | } 141 | 142 | fn read_signature(&mut self) -> Option { 143 | if self.bytes.len() > Signature::LEN && self.bytes[0..4] == Signature::TAG { 144 | // existing signature 145 | let start_array = match <[u8; 4]>::try_from(&self.bytes[4..8]) { 146 | Ok(d) => d, 147 | Err(e) => { 148 | eprintln!("[!] Could not read starting offset: {}", e); 149 | return None; 150 | } 151 | }; 152 | 153 | let end_array = match <[u8; 4]>::try_from(&self.bytes[8..12]) { 154 | Ok(d) => d, 155 | Err(e) => { 156 | eprintln!("[!] Could not read ending offset: {}", e); 157 | return None; 158 | } 159 | }; 160 | 161 | let start_offset = u32::from_le_bytes(start_array); 162 | let end_offset = u32::from_le_bytes(end_array); 163 | 164 | if start_offset > end_offset { 165 | println!("[!] Starting offset is larger than ending offset; exiting."); 166 | process::exit(2); 167 | } 168 | 169 | Some(Signature { 170 | start_offset, 171 | end_offset, 172 | }) 173 | } else { 174 | // new file, should be 0 -> middle, and middle -> end 175 | if let (Ok(middle), Ok(end)) = (u32::try_from(self.middle), u32::try_from(self.size)) { 176 | self.set_signature(0, middle, &SplitHalf::Left); 177 | self.set_signature(middle + 1, end, &SplitHalf::Right); 178 | } else { 179 | eprintln!("[!] Could not convert offsets to unsigned 32bit integers"); 180 | process::exit(3); 181 | } 182 | None 183 | } 184 | } 185 | 186 | fn filename(&self, half: &SplitHalf) -> String { 187 | let sig: &Signature; 188 | 189 | match half { 190 | SplitHalf::Left => { 191 | sig = &self.signature(&SplitHalf::Left); 192 | } 193 | SplitHalf::Right => { 194 | sig = &self.signature(&SplitHalf::Right); 195 | } 196 | }; 197 | 198 | let stem = if let Some(data) = self.path.file_stem() { 199 | data 200 | } else { 201 | eprintln!("[!] Error getting file name; exiting."); 202 | process::exit(4); 203 | }; 204 | 205 | let re = Regex::new(r"^[0-9]+-[0-9]+-").unwrap(); 206 | 207 | if let Some(d) = stem.to_str() { 208 | format!("{}-{}.bin", sig.range_str(), re.replace(d, "")) 209 | } else { 210 | format!("{}-{:?}.bin", sig.range_str(), stem) 211 | } 212 | } 213 | 214 | fn save(&self) { 215 | let leftname = self.filename(&SplitHalf::Left); 216 | println!("{} Saving first half", "[=]".bright_blue()); 217 | 218 | match fs::write( 219 | &leftname, 220 | [&self.left_sig.as_bytes(), self.data(&SplitHalf::Left)].concat(), 221 | ) { 222 | Ok(_d) => println!("{} Successfully saved {}", "[+]".bright_green(), leftname), 223 | Err(e) => println!("{} Could not save {}: {}", "[!]".bright_red(), leftname, e), 224 | } 225 | 226 | let rightname = self.filename(&SplitHalf::Right); 227 | println!("{} Saving second half", "[=]".bright_blue()); 228 | 229 | match fs::write( 230 | &rightname, 231 | [&self.right_sig.as_bytes(), self.data(&SplitHalf::Right)].concat(), 232 | ) { 233 | Ok(_d) => println!("{} Successfully saved {}", "[+]".bright_green(), rightname), 234 | Err(e) => println!("{} Could not save {}: {}", "[!]".bright_red(), leftname, e), 235 | } 236 | } 237 | } 238 | 239 | /// # Errors 240 | /// 241 | /// Will return `Err` if an incomplete file read is performed 242 | pub fn run(args: &Cli) -> Result<(), Box> { 243 | let mut split = SplitFile::from_path(&args.infile); 244 | 245 | let left = split.data(&SplitHalf::Left); 246 | let right = split.data(&SplitHalf::Right); 247 | 248 | if left.len() + right.len() != split.size { 249 | return Err("Could not read entire file.".into()); 250 | } 251 | 252 | if let Some(sig) = split.read_signature() { 253 | println!( 254 | "{} Found a previously split file with signature: {}", 255 | "[=]".bright_blue(), 256 | sig, 257 | ); 258 | 259 | let halfway = (sig.end_offset - sig.start_offset) / 2; 260 | let middle = sig.start_offset + halfway; 261 | 262 | split.set_signature(sig.start_offset, middle, &SplitHalf::Left); 263 | split.set_signature(middle, sig.end_offset, &SplitHalf::Right); 264 | } else { 265 | println!( 266 | "{} No signature found; commencing the first split of a pristine file!", 267 | "[=]".bright_blue() 268 | ); 269 | } 270 | 271 | split.save(); 272 | 273 | Ok(()) 274 | } 275 | --------------------------------------------------------------------------------