Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

obfrust

Post-compilation string obfuscation for Rust binaries. Encrypts all strings in .rodata (ELF) / .rdata (PE) so that strings reveals nothing about your program. Supports self-rekeying to change the binary's hash on demand and an embedded encrypted config store that persists across rekeys.

How it works

  1. Your binary links obfrust-runtime, which embeds a 4KB metadata placeholder, a 4KB config placeholder, and a pre-main init function
  2. You compile normally with cargo build --release
  3. You run obfrust patch on the compiled binary — this encrypts .rodata in-place and fills the metadata placeholder with key material
  4. At runtime, the init function decrypts everything in memory before main() runs
  5. Optionally, the binary can re-key itself (generating a new key + re-encrypting on disk) to change its hash
  6. Optionally, the binary can store and retrieve encrypted config data that survives rekeys

The init-time decryption uses a pure-arithmetic XOR keystream (SplitMix64) that has zero dependencies on .rodata contents, avoiding the chicken-and-egg problem of needing library constants that are themselves encrypted.

Quick start

1. Add the runtime to your project

# Cargo.toml
[dependencies]
obfrust-runtime = { path = "/path/to/obfrust/obfrust-runtime" }

2. Reference the runtime in your code

The runtime must be linked into your binary. At minimum, reference it so the linker doesn't strip it:

fn main() {
    // Ensure obfrust-runtime is linked
    let _ = obfrust_runtime::rekey;

    // ... your code ...
}

3. Build your binary

cargo build --release

4. Build the patcher

# From the obfrust workspace root:
cargo build --release -p obfrust

5. Patch your binary

# Output to a new file:
obfrust patch target/release/my-app -o target/release/my-app.obf

# Or patch in-place:
obfrust patch target/release/my-app --in-place

6. Verify

# Should find zero sensitive strings:
strings target/release/my-app.obf | grep "my secret"

# Should run normally:
./target/release/my-app.obf

Rekeying

Rekeying generates a new random encryption key, re-encrypts all sections, and writes the binary back. This changes the binary's hash (SHA-256, etc.) without recompilation.

External rekey (binary not running)

obfrust rekey target/release/my-app.obf

Self-rekey (from within the running binary)

fn main() {
    if should_rekey() {
        match obfrust_runtime::rekey() {
            Ok(()) => println!("rekeyed"),
            Err(e) => eprintln!("rekey failed: {e}"),
        }
    }
}

Self-rekey modifies the on-disk binary. The currently running process is unaffected — the new key takes effect on the next execution.

Embedded config store

Patched binaries include a 4KB encrypted config region. You can store arbitrary data (up to ~4088 bytes) that is encrypted with the same key material as the obfuscated strings. Config data survives both self-rekey and external rekey — it is decrypted with the old key and re-encrypted with the new key automatically.

Write config

fn main() {
    let license = b"PRO-2026-XYZ789";
    match obfrust_runtime::write_config(license) {
        Ok(()) => println!("config written"),
        Err(e) => eprintln!("write failed: {e}"),
    }
}

Read config

fn main() {
    match obfrust_runtime::read_config() {
        Ok(Some(data)) => {
            let text = String::from_utf8_lossy(&data);
            println!("config: {text}");
        }
        Ok(None) => println!("no config stored"),
        Err(e) => eprintln!("read failed: {e}"),
    }
}

Config is stored on disk in the binary's .obf_config section, encrypted with the same SplitMix64 XOR keystream used for .rodata. The write_config and read_config functions read/modify the on-disk binary (not in-memory), so changes take effect immediately for subsequent reads but don't affect the running process's memory layout.

Inspect metadata

obfrust info target/release/my-app.obf
obfrust metadata found at file offset 0x63148
  version:    1
  binary_len: 535728 bytes
  regions:    1
  key_hash:   b49c7176609231083630e463b8f5f89be09eb66950da224df916427d9160d6ce
  nonce:      [89, 85, 08, b9, 6c, 2b, 92, 18, e3, 12, b9, cf]
  region[0]: file_offset=0x5de0 vaddr=0x5de0 size=0x6720 (26400 bytes)
  config:     23 bytes at file offset 0x62148

CLI reference

obfrust patch <BINARY> [-o <OUTPUT>] [--in-place]
    Encrypt all string-bearing sections and write metadata.

obfrust rekey <BINARY>
    Re-key a patched binary with a new random key.

obfrust info <BINARY>
    Display obfrust metadata (key hash, nonce, regions, config status).

Platform support

Feature Linux (ELF) Windows (PE) macOS (Mach-O)
Patch binary Yes Yes Planned
Init decryption .init_array TLS callback Planned
External rekey Yes Yes Planned
Self-rekey Yes Rename trick Planned
Config store Yes Yes Planned

Notes

  • ELF: Only .rodata is encrypted. .data.rel.ro is excluded because it contains relocated pointers (vtables, dispatch tables) that the dynamic linker patches before init runs.
  • PE: .rdata and .data are encrypted, with exclusion zones for PE data directories (import table, IAT, TLS, debug, exception, etc.).
  • macOS: Mach-O patching is not yet implemented. The architecture is designed for it (sections: __TEXT,__cstring / __TEXT,__const, init: __DATA,__mod_init_func) but the code is a stub.

Architecture

obfrust/                  Workspace root
├── obfrust/              CLI patcher tool
├── obfrust-common/       Shared types: ObfMeta format, crypto helpers
├── obfrust-runtime/      Library linked by target binaries
└── test-binary/          Integration test binary

Encryption scheme

The patcher generates a random 32-byte key_material and 12-byte nonce. For each encrypted region, a deterministic XOR keystream is generated using SplitMix64 seeded from the key material, nonce, and region index. This is a stream cipher — output length equals input length, no padding.

SplitMix64 was chosen over ChaCha20 for the init-time path because the decryption runs before .rodata is available — crypto libraries that read constants from .rodata (ChaCha20's "expand 32-byte k", BLAKE3 initialization vectors) would crash.

OBFRUST_META layout (4096 bytes)

Offset  Size    Field
0       4       magic ("OBFR")
4       4       version (1)
8       4       num_regions
12      4       binary_len low 32 bits
16      32      key_material
48      12      nonce
60      4       binary_len high 32 bits
64      N*24    regions: [(file_offset: u64, vaddr: u64, size: u64)]

Max 168 regions per binary.

OBFRUST_CONFIG layout (4096 bytes)

Offset  Size    Field
0       4       magic ("OBFC")
4       4       data_len
8       N       payload (up to 4088 bytes)

The entire 4096-byte block is encrypted using init_xor_region with region_index = u32::MAX to avoid collision with .rodata region indices. An all-zeros block means no config has been written.

Security considerations

This tool is designed to defeat static string analysis (strings, grep, regex scanning). It does NOT protect against:

  • Dynamic analysis: attaching a debugger, setting breakpoints at the decryption return, or dumping process memory after init runs
  • Determined reverse engineering: the key material is stored in the binary itself (in .obf_meta), so an RE who understands the scheme can decrypt both strings and config manually

The encryption key is not stored as a raw ChaCha20 key — it goes through a BLAKE3 keyed-hash derivation layer that mixes the key material, nonce, and binary length. This makes automated key extraction harder but not impossible.

License

MIT

About

Obfuscator for rust

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages