diff --git a/Cargo.toml b/Cargo.toml index 7d0f932..db2723b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xgadget" -version = "0.4.0" +version = "0.5.0" authors = ["Tiemoko Ballo"] edition = "2018" license = "MIT" @@ -20,31 +20,32 @@ include = [ ] [dependencies] -iced-x86 = "1.9.1" +iced-x86 = "1" goblin = "0.2" rayon = "1" bitflags = "1" colored = "2" rustc-hash = "1" +lazy_static = { version = "1", optional = true } structopt = { version = "0.3", default-features = false, optional = true } num_cpus = { version = "1", optional = true } regex = { version = "1", optional = true } -strip-ansi-escapes = { version = "0.1", optional = true } -lazy_static = { version = "1.4", optional = true } -term_size = { version = "0.3.2", optional = true } -checksec = { version = "0.0.7", features = ["elf", "pe", "color"], optional = true } -memmap = { version = "0.7.0", optional = true } +term_size = { version = "0.3", optional = true } +checksec = { version = "0.0.8", features = ["elf", "pe", "color"], optional = true } +memmap = { version = "0.7", optional = true } [dev-dependencies] +pprof = { version = "0.4", features = ["flamegraph"] } criterion = "0.3" rand = "0.7" dirs = "3" predicates = "1" assert_cmd = "1" tempfile = "3" +regex = "1" [features] -cli-bin = ["structopt", "num_cpus", "regex", "strip-ansi-escapes", "lazy_static", "term_size", "checksec", "memmap"] +cli-bin = ["lazy_static", "structopt", "num_cpus", "regex", "term_size", "checksec", "memmap"] [lib] name = "xgadget" @@ -52,7 +53,7 @@ path = "src/lib.rs" [[bin]] name = "xgadget" -path = "src/cli/cli.rs" +path = "src/cli/main.rs" required-features = ["cli-bin"] [[bench]] @@ -60,11 +61,15 @@ name = "bench_1_misc" harness = false [[bench]] -name = "bench_2_elf_userspace" +name = "bench_2_fmt" harness = false [[bench]] -name = "bench_3_elf_kernels" +name = "bench_3_elf_userspace" +harness = false + +[[bench]] +name = "bench_4_elf_kernels" harness = false [profile.release] diff --git a/README.md b/README.md index 78a1969..ce16204 100644 --- a/README.md +++ b/README.md @@ -13,22 +13,23 @@ Uses the [iced-x86 disassembler library](https://github.com/0xd4d/iced). To the best of my knowledge, `xgadget` is the first gadget search tool to have these features: * JOP search uses instruction semantics - not hardcoded regex for individual encodings - * Optionally filter to JOP "dispatcher" gadgets with flag `--dispatcher` + * Optionally filter to JOP "dispatcher" gadgets with flag `--dispatcher` * Finds gadgets that work across multiple variants of a binary (e.g. different program or compiler versions) - * **Full-match** - Same instruction sequence, same program counter: gadget fully re-usable. - * E.g. `pop rsp; add [rax-0x77], cl; ret ------------------------------------- [ 0xc748d ]` - * **Partial-match** - Same instruction sequence, different program counter: gadget logic portable. - * E.g. `pop rsp; add [rax-0x77], cl; ret; --- [ 'bin_v1.1': 0xc748d, 'bin_v1.2': 0xc9106 ]` - * This is entirely optional, you're free to run this tool on a single binary. + * **Full-match** - Same instruction sequence, same program counter: gadget fully re-usable. + * E.g. `pop rsp; add [rax-0x77], cl; ret ------------------------------------- [ 0xc748d ]` + * **Partial-match** - Same instruction sequence, different program counter: gadget logic portable. + * E.g. `pop rsp; add [rax-0x77], cl; ret; --- [ 'bin_v1.1': 0xc748d, 'bin_v1.2': 0xc9106 ]` + * This is entirely optional, you're free to run this tool on a single binary. +* The stack pointer is explicitly colored in terminal output, for workflow convenience. Other features include: * Both library API and CLI tool -* Supports ELF32, ELF64, PE32, PE32+ [1], and raw files -* Parallel across available cores [2], whether searching a single binary or multiple variants -* CI/CD for automated integration test and binary releases (Linux, 64-bit) [3] -* Statistical benchmark harness for performance tuning [4] -* 8086/x86/x64 only, uses a speed-optimized disassembly backend [5] +* Supports ELF32, ELF64, PE32, PE32+ \[1\], and raw files +* Parallel across available cores \[2\], whether searching a single binary or multiple variants +* CI/CD for automated integration test and binary releases (Linux, 64-bit) \[3\] +* Statistical benchmark harness for performance tuning \[4\] +* 8086/x86/x64 only, uses a speed-optimized disassembly backend \[5\] ### API Usage @@ -38,20 +39,48 @@ Find gadgets: use xgadget; let max_gadget_len = 5; -let search_config = xgadget::SearchConfig::DEFAULT; // Search single binary +let search_config = xgadget::SearchConfig::DEFAULT; let bin_1 = xgadget::Binary::from_path_str("/path/to/bin_v1").unwrap(); let bins = vec![bin_1]; let gadgets = xgadget::find_gadgets(&bins, max_gadget_len, search_config).unwrap(); let stack_pivot_gadgets = xgadget::filter_stack_pivot(&gadgets); -// Search for cross-variant gadgets +// Search for cross-variant gadgets, including partial matches +let search_config = xgadget::SearchConfig::DEFAULT | xgadget::SearchConfig::PART; let bin_1 = xgadget::Binary::from_path_str("/path/to/bin_v1").unwrap(); let bin_2 = xgadget::Binary::from_path_str("/path/to/bin_v2").unwrap(); let bins = vec![bin_1, bin_2]; let cross_gadgets = xgadget::find_gadgets(&bins, max_gadget_len, search_config).unwrap(); -let cross_reg_write_gadgets = xgadget::filter_stack_set_regs(&cross_gadgets); +let cross_reg_pop_gadgets = xgadget::filter_reg_pop_only(&cross_gadgets); +``` + +Custom filters can be created using the [`GadgetAnalysis`](crate::gadget::GadgetAnalysis) object and/or functions from the [`semantics`](crate::semantics) module. +How the above [`filter_stack_pivot`](crate::filters::filter_stack_pivot) function is implemented: + +```rust +use rayon::prelude::*; +use iced_x86; +use xgadget::{Gadget, GadgetAnalysis}; + +/// Parallel filter to gadgets that write the stack pointer +pub fn filter_stack_pivot<'a>(gadgets: &[Gadget<'a>]) -> Vec> { + gadgets + .par_iter() + .filter(|g| { + let regs_overwritten = GadgetAnalysis::new(&g).regs_overwritten(); + if regs_overwritten.contains(&iced_x86::Register::RSP) + || regs_overwritten.contains(&iced_x86::Register::ESP) + || regs_overwritten.contains(&iced_x86::Register::SP) + { + return true; + } + false + }) + .cloned() + .collect() +} ``` ### CLI Usage @@ -59,39 +88,42 @@ let cross_reg_write_gadgets = xgadget::filter_stack_set_regs(&cross_gadgets); Run `xgadget --help`: ``` -xgadget v0.4.0 +xgadget v0.5.0 -About: Fast, parallel, cross-variant ROP/JOP gadget search for x86/x64 binaries. -Cores: 8 logical, 8 physical +About: Fast, parallel, cross-variant ROP/JOP gadget search for x86/x64 binaries. +Cores: 8 logical, 8 physical USAGE: - xgadget [FLAGS] [OPTIONS] ... + xgadget [FLAGS] [OPTIONS] ... FLAGS: - -t, --att Display gadgets using AT&T syntax [default: Intel syntax] - -c, --check-sec Run checksec on the 1+ binaries instead of gadget search - -d, --dispatcher Filter to potential JOP 'dispatcher' gadgets [default: all gadgets] - -e, --extended-fmt Print in terminal-wide format [default: only used for partial match search] - -h, --help Prints help information - --inc-call Include gadgets containing a call [default: don't include] - --inc-imm16 Include '{ret, ret far} imm16' (e.g. add to stack ptr) [default: don't include] - -j, --jop Search for JOP gadgets only [default: ROP, JOP, and SYSCALL] - -n, --no-color Don't color output, useful for UNIX piping [default: color output] - -m, --partial-match Include cross-variant partial matches [default: full matches only] - -w, --reg-write Filter to 'pop {reg} * 1+, {ret or ctrl-ed jmp/call}' gadgets [default: all gadgets] - -r, --rop Search for ROP gadgets only [default: ROP, JOP, and SYSCALL] - -p, --stack-pivot Filter to gadgets that write the stack ptr [default: all gadgets] - -s, --sys Search for SYSCALL gadgets only [default: ROP, JOP, and SYSCALL] - -V, --version Prints version information + -t, --att Display gadgets using AT&T syntax [default: Intel syntax] + -c, --check-sec Run checksec on the 1+ binaries instead of gadget search + -d, --dispatcher Filter to potential JOP 'dispatcher' gadgets [default: all] + -e, --extended-fmt Print in terminal-wide format [default: only used for partial match search] + -h, --help Prints help information + --inc-call Include gadgets containing a call [default: don't include] + --inc-imm16 Include '{ret, ret far} imm16' (e.g. add to stack ptr) [default: don't include] + -j, --jop Search for JOP gadgets only [default: ROP, JOP, and SYSCALL] + -n, --no-color Don't color output [default: color output] + --param-ctrl Filter to gadgets that control function parameters [default: all] + -m, --partial-match Include cross-variant partial matches [default: full matches only] + --reg-pop Filter to 'pop {reg} * 1+, {ret or ctrl-ed jmp/call}' gadgets [default: all] + -r, --rop Search for ROP gadgets only [default: ROP, JOP, and SYSCALL] + -p, --stack-pivot Filter to gadgets that write the stack ptr [default: all] + -s, --sys Search for SYSCALL gadgets only [default: ROP, JOP, and SYSCALL] + -V, --version Prints version information OPTIONS: - -a, --arch For raw (no header) files: specify arch ('x8086', 'x86', or 'x64') [default: x64] - -b, --bad-bytes ... Filter to gadgets whose addrs don't contain given bytes [default: all gadgets] - -l, --max-len Gadgets up to LEN instrs long. If 0: all gadgets, any length [default: 5] - -f, --regex-filter Filter to gadgets matching a regular expression + -a, --arch For raw (no header) files: specify arch ('x8086', 'x86', or 'x64') [default: x64] + -b, --bad-bytes ... Filter to gadgets whose addrs don't contain given bytes [default: all] + -l, --max-len Gadgets up to LEN instrs long. If 0: all gadgets, any length [default: 5] + --no-deref Filter to gadgets that don't deref any regs or a specific reg [default: all] + --reg-ctrl Filter to gadgets that control any reg or a specific reg [default: all] + -f, --regex-filter Filter to gadgets matching a regular expression ARGS: - ... 1+ binaries to gadget search. If > 1: gadgets common to all + ... 1+ binaries to gadget search. If > 1: gadgets common to all ``` ### CLI Build and Install (Recommended) @@ -105,33 +137,39 @@ cargo install xgadget --features cli-bin # Build on host (pre-req: https://ww ### CLI Binary Releases for Linux Commits to this repo's `master` branch automatically run integration tests and build a statically-linked binary for 64-bit Linux. -You can [download it here](https://github.com/entropic-security/xgadget/releases) and use the CLI immediately, instead of building from source. +You can [download it here](https://github.com/entropic-security/xgadget/releases) to try out the CLI immediately, instead of building from source. Static binaries for Windows may also be supported in the future. -The statically-linked binary is about 8x slower, presumably due to the built-in memory allocator for target `x86_64-unknown-linux-musl`. -Building a dynamically-linked binary from source with the above `cargo install` command is *highly* recommended. +Unfortunately the statically-linked binary is several times slower on an i7-9700K, likely due to the built-in memory allocator for target `x86_64-unknown-linux-musl`. +So building a dynamically-linked binary from source with the above `cargo install` command is *highly* recommended for performance (links against your system's allocator). + +### Why No Chain Generation? + +Tools that attempt to automate ROP chain generation require heavyweight analysis - typically symbolic execution of an intermediate representation. +While this works well for small binaries and CTF problems, it tends to be slow and difficult to scale for large, real-world programs. +At present, `xgadget` has a different goal: enable an expert user to manually craft stable exploits by providing fast, accurate gadget discovery. ### ~~Yeah, but can it do 10 OS kernels under 10 seconds?!~~ Repeatable Benchmark Harness ```bash -bash ./benches/bench_setup_ubuntu.sh # Ubuntu-specific, download/build 10 kernel versions -cargo bench # Grab a coffee, this'll take a while... +bash ./benches/bench_setup_ubuntu.sh # Ubuntu-specific, download/build 10 kernel versions +cargo bench # Grab a coffee, this'll take a while... ``` * `bench_setup_ubuntu.sh` downloads and builds 10 consecutive Linux kernels (versions `5.0.1` to `5.0.10` - with `x86_64_defconfig`). * `cargo bench`, among other benchmarks, searches all 10 kernels for common gadgets. -On an i7-9700K (8C/8T, 3.6GHz base, 4.9 GHz max) machine with `gcc` version 8.4.0: the average runtime, to process *all ten 54MB kernels simultaneously* with a max gadget length of 5 instructions and full-match search for all gadget types (ROP, JOP, and syscall gadgets), is *only 5.8 seconds*! Including partial matches as well takes *just 7.2 seconds*. +On an i7-9700K (8C/8T, 3.6GHz base, 4.9 GHz max) machine with `gcc` version 8.4.0: the average runtime, to process *all ten 54MB kernels simultaneously* with a max gadget length of 5 instructions and full-match search for all gadget types (ROP, JOP, and syscall gadgets), is *only 6.3 seconds*! Including partial matches as well takes *just 7.9 seconds*. ### Acknowledgements -This project started as an optimized solution to Chapter 8, exercise 3 of "Practical Binary Analysis" by Dennis Andreisse [6], and builds on the design outlined therein. +This project started as an optimized solution to Chapter 8, exercise 3 of "Practical Binary Analysis" by Dennis Andreisse \[6\], and builds on the design outlined therein. ### References -* [1] [`goblin` crate by Lzu Tao, m4b, Philip Craig, seu, Will Glynn](https://crates.io/crates/goblin) -* [2] [`rayon` crate by Josh Stone, Niko Matsakis](https://crates.io/crates/rayon) -* [3] [`xgadget/.github/workflows`](https://github.com/entropic-security/xgadget/tree/master/.github/workflows) -* [4] [`criterion` crate by Brook Heisler, Jorge Aparicio](https://crates.io/crates/criterion) -* [5] [`iced-x86` crate by 0xd4d](https://crates.io/crates/iced-x86) -* [6] ["Practical Binary Analysis" by Dennis Andreisse](https://practicalbinaryanalysis.com/) +* \[1\] [`goblin` crate by Lzu Tao, m4b, Philip Craig, seu, Will Glynn](https://crates.io/crates/goblin) +* \[2\] [`rayon` crate by Josh Stone, Niko Matsakis](https://crates.io/crates/rayon) +* \[3\] [`xgadget/.github/workflows`](https://github.com/entropic-security/xgadget/tree/master/.github/workflows) +* \[4\] [`criterion` crate by Brook Heisler, Jorge Aparicio](https://crates.io/crates/criterion) +* \[5\] [`iced-x86` crate by 0xd4d](https://crates.io/crates/iced-x86) +* \[6\] ["Practical Binary Analysis" by Dennis Andreisse](https://practicalbinaryanalysis.com/) diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..b040dc4 --- /dev/null +++ b/README.tpl @@ -0,0 +1,6 @@ +# {{crate}} + +![crates.io](https://img.shields.io/crates/v/xgadget.svg) +![GitHub Actions](https://github.com/entropic-security/xgadget/workflows/test/badge.svg) + +{{readme}} \ No newline at end of file diff --git a/benches/README.md b/benches/README.md new file mode 100644 index 0000000..93b9f49 --- /dev/null +++ b/benches/README.md @@ -0,0 +1,20 @@ +### Run all benchmarks + +``` +cargo bench +``` + +### Review flamegraph for a specific benchmark set + +``` +cargo bench --bench -- --profile-time=20 +find . -iname "*flame*.svg" +``` + +E.g. `` == `bench_2_fmt` + +### Troubleshooting if flamegraphs don't work + +``` +echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid +``` \ No newline at end of file diff --git a/benches/bench_1_misc.rs b/benches/bench_1_misc.rs index 697dbbe..ecbdcbc 100644 --- a/benches/bench_1_misc.rs +++ b/benches/bench_1_misc.rs @@ -1,7 +1,13 @@ use criterion::{criterion_group, criterion_main, Criterion}; +use regex::Regex; + // Stack Pivot Filter (sequential baseline) ---------------------------------------------------------------------------- -pub fn filter_stack_pivot_sequential<'a>( +// This implementation has faster per-gadget processing because it doesn't do a full gadget analysis. +// We're comparing it against the actual filter implementation which: +// - Is slower per-gadget but more readable (uses the general purpose gadget analysis) +// - Is faster overall on multi-core systems due to parallel processing +pub fn filter_stack_pivot_seq_fast<'a>( gadgets: &Vec>, ) -> Vec> { let rsp_write = iced_x86::UsedRegister::new(iced_x86::Register::RSP, iced_x86::OpAccess::Write); @@ -11,7 +17,7 @@ pub fn filter_stack_pivot_sequential<'a>( gadgets .iter() .filter(|g| { - for instr in &g.instrs { + for instr in g.instrs() { let mut info_factory = iced_x86::InstructionInfoFactory::new(); let info = info_factory @@ -30,6 +36,25 @@ pub fn filter_stack_pivot_sequential<'a>( .collect() } +// This function string formats gadgets and then uses a regex to find those which pop registers from the stack +// We're comparing it against the actual filter implementation which: +// - Is stricter, only consecutive pop sequences before the tail instruction, no other instrs allowed +// - Is faster, no need to string format and run a regex state machine +pub fn filter_reg_pop_only_regex<'a>( + gadgets: &[xgadget::gadget::Gadget<'a>], +) -> Vec<(String, String)> { + let re = Regex::new(r"^(?:pop)(?:.*(?:pop))*.*(?:ret|call|jmp)").unwrap(); + let mut matches = Vec::new(); + + for (instrs, addrs) in xgadget::fmt_gadget_str_list(&gadgets, false, false) { + if re.is_match(&instrs) { + matches.push((instrs, addrs)); + } + } + + matches +} + fn pivot_bench(c: &mut Criterion) { const MAX_GADGET_LEN: usize = 5; @@ -43,21 +68,48 @@ fn pivot_bench(c: &mut Criterion) { let gdb_gadgets = xgadget::find_gadgets(&bins, MAX_GADGET_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); - c.bench_function("readelf_pivot_filter_seq", |b| { - b.iter(|| filter_stack_pivot_sequential(&readelf_gadgets)) + c.bench_function("readelf_pivot_filter_seq_fast", |b| { + b.iter(|| filter_stack_pivot_seq_fast(&readelf_gadgets)) }); c.bench_function("readelf_pivot_filter_par", |b| { b.iter(|| xgadget::filter_stack_pivot(&readelf_gadgets)) }); - c.bench_function("gdb_pivot_filter_seq", |b| { - b.iter(|| filter_stack_pivot_sequential(&gdb_gadgets)) + c.bench_function("gdb_pivot_filter_seq_fast", |b| { + b.iter(|| filter_stack_pivot_seq_fast(&gdb_gadgets)) }); c.bench_function("gdb_pivot_filter_par", |b| { b.iter(|| xgadget::filter_stack_pivot(&gdb_gadgets)) }); } +fn reg_pop_only_bench(c: &mut Criterion) { + const MAX_GADGET_LEN: usize = 5; + + let readelf_bin = xgadget::Binary::from_path_str("/usr/bin/readelf").unwrap(); + let bins = vec![readelf_bin]; + let readelf_gadgets = + xgadget::find_gadgets(&bins, MAX_GADGET_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + + let gdb_bin = xgadget::Binary::from_path_str("/usr/bin/gdb").unwrap(); + let bins = vec![gdb_bin]; + let gdb_gadgets = + xgadget::find_gadgets(&bins, MAX_GADGET_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + + c.bench_function("readelf_reg_pop_only_filter_par", |b| { + b.iter(|| xgadget::filter_reg_pop_only(&readelf_gadgets)) + }); + c.bench_function("readelf_reg_pop_only_regex", |b| { + b.iter(|| filter_reg_pop_only_regex(&readelf_gadgets)) + }); + c.bench_function("gdb_reg_pop_only_filter_par", |b| { + b.iter(|| xgadget::filter_reg_pop_only(&gdb_gadgets)) + }); + c.bench_function("gdb_reg_pop_only_regex", |b| { + b.iter(|| filter_reg_pop_only_regex(&gdb_gadgets)) + }); +} + // Runner -------------------------------------------------------------------------------------------------------------- -criterion_group!(benches, pivot_bench); +criterion_group!(benches, reg_pop_only_bench, pivot_bench); criterion_main!(benches); diff --git a/benches/bench_2_fmt.rs b/benches/bench_2_fmt.rs new file mode 100644 index 0000000..d381b1d --- /dev/null +++ b/benches/bench_2_fmt.rs @@ -0,0 +1,128 @@ +use colored::Colorize; +use criterion::{criterion_group, criterion_main, Criterion}; + +mod flame_graph; + +const MAX_LEN: usize = 100; + +#[rustfmt::skip] +pub const X_RET_AFTER_JNE_AND_ADJACENT_JMP_X64: &[u8] = &[ + 0x48, 0x8b, 0x84, 0x24, 0xb8, 0x00, 0x00, 0x00, // mov rax,QWORD PTR [rsp+0xb8] + 0x64, 0x48, 0x33, 0x04, 0x25, 0x28, 0x00, 0x00, 0x00, // xor rax,QWORD PTR fs:0x28 + 0x0f, 0x85, 0xf0, 0x01, 0x00, 0x00, // jne a3fc <__sprintf_chk@plt+0x86c> + 0x48, 0x81, 0xc4, 0xc8, 0x00, 0x00, 0x00, // add rsp,0xc8 + 0x44, 0x89, 0xe0, // mov eax,r12d + 0x5b, // pop rbx + 0x5d, // pop rbp + 0x41, 0x5c, // pop r12 + 0x41, 0x5d, // pop r13 + 0x41, 0x5e, // pop r14 + 0x41, 0x5f, // pop r15 + 0xc3, // ret + 0x48, 0x8d, 0x0d, 0xe1, 0xdd, 0x05, 0x00, // lea rcx,[rip+0x5DDE1] + 0xff, 0xe1, // jmp rcx + 0x48, 0x8d, 0x0d, 0xcb, 0xdd, 0x05, 0x00, // lea rax,[rip+0x5DDCB] // Intentionally unused rax + 0xff, 0x21, // jmp [rcx] +]; + +#[rustfmt::skip] +pub const X_RET_AFTER_JNE_AND_ADJACENT_CALL_MIX_MATCH_X64: &[u8] = &[ + 0x48, 0x8b, 0x84, 0x24, 0xb8, 0x00, 0x00, 0x00, // mov rax,QWORD PTR [rsp+0xb8] + 0x64, 0x48, 0x33, 0x04, 0x25, 0x28, 0x00, 0x00, 0x00, // xor rax,QWORD PTR fs:0x28 + 0x0f, 0x85, 0xf0, 0x01, 0x00, 0x00, // jne a3fc <__sprintf_chk@plt+0x86c> + 0x48, 0x81, 0xc4, 0xc8, 0x00, 0x00, 0x00, // add rsp,0xc8 + 0x44, 0x89, 0xe0, // mov eax,r12d + 0x41, 0x5e, // pop r14 + 0x41, 0x5f, // pop r15 + 0xc3, // ret - Partial match, X_RET_AFTER_JNE_AND_ADJACENT_CALL_X64 and X_RET_AFTER_JNE_AND_ADJACENT_JMP_X64 + 0x5b, // pop rbx + 0x5d, // pop rbp + 0x41, 0x5c, // pop r12 + 0x41, 0x5d, // pop r13 + 0x48, 0x8d, 0x1d, 0xe1, 0xdd, 0x05, 0x00, // lea rbx,[rip+0x5DDE1] + 0xff, 0xd3, // call rbx - Full match against X_RET_AFTER_JNE_AND_ADJACENT_CALL_X64 + 0x48, 0x8d, 0x1d, 0xcb, 0xdd, 0x05, 0x00, // lea rbx,[rip+0x5DDCB] + 0xff, 0x21, // jmp [rcx] - Full match against X_RET_AFTER_JNE_AND_ADJACENT_JMP_X64 +]; + +fn get_raw_bin(name: &str, bytes: &[u8]) -> xgadget::Binary { + let mut bin = xgadget::Binary::from_bytes(&name, &bytes).unwrap(); + assert_eq!(bin.format(), xgadget::Format::Raw); + assert_eq!(bin.arch(), xgadget::Arch::Unknown); + bin.set_arch(xgadget::Arch::X64); + + bin +} + +fn collect_strs_seq(gadgets: &[xgadget::Gadget], extended: bool) -> Vec { + let att = false; + let color = true; + let term_width = 150; + gadgets + .iter() + .filter_map(|g| g.fmt(att, color)) + .map(|(instrs, addrs)| match extended { + true => { + let content_len = instrs.len() + addrs.len(); + match term_width > content_len { + true => { + let padding = (0..(term_width - content_len)) + .map(|_| "-") + .collect::(); + + let padding = match color { + true => padding, + false => format!("{}", padding.bright_magenta()), + }; + + format!("{}{} [ {} ]", instrs, padding, addrs) + } + false => { + format!("{} [ {} ]", instrs, addrs) + } + } + } + false => { + format!("{}{} {}", addrs, ":".bright_magenta(), instrs) + } + }) + .collect() +} + +fn fmt_bench(c: &mut Criterion) { + let bin_ret_post_jmp = get_raw_bin("bin_ret_post_jmp", &X_RET_AFTER_JNE_AND_ADJACENT_JMP_X64); + let bins = vec![bin_ret_post_jmp]; + let gadgets = xgadget::find_gadgets(&bins, MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + + c.bench_function("fmt_regular", |b| { + b.iter(|| collect_strs_seq(&gadgets, false)) + }); + + c.bench_function("fmt_extended", |b| { + b.iter(|| collect_strs_seq(&gadgets, true)) + }); +} + +fn fmt_partial_bench(c: &mut Criterion) { + let bin_ret_jmp = get_raw_bin("bin_ret_jmp", &X_RET_AFTER_JNE_AND_ADJACENT_JMP_X64); + let bin_mix = get_raw_bin("bin_mix", &X_RET_AFTER_JNE_AND_ADJACENT_CALL_MIX_MATCH_X64); + + let full_part_match_config = xgadget::SearchConfig::DEFAULT | xgadget::SearchConfig::PART; + assert!(full_part_match_config.intersects(xgadget::SearchConfig::PART)); + + let bins = vec![bin_mix, bin_ret_jmp]; + let gadgets = xgadget::find_gadgets(&bins, MAX_LEN, full_part_match_config).unwrap(); + + c.bench_function("fmt_partial", |b| { + b.iter(|| collect_strs_seq(&gadgets, true)) + }); +} + +// Runner -------------------------------------------------------------------------------------------------------------- + +criterion_group!( + name = benches; + config = Criterion::default().with_profiler(flame_graph::FlamegraphProfiler::new(100)); + targets = fmt_bench, fmt_partial_bench +); +criterion_main!(benches); diff --git a/benches/bench_2_elf_userspace.rs b/benches/bench_3_elf_userspace.rs similarity index 100% rename from benches/bench_2_elf_userspace.rs rename to benches/bench_3_elf_userspace.rs diff --git a/benches/bench_3_elf_kernels.rs b/benches/bench_4_elf_kernels.rs similarity index 100% rename from benches/bench_3_elf_kernels.rs rename to benches/bench_4_elf_kernels.rs diff --git a/benches/flame_graph.rs b/benches/flame_graph.rs new file mode 100644 index 0000000..a2ffa41 --- /dev/null +++ b/benches/flame_graph.rs @@ -0,0 +1,42 @@ +// Source: https://www.jibbow.com/posts/criterion-flamegraphs/ + +use std::{fs::File, os::raw::c_int, path::Path}; + +use criterion::profiler::Profiler; +use pprof::ProfilerGuard; + +pub struct FlamegraphProfiler<'a> { + frequency: c_int, + active_profiler: Option>, +} + +impl<'a> FlamegraphProfiler<'a> { + #[allow(dead_code)] + pub fn new(frequency: c_int) -> Self { + FlamegraphProfiler { + frequency, + active_profiler: None, + } + } +} + +impl<'a> Profiler for FlamegraphProfiler<'a> { + fn start_profiling(&mut self, _benchmark_id: &str, _benchmark_dir: &Path) { + self.active_profiler = Some(ProfilerGuard::new(self.frequency).unwrap()); + } + + fn stop_profiling(&mut self, _benchmark_id: &str, benchmark_dir: &Path) { + std::fs::create_dir_all(benchmark_dir).unwrap(); + let flamegraph_path = benchmark_dir.join("flamegraph.svg"); + let flamegraph_file = File::create(&flamegraph_path) + .expect("File system error while creating flamegraph.svg"); + if let Some(profiler) = self.active_profiler.take() { + profiler + .report() + .build() + .unwrap() + .flamegraph(flamegraph_file) + .expect("Error writing flamegraph"); + } + } +} diff --git a/gen_readme.sh b/gen_readme.sh new file mode 100755 index 0000000..7abcf37 --- /dev/null +++ b/gen_readme.sh @@ -0,0 +1 @@ +cargo readme --no-indent-headings > README.md \ No newline at end of file diff --git a/src/binary/arch.rs b/src/binary/arch.rs new file mode 100644 index 0000000..f9e8b28 --- /dev/null +++ b/src/binary/arch.rs @@ -0,0 +1,31 @@ +use std::str::FromStr; + +/// Architecture +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Arch { + Unknown = 0, + X8086 = 16, + X86 = 32, + X64 = 64, +} + +impl Arch { + /// Arch -> bitness + pub fn bits(&self) -> u32 { + *self as u32 + } +} + +impl FromStr for Arch { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "unknown" => Ok(Arch::Unknown), + "x8086" => Ok(Arch::X8086), + "x86" => Ok(Arch::X86), + "x64" => Ok(Arch::X64), + _ => Err("Could not parse architecture string to enum"), + } + } +} diff --git a/src/binary.rs b/src/binary/binary.rs similarity index 53% rename from src/binary.rs rename to src/binary/binary.rs index 28073ba..af8d1f1 100644 --- a/src/binary.rs +++ b/src/binary/binary.rs @@ -2,115 +2,32 @@ use std::error::Error; use std::fmt; use std::fs; use std::path::Path; -use std::str::FromStr; -use rayon::prelude::*; +use colored::Colorize; use rustc_hash::FxHashSet as HashSet; -// Segment ------------------------------------------------------------------------------------------------------------- - -/// A single executable segment -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub struct Segment { - pub addr: u64, - pub bytes: Vec, -} - -impl Segment { - /// Constructor - pub fn new(addr: u64, bytes: Vec) -> Segment { - Segment { addr, bytes } - } - - /// Check if contains address - pub fn contains(&self, addr: u64) -> bool { - (self.addr <= addr) && (addr < (self.addr + self.bytes.len() as u64)) - } - - /// Get offsets of byte occurrences - pub fn get_matching_offsets(&self, vals: &[u8]) -> Vec { - self.bytes - .par_iter() - .enumerate() - .filter(|&(_, b)| vals.contains(b)) - .map(|(i, _)| i) - .collect() - } -} +use super::arch::Arch; +use super::consts::*; +use super::file_format::Format; +use super::segment::Segment; // Binary -------------------------------------------------------------------------------------------------------------- -/// File format -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum Format { - Unknown, - ELF, - PE, - Raw, -} - -impl FromStr for Format { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "unknown" => Ok(Format::Unknown), - "elf" => Ok(Format::ELF), - "pe" => Ok(Format::PE), - "raw" => Ok(Format::Raw), - _ => Err("Could not parse format string to enum"), - } - } -} - -/// Architecture -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum Arch { - Unknown = 0, - X8086 = 16, - X86 = 32, - X64 = 64, -} - -impl Arch { - /// Arch -> bitness - pub fn bits(&self) -> u32 { - *self as u32 - } -} - -impl FromStr for Arch { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "unknown" => Ok(Arch::Unknown), - "x8086" => Ok(Arch::X8086), - "x86" => Ok(Arch::X86), - "x64" => Ok(Arch::X64), - _ => Err("Could not parse architecture string to enum"), - } - } -} - /// File format agnostic binary #[derive(Debug, PartialEq, Eq, Clone)] pub struct Binary { - pub name: String, - pub format: Format, - pub arch: Arch, - pub entry: u64, - pub segments: HashSet, + name: String, + format: Format, + arch: Arch, + entry: u64, + param_regs: Option<&'static [iced_x86::Register]>, + segments: HashSet, + color_display: bool, } impl Binary { // Binary Public API ----------------------------------------------------------------------------------------------- - /// Binary -> bitness - pub fn bits(&self) -> u32 { - self.arch.bits() - } - /// Byte slice -> Binary pub fn from_bytes(name: &str, bytes: &[u8]) -> Result> { Binary::priv_from_buf(name, bytes) @@ -125,6 +42,51 @@ impl Binary { Binary::priv_from_buf(name_str, &bytes) } + /// Get name + pub fn name(&self) -> &str { + self.name.as_str() + } + + /// Get format + pub fn format(&self) -> Format { + self.format + } + + /// Get arch + pub fn arch(&self) -> Arch { + self.arch + } + + /// Set arch + pub fn set_arch(&mut self, arch: Arch) { + self.arch = arch + } + + /// Get entry point + pub fn entry(&self) -> u64 { + self.entry + } + + /// Get param registers + pub fn param_regs(&self) -> &Option<&'static [iced_x86::Register]> { + &self.param_regs + } + + /// Get segments + pub fn segments(&self) -> &HashSet { + &self.segments + } + + /// Binary -> bitness + pub fn bits(&self) -> u32 { + self.arch.bits() + } + + /// Enable/disable colored `Display` + pub fn set_color_display(&mut self, enable: bool) { + self.color_display = enable; + } + // Binary Private API ---------------------------------------------------------------------------------------------- // Construction helper @@ -134,17 +96,22 @@ impl Binary { format: Format::Unknown, arch: Arch::Unknown, entry: 0, + param_regs: None, segments: HashSet::default(), + color_display: true, } } // Bytes -> Binary fn priv_from_buf(name: &str, bytes: &[u8]) -> Result> { - match goblin::Object::parse(&bytes)? { - goblin::Object::Elf(elf) => Binary::from_elf(name, &bytes, &elf), - goblin::Object::PE(pe) => Binary::from_pe(name, &bytes, &pe), - goblin::Object::Unknown(_) => Ok(Binary::from_raw(name, bytes)), - _ => Err("Unsupported file format!".into()), + match goblin::Object::parse(&bytes) { + Ok(obj) => match obj { + goblin::Object::Unknown(_) => Ok(Binary::from_raw(name, bytes)), + goblin::Object::Elf(elf) => Binary::from_elf(name, &bytes, &elf), + goblin::Object::PE(pe) => Binary::from_pe(name, &bytes, &pe), + _ => Err("Unsupported file format!".into()), + }, + _ => Ok(Binary::from_raw(name, bytes)), } } @@ -169,6 +136,11 @@ impl Binary { } }; + // Argument registers + if bin.arch == Arch::X64 { + bin.param_regs = Some(X64_ELF_PARAM_REGS); + } + // Executable segments for prog_hdr in elf .program_headers @@ -205,6 +177,11 @@ impl Binary { } }; + // Argument registers + if bin.arch == Arch::X64 { + bin.param_regs = Some(X64_PE_PARAM_REGS); + } + // Executable segments for sec_tab in pe.sections.iter().filter(|&p| { (p.characteristics & goblin::pe::section_table::IMAGE_SCN_MEM_EXECUTE) != 0 @@ -260,17 +237,91 @@ impl Binary { // Summary print impl fmt::Display for Binary { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let color_punctuation = |s: &str| { + if self.color_display { + s.bright_magenta() + } else { + s.normal() + } + }; + + let seg_cnt = self.segments.len(); + + let bytes = self + .segments + .iter() + .fold(0, |bytes, seg| bytes + seg.bytes.len()); + + let single_quote = color_punctuation("'"); + let forward_slash = color_punctuation("/"); + let dash = color_punctuation("-"); + let colon = color_punctuation(":"); + let comma = color_punctuation(","); + write!( f, - "\'{}\': {:?}-{:?}, entry 0x{:016x}, {}/{} executable bytes/segments", - self.name, - self.format, - self.arch, - self.entry, - self.segments - .iter() - .fold(0, |bytes, seg| bytes + seg.bytes.len()), - self.segments.len(), + "{}{}{}{} {}{}{}{} {} entry{} {}{}{} executable bytes{}segments", + single_quote, + { + match self.color_display { + true => self.name.cyan(), + false => self.name.normal(), + } + }, + single_quote, + colon, + { + match self.color_display { + true => format!("{:?}", self.format).yellow(), + false => format!("{:?}", self.format).normal(), + } + }, + dash, + { + match self.color_display { + true => format!("{:?}", self.arch).yellow(), + false => format!("{:?}", self.arch).normal(), + } + }, + comma, + { + match self.color_display { + true => format!("{:#016x}", self.entry).green(), + false => format!("{:#016x}", self.entry).normal(), + } + }, + comma, + { + match self.color_display { + true => format!("{}", bytes).bright_blue(), + false => format!("{}", bytes).normal(), + } + }, + forward_slash, + { + match self.color_display { + true => format!("{}", seg_cnt).bright_blue(), + false => format!("{}", seg_cnt).normal(), + } + }, + forward_slash, ) } } + +// Misc Helpers -------------------------------------------------------------------------------------------------------- + +/// Get set union of all parameter registers for a list of binaries +pub fn get_all_param_regs(bins: &[Binary]) -> Vec { + let mut param_regs = HashSet::default(); + + for b in bins { + if let Some(regs) = b.param_regs { + for reg in regs { + param_regs.insert(*reg); + } + } + } + + param_regs.into_iter().collect() +} diff --git a/src/binary/consts.rs b/src/binary/consts.rs new file mode 100644 index 0000000..1bcccfa --- /dev/null +++ b/src/binary/consts.rs @@ -0,0 +1,21 @@ +// Argument Registers -------------------------------------------------------------------------------------------------- + +/// x64 ELF argument registers +#[rustfmt::skip] +pub static X64_ELF_PARAM_REGS: &[iced_x86::Register] = &[ + iced_x86::Register::RDI, + iced_x86::Register::RSI, + iced_x86::Register::RDX, + iced_x86::Register::RCX, + iced_x86::Register::R8, + iced_x86::Register::R9, +]; + +/// x64 PE argument registers +#[rustfmt::skip] +pub static X64_PE_PARAM_REGS: &[iced_x86::Register] = &[ + iced_x86::Register::RCX, + iced_x86::Register::RDX, + iced_x86::Register::R8, + iced_x86::Register::R9, +]; diff --git a/src/binary/file_format.rs b/src/binary/file_format.rs new file mode 100644 index 0000000..fae8a06 --- /dev/null +++ b/src/binary/file_format.rs @@ -0,0 +1,24 @@ +use std::str::FromStr; + +/// File format +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Format { + Unknown, + ELF, + PE, + Raw, +} + +impl FromStr for Format { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "unknown" => Ok(Format::Unknown), + "elf" => Ok(Format::ELF), + "pe" => Ok(Format::PE), + "raw" => Ok(Format::Raw), + _ => Err("Could not parse format string to enum"), + } + } +} diff --git a/src/binary/mod.rs b/src/binary/mod.rs new file mode 100644 index 0000000..e0e40d8 --- /dev/null +++ b/src/binary/mod.rs @@ -0,0 +1,15 @@ +mod arch; +pub use arch::*; + +#[allow(clippy::module_inception)] +mod binary; +pub use binary::*; + +mod consts; +pub use consts::*; + +mod file_format; +pub use file_format::*; + +mod segment; +pub use segment::*; diff --git a/src/binary/segment.rs b/src/binary/segment.rs new file mode 100644 index 0000000..29d6567 --- /dev/null +++ b/src/binary/segment.rs @@ -0,0 +1,30 @@ +use rayon::prelude::*; + +/// A single executable segment +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct Segment { + pub addr: u64, + pub bytes: Vec, +} + +impl Segment { + /// Constructor + pub fn new(addr: u64, bytes: Vec) -> Segment { + Segment { addr, bytes } + } + + /// Check if contains address + pub fn contains(&self, addr: u64) -> bool { + (self.addr <= addr) && (addr < (self.addr + self.bytes.len() as u64)) + } + + /// Get offsets of byte occurrences + pub fn get_matching_offsets(&self, vals: &[u8]) -> Vec { + self.bytes + .par_iter() + .enumerate() + .filter(|&(_, b)| vals.contains(b)) + .map(|(i, _)| i) + .collect() + } +} diff --git a/src/cli/cli.rs b/src/cli/cli.rs index e987600..5f4100a 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -1,22 +1,13 @@ use std::fmt; use std::fs; -use std::time::Instant; use checksec::elf::ElfCheckSecResults; use checksec::pe::PECheckSecResults; use colored::Colorize; use goblin::Object; -use rayon::prelude::*; -use regex::Regex; use structopt::StructOpt; -mod checksec_fmt; -use checksec_fmt::{CustomElfCheckSecResults, CustomPeCheckSecResults}; - -#[macro_use] -extern crate lazy_static; - -// CLI State ----------------------------------------------------------------------------------------------------------- +use super::checksec_fmt::{CustomElfCheckSecResults, CustomPeCheckSecResults}; lazy_static! { static ref ABOUT_STR: String = format!( @@ -33,26 +24,26 @@ lazy_static! { #[derive(StructOpt, Debug)] #[structopt(name = "xgadget", version = VERSION_STR.as_str(), about = ABOUT_STR.as_str())] -struct CLIOpts { +pub(crate) struct CLIOpts { /// 1+ binaries to gadget search. If > 1: gadgets common to all #[structopt(required = true, min_values = 1, value_name = "FILE(S)")] - bin_paths: Vec, + pub(crate) bin_paths: Vec, /// For raw (no header) files: specify arch ('x8086', 'x86', or 'x64') #[structopt(short, long, default_value = "x64", value_name = "ARCH")] - arch: xgadget::Arch, + pub(crate) arch: xgadget::Arch, /// Display gadgets using AT&T syntax [default: Intel syntax] #[structopt(short = "t", long)] - att: bool, + pub(crate) att: bool, - /// Don't color output, useful for UNIX piping [default: color output] + /// Don't color output [default: color output] #[structopt(short, long)] - no_color: bool, + pub(crate) no_color: bool, /// Print in terminal-wide format [default: only used for partial match search] #[structopt(short, long)] - extended_fmt: bool, + pub(crate) extended_fmt: bool, /// Gadgets up to LEN instrs long. If 0: all gadgets, any length #[structopt( @@ -62,64 +53,77 @@ struct CLIOpts { default_value = "5", value_name = "LEN" )] - max_len: usize, + pub(crate) max_len: usize, /// Search for ROP gadgets only [default: ROP, JOP, and SYSCALL] #[structopt(short, long)] - rop: bool, + pub(crate) rop: bool, /// Search for JOP gadgets only [default: ROP, JOP, and SYSCALL] #[structopt(short, long, conflicts_with = "rop")] - jop: bool, + pub(crate) jop: bool, /// Search for SYSCALL gadgets only [default: ROP, JOP, and SYSCALL] #[structopt(short, long, conflicts_with = "jop")] - sys: bool, + pub(crate) sys: bool, /// Include '{ret, ret far} imm16' (e.g. add to stack ptr) [default: don't include] #[structopt(long, conflicts_with = "jop")] - inc_imm16: bool, + pub(crate) inc_imm16: bool, /// Include gadgets containing a call [default: don't include] #[structopt(long)] - inc_call: bool, + pub(crate) inc_call: bool, /// Include cross-variant partial matches [default: full matches only] #[structopt(short = "m", long)] - partial_match: bool, + pub(crate) partial_match: bool, - /// Filter to gadgets that write the stack ptr [default: all gadgets] + /// Filter to gadgets that write the stack ptr [default: all] #[structopt(short = "p", long)] - stack_pivot: bool, + pub(crate) stack_pivot: bool, - /// Filter to potential JOP 'dispatcher' gadgets [default: all gadgets] + /// Filter to potential JOP 'dispatcher' gadgets [default: all] #[structopt(short, long, conflicts_with_all = &["rop", "stack_pivot"])] - dispatcher: bool, + pub(crate) dispatcher: bool, + + /// Filter to 'pop {reg} * 1+, {ret or ctrl-ed jmp/call}' gadgets [default: all] + #[structopt(long, conflicts_with = "dispatcher")] + pub(crate) reg_pop: bool, + + /// Filter to gadgets that don't deref any regs or a specific reg [default: all] + #[structopt(long, value_name = "OPT_REG")] + pub(crate) no_deref: Option>, - /// Filter to 'pop {reg} * 1+, {ret or ctrl-ed jmp/call}' gadgets [default: all gadgets] - #[structopt(short = "w", long, conflicts_with = "dispatcher")] - reg_write: bool, + /// Filter to gadgets that control any reg or a specific reg [default: all] + #[structopt(long, value_name = "OPT_REG")] + pub(crate) reg_ctrl: Option>, - /// Filter to gadgets whose addrs don't contain given bytes [default: all gadgets] + /// Filter to gadgets that control function parameters [default: all] + #[structopt(long)] + pub(crate) param_ctrl: bool, + + /// Filter to gadgets whose addrs don't contain given bytes [default: all] #[structopt(short, long, min_values = 1, value_name = "BYTE(S)")] - bad_bytes: Vec, + pub(crate) bad_bytes: Vec, /// Filter to gadgets matching a regular expression #[structopt(short = "f", long = "regex-filter", value_name = "EXPR")] - usr_regex: Option, + pub(crate) usr_regex: Option, /// Run checksec on the 1+ binaries instead of gadget search #[structopt(short, long, conflicts_with_all = &[ "arch", "att", "extended_fmt", "max_len", "rop", "jop", "sys", "imm16", "partial_match", - "stack_pivot", "dispatcher", "reg_write", "usr_regex" - ])] // TODO: Custom short name (e.g. "-m" for "--partial-match" not tagged as conflict) - check_sec: bool, + "stack_pivot", "dispatcher", "reg_pop", "usr_regex" + ])] + // TODO: Custom short name (e.g. "-m" for "--partial-match" not tagged as conflict) - why? + pub(crate) check_sec: bool, } impl CLIOpts { // User flags -> Search config bitfield - fn get_search_config(&self) -> xgadget::SearchConfig { + pub(crate) fn get_search_config(&self) -> xgadget::SearchConfig { let mut search_config = xgadget::SearchConfig::DEFAULT; // Add to default @@ -156,38 +160,8 @@ impl CLIOpts { search_config } - // If partial match or extended format flag, addr(s) right of instr(s), else addr left of instr(s) - fn fmt_gadget_output(&self, addrs: String, instrs: String, term_width: usize) -> String { - let plaintext_instrs_len = strip_ansi_escapes::strip(&instrs).unwrap().len(); - let plaintext_addrs_len = strip_ansi_escapes::strip(&addrs).unwrap().len(); - let content_len = plaintext_instrs_len + plaintext_addrs_len; - let mut output = instrs; - - if self.extended_fmt || self.partial_match { - if term_width > content_len { - let padding = (0..(term_width - 1 - content_len)) - .map(|_| "-") - .collect::(); - - if self.no_color { - output.push_str(&padding); - } else { - output.push_str(&format!("{}", padding.bright_magenta())); - //output.push_str(&padding.bright_magenta()); // TODO: why doesn't this color, bug? - } - } - - output.push_str(&format!(" {}", addrs)); - } else { - let addr_no_bracket = &addrs[1..(addrs.len() - 1)].trim(); - output = format!("{}{} {}", addr_no_bracket, ":".bright_magenta(), output); - } - - output - } - // Helper for summary print - fn fmt_summary_item(&self, item: String, is_hdr: bool) -> colored::ColoredString { + pub(crate) fn fmt_summary_item(&self, item: String, is_hdr: bool) -> colored::ColoredString { let hdr = |s: String| { if self.no_color { s.trim().normal() @@ -211,7 +185,7 @@ impl CLIOpts { } // Helper for running checksec on requested binaries - fn run_checksec(&self) { + pub(crate) fn run_checksec(&self) { for path in &self.bin_paths { println!("\n{}:", self.fmt_summary_item(path.to_string(), false)); let buf = fs::read(path).unwrap(); @@ -243,29 +217,48 @@ impl fmt::Display for CLIOpts { "{} [ search: {}, x_match: {}, max_len: {}, syntax: {}, regex_filter: {} ]", { self.fmt_summary_item("CONFIG".to_string(), true) }, { - let mut search_mode = String::from(""); + let mut search_mode = String::from("ROP-JOP-SYS (default)"); if self.rop { - search_mode = format!("{} {}", search_mode, "ROP-only") + search_mode = String::from("ROP-only"); }; if self.jop { - search_mode = format!("{} {}", search_mode, "JOP-only") + search_mode = String::from("JOP-only"); }; if self.sys { - search_mode = format!("{} {}", search_mode, "SYS-only") + search_mode = String::from("SYS-only"); }; if self.stack_pivot { - search_mode = format!("{} {}", search_mode, "Stack-pivot-only") + search_mode = String::from("Stack-pivot-only"); }; if self.dispatcher { - search_mode = format!("{} {}", search_mode, "Dispatcher-only") + search_mode = String::from("Dispatcher-only"); }; - if self.reg_write { - search_mode = format!("{} {}", search_mode, "Register-pop-only") + if self.reg_pop { + search_mode = String::from("Register-pop-only"); }; - if search_mode.is_empty() { - search_mode = String::from("ROP-JOP-SYS (default)") + if self.param_ctrl { + search_mode = String::from("Param-ctrl-only"); + }; + if let Some(opt_reg) = &self.reg_ctrl { + match opt_reg { + Some(reg) => { + search_mode = format!("Reg-ctrl-{}-only", reg.to_lowercase()); + } + None => { + search_mode = String::from("Reg-ctrl-only"); + } + } + }; + if let Some(opt_reg) = &self.no_deref { + match opt_reg { + Some(reg) => { + search_mode = format!("No-deref-{}-only", reg.to_lowercase()); + } + None => { + search_mode = String::from("No-deref-only"); + } + } }; - self.fmt_summary_item(search_mode, false) }, { @@ -297,124 +290,3 @@ impl fmt::Display for CLIOpts { ) } } - -// CLI Runner ---------------------------------------------------------------------------------------------------------- - -fn main() { - let cli = CLIOpts::from_args(); - - let mut filter_matches = 0; - let filter_regex = Regex::new( - &cli.usr_regex - .clone() - .unwrap_or_else(|| "unused_but_initialized".to_string()) - .trim(), - ) - .unwrap(); - - // Checksec requested ---------------------------------------------------------------------------------------------- - - if cli.check_sec { - cli.run_checksec(); - std::process::exit(0); - } - - // Process 1+ files ------------------------------------------------------------------------------------------------ - - // File paths -> Binaries - let bins: Vec = cli - .bin_paths - .par_iter() - .map(|path| xgadget::Binary::from_path_str(&path).unwrap()) - .map(|mut binary| { - if binary.arch == xgadget::Arch::Unknown { - binary.arch = cli.arch; - assert!( - binary.arch != xgadget::Arch::Unknown, - "Please set \'--arch\' to \'x8086\' (16-bit), \'x86\' (32-bit), or \'x64\' (64-bit)" - ); - } - binary - }) - .collect(); - - for (i, bin) in bins.iter().enumerate() { - println!("TARGET {} - {} ", i, bin); - } - - // Search ---------------------------------------------------------------------------------------------------------- - - let start_time = Instant::now(); - let mut gadgets = xgadget::find_gadgets(&bins, cli.max_len, cli.get_search_config()).unwrap(); - - if cli.stack_pivot { - gadgets = xgadget::filter_stack_pivot(&gadgets); - } - - if cli.dispatcher { - gadgets = xgadget::filter_dispatcher(&gadgets); - } - - if cli.reg_write { - gadgets = xgadget::filter_stack_set_regs(&gadgets); - } - - if !cli.bad_bytes.is_empty() { - let bytes = cli - .bad_bytes - .iter() - .map(|s| s.trim_start_matches("0x")) - .map(|s| u8::from_str_radix(s, 16).unwrap()) - .collect::>(); - - gadgets = xgadget::filter_bad_addr_bytes(&gadgets, bytes.as_slice()); - } - - let run_time = start_time.elapsed(); - - // Print Gadgets --------------------------------------------------------------------------------------------------- - - let term_width: usize = match term_size::dimensions() { - Some((w, _)) => w, - None => 0, - }; - - println!(); - for (instrs, addrs) in xgadget::str_fmt_gadgets(&gadgets, cli.att, !cli.no_color) { - let plaintext_instrs_bytes = strip_ansi_escapes::strip(&instrs).unwrap(); - let plaintext_instrs_str = std::str::from_utf8(&plaintext_instrs_bytes).unwrap(); - - if (cli.usr_regex.is_none()) || filter_regex.is_match(plaintext_instrs_str) { - println!("{}", cli.fmt_gadget_output(addrs, instrs, term_width)); - if cli.usr_regex.is_some() { - filter_matches += 1; - } - } - } - - // Print Summary --------------------------------------------------------------------------------------------------- - - println!("\n{}", cli); - println!( - "{} [ {}: {}, search_time: {}, print_time: {} ]", - { cli.fmt_summary_item("RESULT".to_string(), true) }, - { - if bins.len() > 1 { - "unique_x_variant_gadgets".to_string() - } else { - "unique_gadgets".to_string() - } - }, - { - let found_cnt = if cli.usr_regex.is_some() { - filter_matches.to_string() - } else { - gadgets.len().to_string() - }; - - cli.fmt_summary_item(found_cnt, false) - }, - { cli.fmt_summary_item(format!("{:?}", run_time), false) }, - { cli.fmt_summary_item(format!("{:?}", start_time.elapsed() - run_time), false) } - ); -} diff --git a/src/cli/main.rs b/src/cli/main.rs new file mode 100644 index 0000000..6874af7 --- /dev/null +++ b/src/cli/main.rs @@ -0,0 +1,222 @@ +use std::time::Instant; + +use colored::Colorize; +use rayon::prelude::*; +use regex::Regex; +use structopt::StructOpt; + +mod reg_str; +use reg_str::str_to_reg; + +mod cli; +use cli::CLIOpts; + +mod checksec_fmt; + +#[macro_use] +extern crate lazy_static; + +fn main() { + let cli = CLIOpts::from_args(); + + let mut filter_matches = 0; + let filter_regex = cli.usr_regex.clone().map(|r| Regex::new(&r).unwrap()); + + // Checksec requested ---------------------------------------------------------------------------------------------- + + if cli.check_sec { + cli.run_checksec(); + std::process::exit(0); + } + + // Process 1+ files ------------------------------------------------------------------------------------------------ + + // File paths -> Binaries + let bins: Vec = cli + .bin_paths + .par_iter() + .map(|path| xgadget::Binary::from_path_str(&path).unwrap()) + .map(|mut binary| { + if binary.arch() == xgadget::Arch::Unknown { + binary.set_arch(cli.arch); + assert!( + binary.arch() != xgadget::Arch::Unknown, + "Please set \'--arch\' to \'x8086\' (16-bit), \'x86\' (32-bit), or \'x64\' (64-bit)" + ); + } + binary.set_color_display(!cli.no_color); + binary + }) + .collect(); + + for (i, bin) in bins.iter().enumerate() { + println!( + "TARGET {} - {} ", + { + match cli.no_color { + true => format!("{}", i).normal(), + false => format!("{}", i).red(), + } + }, + bin + ); + } + + // Search ---------------------------------------------------------------------------------------------------------- + + let start_time = Instant::now(); + let mut gadgets = xgadget::find_gadgets(&bins, cli.max_len, cli.get_search_config()).unwrap(); + + if cli.stack_pivot { + gadgets = xgadget::filter_stack_pivot(&gadgets); + } + + if cli.dispatcher { + gadgets = xgadget::filter_dispatcher(&gadgets); + } + + if cli.reg_pop { + gadgets = xgadget::filter_reg_pop_only(&gadgets); + } + + if let Some(opt_reg) = &cli.reg_ctrl { + match opt_reg { + Some(reg_str) => { + let reg = str_to_reg(reg_str) + .unwrap_or_else(|| panic!("Invalid register: {:?}", reg_str)); + gadgets = xgadget::filter_regs_overwritten(&gadgets, Some(&[reg])) + } + None => gadgets = xgadget::filter_regs_overwritten(&gadgets, None), + } + } + + if let Some(opt_reg) = &cli.no_deref { + match opt_reg { + Some(reg_str) => { + let reg = str_to_reg(reg_str) + .unwrap_or_else(|| panic!("Invalid register: {:?}", reg_str)); + gadgets = xgadget::filter_no_deref(&gadgets, Some(&[reg])) + } + None => gadgets = xgadget::filter_no_deref(&gadgets, None), + } + } + + if cli.param_ctrl { + let param_regs = xgadget::get_all_param_regs(&bins); + gadgets = xgadget::filter_set_params(&gadgets, ¶m_regs); + } + + if !cli.bad_bytes.is_empty() { + let bytes = cli + .bad_bytes + .iter() + .map(|s| s.trim_start_matches("0x")) + .map(|s| u8::from_str_radix(s, 16).unwrap()) + .collect::>(); + + gadgets = xgadget::filter_bad_addr_bytes(&gadgets, bytes.as_slice()); + } + + let run_time = start_time.elapsed(); + + // Print Gadgets --------------------------------------------------------------------------------------------------- + + let gadgets_and_strs: Vec<(xgadget::Gadget, String)> = gadgets + .into_par_iter() + .map(|g| (g.fmt_for_filter(cli.att), g)) + .map(|(s, g)| (g, s)) + .collect(); + + let mut filtered_gadgets: Vec<(xgadget::Gadget, String)> = gadgets_and_strs + .into_iter() + .filter(|(_, s)| match &filter_regex { + Some(r) => match r.is_match(&s) { + true => { + filter_matches += 1; + true + } + false => false, + }, + None => true, + }) + .collect(); + + filtered_gadgets.sort_unstable_by(|(_, s1), (_, s2)| s1.cmp(s2)); + + let printable_gadgets: Vec = + filtered_gadgets.into_iter().map(|(g, _)| g).collect(); + + let mut term_width: usize = match term_size::dimensions() { + Some((w, _)) => w, + None => 0, + }; + + // Account for extra chars in our fmt string + if term_width >= 5 { + term_width -= 5; + } + + let gadget_strs: Vec = printable_gadgets + .par_iter() + .filter_map(|g| g.fmt(cli.att, !cli.no_color)) + .map(|(instrs, addrs)| { + // If partial match or extended format flag, addr(s) right of instr(s), else addr left of instr(s) + match cli.extended_fmt || cli.partial_match { + true => { + let content_len = instrs.len() + addrs.len(); + match term_width > content_len { + true => { + let padding = (0..(term_width - content_len)) + .map(|_| "-") + .collect::(); + + let padding = match cli.no_color { + true => padding, + false => format!("{}", padding.bright_magenta()), + }; + + format!("{}{} [ {} ]", instrs, padding, addrs) + } + false => { + format!("{} [ {} ]", instrs, addrs) + } + } + } + false => match cli.no_color { + true => format!("{}: {}", addrs, instrs), + false => format!("{}{} {}", addrs, ":".bright_magenta(), instrs), + }, + } + }) + .collect(); + + println!(); + for s in gadget_strs { + println!("{}", s); + } + + // Print Summary --------------------------------------------------------------------------------------------------- + + println!("\n{}", cli); + println!( + "{} [ {}: {}, search_time: {}, print_time: {} ]", + { cli.fmt_summary_item("RESULT".to_string(), true) }, + { + if bins.len() > 1 { + "unique_x_variant_gadgets".to_string() + } else { + "unique_gadgets".to_string() + } + }, + { + let found_cnt = match filter_regex { + Some(_) => filter_matches.to_string(), + None => printable_gadgets.len().to_string(), + }; + + cli.fmt_summary_item(found_cnt, false) + }, + { cli.fmt_summary_item(format!("{:?}", run_time), false) }, + { cli.fmt_summary_item(format!("{:?}", start_time.elapsed() - run_time), false) } + ); +} diff --git a/src/cli/reg_str.rs b/src/cli/reg_str.rs new file mode 100644 index 0000000..cae74d7 --- /dev/null +++ b/src/cli/reg_str.rs @@ -0,0 +1,56 @@ +use rustc_hash::FxHashMap as HashMap; + +// Dynamic Init -------------------------------------------------------------------------------------------------------- + +lazy_static! { + static ref STR_REG_MAP: HashMap = { + let mut srm = HashMap::default(); + + for reg in iced_x86::Register::values() { + if reg != iced_x86::Register::None { + let reg_str = format!("{:?}", reg).to_uppercase(); + + // Secondary key: R8L-R15L -> R8B-R15B + if (iced_x86::Register::R8L <= reg) && (reg <= iced_x86::Register::R15L) { + srm.insert(reg_str.replace("L", "B"), reg); + } + + srm.insert(reg_str, reg); + } + } + + srm + }; +} + +// Public API ---------------------------------------------------------------------------------------------------------- + +/// Case-insensitive string to register enum conversion +pub fn str_to_reg(rs: &str) -> Option { + match STR_REG_MAP.get(&rs.to_uppercase()) { + Some(reg) => Some(*reg), + None => None, + } +} + +// Test ---------------------------------------------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_reg_strs() { + let mut count = 0; + for reg in iced_x86::Register::values() { + let reg_str = format!("{:?}", reg); + count += 1; + + if reg != iced_x86::Register::None { + println!("{}", reg_str); + assert_eq!(reg, str_to_reg(®_str).unwrap()); + } + } + assert_eq!(count, 249); + } +} diff --git a/src/filters.rs b/src/filters.rs index 28707a7..8f1c2fb 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -7,25 +7,15 @@ use crate::semantics; /// Parallel filter to gadgets that write the stack pointer pub fn filter_stack_pivot<'a>(gadgets: &[gadget::Gadget<'a>]) -> Vec> { - let rsp_write = iced_x86::UsedRegister::new(iced_x86::Register::RSP, iced_x86::OpAccess::Write); - let esp_write = iced_x86::UsedRegister::new(iced_x86::Register::ESP, iced_x86::OpAccess::Write); - let sp_write = iced_x86::UsedRegister::new(iced_x86::Register::SP, iced_x86::OpAccess::Write); - gadgets .par_iter() .filter(|g| { - for instr in &g.instrs { - let mut info_factory = iced_x86::InstructionInfoFactory::new(); - - let info = info_factory - .info_options(&instr, iced_x86::InstructionInfoOptions::NO_MEMORY_USAGE); - - if info.used_registers().contains(&rsp_write) - || info.used_registers().contains(&esp_write) - || info.used_registers().contains(&sp_write) - { - return true; - } + let regs_overwritten = gadget::GadgetAnalysis::new(&g).regs_overwritten(); + if regs_overwritten.contains(&iced_x86::Register::RSP) + || regs_overwritten.contains(&iced_x86::Register::ESP) + || regs_overwritten.contains(&iced_x86::Register::SP) + { + return true; } false }) @@ -40,14 +30,6 @@ pub fn filter_dispatcher<'a>(gadgets: &[gadget::Gadget<'a>]) -> Vec(gadgets: &[gadget::Gadget<'a>]) -> Vec(gadgets: &[gadget::Gadget<'a>]) -> Vec> { +pub fn filter_reg_pop_only<'a>(gadgets: &[gadget::Gadget<'a>]) -> Vec> { gadgets .par_iter() .filter(|g| { @@ -97,6 +78,90 @@ pub fn filter_stack_set_regs<'a>(gadgets: &[gadget::Gadget<'a>]) -> Vec( + gadgets: &[gadget::Gadget<'a>], + param_regs: &[iced_x86::Register], +) -> Vec> { + gadgets + .par_iter() + .filter(|g| { + for instr in &g.instrs { + // Stack push all regs + if instr.mnemonic() == iced_x86::Mnemonic::Pusha + || instr.mnemonic() == iced_x86::Mnemonic::Pushad + { + return true; + } + + // Stack push any reg + if instr.mnemonic() == iced_x86::Mnemonic::Push { + if let Ok(op_kind) = instr.try_op_kind(0) { + if op_kind == iced_x86::OpKind::Register { + return true; + } + } + } + + // Sets param reg + for reg in param_regs { + if semantics::is_reg_set(&instr, ®) { + return true; + } + } + } + false + }) + .cloned() + .collect() +} + +/// Parallel filter to gadgets that don't dereference any registers (if `opt_regs.is_none()`), +/// or don't dereference specific registers (if `opt_regs.is_some()`). +/// Doesn't count the stack pointer unless explicitly provided in `opt_regs`. +pub fn filter_no_deref<'a>( + gadgets: &[gadget::Gadget<'a>], + opt_regs: Option<&[iced_x86::Register]>, +) -> Vec> { + gadgets + .par_iter() + .filter(|g| { + let mut regs_derefed = gadget::GadgetAnalysis::new(&g).regs_dereferenced(); + match opt_regs { + Some(regs) => regs.iter().all(|r| !regs_derefed.contains(r)), + None => { + // Don't count stack pointer + regs_derefed.retain(|r| r != &iced_x86::Register::RSP); + regs_derefed.retain(|r| r != &iced_x86::Register::ESP); + regs_derefed.retain(|r| r != &iced_x86::Register::SP); + + regs_derefed.is_empty() + } + } + }) + .cloned() + .collect() +} + +/// Parallel filter to gadgets that write any register (if `opt_regs.is_none()`), +/// or write specific registers (if `opt_regs.is_some()`). +pub fn filter_regs_overwritten<'a>( + gadgets: &[gadget::Gadget<'a>], + opt_regs: Option<&[iced_x86::Register]>, +) -> Vec> { + gadgets + .par_iter() + .filter(|g| { + let regs_overwritten = gadget::GadgetAnalysis::new(&g).regs_overwritten(); + match opt_regs { + Some(regs) => regs.iter().all(|r| regs_overwritten.contains(r)), + None => !regs_overwritten.is_empty(), + } + }) + .cloned() + .collect() +} + // TODO: use drain_filter() once on stable /// Parallel filter to gadget's whose addresses don't contain specified bytes pub fn filter_bad_addr_bytes<'a>( diff --git a/src/gadget.rs b/src/gadget.rs deleted file mode 100644 index a86722a..0000000 --- a/src/gadget.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet}; -use std::hash::{Hash, Hasher}; - -use crate::binary; - -// TODO: implement Ord for binary, use BTReeSet instead of Vector to maintain sorted order on insertion - will have nicer output at partial match at cost of speed (how much?) - -/// Gadget instructions (data) coupled with occurrence addresses for full and partial matches (metadata). -/// Gadgets sortable by lowest occurrence address. -/// Hash and equality consider only gadget instructions, not occurrence addresses (fast de-duplication via sets). -#[derive(Clone, Debug)] -pub struct Gadget<'a> { - pub instrs: Vec, - pub full_matches: BTreeSet, - pub partial_matches: BTreeMap>, -} - -// TODO: other/getter/setter APIs and private fields? -impl<'a> Gadget<'a> { - /// Assumes instructions are correctly sorted, address guaranteed to be sorted - pub fn new(instrs: Vec, full_matches: BTreeSet) -> Gadget<'a> { - Gadget { - instrs, - full_matches, - partial_matches: BTreeMap::new(), - } - } - - /// Get tail - pub fn last_instr(&self) -> Option<&iced_x86::Instruction> { - self.instrs.iter().next_back() - } - - /// Get first full match - pub fn first_full_match(&self) -> Option { - match self.full_matches.iter().next() { - Some(addr) => Some(*addr), - None => None, - } - } - - // TODO: use this API - /// Add a new partial match address/binary tuple - pub fn add_partial_match(&mut self, addr: u64, bin: &'a binary::Binary) { - match self.partial_matches.get_mut(&addr) { - Some(bins) => bins.push(bin), - None => { - self.partial_matches.insert(addr, vec![bin]); - } - }; - } - - // Ord helper: Lowest gadget occurrence address, full matches preferred - fn min_addr(&self) -> Option<&u64> { - if let Some(min_full) = self.full_matches.iter().next() { - Some(min_full) - } else if let Some(min_part) = self.partial_matches.keys().next() { - Some(min_part) - } else { - None - } - } -} - -impl Eq for Gadget<'_> {} -impl PartialEq for Gadget<'_> { - fn eq(&self, other: &Self) -> bool { - self.instrs == other.instrs - } -} - -impl Ord for Gadget<'_> { - fn cmp(&self, other: &Self) -> Ordering { - if let Some(self_min_addr) = self.min_addr() { - // Both have a minimum address -> compare minimums - if let Some(other_min_addr) = other.min_addr() { - (*self_min_addr).cmp(other_min_addr) - // Self addresses non-empty, other addresses empty -> other is less - } else { - Ordering::Greater - } - } else { - // Self addresses empty, other addresses non-empty -> self is less - if other.min_addr().is_some() { - Ordering::Less - // Self addresses empty, other addresses empty -> equal - } else { - Ordering::Equal - } - } - } -} - -impl PartialOrd for Gadget<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Hash for Gadget<'_> { - fn hash(&self, state: &mut H) { - self.instrs.hash(state); - } -} diff --git a/src/gadget/analysis.rs b/src/gadget/analysis.rs new file mode 100644 index 0000000..ed543e0 --- /dev/null +++ b/src/gadget/analysis.rs @@ -0,0 +1,148 @@ +use rustc_hash::FxHashSet as HashSet; + +use super::gadget::Gadget; + +// Gadget Analysis ----------------------------------------------------------------------------------------------------- + +/// Determines gadget register usage properties. +/// +/// * Registers overwritten (written without reading previous value) +/// * Registers updated (read and then written, within single instruction) +/// * Registers dereferenced for read +/// * Registers dereferenced for write +/// +/// # Limitations +/// * Current logic does not account for all cases of conditional behavior +#[derive(Clone, Debug)] +pub struct GadgetAnalysis { + used_regs: HashSet, + used_mem: HashSet, +} + +impl GadgetAnalysis { + // GadgetAnalysis Public API --------------------------------------------------------------------------------------- + + /// Analyze gadget + pub fn new(gadget: &Gadget) -> Self { + let mut info_factory = iced_x86::InstructionInfoFactory::new(); + let mut unique_used_regs = HashSet::default(); + let mut unique_used_mem = HashSet::default(); + + for instr in &gadget.instrs { + let info = info_factory.info(&instr); + + for ur in info.used_registers() { + unique_used_regs.insert(*ur); + } + + for um in info.used_memory() { + unique_used_mem.insert(*um); + } + } + + GadgetAnalysis { + used_regs: unique_used_regs, + used_mem: unique_used_mem, + } + } + + /// Get full register usage info + pub fn used_regs(&self) -> Vec { + self.used_regs.iter().cloned().collect() + } + + /// Get full memory usage info + pub fn used_mem(&self) -> Vec { + self.used_mem.iter().cloned().collect() + } + + /// Get registers overwritten by gadget (written without reading previous value) + pub fn regs_overwritten(&self) -> Vec { + self.used_regs + .iter() + .filter(|ur| ur.access() == iced_x86::OpAccess::Write) + .map(|ur| ur.register()) + .collect() + } + + /// Get registers updated by gadget (read and then written) + pub fn regs_updated(&self) -> Vec { + self.used_regs + .iter() + .filter(|ur| ur.access() == iced_x86::OpAccess::ReadWrite) + .map(|ur| ur.register()) + // TODO: overwrite taking precedence doesn't take into account conditional behavior + .filter(|r| !self.regs_overwritten().contains(&r)) + .collect() + } + + /// Get registers dereferenced by gadget + pub fn regs_dereferenced(&self) -> Vec { + let mut regs = HashSet::default(); + + for r in self.regs_dereferenced_read() { + regs.insert(r); + } + + for r in self.regs_dereferenced_write() { + regs.insert(r); + } + + regs.into_iter().collect() + } + + /// Get registers dereferenced for read by gadget + pub fn regs_dereferenced_read(&self) -> Vec { + let mem_reads = self + .used_mem + .iter() + .filter(|um| { + let access = um.access(); + (access == iced_x86::OpAccess::Read) + || (access == iced_x86::OpAccess::CondRead) + || (access == iced_x86::OpAccess::ReadWrite) + || (access == iced_x86::OpAccess::ReadCondWrite) + }) + .collect(); + + Self::unique_regs_dereferenced(mem_reads) + } + + /// Get registers dereferenced for write by gadget + pub fn regs_dereferenced_write(&self) -> Vec { + let mem_writes = self + .used_mem + .iter() + .filter(|um| { + let access = um.access(); + (access == iced_x86::OpAccess::Write) + || (access == iced_x86::OpAccess::CondWrite) + || (access == iced_x86::OpAccess::ReadWrite) + || (access == iced_x86::OpAccess::ReadCondWrite) + }) + .collect(); + + Self::unique_regs_dereferenced(mem_writes) + } + + // GadgetAnalysis Private API -------------------------------------------------------------------------------------- + + // Private helper for deref reg collection + fn unique_regs_dereferenced(used_mem: Vec<&iced_x86::UsedMemory>) -> Vec { + let mut regs = HashSet::default(); + + for um in used_mem { + let base_reg = um.base(); + if base_reg != iced_x86::Register::None { + regs.insert(base_reg); + } + + let index_reg = um.index(); + if index_reg != iced_x86::Register::None { + regs.insert(index_reg); + } + } + + regs.into_iter().collect() + } +} diff --git a/src/gadget/fmt.rs b/src/gadget/fmt.rs new file mode 100644 index 0000000..865210c --- /dev/null +++ b/src/gadget/fmt.rs @@ -0,0 +1,185 @@ +use std::collections::BTreeMap; +use std::fmt::Display; +use std::hash::Hasher; + +use colored::Colorize; +use rayon::prelude::*; +use rustc_hash::FxHasher; + +use crate::gadget; + +// Public Types and Traits --------------------------------------------------------------------------------------------- + +/// Implementors track count of visible terminal characters for `Display`'s `fmt` function (for colored strings). +pub trait DisplayLen: Display { + /// Get the count of visible terminal characters for `Display`'s `fmt` function + fn len(&self) -> usize; + + /// Check if display length is zero + fn is_empty(&self) -> bool; +} + +/// String wrapper that tracks count of visible terminal characters for `Display`'s `fmt` function. +pub struct DisplayString(pub String); + +impl DisplayLen for DisplayString { + /// Get the count of visible terminal characters for `Display`'s `fmt` function + fn len(&self) -> usize { + self.0.len() + } + + /// Check if display length is zero + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl Display for DisplayString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +// Public API ---------------------------------------------------------------------------------------------------------- + +/// Format list of gadgets in parallel, return alphabetically sorted `String`s +pub fn fmt_gadget_str_list( + gadgets: &[gadget::Gadget], + att_syntax: bool, + color: bool, +) -> Vec<(String, String)> { + let mut instr_addr_str_tuples = gadgets + .par_iter() + .map(|g| { + let output_instrs = g.fmt_instrs(att_syntax, color); + let output_addrs = g + .fmt_best_match_addrs(color) + .unwrap_or_else(|| Box::new(DisplayString(String::new()))); + + (format!("{}", output_instrs), format!("{}", output_addrs)) + }) + .collect::>(); + + instr_addr_str_tuples.sort_unstable(); // Alphabetical order, for analyst workflow + instr_addr_str_tuples +} + +/// Get instruction formatter (ATT or Intel syntax). +pub fn get_formatter(att_syntax: bool) -> Box { + match att_syntax { + true => { + let mut formatter = iced_x86::GasFormatter::new(); + config_formatter(&mut formatter); + Box::new(formatter) + } + false => { + let mut formatter = iced_x86::IntelFormatter::new(); + config_formatter(&mut formatter); + Box::new(formatter) + } + } +} + +// Private Gadget Formatter -------------------------------------------------------------------------------------------- + +// Custom instruction formatter output, enables coloring. +// Avoids storing duplicate colored strings - good for longer gadgets with repeating operators/operands +pub(crate) struct GadgetFormatterOutput { + tokens: BTreeMap, + order: Vec, + display_len: usize, +} + +impl GadgetFormatterOutput { + /// Constructor + pub fn new() -> Self { + Self { + tokens: BTreeMap::new(), + order: Vec::new(), + display_len: 0, + } + } + + // Compute hash for a (text, kind) tuple + #[inline] + fn compute_hash(text: &str, kind: iced_x86::FormatterTextKind) -> u64 { + let mut h = FxHasher::default(); + h.write(text.as_bytes()); + h.write_u8(kind as u8); + + h.finish() + } +} + +impl iced_x86::FormatterOutput for GadgetFormatterOutput { + fn write(&mut self, text: &str, kind: iced_x86::FormatterTextKind) { + self.display_len += text.len(); + let hash = Self::compute_hash(text, kind); + match self.tokens.contains_key(&hash) { + true => self.order.push(hash), + false => { + self.tokens.insert(hash, color_token(text, kind)); + self.order.push(hash); + } + } + } +} + +impl Display for GadgetFormatterOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for hash in &self.order { + if let Some(token) = self.tokens.get(hash) { + write!(f, "{}", token)?; + } + } + Ok(()) + } +} + +impl DisplayLen for GadgetFormatterOutput { + fn len(&self) -> usize { + self.display_len + } + + fn is_empty(&self) -> bool { + self.display_len == 0 + } +} + +// Private API --------------------------------------------------------------------------------------------------------- + +// Configure instruction formatter +#[inline] +fn config_formatter(formatter: &mut F) { + let fmt_opts = formatter.options_mut(); + fmt_opts.set_first_operand_char_index(0); + fmt_opts.set_uppercase_hex(false); + fmt_opts.set_rip_relative_addresses(true); + fmt_opts.set_hex_prefix("0x"); + fmt_opts.set_hex_suffix(""); + fmt_opts.set_small_hex_numbers_in_decimal(false); + fmt_opts.set_space_after_operand_separator(true); +} + +// Coloring ruleset +#[inline] +fn color_token(s: &str, kind: iced_x86::FormatterTextKind) -> colored::ColoredString { + match kind { + iced_x86::FormatterTextKind::Directive | iced_x86::FormatterTextKind::Keyword => s.blue(), + iced_x86::FormatterTextKind::Prefix | iced_x86::FormatterTextKind::Mnemonic => s.cyan(), + iced_x86::FormatterTextKind::Label | iced_x86::FormatterTextKind::Function => { + s.bright_green() + } + iced_x86::FormatterTextKind::LabelAddress + | iced_x86::FormatterTextKind::FunctionAddress => s.green(), + iced_x86::FormatterTextKind::Punctuation => s.bright_magenta(), + iced_x86::FormatterTextKind::Register => { + // Special case the stack pointer - typically don't want to overwrite + match s { + "rsp" | "esp" | "sp" => s.red(), + _ => s.yellow(), + } + } + _ => s.white(), + } +} diff --git a/src/gadget/gadget.rs b/src/gadget/gadget.rs new file mode 100644 index 0000000..c81bf5f --- /dev/null +++ b/src/gadget/gadget.rs @@ -0,0 +1,336 @@ +use std::cmp::Ordering; +use std::collections::{BTreeMap, BTreeSet}; +use std::hash::{Hash, Hasher}; +use std::marker::Send; + +use iced_x86::FormatterOutput; + +use super::fmt; +use super::fmt::DisplayLen; +use crate::binary; + +// TODO: implement Ord for binary, use BTReeSet instead of Vector to maintain sorted order on insertion +// will have nicer output at partial match at cost of speed (how much?) + +// Gadget -------------------------------------------------------------------------------------------------------------- + +/// Gadget instructions (data) coupled with occurrence addresses for full and partial matches (metadata). +/// Gadgets sortable by lowest occurrence address. +/// Hash and equality consider only gadget instructions, not occurrence addresses (fast de-duplication via sets). +#[derive(Clone, Debug)] +pub struct Gadget<'a> { + pub(crate) bin_cnt: usize, + pub(crate) instrs: Vec, + pub(crate) full_matches: BTreeSet, + pub(crate) partial_matches: BTreeMap>, +} + +impl<'a> Gadget<'a> { + // Public API ------------------------------------------------------------------------------------------------------ + + /// Convenience constructor for single-binary gadgets. + /// + /// # Arguments + /// + /// * `instrs` - instructions, should be sorted in execution order. + /// * `occurrence_addrs` - Addresses where gadget appears. + pub fn new(instrs: Vec, occurrence_addrs: BTreeSet) -> Self { + Gadget { + bin_cnt: 1, + instrs, + full_matches: occurrence_addrs, + partial_matches: BTreeMap::new(), + } + } + + /// Constructor for multi-binary gadgets. + /// + /// # Arguments + /// + /// * `instrs` - instructions, should be sorted in execution order. + /// * `full_matches` - full match addresses (same in all binaries). + /// * `bin_cnt` - count of binaries for which this gadget tracks full/partial matches. + pub fn new_multi_bin( + instrs: Vec, + full_matches: BTreeSet, + bin_cnt: usize, + ) -> Self { + Gadget { + bin_cnt, + instrs, + full_matches, + partial_matches: BTreeMap::new(), + } + } + + /// Get a instructions + pub fn instrs(&self) -> &[iced_x86::Instruction] { + &self.instrs + } + + /// Get tail + pub fn last_instr(&self) -> Option<&iced_x86::Instruction> { + self.instrs.iter().next_back() + } + + /// Get a full matches + pub fn full_matches(&self) -> &BTreeSet { + &self.full_matches + } + + /// Get partial matches + pub fn partial_matches(&self) -> &BTreeMap> { + &self.partial_matches + } + + /// Get first full match + pub fn first_full_match(&self) -> Option { + match self.full_matches.iter().next() { + Some(addr) => Some(*addr), + None => None, + } + } + + /// Get count of binaries for which this gadget tracks partial matches + pub fn bin_cnt(&self) -> usize { + self.bin_cnt + } + + // TODO: use this API in search instead of modifying fields directly? + /// Add a new partial match address/binary tuple + pub fn add_partial_match(&mut self, addr: u64, bin: &'a binary::Binary) { + match self.partial_matches.get_mut(&addr) { + Some(bins) => bins.push(bin), + None => { + self.partial_matches.insert(addr, vec![bin]); + } + }; + } + + /// String format gadget instructions + pub fn fmt_instrs(&self, att_syntax: bool, color: bool) -> Box { + match color { + true => { + let mut formatter = fmt::get_formatter(att_syntax); + let mut output = fmt::GadgetFormatterOutput::new(); + for instr in &self.instrs { + formatter.format(&instr, &mut output); + output.write("; ", iced_x86::FormatterTextKind::Punctuation); + } + Box::new(output) + } + false => Box::new(fmt::DisplayString(self.write_instrs_internal(att_syntax))), + } + } + + /// String format first full match address, if any + pub fn fmt_first_full_match_addr(&self, color: bool) -> Option> { + match &self.first_full_match() { + Some(addr) => match color { + true => { + let mut output = fmt::GadgetFormatterOutput::new(); + output.write( + &format!("{:#016x}", addr), + iced_x86::FormatterTextKind::LabelAddress, + ); + Some(Box::new(output)) + } + false => { + let mut output = String::new(); + output.write( + &format!("{:#016x}", addr), + iced_x86::FormatterTextKind::LabelAddress, + ); + Some(Box::new(fmt::DisplayString(output))) + } + }, + None => None, + } + } + + /// String format partial match addresses, if any + pub fn fmt_partial_match_addrs(&self, color: bool) -> Option> { + match color { + true => { + let mut output = fmt::GadgetFormatterOutput::new(); + let fmted_bin_cnt = Self::fmt_partial_matches_internal( + &mut output, + &mut self.partial_matches.clone(), + ); + match fmted_bin_cnt != self.bin_cnt() { + true => None, + false => Some(Box::new(output)), + } + } + false => { + let mut output = String::new(); + let fmted_bin_cnt = Self::fmt_partial_matches_internal( + &mut output, + &mut self.partial_matches.clone(), + ); + match fmted_bin_cnt != self.bin_cnt() { + true => None, + false => Some(Box::new(fmt::DisplayString(output))), + } + } + } + } + + /// String format match addresses, prioritizing full matches over partial, if any + pub fn fmt_best_match_addrs(&self, color: bool) -> Option> { + match self.first_full_match() { + Some(_) => self.fmt_first_full_match_addr(color), + None => match self.partial_matches.is_empty() { + false => self.fmt_partial_match_addrs(color), + true => None, + }, + } + } + + // Returns instruction string for regex filtering + pub fn fmt_for_filter(&self, att_syntax: bool) -> String { + self.write_instrs_internal(att_syntax) + } + + /// Format a single gadget, return an `(instrs, addr(s))` tuple + /// Returns `None` is the gadget has no full or partial matches across binaries. + pub fn fmt( + &self, + att_syntax: bool, + color: bool, + ) -> Option<(Box, Box)> { + match self.fmt_best_match_addrs(color) { + Some(output_addrs) => { + let output_instrs = self.fmt_instrs(att_syntax, color); + Some((output_instrs, output_addrs)) + } + None => None, + } + } + + // Private API ----------------------------------------------------------------------------------------------------- + + // Ord helper: Lowest gadget occurrence address, full matches preferred + #[inline] + fn min_addr(&self) -> Option<&u64> { + if let Some(min_full) = self.full_matches.iter().next() { + Some(min_full) + } else if let Some(min_part) = self.partial_matches.keys().next() { + Some(min_part) + } else { + None + } + } + + #[inline] + fn write_instrs_internal(&self, att_syntax: bool) -> String { + let mut formatter = fmt::get_formatter(att_syntax); + let mut output = String::new(); + for instr in &self.instrs { + formatter.format(&instr, &mut output); + output.write("; ", iced_x86::FormatterTextKind::Punctuation); + } + output + } + + // Partial match format helper, shrinks a working set + #[inline] + fn fmt_partial_matches_internal( + match_str: &mut impl iced_x86::FormatterOutput, + partial_matches: &mut BTreeMap>, + ) -> usize { + let mut add_sep = false; + let mut fmted_bin_cnt = 0; + + // Find largest subset of binaries with match for a given address (best partial match) + while let Some((bpm_addr, bpm_bins)) = partial_matches + .iter() + .max_by(|a, b| a.1.len().cmp(&b.1.len())) + { + // This pair of clones ends borrow of partial_matches and lets us remove from it later + let bpm_addr = *bpm_addr; + let mut bpm_bins = bpm_bins.clone(); + bpm_bins.sort_by_key(|b1| b1.name().to_lowercase()); + + // Commit best partial match + match bpm_bins.split_last() { + Some((last_bin, prior_bpm_bins)) => { + if add_sep { + match_str.write(", ", iced_x86::FormatterTextKind::Punctuation); + } else { + add_sep = true; + } + + for pb in prior_bpm_bins { + Self::write_bin_name(&pb.name(), match_str); + fmted_bin_cnt += 1; + } + + Self::write_bin_name(&last_bin.name(), match_str); + fmted_bin_cnt += 1; + match_str.write( + &format!("{:#016x}", bpm_addr), + iced_x86::FormatterTextKind::LabelAddress, + ); + } + None => break, + } + + // Remove committed binaries from the remainder of partial matches + partial_matches.remove(&bpm_addr); + partial_matches + .iter_mut() + .for_each(|(_, bins)| bins.retain(|&b| !bpm_bins.contains(&b))); + } + + fmted_bin_cnt + } + + #[inline] + fn write_bin_name(name: &str, output: &mut impl iced_x86::FormatterOutput) { + output.write("'", iced_x86::FormatterTextKind::Punctuation); + output.write(name, iced_x86::FormatterTextKind::Text); + output.write("': ", iced_x86::FormatterTextKind::Punctuation); + } +} + +impl Eq for Gadget<'_> {} +impl PartialEq for Gadget<'_> { + fn eq(&self, other: &Self) -> bool { + self.instrs == other.instrs + } +} + +impl Ord for Gadget<'_> { + fn cmp(&self, other: &Self) -> Ordering { + if let Some(self_min_addr) = self.min_addr() { + // Both have a minimum address -> compare minimums + if let Some(other_min_addr) = other.min_addr() { + (*self_min_addr).cmp(other_min_addr) + // Self addresses non-empty, other addresses empty -> other is less + } else { + Ordering::Greater + } + } else { + // Self addresses empty, other addresses non-empty -> self is less + if other.min_addr().is_some() { + Ordering::Less + // Self addresses empty, other addresses empty -> equal + } else { + Ordering::Equal + } + } + } +} + +impl PartialOrd for Gadget<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Hash for Gadget<'_> { + fn hash(&self, state: &mut H) { + self.instrs.hash(state); + } +} diff --git a/src/gadget/mod.rs b/src/gadget/mod.rs new file mode 100644 index 0000000..a7987e4 --- /dev/null +++ b/src/gadget/mod.rs @@ -0,0 +1,9 @@ +mod analysis; +pub use analysis::*; + +#[allow(clippy::module_inception)] +mod gadget; +pub use gadget::*; + +mod fmt; +pub use fmt::*; diff --git a/src/lib.rs b/src/lib.rs index 563c99d..3af6cef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -//!# xgadget -//! //!Fast, parallel, cross-variant ROP/JOP gadget search for x86 (32-bit) and x64 (64-bit) binaries. //!Uses the [iced-x86 disassembler library](https://github.com/0xd4d/iced). //! @@ -17,6 +15,7 @@ //! * **Partial-match** - Same instruction sequence, different program counter: gadget logic portable. //! * E.g. `pop rsp; add [rax-0x77], cl; ret; --- [ 'bin_v1.1': 0xc748d, 'bin_v1.2': 0xc9106 ]` //! * This is entirely optional, you're free to run this tool on a single binary. +//!* The stack pointer is explicitly colored in terminal output, for workflow convenience. //! //!Other features include: //! @@ -35,20 +34,48 @@ //!use xgadget; //! //!let max_gadget_len = 5; -//!let search_config = xgadget::SearchConfig::DEFAULT; //! //!// Search single binary +//!let search_config = xgadget::SearchConfig::DEFAULT; //!let bin_1 = xgadget::Binary::from_path_str("/path/to/bin_v1").unwrap(); //!let bins = vec![bin_1]; //!let gadgets = xgadget::find_gadgets(&bins, max_gadget_len, search_config).unwrap(); //!let stack_pivot_gadgets = xgadget::filter_stack_pivot(&gadgets); //! -//!// Search for cross-variant gadgets +//!// Search for cross-variant gadgets, including partial matches +//!let search_config = xgadget::SearchConfig::DEFAULT | xgadget::SearchConfig::PART; //!let bin_1 = xgadget::Binary::from_path_str("/path/to/bin_v1").unwrap(); //!let bin_2 = xgadget::Binary::from_path_str("/path/to/bin_v2").unwrap(); //!let bins = vec![bin_1, bin_2]; //!let cross_gadgets = xgadget::find_gadgets(&bins, max_gadget_len, search_config).unwrap(); -//!let cross_reg_write_gadgets = xgadget::filter_stack_set_regs(&cross_gadgets); +//!let cross_reg_pop_gadgets = xgadget::filter_reg_pop_only(&cross_gadgets); +//!``` +//! +//!Custom filters can be created using the [`GadgetAnalysis`](crate::gadget::GadgetAnalysis) object and/or functions from the [`semantics`](crate::semantics) module. +//!How the above [`filter_stack_pivot`](crate::filters::filter_stack_pivot) function is implemented: +//! +//!```rust +//!use rayon::prelude::*; +//!use iced_x86; +//!use xgadget::{Gadget, GadgetAnalysis}; +//! +//!/// Parallel filter to gadgets that write the stack pointer +//!pub fn filter_stack_pivot<'a>(gadgets: &[Gadget<'a>]) -> Vec> { +//! gadgets +//! .par_iter() +//! .filter(|g| { +//! let regs_overwritten = GadgetAnalysis::new(&g).regs_overwritten(); +//! if regs_overwritten.contains(&iced_x86::Register::RSP) +//! || regs_overwritten.contains(&iced_x86::Register::ESP) +//! || regs_overwritten.contains(&iced_x86::Register::SP) +//! { +//! return true; +//! } +//! false +//! }) +//! .cloned() +//! .collect() +//!} //!``` //! //!### CLI Usage @@ -56,10 +83,10 @@ //!Run `xgadget --help`: //! //!```ignore -//!xgadget v0.4.0 +//!xgadget v0.5.0 //! -//!About: Fast, parallel, cross-variant ROP/JOP gadget search for x86/x64 binaries. -//!Cores: 8 logical, 8 physical +//!About: Fast, parallel, cross-variant ROP/JOP gadget search for x86/x64 binaries. +//!Cores: 8 logical, 8 physical //! //!USAGE: //! xgadget [FLAGS] [OPTIONS] ... @@ -67,24 +94,27 @@ //!FLAGS: //! -t, --att Display gadgets using AT&T syntax [default: Intel syntax] //! -c, --check-sec Run checksec on the 1+ binaries instead of gadget search -//! -d, --dispatcher Filter to potential JOP 'dispatcher' gadgets [default: all gadgets] +//! -d, --dispatcher Filter to potential JOP 'dispatcher' gadgets [default: all] //! -e, --extended-fmt Print in terminal-wide format [default: only used for partial match search] //! -h, --help Prints help information //! --inc-call Include gadgets containing a call [default: don't include] //! --inc-imm16 Include '{ret, ret far} imm16' (e.g. add to stack ptr) [default: don't include] //! -j, --jop Search for JOP gadgets only [default: ROP, JOP, and SYSCALL] -//! -n, --no-color Don't color output, useful for UNIX piping [default: color output] +//! -n, --no-color Don't color output [default: color output] +//! --param-ctrl Filter to gadgets that control function parameters [default: all] //! -m, --partial-match Include cross-variant partial matches [default: full matches only] -//! -w, --reg-write Filter to 'pop {reg} * 1+, {ret or ctrl-ed jmp/call}' gadgets [default: all gadgets] +//! --reg-pop Filter to 'pop {reg} * 1+, {ret or ctrl-ed jmp/call}' gadgets [default: all] //! -r, --rop Search for ROP gadgets only [default: ROP, JOP, and SYSCALL] -//! -p, --stack-pivot Filter to gadgets that write the stack ptr [default: all gadgets] +//! -p, --stack-pivot Filter to gadgets that write the stack ptr [default: all] //! -s, --sys Search for SYSCALL gadgets only [default: ROP, JOP, and SYSCALL] //! -V, --version Prints version information //! //!OPTIONS: //! -a, --arch For raw (no header) files: specify arch ('x8086', 'x86', or 'x64') [default: x64] -//! -b, --bad-bytes ... Filter to gadgets whose addrs don't contain given bytes [default: all gadgets] +//! -b, --bad-bytes ... Filter to gadgets whose addrs don't contain given bytes [default: all] //! -l, --max-len Gadgets up to LEN instrs long. If 0: all gadgets, any length [default: 5] +//! --no-deref Filter to gadgets that don't deref any regs or a specific reg [default: all] +//! --reg-ctrl Filter to gadgets that control any reg or a specific reg [default: all] //! -f, --regex-filter Filter to gadgets matching a regular expression //! //!ARGS: @@ -102,23 +132,29 @@ //!### CLI Binary Releases for Linux //! //!Commits to this repo's `master` branch automatically run integration tests and build a statically-linked binary for 64-bit Linux. -//!You can [download it here](https://github.com/entropic-security/xgadget/releases) and use the CLI immediately, instead of building from source. +//!You can [download it here](https://github.com/entropic-security/xgadget/releases) to try out the CLI immediately, instead of building from source. //!Static binaries for Windows may also be supported in the future. //! -//!The statically-linked binary is about 8x slower, presumably due to the built-in memory allocator for target `x86_64-unknown-linux-musl`. -//!Building a dynamically-linked binary from source with the above `cargo install` command is *highly* recommended. +//!Unfortunately the statically-linked binary is several times slower on an i7-9700K, likely due to the built-in memory allocator for target `x86_64-unknown-linux-musl`. +//!So building a dynamically-linked binary from source with the above `cargo install` command is *highly* recommended for performance (links against your system's allocator). +//! +//!### Why No Chain Generation? +//! +//!Tools that attempt to automate ROP chain generation require heavyweight analysis - typically symbolic execution of an intermediate representation. +//!While this works well for small binaries and CTF problems, it tends to be slow and difficult to scale for large, real-world programs. +//!At present, `xgadget` has a different goal: enable an expert user to manually craft stable exploits by providing fast, accurate gadget discovery. //! //!### ~~Yeah, but can it do 10 OS kernels under 10 seconds?!~~ Repeatable Benchmark Harness //! //!```bash -//!bash ./benches/bench_setup_ubuntu.sh # Ubuntu-specific, download/build 10 kernel versions -//!cargo bench # Grab a coffee, this'll take a while... +//!bash ./benches/bench_setup_ubuntu.sh # Ubuntu-specific, download/build 10 kernel versions +//!cargo bench # Grab a coffee, this'll take a while... //!``` //! //!* `bench_setup_ubuntu.sh` downloads and builds 10 consecutive Linux kernels (versions `5.0.1` to `5.0.10` - with `x86_64_defconfig`). //!* `cargo bench`, among other benchmarks, searches all 10 kernels for common gadgets. //! -//!On an i7-9700K (8C/8T, 3.6GHz base, 4.9 GHz max) machine with `gcc` version 8.4.0: the average runtime, to process *all ten 54MB kernels simultaneously* with a max gadget length of 5 instructions and full-match search for all gadget types (ROP, JOP, and syscall gadgets), is *only 5.8 seconds*! Including partial matches as well takes *just 7.2 seconds*. +//!On an i7-9700K (8C/8T, 3.6GHz base, 4.9 GHz max) machine with `gcc` version 8.4.0: the average runtime, to process *all ten 54MB kernels simultaneously* with a max gadget length of 5 instructions and full-match search for all gadget types (ROP, JOP, and syscall gadgets), is *only 6.3 seconds*! Including partial matches as well takes *just 7.9 seconds*. //! //!### Acknowledgements //! @@ -154,6 +190,3 @@ pub use crate::filters::*; pub mod semantics; pub use crate::semantics::*; - -pub mod str_fmt; -pub use crate::str_fmt::*; diff --git a/src/search.rs b/src/search.rs index 9559b5e..ea02553 100644 --- a/src/search.rs +++ b/src/search.rs @@ -30,15 +30,22 @@ bitflags! { // Public API ---------------------------------------------------------------------------------------------------------- /// Search 1+ binaries for ROP gadgets (common gadgets if > 1) -pub fn find_gadgets<'a>( - bins: &'a [binary::Binary], +pub fn find_gadgets( + bins: &[binary::Binary], max_len: usize, s_config: SearchConfig, -) -> Result>, Box> { +) -> Result, Box> { + let bin_cnt = bins.len(); + // Process binaries in parallel let parallel_results: Vec<(&binary::Binary, HashSet)> = bins .par_iter() - .map(|bin| (bin, find_gadgets_single_bin(bin, max_len, s_config))) + .map(|bin| { + ( + bin, + find_gadgets_single_bin(bin, max_len, bin_cnt, s_config), + ) + }) .collect(); // Filter to cross-variant gadgets @@ -51,7 +58,7 @@ pub fn find_gadgets<'a>( // Filter common gadgets (set intersection) common_gadgets.retain(|g| next_set.contains(&g)); - // TODO (tnballo): there has to be a cleaner way to implement this! Once drain_filter() on stable? + // TODO: there has to be a cleaner way to implement this! Once drain_filter() on stable? // Update full and partial matches let mut temp_gadgets = HashSet::default(); for common_g in common_gadgets { @@ -71,7 +78,11 @@ pub fn find_gadgets<'a>( } // Cross-variant gadget! - let mut updated_g = gadget::Gadget::new(common_g.instrs, full_matches); + let mut updated_g = gadget::Gadget::new_multi_bin( + common_g.instrs, + full_matches, + bin_cnt, + ); // Partial matches (optional) if s_config.intersects(SearchConfig::PART) { @@ -208,7 +219,7 @@ fn iterative_decode(d_config: &DecodeConfig) -> Vec<(Vec, let pc = i.ip(); if (pc > tail_addr) || ((pc != tail_addr) && semantics::is_gadget_tail(&i)) - || (semantics::is_fixed_call(&i) + || (semantics::is_direct_call(&i) && !d_config.s_config.intersects(SearchConfig::CALL)) || (semantics::is_uncond_fixed_jmp(&i)) || (semantics::is_int(&i)) @@ -227,14 +238,15 @@ fn iterative_decode(d_config: &DecodeConfig) -> Vec<(Vec, // Find gadgets. Awww yisss. if let Some(i) = instrs.last() { // ROP - if (semantics::is_ret(&i) && (instrs.len() > 1)) + // Note: 1 instr gadget (e.g. "ret;") for 16 byte re-alignment of stack pointer (avoid movaps segfault) + if (semantics::is_ret(&i)) // JOP || (semantics::is_jop_gadget_tail(&i)) // SYS || (semantics::is_syscall(&i) - || (semantics::is_legacy_linux_syscall(&i) && (d_config.bin.format == binary::Format::ELF))) + || (semantics::is_legacy_linux_syscall(&i) && (d_config.bin.format() == binary::Format::ELF))) { debug_assert!(instrs[0].ip() == buf_start_addr); instr_sequences.push((instrs, buf_start_addr)); @@ -246,15 +258,16 @@ fn iterative_decode(d_config: &DecodeConfig) -> Vec<(Vec, } /// Search a binary for gadgets -fn find_gadgets_single_bin<'a>( - bin: &'a binary::Binary, +fn find_gadgets_single_bin( + bin: &binary::Binary, max_len: usize, + bin_cnt: usize, s_config: SearchConfig, -) -> HashSet> { +) -> HashSet { let mut gadget_collector: HashMap, BTreeSet> = HashMap::default(); - for seg in &bin.segments { + for seg in bin.segments() { // Search backward for all potential tails (possible duplicates) let parallel_results: Vec<(Vec, u64)> = get_gadget_tail_offsets(bin, seg, s_config) @@ -281,6 +294,6 @@ fn find_gadgets_single_bin<'a>( // Finalize parallel results gadget_collector .into_iter() - .map(|(instrs, addrs)| gadget::Gadget::new(instrs, addrs)) + .map(|(instrs, addrs)| gadget::Gadget::new_multi_bin(instrs, addrs, bin_cnt)) .collect() } diff --git a/src/semantics.rs b/src/semantics.rs index 0555eab..2694321 100644 --- a/src/semantics.rs +++ b/src/semantics.rs @@ -23,19 +23,13 @@ pub fn is_sys_gadget_tail(instr: &iced_x86::Instruction) -> bool { /// Check if call instruction with register-controlled target #[inline(always)] pub fn is_reg_indirect_call(instr: &iced_x86::Instruction) -> bool { - (instr.flow_control() == iced_x86::FlowControl::IndirectCall) - && ((instr.op0_kind() == iced_x86::OpKind::Register) - || ((instr.op0_kind() == iced_x86::OpKind::Memory) - && instr.memory_base() != iced_x86::Register::None)) + (instr.flow_control() == iced_x86::FlowControl::IndirectCall) && (has_ctrled_ops_only(instr)) } /// Check if jump instruction with register-controlled target #[inline(always)] pub fn is_reg_indirect_jmp(instr: &iced_x86::Instruction) -> bool { - (instr.flow_control() == iced_x86::FlowControl::IndirectBranch) - && ((instr.op0_kind() == iced_x86::OpKind::Register) - || ((instr.op0_kind() == iced_x86::OpKind::Memory) - && instr.memory_base() != iced_x86::Register::None)) + (instr.flow_control() == iced_x86::FlowControl::IndirectBranch) && (has_ctrled_ops_only(instr)) } /// Check if return instruction @@ -50,9 +44,9 @@ pub fn is_ret_imm16(instr: &iced_x86::Instruction) -> bool { is_ret(instr) && (instr.op_count() != 0) } -/// Check if call instruction +/// Check if direct call instruction #[inline(always)] -pub fn is_fixed_call(instr: &iced_x86::Instruction) -> bool { +pub fn is_direct_call(instr: &iced_x86::Instruction) -> bool { (instr.mnemonic() == iced_x86::Mnemonic::Call) && (!is_reg_indirect_call(instr)) } @@ -67,12 +61,6 @@ pub fn is_int(instr: &iced_x86::Instruction) -> bool { instr.flow_control() == iced_x86::FlowControl::Interrupt } -/// Check if interrupt instruction that specifies vector -#[inline(always)] -pub fn is_int_imm8(instr: &iced_x86::Instruction) -> bool { - instr.mnemonic() == iced_x86::Mnemonic::Int -} - /// Check if syscall/sysenter instruction #[inline(always)] pub fn is_syscall(instr: &iced_x86::Instruction) -> bool { @@ -83,7 +71,10 @@ pub fn is_syscall(instr: &iced_x86::Instruction) -> bool { /// Check if legacy Linux syscall #[inline(always)] pub fn is_legacy_linux_syscall(instr: &iced_x86::Instruction) -> bool { - is_int_imm8(instr) && (instr.immediate(0) == 0x80) + match instr.try_immediate(0) { + Ok(imm) => (imm == 0x80) && (instr.mnemonic() == iced_x86::Mnemonic::Int), + _ => false, + } } // Properties ---------------------------------------------------------------------------------------------------------- @@ -97,3 +88,47 @@ pub fn is_reg_rw(instr: &iced_x86::Instruction, reg: &iced_x86::Register) -> boo info.used_registers().contains(®_rw) } + +/// Check if sets register from another register or stack (e.g. exclude constant write) +#[inline(always)] +pub fn is_reg_set(instr: &iced_x86::Instruction, reg: &iced_x86::Register) -> bool { + let mut info_factory = iced_x86::InstructionInfoFactory::new(); + let info = info_factory.info_options(&instr, iced_x86::InstructionInfoOptions::NO_MEMORY_USAGE); + let reg_w = iced_x86::UsedRegister::new(*reg, iced_x86::OpAccess::Write); + + let reg_read = |ur: iced_x86::UsedRegister| { + ur.access() == iced_x86::OpAccess::Read || ur.access() == iced_x86::OpAccess::ReadWrite + }; + + if info.used_registers().iter().any(|ur| reg_read(*ur)) + && info.used_registers().contains(®_w) + { + return true; + } + + false +} + +/// Check if instruction has controllable operands only +#[inline(always)] +pub fn has_ctrled_ops_only(instr: &iced_x86::Instruction) -> bool { + let op_cnt = instr.op_count(); + for op_idx in 0..op_cnt { + match instr.try_op_kind(op_idx) { + Ok(kind) => match kind { + iced_x86::OpKind::Register => continue, + iced_x86::OpKind::Memory => match instr.memory_base() { + iced_x86::Register::None => return false, + iced_x86::Register::RIP => return false, + iced_x86::Register::EIP => return false, + //iced_x86::Register::IP => false, // TODO: why missing? + _ => continue, + }, + _ => return false, + }, + _ => return false, + } + } + + op_cnt > 0 +} diff --git a/src/str_fmt.rs b/src/str_fmt.rs deleted file mode 100644 index 949392a..0000000 --- a/src/str_fmt.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::collections::BTreeMap; - -use colored::Colorize; -use rayon::prelude::*; - -use crate::binary; -use crate::gadget; - -// Public API ---------------------------------------------------------------------------------------------------------- - -/// Format list of gadgets in parallel, return alphabetically sorted -pub fn str_fmt_gadgets( - gadgets: &[gadget::Gadget], - att_syntax: bool, - color: bool, -) -> Vec<(String, String)> { - let mut instr_addr_str_tuples = gadgets - .par_iter() - .map(|g| { - // Thread state - let mut instr_str = String::new(); - let mut addrs_str = String::new(); - let mut formatter = get_formatter(att_syntax); - let mut output = GadgetFormatterOutput::new(); - - // Instruction - for instr in &g.instrs { - // Instruction contents - output.tokens.clear(); - formatter.format(&instr, &mut output); - for (text, kind) in output.tokens.iter() { - if color { - if (*kind == iced_x86::FormatterTextKind::Register) && (text.contains("sp")) - { - instr_str.push_str(&format!("{}", text.as_str().red())); - } else { - instr_str.push_str(&format!("{}", set_color(text.as_str(), *kind))); - } - } else { - instr_str.push_str(text.as_str()); - } - } - - // Instruction separator - if color { - instr_str.push_str(&format!("{} ", ";".bright_magenta())); - } else { - instr_str.push_str("; "); - } - } - - // Full match address - if let Some(addr) = g.first_full_match() { - if color { - addrs_str.push_str(&format!("[ {} ]", format!("0x{:016x}", addr).green())); - } else { - addrs_str.push_str(&format!("[ 0x{:016x} ]", addr)); - } - - // Partial match address(es) - } else if let Some(partial_match_str) = str_fmt_partial_matches(&g, color) { - addrs_str.push_str(&format!("[ {} ]", &partial_match_str)); - } - - (instr_str, addrs_str) - }) - .collect::>(); - - instr_addr_str_tuples.sort(); // Alphabetical order, for analyst workflow - instr_addr_str_tuples -} - -/// Format partial matches for a given gadget -pub fn str_fmt_partial_matches(gadget: &gadget::Gadget, color: bool) -> Option { - str_fmt_partial_matches_internal(&mut gadget.partial_matches.clone(), color) -} - -// Private API --------------------------------------------------------------------------------------------------------- - -// Get instruction formatter -fn get_formatter(att_syntax: bool) -> Box { - if att_syntax { - let mut formatter = iced_x86::GasFormatter::new(); - config_formatter(&mut formatter); - Box::new(formatter) - } else { - let mut formatter = iced_x86::IntelFormatter::new(); - config_formatter(&mut formatter); - Box::new(formatter) - } -} - -// Configure instruction formatter -fn config_formatter(formatter: &mut F) { - formatter.options_mut().set_first_operand_char_index(0); - formatter.options_mut().set_uppercase_hex(false); - formatter.options_mut().set_rip_relative_addresses(true); - formatter - .options_mut() - .set_hex_prefix_string("0x".to_string()); - formatter - .options_mut() - .set_hex_suffix_string("".to_string()); - formatter - .options_mut() - .set_small_hex_numbers_in_decimal(false); - formatter - .options_mut() - .set_space_after_operand_separator(true); -} - -// Partial match format helper, recursively shrinks a working set -fn str_fmt_partial_matches_internal( - mut partial_matches: &mut BTreeMap>, - color: bool, -) -> Option { - // Find largest subset of binaries with match for a given address (best partial match) - match partial_matches - .iter() - .max_by(|a, b| a.1.len().cmp(&b.1.len())) - { - Some((bpm_addr, bpm_bins)) => { - let mut match_str = String::new(); - - // This pair of clones ends a borrow fo partial_matches and lets us remove from it later - // This eliminates the need to clone the whole map each level of recursion - let bpm_addr = *bpm_addr; - let mut bpm_bins = bpm_bins.clone(); - bpm_bins.sort_by(|b1, b2| b1.name.to_lowercase().cmp(&b2.name.to_lowercase())); - - // Commit best partial match - match bpm_bins.split_last() { - Some((last_bin, prior_bpm_bins)) => { - for pb in prior_bpm_bins { - match_str.push_str(&format!("'{}', ", pb.name)); - } - match_str.push_str(&format!("'{}': ", last_bin.name)); - if color { - match_str.push_str(&format!("{}", format!("0x{:016x}", bpm_addr).green())); - } else { - match_str.push_str(&format!("0x{:016x}", bpm_addr)); - } - } - None => return None, - } - - // Remove committed binaries from the remainder of partial matches - partial_matches.remove(&bpm_addr); - partial_matches - .iter_mut() - .for_each(|(_, bins)| bins.retain(|&b| !bpm_bins.contains(&b))); - - // Recursion depth bound by number of binaries - match str_fmt_partial_matches_internal(&mut partial_matches, color) { - Some(remaining_match_str) => { - match_str.push_str(", "); - match_str.push_str(&remaining_match_str); - Some(match_str) - } - None => Some(match_str), - } - } - None => None, - } -} - -// Coloring ------------------------------------------------------------------------------------------------------------ - -// Custom instruction formatter output, enables coloring -struct GadgetFormatterOutput { - tokens: Vec<(String, iced_x86::FormatterTextKind)>, -} - -impl GadgetFormatterOutput { - pub fn new() -> Self { - Self { tokens: Vec::new() } - } -} - -impl iced_x86::FormatterOutput for GadgetFormatterOutput { - fn write(&mut self, text: &str, kind: iced_x86::FormatterTextKind) { - self.tokens.push((String::from(text), kind)); - } -} - -// Coloring ruleset, but doesn't account for special casing the stack pointer -fn set_color(s: &str, kind: iced_x86::FormatterTextKind) -> colored::ColoredString { - match kind { - iced_x86::FormatterTextKind::Directive | iced_x86::FormatterTextKind::Keyword => s.blue(), - iced_x86::FormatterTextKind::Prefix | iced_x86::FormatterTextKind::Mnemonic => s.cyan(), - iced_x86::FormatterTextKind::Register => s.yellow(), - iced_x86::FormatterTextKind::Punctuation => s.bright_magenta(), - _ => s.white(), - } -} diff --git a/tests/common.rs b/tests/common.rs index 52aad48..de89a5a 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -127,6 +127,46 @@ pub const FILTERS_X64: &[u8] = &[ 0xc3, // ret 0x58, // pop rax 0xff, 0xe0, // jmp rax + 0x41, 0x58, // pop r8 + 0xc3, // ret + 0x48, 0x89, 0xc1, // mov rcx, rax + 0xc3, // ret + 0x50, // push rax + 0xc3, // ret +]; + +// http://bodden.de/pubs/fbt+16pshape.pdf +#[allow(dead_code)] +#[rustfmt::skip] +pub const PSHAPE_PG_5_X64: &[u8] = &[ + 0x48, 0x89, 0xe0, // mov rax, rsp + 0x4c, 0x89, 0x48, 0x20, // mov [rax+0x20], r9 + 0x4c, 0x89, 0x40, 0x18, // mov [rax+0x18], r8 + 0x48, 0x89, 0x50, 0x10, // mov [rax+0x10], rdx + 0x48, 0x89, 0x48, 0x08, // mov [rax+0x8], rcx + 0x4c, 0x89, 0xc9, // mov rcx, r9 + 0x48, 0x8b, 0x01, // mov rax, [rcx] + 0x48, 0xff, 0xc0, // inc rax + 0x48, 0x89, 0x41, 0x08, // mov [rcx+0x8], rax + 0x48, 0x8b, 0x41, 0x04, // mov rax, [rcx+0x4] + 0x48, 0xff, 0xc0, // inc rax + 0x48, 0x89, 0x41, 0x0c, // mov [rcx+0x0C], rax + 0xc3, // ret +]; + +#[allow(dead_code)] +#[rustfmt::skip] +pub const MISC_1: &[u8] = &[ + 0x87, 0x48, 0x1, // xchg [rax+0x1], ecx + 0xf8, // clc + 0xff, 0xe0, // jump rax +]; + +#[allow(dead_code)] +#[rustfmt::skip] +pub const MISC_2: &[u8] = &[ + 0x48, 0xff, 0xc0, // inc rax // TODO: remove this line + 0xff, 0x25, 0xbd, 0x66, 0x09, 0x00, // jmp qword ptr [rip+0x966bd] ]; // Test Utils ---------------------------------------------------------------------------------------------------------- @@ -142,9 +182,9 @@ pub fn decode_single_x64_instr(ip: u64, bytes: &[u8]) -> iced_x86::Instruction { #[allow(dead_code)] pub fn get_raw_bin(name: &str, bytes: &[u8]) -> xgadget::Binary { let mut bin = xgadget::Binary::from_bytes(&name, &bytes).unwrap(); - assert_eq!(bin.format, xgadget::Format::Raw); - assert_eq!(bin.arch, xgadget::Arch::Unknown); - bin.arch = xgadget::Arch::X64; + assert_eq!(bin.format(), xgadget::Format::Raw); + assert_eq!(bin.arch(), xgadget::Arch::Unknown); + bin.set_arch(xgadget::Arch::X64); bin } @@ -152,7 +192,7 @@ pub fn get_raw_bin(name: &str, bytes: &[u8]) -> xgadget::Binary { #[allow(dead_code)] pub fn get_gadget_strs(gadgets: &Vec, att_syntax: bool) -> Vec { let mut strs = Vec::new(); - for (mut instr, addrs) in xgadget::str_fmt_gadgets(&gadgets, att_syntax, false) { + for (mut instr, addrs) in xgadget::fmt_gadget_str_list(&gadgets, att_syntax, false) { instr.push(' '); strs.push(format!("{:-<150} {}", instr, addrs)); } @@ -183,3 +223,137 @@ pub fn hash(t: &T) -> u64 { t.hash(&mut s); s.finish() } + +// Adapted from https://docs.rs/iced-x86/1.10.0/iced_x86/?search=#get-instruction-info-eg-readwritten-regsmem-control-flow-info-etc +// TODO: check against updated docs +#[allow(dead_code)] +pub fn dump_instr(instr: &iced_x86::Instruction) { + let mut info_factory = iced_x86::InstructionInfoFactory::new(); + let op_code = instr.op_code(); + let info = info_factory.info(&instr); + let fpu_info = instr.fpu_stack_increment_info(); + println!("\n\tOpCode: {}", op_code.op_code_string()); + println!("\tInstruction: {}", op_code.instruction_string()); + println!("\tEncoding: {:?}", instr.encoding()); + println!("\tMnemonic: {:?}", instr.mnemonic()); + println!("\tCode: {:?}", instr.code()); + println!( + "\tCpuidFeature: {}", + instr + .cpuid_features() + .iter() + .map(|&a| format!("{:?}", a)) + .collect::>() + .join(" and ") + ); + println!("\tFlowControl: {:?}", instr.flow_control()); + + if fpu_info.writes_top() { + if fpu_info.increment() == 0 { + println!("\tFPU TOP: the instruction overwrites TOP"); + } else { + println!("\tFPU TOP inc: {}", fpu_info.increment()); + } + println!( + "\tFPU TOP cond write: {}", + if fpu_info.conditional() { + "true" + } else { + "false" + } + ); + } + if instr.is_stack_instruction() { + println!("\tSP Increment: {}", instr.stack_pointer_increment()); + } + if instr.condition_code() != iced_x86::ConditionCode::None { + println!("\tCondition code: {:?}", instr.condition_code()); + } + if instr.rflags_read() != iced_x86::RflagsBits::NONE { + println!("\tRFLAGS Read: {}", flags(instr.rflags_read())); + } + if instr.rflags_written() != iced_x86::RflagsBits::NONE { + println!("\tRFLAGS Written: {}", flags(instr.rflags_written())); + } + if instr.rflags_cleared() != iced_x86::RflagsBits::NONE { + println!("\tRFLAGS Cleared: {}", flags(instr.rflags_cleared())); + } + if instr.rflags_set() != iced_x86::RflagsBits::NONE { + println!("\tRFLAGS Set: {}", flags(instr.rflags_set())); + } + if instr.rflags_undefined() != iced_x86::RflagsBits::NONE { + println!("\tRFLAGS Undefined: {}", flags(instr.rflags_undefined())); + } + if instr.rflags_modified() != iced_x86::RflagsBits::NONE { + println!("\tRFLAGS Modified: {}", flags(instr.rflags_modified())); + } + for i in 0..instr.op_count() { + let op_kind = instr.try_op_kind(i).unwrap(); + if op_kind == iced_x86::OpKind::Memory { + let size = instr.memory_size().size(); + if size != 0 { + println!("\tMemory size: {}", size); + } + break; + } + } + for i in 0..instr.op_count() { + //println!("\tOp{}Access: {:?}", i, info.try_op_access(i).unwrap()); + println!("\tOp{}Access: {:?}", i, info.op_access(i)); + } + for i in 0..op_code.op_count() { + //println!("\tOp{}: {:?}", i, op_code.try_op_kind(i).unwrap()); + println!("\tOp{}: {:?}", i, op_code.op_kind(i)); + } + for reg_info in info.used_registers() { + println!("\tUsed reg: {:?}", reg_info); + } + for mem_info in info.used_memory() { + println!("\tUsed mem: {:?}", mem_info); + } +} + +fn flags(rf: u32) -> String { + fn append(sb: &mut String, s: &str) { + if !sb.is_empty() { + sb.push_str(", "); + } + sb.push_str(s); + } + + let mut sb = String::new(); + if (rf & iced_x86::RflagsBits::OF) != 0 { + append(&mut sb, "OF"); + } + if (rf & iced_x86::RflagsBits::SF) != 0 { + append(&mut sb, "SF"); + } + if (rf & iced_x86::RflagsBits::ZF) != 0 { + append(&mut sb, "ZF"); + } + if (rf & iced_x86::RflagsBits::AF) != 0 { + append(&mut sb, "AF"); + } + if (rf & iced_x86::RflagsBits::CF) != 0 { + append(&mut sb, "CF"); + } + if (rf & iced_x86::RflagsBits::PF) != 0 { + append(&mut sb, "PF"); + } + if (rf & iced_x86::RflagsBits::DF) != 0 { + append(&mut sb, "DF"); + } + if (rf & iced_x86::RflagsBits::IF) != 0 { + append(&mut sb, "IF"); + } + if (rf & iced_x86::RflagsBits::AC) != 0 { + append(&mut sb, "AC"); + } + if (rf & iced_x86::RflagsBits::UIF) != 0 { + append(&mut sb, "UIF"); + } + if sb.is_empty() { + sb.push_str(""); + } + sb +} diff --git a/tests/test_binary.rs b/tests/test_binary.rs index ea8822c..87fd2b8 100644 --- a/tests/test_binary.rs +++ b/tests/test_binary.rs @@ -4,14 +4,14 @@ mod common; #[test] fn test_elf() { let bin = xgadget::Binary::from_path_str("/bin/cat").unwrap(); - assert_eq!(bin.name, "cat"); - assert_eq!(bin.format, xgadget::Format::ELF); + assert_eq!(bin.name(), "cat"); + assert_eq!(bin.format(), xgadget::Format::ELF); #[cfg(target_arch = "x86")] - assert_eq!(bin.arch, xgadget::Arch::X86); + assert_eq!(bin.arch(), xgadget::Arch::X86); #[cfg(target_arch = "x86_64")] - assert_eq!(bin.arch, xgadget::Arch::X64); + assert_eq!(bin.arch(), xgadget::Arch::X64); // bin.entry and bin.segments is version dependant diff --git a/tests/test_cli.rs b/tests/test_cli.rs index 297f4b2..959ef69 100644 --- a/tests/test_cli.rs +++ b/tests/test_cli.rs @@ -1,10 +1,20 @@ +use std::io::Write; +//use std::io::Read; + use assert_cmd::Command; use predicates::prelude::*; -use std::io::Write; use tempfile::NamedTempFile; mod common; +// TARGET-SPECIFIC PARAMS ---------------------------------------------------------------------------------------------- + +#[cfg(target_arch = "x86")] +static REG_NAME: &str = "eax"; + +#[cfg(target_arch = "x86_64")] +static REG_NAME: &str = "rax"; + // Non-exhaustive Error Cases ------------------------------------------------------------------------------------------ #[test] @@ -57,14 +67,14 @@ fn test_conflicting_flags_dispatcher_stack_set_reg() { xgadget_bin .arg("/usr/bin/some_file_83bb57de34d8713f6e4940b4bdda4bea") - .arg("-w") + .arg("--reg-pop") .arg("-d"); xgadget_bin .assert() .failure() .stderr(predicate::str::contains( - "The argument '--dispatcher' cannot be used with '--reg-write'", + "The argument '--dispatcher' cannot be used with '--reg-pop'", )); } @@ -86,6 +96,36 @@ fn test_conflicting_flags_imm16_jop() { )); } +#[test] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_invalid_bad_bytes() { + let mut xgadget_bin = Command::cargo_bin("xgadget").unwrap(); + + xgadget_bin + .arg("/bin/cat") + .arg("-b") + .arg("0xff") + .arg("0xgg"); + + xgadget_bin + .assert() + .failure() + .stderr(predicate::str::contains("InvalidDigit")); +} + +#[test] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_invalid_reg_name() { + let mut xgadget_bin = Command::cargo_bin("xgadget").unwrap(); + + xgadget_bin.arg("/bin/cat").arg("--reg-ctrl").arg("r42"); + + xgadget_bin + .assert() + .failure() + .stderr(predicate::str::contains("'Invalid register: \"r42\"'")); +} + // Non-exhaustive Success Cases ---------------------------------------------------------------------------------------- #[test] @@ -116,6 +156,7 @@ fn test_single_bin() { let mut xgadget_bin = Command::cargo_bin("xgadget").unwrap(); xgadget_bin.arg("/bin/cat"); + xgadget_bin.assert().success(); } @@ -194,55 +235,45 @@ fn test_search_args() { ) .unwrap(); - let output_jop = String::from_utf8( - Command::cargo_bin("xgadget") - .unwrap() - .arg("/bin/cat") - .arg("-j") - .output() - .unwrap() - .stdout, - ) - .unwrap(); - - let output_sys = String::from_utf8( + let output_rop_call = String::from_utf8( Command::cargo_bin("xgadget") .unwrap() .arg("/bin/cat") - .arg("-s") + .arg("-r") + .arg("--inc-call") .output() .unwrap() .stdout, ) .unwrap(); - let output_stack_pivot = String::from_utf8( + let output_jop = String::from_utf8( Command::cargo_bin("xgadget") .unwrap() .arg("/bin/cat") - .arg("-p") + .arg("-j") .output() .unwrap() .stdout, ) .unwrap(); - let output_dispatch = String::from_utf8( + let output_sys = String::from_utf8( Command::cargo_bin("xgadget") .unwrap() .arg("/bin/cat") - .arg("-d") + .arg("-s") .output() .unwrap() .stdout, ) .unwrap(); - let output_reg_ctrl = String::from_utf8( + let output_stack_pivot = String::from_utf8( Command::cargo_bin("xgadget") .unwrap() .arg("/bin/cat") - .arg("-c") + .arg("-p") .output() .unwrap() .stdout, @@ -253,9 +284,8 @@ fn test_search_args() { assert!(output_all.len() >= output_jop.len()); assert!(output_all.len() >= output_sys.len()); assert!(output_all.len() >= output_stack_pivot.len()); - assert!(output_all.len() >= output_dispatch.len()); - assert!(output_all.len() >= output_reg_ctrl.len()); assert!(output_rop_imm16.len() >= output_rop.len()); + assert!(output_rop_call.len() >= output_rop.len()); } #[test] @@ -291,23 +321,19 @@ fn test_max_len() { #[cfg(target_os = "linux")] #[cfg_attr(not(feature = "cli-bin"), ignore)] fn test_color_filter_line_count() { - #[cfg(target_arch = "x86")] - let reg_name = "eax"; - - #[cfg(target_arch = "x86_64")] - let reg_name = "rax"; - let output_color = String::from_utf8( Command::cargo_bin("xgadget") .unwrap() .arg("/bin/cat") - .arg(format!("-f mov {}", reg_name)) + .arg("-f") + .arg(format!("mov {}", REG_NAME)) .output() .unwrap() .stdout, ) .unwrap(); + println!("OUTPUT_COLOR: {}", output_color); let output_color_line_cnt = output_color.lines().count(); let output_no_color = String::from_utf8( @@ -315,13 +341,15 @@ fn test_color_filter_line_count() { .unwrap() .arg("/bin/cat") .arg("-n") - .arg(format!("-f mov {}", reg_name)) + .arg("-f") + .arg(format!("mov {}", REG_NAME)) .output() .unwrap() .stdout, ) .unwrap(); + println!("OUTPUT_NO_COLOR: {}", output_no_color); let output_no_color_line_cnt = output_no_color.lines().count(); assert!(output_color_line_cnt == output_no_color_line_cnt); @@ -341,6 +369,7 @@ fn test_extended_line_count() { ) .unwrap(); + println!("OUTPUT_DEFAULT: {}", output_default); let output_default_line_cnt = output_default.lines().count(); let output_extended = String::from_utf8( @@ -354,36 +383,331 @@ fn test_extended_line_count() { ) .unwrap(); + println!("OUTPUT_EXTENDED: {}", output_extended); let output_extended_line_cnt = output_extended.lines().count(); assert!(output_default_line_cnt == output_extended_line_cnt); } +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_att_intel_syntax_line_count() { + let output_intel = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("OUTPUT_INTEL: {}", output_intel); + let output_intel_line_cnt = output_intel.lines().count(); + + let output_att = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("--att") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("OUTPUT_ATT: {}", output_att); + let output_att_line_cnt = output_att.lines().count(); + + assert!(output_intel_line_cnt == output_att_line_cnt); +} + #[test] #[cfg(target_os = "linux")] #[cfg_attr(not(feature = "cli-bin"), ignore)] fn test_regex() { - let pop_pop_ret_regex = String::from_utf8( + let reg_pop_regex = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("-f") + .arg(r"^(?:pop)(?:.*(?:pop))*.*(?:ret|call|jmp)") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + let reg_pop_filter = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("--reg-pop") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("REG_POP_REGEX: {}", reg_pop_regex); + println!("REG_POP_FILTER: {}", reg_pop_filter); + assert!(reg_pop_regex.len() >= reg_pop_filter.len()); +} + +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_dispatcher_filter() { + let output_all = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + let output_dispatch_filter = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("-d") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("ALL: {}", output_all); + println!("DISPATCHER: {}", output_dispatch_filter); + assert!(output_all.len() >= output_dispatch_filter.len()); +} + +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_reg_pop_filter() { + let output_all = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + let output_reg_pop_filter = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("--reg-pop") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("ALL: {}", output_all); + println!("REG_POP: {}", output_reg_pop_filter); + assert!(output_all.len() >= output_reg_pop_filter.len()); +} + +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_param_ctrl_filter() { + let output_all = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + let output_param_ctrl_filter = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("--param-ctrl") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("ALL: {}", output_all); + println!("PARAM_CTRL: {}", output_param_ctrl_filter); + assert!(output_all.len() >= output_param_ctrl_filter.len()); +} + +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_no_deref_filter() { + let output_no_deref_rax_filter = String::from_utf8( Command::cargo_bin("xgadget") .unwrap() .arg("/bin/cat") - .arg(format!("-f {}", r"^(?:pop)(?:.*(?:pop))*.*ret")) + .arg("--no-deref") + .arg(REG_NAME) .output() .unwrap() .stdout, ) .unwrap(); - let reg_ctrl_filter = String::from_utf8( + let output_no_deref_all_regs_filter = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("--no-deref") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("NO_DEREF_RAX: {}", output_no_deref_rax_filter); + println!("NO_DEREF: {}", output_no_deref_all_regs_filter); + assert!(output_no_deref_rax_filter.len() >= output_no_deref_all_regs_filter.len()); +} + +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_reg_ctrl_filter() { + let output_reg_ctrl_rax_filter = String::from_utf8( Command::cargo_bin("xgadget") .unwrap() .arg("/bin/cat") .arg("--reg-ctrl") + .arg(REG_NAME) .output() .unwrap() .stdout, ) .unwrap(); - assert!(pop_pop_ret_regex.len() >= reg_ctrl_filter.len()); + let output_reg_ctrl_all_regs_filter = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("--reg-ctrl") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("REG_CTRL_RAX: {}", output_reg_ctrl_rax_filter); + println!("REG_CTRL_ALL: {}", output_reg_ctrl_all_regs_filter); + assert!(output_reg_ctrl_all_regs_filter.len() >= output_reg_ctrl_rax_filter.len()); +} + +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_bad_bytes_filter() { + let output_all_bytes = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + let output_bad_bytes = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("-b") + .arg("0x40") + .arg("0x55") + .arg("ff") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("ALL_BYTES: {}", output_all_bytes); + println!("BAD_BYTES: {}", output_bad_bytes); + assert!(output_all_bytes.len() >= output_bad_bytes.len()); +} + +#[test] +#[cfg(all(target_os = "linux", target_arch = "x86_64"))] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_reg_equivalence() { + let no_deref_r8l_filter = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("--no-deref") + .arg("r8l") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + let no_deref_r8b_filter = String::from_utf8( + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("--no-deref") + .arg("r8b") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + + println!("NO_DEREF_R8L: {}", no_deref_r8l_filter); + println!("NO_DEREF_R8B: {}", no_deref_r8b_filter); + assert!(no_deref_r8l_filter.lines().count() == no_deref_r8b_filter.lines().count()); +} + +/* +// TODO: can UNIX piping be tested this way? +#[test] +#[cfg_attr(not(feature = "cli-bin"), ignore)] +fn test_tty_piping() { + let mut out_file_color = NamedTempFile::new().unwrap(); + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg(">") + .arg(out_file_color.path()) + .output() + .unwrap(); + + let mut out_file_no_color = NamedTempFile::new().unwrap(); + Command::cargo_bin("xgadget") + .unwrap() + .arg("/bin/cat") + .arg("--no-color") + .arg(">") + .arg(out_file_no_color.path()) + .output() + .unwrap(); + + let mut out_file_color_contents = Vec::new(); + let out_file_color_bytes = out_file_color.read(&mut out_file_color_contents).unwrap(); + + let mut out_file_no_color_contents = Vec::new(); + let out_file_no_color_bytes = out_file_no_color.read(&mut out_file_no_color_contents).unwrap(); + + assert!(out_file_color_bytes > 0); + assert!(out_file_no_color_bytes > 0); + + assert!(out_file_color_bytes == out_file_no_color_bytes); } +*/ diff --git a/tests/test_gadget.rs b/tests/test_gadget.rs new file mode 100644 index 0000000..b294848 --- /dev/null +++ b/tests/test_gadget.rs @@ -0,0 +1,92 @@ +use std::collections::{BTreeSet, HashSet}; + +mod common; + +#[test] +fn test_gadget_hasher() { + let pop_r15: [u8; 2] = [0x41, 0x5f]; + let jmp_rax: [u8; 2] = [0xff, 0xe0]; + let jmp_rax_deref: [u8; 2] = [0xff, 0x20]; + + let jmp_rax_instr = common::decode_single_x64_instr(0, &jmp_rax); + let jmp_rax_deref_instr = common::decode_single_x64_instr(0, &jmp_rax_deref); + let pop_r15_instr = common::decode_single_x64_instr(0, &pop_r15); + + let mut addr_1 = BTreeSet::new(); + addr_1.insert(0); + + let mut addr_2 = BTreeSet::new(); + addr_2.insert(1); + + // Different instructions, different address - custom hash mismatch + let g1 = xgadget::Gadget::new( + vec![pop_r15_instr.clone(), jmp_rax_instr.clone()], + addr_1.clone(), + ); + let g2 = xgadget::Gadget::new( + vec![pop_r15_instr.clone(), jmp_rax_deref_instr.clone()], + addr_2.clone(), + ); + assert!(common::hash(&g1) != common::hash(&g2)); + + // Different instructions, same address - custom hash mismatch + let g1 = xgadget::Gadget::new( + vec![pop_r15_instr.clone(), jmp_rax_instr.clone()], + addr_1.clone(), + ); + let g2 = xgadget::Gadget::new( + vec![pop_r15_instr.clone(), jmp_rax_deref_instr.clone()], + addr_1.clone(), + ); + assert!(common::hash(&g1) != common::hash(&g2)); + + // Same instructions, same address - custom hash match + let g1 = xgadget::Gadget::new( + vec![pop_r15_instr.clone(), jmp_rax_instr.clone()], + addr_1.clone(), + ); + let g2 = xgadget::Gadget::new( + vec![pop_r15_instr.clone(), jmp_rax_instr.clone()], + addr_1.clone(), + ); + assert!(common::hash(&g1) == common::hash(&g2)); + + // Same instructions, different address - custom hash match + let g1 = xgadget::Gadget::new( + vec![pop_r15_instr.clone(), jmp_rax_instr.clone()], + addr_1.clone(), + ); + let g2 = xgadget::Gadget::new( + vec![pop_r15_instr.clone(), jmp_rax_instr.clone()], + addr_2.clone(), + ); + assert!(common::hash(&g1) == common::hash(&g2)); + + // Same instructions, different decode addresses - custom hash match + // https://github.com/0xd4d/iced/blob/3ed6e0eadffb61daa50e041eb28633f17a9957e9/src/rust/iced-x86/src/instruction.rs#L7574 + let decode_addr_5 = 5; + let decode_addr_10 = 10; + let jmp_rax_instr_5 = common::decode_single_x64_instr(decode_addr_5, &jmp_rax); + let jmp_rax_instr_10 = common::decode_single_x64_instr(decode_addr_10, &jmp_rax); + + let g1 = xgadget::Gadget::new(vec![jmp_rax_instr_5.clone()], addr_1.clone()); + let g2 = xgadget::Gadget::new(vec![jmp_rax_instr_10.clone()], addr_1.clone()); + let g3 = xgadget::Gadget::new(vec![jmp_rax_instr_10.clone()], addr_2); + assert!(common::hash(&g1) == common::hash(&g2)); + assert!(common::hash(&g2) == common::hash(&g3)); + + // Hash set intersection + let g1 = xgadget::Gadget::new(vec![pop_r15_instr.clone(), jmp_rax_instr], addr_1.clone()); + let g2 = xgadget::Gadget::new(vec![pop_r15_instr, jmp_rax_deref_instr], addr_1); + + let mut g_set_1: HashSet<_> = HashSet::default(); + g_set_1.insert(g1.clone()); + g_set_1.insert(g2.clone()); + + let mut g_set_2 = HashSet::default(); + g_set_2.insert(g1.clone()); + + let g_set_intersect: HashSet<_> = g_set_1.intersection(&g_set_2).collect(); + assert!(g_set_intersect.contains(&g1)); + assert!(!g_set_intersect.contains(&g2)); +} diff --git a/tests/test_gadget_analysis.rs b/tests/test_gadget_analysis.rs new file mode 100644 index 0000000..0e16eb7 --- /dev/null +++ b/tests/test_gadget_analysis.rs @@ -0,0 +1,99 @@ +mod common; + +#[test] +fn test_regs_deref() { + let bin_pshape = common::get_raw_bin("pshape_example", &common::PSHAPE_PG_5_X64); + let bins = vec![bin_pshape]; + let mut gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + + gadgets.retain(|g| g.full_matches().contains(&0x0)); + assert!(gadgets.len() == 1); + assert!(xgadget::filter_no_deref(&gadgets, None).is_empty()); + + let analysis = xgadget::gadget::GadgetAnalysis::new(&gadgets[0]); + + assert!(analysis.regs_dereferenced().len() == 3); + assert!(analysis + .regs_dereferenced() + .contains(&iced_x86::Register::RAX)); + assert!(analysis + .regs_dereferenced() + .contains(&iced_x86::Register::RCX)); + assert!(analysis + .regs_dereferenced() + .contains(&iced_x86::Register::RSP)); + + assert!(analysis.regs_dereferenced_write().len() == 2); + assert!(analysis + .regs_dereferenced_write() + .contains(&iced_x86::Register::RAX)); + assert!(analysis + .regs_dereferenced_write() + .contains(&iced_x86::Register::RCX)); + + assert!(analysis.regs_dereferenced_read().len() == 2); + assert!(analysis + .regs_dereferenced_read() + .contains(&iced_x86::Register::RCX)); + assert!(analysis + .regs_dereferenced_read() + .contains(&iced_x86::Register::RSP)); +} + +#[test] +fn test_regs_updated() { + let bin_pshape = common::get_raw_bin("pshape_example", &common::PSHAPE_PG_5_X64); + let bins = vec![bin_pshape]; + let mut gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + + gadgets.retain(|g| g.full_matches().contains(&0x24)); + assert!(gadgets.len() == 1); + assert!(xgadget::filter_no_deref(&gadgets, None).is_empty()); + + let analysis = xgadget::gadget::GadgetAnalysis::new(&gadgets[0]); + + assert!(analysis.regs_updated().len() == 2); + assert!(analysis.regs_updated().contains(&iced_x86::Register::RAX)); + assert!(analysis.regs_updated().contains(&iced_x86::Register::RSP)); +} + +#[test] +fn test_regs_overwritten() { + let bin_pshape = common::get_raw_bin("pshape_example", &common::PSHAPE_PG_5_X64); + let bins = vec![bin_pshape]; + let mut gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + + gadgets.retain(|g| g.full_matches().contains(&0x20)); + assert!(gadgets.len() == 1); + assert!(xgadget::filter_no_deref(&gadgets, None).is_empty()); + + let analysis = xgadget::gadget::GadgetAnalysis::new(&gadgets[0]); + + assert!(analysis.regs_overwritten().len() == 1); + assert!(analysis + .regs_overwritten() + .contains(&iced_x86::Register::RAX)); +} + +#[test] +fn test_no_deref_1() { + let bin_misc_1 = common::get_raw_bin("misc_1", &common::MISC_1); + let bins = vec![bin_misc_1]; + let mut gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + + gadgets.retain(|g| g.full_matches().contains(&0x0)); + assert!(gadgets.len() == 1); + + let analysis = xgadget::gadget::GadgetAnalysis::new(&gadgets[0]); + assert!(!analysis.regs_dereferenced().is_empty()); + + for instr in gadgets[0].instrs() { + common::dump_instr(&instr); + } + + assert!(xgadget::filter_no_deref(&gadgets, None).is_empty()); +} diff --git a/tests/test_semantics.rs b/tests/test_semantics.rs index 44e813b..baa27e5 100644 --- a/tests/test_semantics.rs +++ b/tests/test_semantics.rs @@ -119,12 +119,65 @@ fn test_sys_semantics() { #[test] fn test_rw_semantics() { + // Positive test let add_rax_0x08: [u8; 4] = [0x48, 0x83, 0xc0, 0x08]; let instr = common::decode_single_x64_instr(0, &add_rax_0x08); assert!(xgadget::semantics::is_reg_rw( &instr, &iced_x86::Register::RAX )); + + // Negative test + let pop_r15: [u8; 2] = [0x41, 0x5f]; + let instr = common::decode_single_x64_instr(0, &pop_r15); + assert!(!xgadget::semantics::is_reg_rw( + &instr, + &iced_x86::Register::R15 + )); +} + +#[test] +fn test_reg_set_semantics() { + // Positive test + let pop_r15: [u8; 2] = [0x41, 0x5f]; + let instr = common::decode_single_x64_instr(0, &pop_r15); + assert!(xgadget::semantics::is_reg_set( + &instr, + &iced_x86::Register::R15 + )); + + // Negative test + let add_rax_0x08: [u8; 4] = [0x48, 0x83, 0xc0, 0x08]; + let instr = common::decode_single_x64_instr(0, &add_rax_0x08); + assert!(!xgadget::semantics::is_reg_set( + &instr, + &iced_x86::Register::RAX + )); +} + +#[test] +fn test_has_ctrled_ops() { + // Positive test + let jmp_rax: [u8; 2] = [0xff, 0xe0]; + let instr = common::decode_single_x64_instr(0, &jmp_rax); + assert!(xgadget::semantics::has_ctrled_ops_only(&instr)); + + let jmp_rax_deref: [u8; 2] = [0xff, 0x20]; + let instr = common::decode_single_x64_instr(0, &jmp_rax_deref); + assert!(xgadget::semantics::has_ctrled_ops_only(&instr)); + + let jmp_rax_deref_offset: [u8; 3] = [0xff, 0x60, 0x10]; + let instr = common::decode_single_x64_instr(0, &jmp_rax_deref_offset); + assert!(xgadget::semantics::has_ctrled_ops_only(&instr)); + + let mov_rax_rbx: [u8; 3] = [0x48, 0x89, 0xd8]; + let instr = common::decode_single_x64_instr(0, &mov_rax_rbx); + assert!(xgadget::semantics::has_ctrled_ops_only(&instr)); + + // Negative test + let add_rax_0x08: [u8; 4] = [0x48, 0x83, 0xc0, 0x08]; + let instr = common::decode_single_x64_instr(0, &add_rax_0x08); + assert!(!xgadget::semantics::has_ctrled_ops_only(&instr)); } #[test] @@ -181,9 +234,26 @@ fn test_gadget_hasher() { vec![pop_r15_instr.clone(), jmp_rax_instr.clone()], addr_1.clone(), ); - let g2 = xgadget::Gadget::new(vec![pop_r15_instr.clone(), jmp_rax_instr.clone()], addr_2); + let g2 = xgadget::Gadget::new( + vec![pop_r15_instr.clone(), jmp_rax_instr.clone()], + addr_2.clone(), + ); + assert!(common::hash(&g1) == common::hash(&g2)); + + // Same instructions, different decode addresses - custom hash match + // https://github.com/0xd4d/iced/blob/3ed6e0eadffb61daa50e041eb28633f17a9957e9/src/rust/iced-x86/src/instruction.rs#L7574 + let decode_addr_5 = 5; + let decode_addr_10 = 10; + let jmp_rax_instr_5 = common::decode_single_x64_instr(decode_addr_5, &jmp_rax); + let jmp_rax_instr_10 = common::decode_single_x64_instr(decode_addr_10, &jmp_rax); + + let g1 = xgadget::Gadget::new(vec![jmp_rax_instr_5.clone()], addr_1.clone()); + let g2 = xgadget::Gadget::new(vec![jmp_rax_instr_10.clone()], addr_1.clone()); + let g3 = xgadget::Gadget::new(vec![jmp_rax_instr_10.clone()], addr_2); assert!(common::hash(&g1) == common::hash(&g2)); + assert!(common::hash(&g2) == common::hash(&g3)); + // Hash set intersection let g1 = xgadget::Gadget::new(vec![pop_r15_instr.clone(), jmp_rax_instr], addr_1.clone()); let g2 = xgadget::Gadget::new(vec![pop_r15_instr, jmp_rax_deref_instr], addr_1); diff --git a/tests/test_x64_filter.rs b/tests/test_x64_filter.rs index 8851845..b4150b9 100644 --- a/tests/test_x64_filter.rs +++ b/tests/test_x64_filter.rs @@ -79,12 +79,12 @@ fn test_x64_filter_dispatcher() { } #[test] -fn test_x64_filter_stack_set_regs() { +fn test_x64_filter_reg_pop_only() { let bin_filters = common::get_raw_bin("bin_filters", &common::FILTERS_X64); let bins = vec![bin_filters]; let gadgets = xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); - let loader_gadgets = xgadget::filter_stack_set_regs(&gadgets); + let loader_gadgets = xgadget::filter_reg_pop_only(&gadgets); let loader_gadget_strs = common::get_gadget_strs(&loader_gadgets, false); common::print_gadget_strs(&loader_gadget_strs); @@ -105,6 +105,10 @@ fn test_x64_filter_stack_set_regs() { &loader_gadget_strs, "pop rax; jmp rax;" )); + assert!(common::gadget_strs_contains_sub_str( + &loader_gadget_strs, + "pop r8; ret;" + )); // Negative assert!(!common::gadget_strs_contains_sub_str( @@ -141,3 +145,202 @@ fn test_x64_filter_bad_bytes() { "jmp rax;" )); } + +#[test] +fn test_x64_filter_set_params() { + let bin_filters = common::get_raw_bin("bin_filters", &common::FILTERS_X64); + let bins = vec![bin_filters]; + let gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + let param_ctrl_gadgets = + xgadget::filter_set_params(&gadgets, xgadget::binary::X64_ELF_PARAM_REGS); + let param_ctrl_gadget_strs = common::get_gadget_strs(¶m_ctrl_gadgets, false); + common::print_gadget_strs(¶m_ctrl_gadget_strs); + + // Positive + assert!(common::gadget_strs_contains_sub_str( + ¶m_ctrl_gadget_strs, + "pop r8; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + ¶m_ctrl_gadget_strs, + "mov rcx, rax; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + ¶m_ctrl_gadget_strs, + "push rax; ret;" + )); + + // Negative + assert!(!common::gadget_strs_contains_sub_str( + ¶m_ctrl_gadget_strs, + "jmp rax;" + )); + assert!(!common::gadget_strs_contains_sub_str( + ¶m_ctrl_gadget_strs, + "pop rsp; ret;" + )); + assert!(!common::gadget_strs_contains_sub_str( + ¶m_ctrl_gadget_strs, + "pop rax; pop rbx; ret;" + )); + assert!(!common::gadget_strs_contains_sub_str( + ¶m_ctrl_gadget_strs, + "pop rbx; ret;" + )); + assert!(!common::gadget_strs_contains_sub_str( + ¶m_ctrl_gadget_strs, + "pop rax; jmp rax;" + )); +} + +#[test] +fn test_x64_filter_no_deref_1() { + let bin_filters = common::get_raw_bin("bin_filters", &common::FILTERS_X64); + let bins = vec![bin_filters]; + let gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + let no_deref_gadgets = xgadget::filter_no_deref(&gadgets, None); + let no_deref_gadget_strs = common::get_gadget_strs(&no_deref_gadgets, false); + common::print_gadget_strs(&no_deref_gadget_strs); + + // Positive + assert!(common::gadget_strs_contains_sub_str( + &no_deref_gadget_strs, + "pop r8; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + &no_deref_gadget_strs, + "mov rcx, rax; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + &no_deref_gadget_strs, + "push rax; ret;" + )); + + // Negative + assert!(!common::gadget_strs_contains_sub_str( + &no_deref_gadget_strs, + "mov rax, 0x1337; jmp qword ptr [rax];" + )); +} + +#[test] +fn test_x64_filter_no_deref_2() { + let bin_filters = common::get_raw_bin("bin_filters", &common::FILTERS_X64); + let bins = vec![bin_filters]; + let gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + let no_deref_gadgets = xgadget::filter_no_deref(&gadgets, Some(&vec![iced_x86::Register::RCX])); + let no_deref_gadget_strs = common::get_gadget_strs(&no_deref_gadgets, false); + common::print_gadget_strs(&no_deref_gadget_strs); + + // Positive + assert!(common::gadget_strs_contains_sub_str( + &no_deref_gadget_strs, + "pop r8; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + &no_deref_gadget_strs, + "mov rcx, rax; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + &no_deref_gadget_strs, + "push rax; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + &no_deref_gadget_strs, + "mov rax, 0x1337; jmp qword ptr [rax];" + )); +} + +#[test] +fn test_x64_filter_regs_overwritten_1() { + let bin_filters = common::get_raw_bin("bin_filters", &common::FILTERS_X64); + let bins = vec![bin_filters]; + let gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + let param_ctrl_gadgets = xgadget::filter_regs_overwritten(&gadgets, None); + let reg_ctrl_gadget_strs = common::get_gadget_strs(¶m_ctrl_gadgets, false); + common::print_gadget_strs(®_ctrl_gadget_strs); + + // Positive + assert!(common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop r8; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "mov rcx, rax; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop rsp; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop rax; pop rbx; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop rbx; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop rax; jmp rax;" + )); + + // Negative + assert!(!common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "push rax; ret;" + )); +} + +#[test] +fn test_x64_filter_regs_overwritten_2() { + let bin_filters = common::get_raw_bin("bin_filters", &common::FILTERS_X64); + let bins = vec![bin_filters]; + let gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + let param_ctrl_gadgets = + xgadget::filter_regs_overwritten(&gadgets, Some(&vec![iced_x86::Register::RCX])); + let reg_ctrl_gadget_strs = common::get_gadget_strs(¶m_ctrl_gadgets, false); + common::print_gadget_strs(®_ctrl_gadget_strs); + + // Positive + assert!(common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "mov rcx, rax; ret;" + )); + assert!(common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "mov ecx, eax; ret;" + )); + + // Negative + assert!(!common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "push rax; ret;" + )); + assert!(!common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop r8; ret;" + )); + assert!(!common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop rsp; ret;" + )); + assert!(!common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop rax; pop rbx; ret;" + )); + assert!(!common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop rbx; ret;" + )); + assert!(!common::gadget_strs_contains_sub_str( + ®_ctrl_gadget_strs, + "pop rax; jmp rax;" + )); +} diff --git a/tests/test_x64_xgadget.rs b/tests/test_x64_search_multi_bin.rs similarity index 98% rename from tests/test_x64_xgadget.rs rename to tests/test_x64_search_multi_bin.rs index 6c6df49..050db8b 100644 --- a/tests/test_x64_xgadget.rs +++ b/tests/test_x64_search_multi_bin.rs @@ -331,8 +331,14 @@ fn test_x64_cross_variant_full_and_partial_matches_3() { println!("\n{:#^1$}\n", " Mix vs. ret_call vs. ret_jmp (FULL) ", 175); common::print_gadget_strs(&gadget_strs_full_match); + // Positive + assert!(common::gadget_strs_contains_sub_str( + &gadget_strs_full_match, + "ret far;" + )); + // Negative - assert!(gadgets.is_empty()); + assert!(gadgets.len() == 1); // Partial match against common::X_RET_AFTER_JNE_AND_ADJACENT_CALL_X64 and common::X_RET_AFTER_JNE_AND_ADJACENT_CALL_X64 let gadgets = xgadget::find_gadgets(&bins, common::MAX_LEN, full_part_match_config).unwrap(); diff --git a/tests/test_x64_gadget.rs b/tests/test_x64_search_single_bin.rs similarity index 96% rename from tests/test_x64_gadget.rs rename to tests/test_x64_search_single_bin.rs index 8fb56a4..bd83a52 100644 --- a/tests/test_x64_gadget.rs +++ b/tests/test_x64_search_single_bin.rs @@ -211,3 +211,13 @@ fn test_x64_adjacent_jmp() { "or eax, 0x5dde1; jmp rcx;" )); } + +#[test] +fn test_jmp_rip() { + let bin_misc_2 = common::get_raw_bin("misc_2", &common::MISC_2); + let bins = vec![bin_misc_2]; + let gadgets = + xgadget::find_gadgets(&bins, common::MAX_LEN, xgadget::SearchConfig::DEFAULT).unwrap(); + + assert!(gadgets.len() == 0); +}