Extract obfuscated strings from binaries. Library + CLI. PE, ELF, Mach-O (including fat binaries), and raw shellcode. Static, stack, and decoded strings, with structured JSON output.
ASCII and valid UTF-8 payloads returned by the static scanner borrow directly
from the input (Cow::Borrowed(&str)). Go/Rust extraction also borrows when the
text exists directly in the input. The CLI reads through an mmap. Filtering,
tagging, scoring and deduplication do not copy or rewrite these payloads.
Result vectors, metadata, model loading and output formatting still allocate.
UTF-16LE/BE must be transcoded into UTF-8, so those results are Cow::Owned.
Base64 and emulation results also own their text because the decoded bytes do
not exist as a contiguous UTF-8 slice in the original input. into_owned() is
available when the caller deliberately wants to release the input buffer.
- General UTF-8 static extraction, including multilingual text, with borrowed payloads and original byte offsets.
- Full UTF-16LE/BE decoding, including surrogate pairs and odd offsets. Invalid code units split strings instead of inserting replacement characters.
--encoding ascii,utf8,utf16le,utf16beselects static encodings. Raw UTF-16 can have multiple plausible endian/alignment interpretations; select the known encoding when appropriate. Unicode is preserved without a language model.--decode-base64(or--only base64) enables standard/URL-safe, padded or unpadded token recovery. It is single-layer, limited to 1 MiB per encoded token and 16 MiB decoded bytes per input. Wrapped/whitespace-separated containers are not joined.location.offsetidentifies the original encoded token;analysis.decoded_offsetidentifies text inside its decoded body, andanalysis.source_lengthrecords the encoded source length.--annotateadds non-destructive URL, IP, email, path, registry, library, executable-section, duplicate and Base64 tags.--tag url,registrykeeps matches for any selected tag. These are candidate indicators, not verdicts. No embedded global-prevalence/OSS/capa database is claimed or downloaded.--model PATH.sngloads an optional ASCII trigram count model once. Scoring reads borrowed text without copying it.--min-trigram-score -5optionally filters by the mean smoothed log10 probability. These are not Ghidra's calibrated scores/acceptance thresholds. Unicode and strings shorter than three characters remain unscored and are retained by this filter. Original text is never transliterated or normalized in the output. No model is bundled or fetched implicitly; the model is opt-in for both CLI and library callers.--jsonlwrites one string record per line, including optional annotations.
strix --only static --encoding ascii,utf8 --annotate sample.bin
strix --decode-base64 --annotate --tag url,registry --jsonl sample.exe
strix --only static --encoding ascii --model custom.sng --min-trigram-score -5 sample.binInspired by the multi-encoding, Base64 and optional text-scoring ideas in Stranger Strings, and the non-destructive layout/tagging approach in FLOSS. Strix retains its own scanners, borrowed result types and emulation pipeline; it does not embed either tool or their tag/model datasets.
All workspace packages are now 0.2.0. Encoding and StringKind have new
variants; update exhaustive matches. ExtractOptions has new opt-in fields;
prefer ..Default::default() in struct literals. ExtractedString carries a
serde-defaulted analysis field, omitted from JSON when empty. Older JSON still
loads. Default static extraction now includes UTF-8 and both UTF-16 byte orders,
so it can return additional Unicode candidates. Select only the old static kinds
for legacy coverage. Base64, annotations and model scoring remain disabled by
default.
Download from Releases for your platform:
| Platform | Architecture | Archive |
|---|---|---|
| Linux | x86_64 | strix-<tag>-x86_64-unknown-linux-gnu.tar.gz |
| Linux | aarch64 | strix-<tag>-aarch64-unknown-linux-gnu.tar.gz |
| macOS | aarch64 | strix-<tag>-aarch64-apple-darwin.tar.gz |
| Windows | x86_64 | strix-<tag>-x86_64-pc-windows-msvc.zip |
Each archive contains a single strix (or strix.exe) binary plus a
SHA-256 checksum.
git clone https://github.com/marirs/strix
cd strix
cargo build --release --features unicorn -p strix-cli
# binary: target/release/strixWithout --features unicorn you still get static, language, and
stack-string extraction (x86, x86_64, and AArch64). The unicorn
feature adds the brute-force emulation pipeline for decoded strings;
it pulls in the unicorn-engine C library and requires cmake + a
C toolchain at build time.
AArch64 (ARM64) function discovery and stack-string pattern matching are always on — bad64 is a pure-Rust disassembler so it adds no system dependencies. Decoded-string extraction on AArch64 binaries still requires the AArch64 emulator backend (planned).
# Default: grouped, human-readable output to stdout
strix malware.exe
# JSON
strix --json malware.exe
# Indented JSON, written to a file
strix --json --pretty -o malware.json malware.exe
# Drop duplicates (same value + kind + encoding)
strix --dedupe malware.exe
# Filter out static strings that land in executable sections
# (eliminates most assembly-byte false positives like AWAVAUATSH)
strix --no-code malware.exe
# Drop CRT / libc / Windows-API boilerplate noise
# (kernel32.dll, GetProcAddress, "Runtime Error!", ...)
strix --no-library malware.exe
# Drop low-entropy noise (AAAAAA, ////////, +++++++)
strix --min-quality 0.4 malware.exe
# Group emulation-recovered strings by source function VA
strix --by-function malware.exe
# Combine: typical analyst usage
strix --dedupe --no-code --no-library --min-quality 0.4 --by-function malware.exe
# Only run specific extractor groups
strix --only static malware.exe
strix --only lang malware.exe # Go / Rust runtime strings
strix --only decoded stack malware.exe
# Skip specific extractor groups
strix --no decoded stack malware.exe
# Raise the static-string minimum length (default 4)
strix --min-length 8 malware.exe
# Force a file format (skip auto-detection)
strix --format pe malware.exe
strix --format elf binary
strix --format macho /usr/bin/ls
strix --format sc64 shellcode.bin # raw 64-bit shellcode
strix --format sc32 shellcode.bin # raw 32-bit shellcode
# Quiet: no section headers, banner, or warnings — just the strings
strix --quiet malware.exe | sort -uRun strix --help for the full flag surface.
In human-readable mode (the default), strix groups by string kind:
strix: format=pe, size=131072 bytes
arch=x86_64, bits=64
=== static strings (412) ===
0x0000000140001000 Microsoft Visual C++ Runtime Library
0x0000000140001028 Runtime Error!
...
=== decoded strings (7) ===
0x0000000000000000 http://c2.example.com/beacon
0x0000000000000000 kernel32.dll
...
=== stack strings (12) ===
0x0000000140002a30 ntdll.dll
0x0000000140002a48 LdrLoadDll
...
warnings:
- 3 of 14 emulated candidates failed (faulted on unmapped memory ...)
--json emits the same data in a machine-readable shape.
strix is distributed via this repository rather than crates.io.
Add it to your Cargo.toml as a git dependency:
[dependencies]
strix = { git = "https://github.com/marirs/strix", branch = "master", features = ["unicorn"] }
serde_json = "1"Use branch = "master" instead of tag to track the latest unreleased
changes, or rev = "<sha>" to pin a specific commit.
The unicorn feature is optional — leave it off if you only need
static, language, and stack-string extraction. With it off you don't
need cmake or a C toolchain at build time:
[dependencies]
strix = { git = "https://github.com/marirs/strix", branch = "master" }use strix::{extract, ExtractOptions};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let bytes = std::fs::read("malware.exe")?;
let result = extract(&bytes, &ExtractOptions::default())?;
for s in &result.strings {
println!("{:?} {} {}", s.kind, s.encoding, s.value);
}
Ok(())
}use strix::{extract, ExtractOptions, FormatHint, StringKind};
let opts = ExtractOptions {
min_length: 6, // skip short noise
enabled: Some(vec![ // only these kinds
StringKind::StaticAscii,
StringKind::Decoded,
StringKind::Stack,
]),
format_override: Some(FormatHint::Pe),
max_emulation_steps: 20_000, // cap per emulated function
dedupe: true, // drop duplicate (value, kind, encoding)
skip_code_sections: true, // drop static strings in .text / __TEXT,__text
skip_library_strings: true, // drop CRT/libc/Windows-API noise
min_quality: 0.4, // drop AAAAAA, //////, +++++ noise
};
let bytes = std::fs::read("malware.exe")?;
let result = strix::extract(&bytes, &opts)?;For huge binaries, mmap the input so static-string slices borrow directly into the mapping with no allocation:
use memmap2::Mmap;
let file = std::fs::File::open("malware.exe")?;
let mmap = unsafe { Mmap::map(&file)? };
let result = strix::extract(&mmap, &strix::ExtractOptions::default())?;
// The result borrows from `mmap`. Bind both to names that outlive
// the use, or call `result.into_owned()` to detach.let json = serde_json::to_string_pretty(&result)?;
std::fs::write("malware.strix.json", json)?;The JSON schema matches the CLI's --json output exactly:
{
"version": "0.2.0",
"input": {
"format": "pe",
"arch": "x86_64",
"bits": 64,
"size": 131072
},
"strings": [
{
"value": "kernel32.dll",
"kind": "decoded",
"encoding": "ascii",
"location": {
"offset": 0,
"address": 4294967296,
"section": "scratch"
}
}
],
"warnings": []
}use strix::StringKind;
let decoded: Vec<&str> = result.strings.iter()
.filter(|s| s.kind == StringKind::Decoded)
.map(|s| s.value.as_ref())
.collect();
let stack: Vec<&str> = result.strings.iter()
.filter(|s| s.kind == StringKind::Stack)
.map(|s| s.value.as_ref())
.collect();The library doesn't care where the bytes came from — file, network, embedded resource, all work:
let bytes: Vec<u8> = fetch_sample_from_some_api()?;
let result = strix::extract(&bytes, &strix::ExtractOptions::default())?;| Kind | Source |
|---|---|
static_ascii / static_utf16_le |
Printable byte runs in any section. |
go |
UTF-8 strings from Go binaries (function names from pclntab when present). |
rust |
UTF-8 strings from Rust binaries (crate metadata from .rustc when present). |
stack |
Strings built on the stack via mov / push / reg-then-store patterns. x86, x86_64, and AArch64. |
decoded |
Strings produced by in-memory decoder routines, recovered via Unicorn-backed brute-force emulation with import stubs and call-site argument extraction. x86, x86_64, and AArch64. |
tight |
Stack strings whose writing instruction lies inside a discovered loop body. Distinguished from stack via CFG back-edge detection. |
The emulation pipeline (brute-force decoder fuzzing, caller emulation,
and call-site dataflow runs) parallelizes across cores via rayon. Each
worker thread builds its own Unicorn instance; the per-worker
construction cost is amortized across all candidates that thread is
assigned. Set RAYON_NUM_THREADS=1 for deterministic single-threaded
runs.
Indicative wall-clock on an Apple Silicon laptop:
$ time target/release/strix tests/fixtures/elf-Linux-ARM64-bash > /dev/null
real 0m0.265s
user 0m0.910s
sys 0m0.138s
The 3.4x ratio of user time to real time on that run shows roughly three-and-a-half cores busy in parallel during the emulation pass. The speedup scales with the number of decoder candidates the heuristic flags above the score threshold; binaries with only a handful of candidates see little benefit because rayon overhead dominates.
crates/
strix-core types, JSON schema, errors, traits
strix-format PE / ELF / Mach-O / shellcode parsing (goblin)
strix-static borrowed ASCII/UTF-8, owned UTF-16LE/BE and optional Base64
strix-lang Go and Rust language-specific extraction
strix-emulator Unicorn-backed emulator + iced-x86 + bad64 analyzers
+ x86 and AArch64 stack-string pattern matchers
+ x86 and AArch64 call-site dataflow analyzers
strix umbrella library, ties everything together
strix-cli CLI binary
Apache-2.0