Boo is a workspace of two crates for hardening a compiled Rust binary against static and runtime analysis:
boo-rs: compile-time literal encryption, opaque-predicate branching, and runtime value mangling.boo-nodebug: cross-platform debugger-presence detection.
Boo encrypts literal data in the final binary, at compile time, preventing static analysis tools from
reading values.
At runtime, decrypted data is exposed in memory only for the shortest possible time.
It supports many literal types beyond strings, including numbers, characters, byte strings,
C-strings,
arrays, tuples, nested structures...
Use the boo!() macro to effortlessly encrypt your data.
Add the dependency:
[dependencies]
boo-rs = "0.1"Set an optional encryption key (or fallback to a 64-byte randomly generated one):
export BOO_KEY="secret-key"Example:
extern crate alloc;
#[macro_use]
extern crate boo_rs;
boo_init!();
#[allow(unused_variables)]
fn main() {
let n = boo!(3);
let text = boo!("hello");
let bytes = boo!(b"\x01\x02\x03");
let pair = boo!(("host", 443));
let nested = boo!([[1, 2], [3, 4]]);
}boo_init!() must be called once before using the boo!() macro.
After that, the macro can be used anywhere to encrypt almost all Rust literal values.
Boo supports:
- booleans
- bytes
- integers and floats
- characters
- strings
- byte strings (
b"...") - C-strings (
c"...") - tuples (containing any mix of supported types)
- arrays and nested arrays (containing any of supported types)
For a full reference, see the showcase file.
Branches on a boolean condition through an opaque-predicate dispatch instead of a plain
if/else: the taken branch is decided by an XOR-masked comparison salted per call site, not a
direct test of the condition. else if chains aren't supported.
# extern crate alloc;
# #[macro_use] extern crate boo_rs;
boo_init!();
fn main() {
let licensed = true;
assert_eq!(boo_branch!(licensed { "full" } else { "trial" }), "full");
}An optimizing compiler may fold the mask back to an equivalent plain branch in a release build, and this offers no resistance to a live debugger stepping through the code, same limits as the literal encryption above.
Publishes Mangled, mangling a fixed-size value against this process's cookie and a fresh
per-value nonce for storage: Mangled::new(value) to store, mangled.reveal_with(|plain| ..) to
read it back, plaintext never outliving that closure. Mirrors glibc's PTR_MANGLE and Windows'
EncodePointer. The cookie is fresh every process start: a value a live memory-editing tool
saves in one run decodes to garbage after a restart.
# extern crate alloc;
# #[macro_use] extern crate boo_rs;
boo_init!();
boo_mangle_init!();
fn main() {
let original = [1u8, 2, 3, 4];
let mangled = Mangled::new(original);
mangled.reveal_with(|plain| assert_eq!(*plain, original));
}boo_mangle_init!() must be called once, at the crate root, before using Mangled.
Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
- All decrypted types except
StringandCStrare stored on the stack without performance overhead. &strand&CStrdecryption are stored into their heap-allocated variants.- Special case: binary strings are decrypted into owned
[u8]arrays.
- Stack allocated str using a wrapper struct around a fixed
u8array. - Stack allocated Cstr using a wrapper struct around a fixed
u8array. - Numeric suffix support (ex.
1u8,1u16,0f32) usingsyn::Lit::suffix(). - Wide string support using the custom syntax:
w"Wide null terminated".
Cross-platform debugger-presence detection primitives. Every function returns a best-effort signal, never a verdict: fold the result into a poison or perturbation input, never branch accept/reject on it directly, a self-check is patchable by the same skill that defeats any other branch in a shipped binary.
[dependencies]
boo-nodebug = { git = "https://github.com/agmbk/boo" }| Module | Platform | Detects |
|---|---|---|
ld_preload |
Linux | LD_PRELOAD set in this process's own environment, one bypass of a TracerPid-style check. |
parent_process |
Linux, Windows | The parent process's executable name matching a caller-supplied denylist (gdb, windbg, ...). |
trap_flag |
Windows, x86_64 | The EFLAGS trap-flag single-step not firing as expected, indicating a debugger's own single-step machinery or an earlier exception handler. |
use boo_nodebug::parent_process;
let suspicious = parent_process::matches_denylist(&["gdb", "windbg", "x64dbg"]);Each module documents the specific bypass it does not cover, see the module-level docs in
boo-nodebug/src.
MIT - See LICENSE for details.
- LITCRYPT, for being the first crate proposing compile-time string encryption
