Skip to content

ProtocolTheta/obfrust

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

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages