
Binary visualiser and triage tool — entropy, byte-class and Hilbert surfaces, dot plots and control-flow graphs over one shared address-space model.
Binary visualiser & triage tool: linked interactive views (entropy, histograms, image/dot-plot surfaces, control-flow graphs) over a single shared address-space model.
pipx install binviz && binviz serve
Open a file and every view is looking at the same address space. Select a range in one and the rest follow — the point is to answer "what is this region" by looking at it several ways at once.
binviz model parses ELF/PE/Mach-O through LIEF into regions,
symbols and an offset↔virtual-address mapping, materialising gaps and
overlays. Malformed input falls back to a raw model rather than failing.binviz triage says what the file looks like and why;
in the UI each finding clicks through to the bytes it was derived from.The UI is five workspaces — Overview, Bytes, Patterns, Code, and All — over the same selection. Static analysis only: samples are parsed, never executed.




Rendered by the same code the UI draws with, straight from the CLI —
regenerate with python docs/make_plates.py.
| A static binary | The same program, UPX-packed |
|---|---|
![]() | ![]() |
| Code, strings and padding separate into visible territories. | Structure collapses into uniform noise — the signature of packing. |
![]() | ![]() |
| Windowed entropy stays banded and low. | Flat and high, right up to the unpacking stub. |
| Right row stride | Wrong row stride |
|---|---|
![]() | ![]() |
Same bytes, one number different. That is why the stride suggester exists: the wrong row stride turns a photograph into diagonal noise, and you conclude there is no photograph.
ARCHITECTURE.md is how it is put together: what ships, the branding every
surface inherits, the conventions a new screen must follow, and the
limitations that are deliberate. SECURITY.md is the security posture.
python -m venv .venv
# -c pins to the exact versions the suite is green against; pyproject.toml
# publishes ranges, so without it you get whatever resolves today
.venv/Scripts/pip install -e ".[dev]" -c constraints-dev.txt # POSIX: .venv/bin/pip
# build the ground-truth corpus (uses zig cc from the ziglang pip package;
# needs UPX on PATH, in $UPX, or unzipped into corpus/tools/upx-*/)
make -C corpus # or: python corpus/build.py
# thresholds are measured, never hardcoded (see ARCHITECTURE.md §2.1)
python corpus/calibrate.py # writes corpus/calibration.json
pytest # functional suite
pytest -m perf -s # 100 MB performance targets
binviz probe corpus/out/hello_O2
binviz model corpus/out/hello_upx
binviz signal corpus/out/hello_upx --name entropy_4096 --png out.png
binviz hist corpus/out/ramp16.bin --n 2 --dtype u16le --png bigram.png
# surfaces: -p passes surface parameters
binviz surface corpus/out/hello_static --name hilbert -p mode=byteclass --png h.png
binviz surface corpus/out/rgb_raw.bin --name image -p mode=rgb8 -p width=320 --png i.png
binviz surface corpus/out/repeats.bin --name dotplot -p mode=exact --png d.png
binviz stride corpus/out/bayer_raw.bin --mode bayer_RGGB_RGB_12
# code
binviz disasm corpus/out/hello_O2 --limit 20
binviz functions corpus/out/hello_static --sort size
binviz cfg corpus/out/hello_O2 --func main --dot main.dot
# the verdict, and why
binviz triage corpus/out/hello_upx
binviz serve # 127.0.0.1:8000
It prints a URL containing a session token — open that. Every /api route
requires the token, because "it only listens on localhost" is not a defence
against a web page in another tab, which reaches 127.0.0.1 just like any
other origin. SECURITY.md has the reasoning.
File access is confined to --root (default: the working directory), so
paths outside it are refused.
All four have a flag and an environment variable, and all four exist to stop a local caller consuming more than you intended. Defaults are chosen for a laptop; raise them if your machine is bigger.
| Flag | Env | Default | What it bounds |
|---|---|---|---|
--max-cache BYTES | BINVIZ_MAX_CACHE | 5 GiB | Total size of cached analyses. Past this, least-recently-used entries are evicted — never one being analysed or viewed. |
--max-upload BYTES | BINVIZ_MAX_UPLOAD | 8 GiB | Largest accepted upload. |
--max-analyses N | — | 4 | Simultaneous analyses; beyond it /api/open returns 503. |
--root DIR | — | cwd | Directory the server may read files from. |
Analyses are cached under ~/.cache/binviz (or $BINVIZ_CACHE), keyed by
content hash, so reopening a binary is instant. Raise --max-cache if you
would rather keep more of them; the cache is safe to delete by hand at any
time — the worst case is that the next open re-analyses.
Other flags: --token to pin a token across restarts (useful with the Vite
dev proxy, which reads BINVIZ_TOKEN), --port, --cache, and --no-auth
for CI. --no-auth prints a banner telling you what it turned off; do not
use it on a machine you share.
pip install "binviz[app]"
binviz app # native window; --browser for your browser
Same server, same token, same --root confinement as binviz serve — the
only difference is what displays it. Without pywebview installed, binviz app opens your browser instead.
It prints the URL it is serving on, deliberately: wrapping the UI in a
window does not remove the network listener, it only makes it easier to
forget there is one. The listener is authenticated either way, and there is
no --no-auth on binviz app.
The window exposes exactly one function to the page — a native file picker —
and nothing else. See src/binviz/app.py for why that list is as short as
it is.
Releases ship a wheel and nothing else. An unsigned frozen Python executable that bundles capstone and lief and exists to dissect packed binaries is exactly the profile SmartScreen and AV heuristics false-positive on — so instead of shipping one, the repo carries what you need to build it yourself, which sidesteps code signing entirely.
pip install pyinstaller # 6.x
python tools/build_ui.py # builds web/ and stages it into the package
pyinstaller packaging/binviz.spec # -> dist/binviz/
Expect ~100 MB, dominated by numpy and lief. It is a onedir bundle, not a
single self-extracting file: launch dist/binviz/binviz.exe (or
double-click it) for the desktop window, or give it any subcommand —
dist/binviz/binviz.exe triage sample.exe — because the frozen build is
the whole CLI, not just the window.
The staging step is not optional. web/dist lives outside the Python
package, so skipping it produces an app whose window opens on a JSON 404;
the spec refuses to build rather than let that happen quietly.
On macOS the same command also produces dist/Striate.app, branded from
packaging/icons/icon.icns. Neither has been run on a Mac — see
ARCHITECTURE.md §5.
--root still defaults to the working directory, so a double-clicked
executable is confined to the folder it starts in — which is usually the
app's own folder. Set the shortcut's "Start in", or launch it with
--root DIR.
A double-clicked executable asks for a credential. With no arguments the
frozen build runs binviz app --auth local, which is the one difference from
the wheel's own default of no sign-in screen. The two answer different
questions: binviz app typed into a terminal is already a deliberate act by
whoever owns the session, while a double-click establishes nothing — it is the
only launch path with no terminal, no typed command and no confinement
decision behind it. Asking for the credential is how the window says out loud
what the terminal would have said. Run binviz passwd first to set one, or
pass --auth none explicitly to skip it; anything you supply on the command
line still wins.
By default there is no login screen and nothing to copy: the server mints a
session token and injects it into the page it serves, so opening
http://127.0.0.1:8000/ just works while every API call is still
authenticated.
On a machine you share, turn on the sign-in screen:
binviz passwd # prompts; scrypt digest, mode 0600
binviz serve --auth local
If you skip binviz passwd, the first sign-in claims the install — the
startup banner warns about that, because whoever reaches the port first
becomes the account.
A double-clicked frozen executable turns --auth local on for itself; see
Building a standalone app for why that default
differs from the wheel's.
The login screen is not the security boundary; the token check on every
/api route is. Anything on the machine can skip the form and call the API
directly, which is exactly why the token exists. See SECURITY.md.
binviz opens files an attacker chose — that is the job, not an edge case, and a triage tool where analysing malware compromises the analyst is the worst failure available. Samples are parsed, never executed. What is done about the rest:
Against a hostile binary
id in every /api/{id}/… route
must be exactly 64 hex characters before it is used to build a path.Against a hostile browser — the threat "it only listens on localhost" does
not address, because a page in another tab reaches 127.0.0.1 like any other
origin:
/api route requires a token. It is minted at startup and injected
into the page, so nothing is pasted by hand and no route is left open.--root, which defaults to the working
directory. Paths outside it are refused.Host allowlist and narrow CORS, so the origin that needs access is the
only one that gets it.The desktop window does not remove the network listener, it only makes it
easier to forget. So there is no --no-auth on binviz app, and the js_api
bridge exposes exactly one method — pick_file(), which takes no arguments and
returns a path through the same --root confinement. A test fails if a second
method ever appears.
Credentials for --auth local are scrypt digests written mode 0600; binviz
stores no plaintext password.
SECURITY.md has the threat model, the reasoning behind each control, what
is deliberately not done yet, and how to report a vulnerability privately.
MIT — see LICENSE.
The corpus cross-compiles ELF samples with zig cc, so no Linux toolchain is
needed on Windows/macOS — samples are parsed, never executed.