Skip to main content

StegTriage

Automated steganography and forensics triage for CTF challenges and digital forensics practice.

Point it at an image, get back a ranked table of everything suspicious — hidden files, flag strings, LSB stego, EXIF secrets, trailing data — in seconds.

Scope: StegTriage is a detection and extraction tool for CTF and defensive forensics work. It does not embed or hide data. Never use it against images you do not own or have explicit permission to analyse.


Installation

1. Python package

git clone git@github.com:aryan-errs/stegtriage.git
cd stegtriage

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate

pip install -e .

Requires Python 3.10+. Python dependencies (pillow, numpy, rich, click) are installed automatically.

2. External tools

StegTriage shells out to several binaries. All are optional — the tool degrades gracefully when any are absent, marking those modules as SKIPPED with an install hint. Never crashes because a tool is missing.

Tool Used for Linux (apt) macOS (brew) Other
exiftool EXIF / metadata extraction apt install libimage-exiftool-perl brew install exiftool exiftool.org
binwalk Embedded file signatures + extraction apt install binwalk brew install binwalk pip install binwalk
zsteg PNG/BMP LSB stego (all orderings) — — gem install zsteg (requires Ruby)
steghide JPEG/BMP/WAV passphrase extraction apt install steghide brew install steghide —
file Magic-byte type identification apt install file built-in —

Check which tools are available on your system:

stegtriage          # no image → prints tool availability table

3. Run the tests

pip install -e ".[dev]"
python -m pytest

The test suite is self-contained — it synthesises all required fixture images at session start so no real challenge files are needed.


Usage

stegtriage IMAGE [OPTIONS]

Basic

# Full analysis
stegtriage challenge.png

# Only run specific modules
stegtriage challenge.png --only lsb,strings,binwalk

# Skip a slow module
stegtriage challenge.png --skip steghide

# Custom artifact directory
stegtriage challenge.png --outdir /tmp/ctf_work/

steghide passphrase

# Try a single known passphrase
stegtriage challenge.bmp --only steghide --password "DUEDILIGENCE"

# Brute-force with a custom wordlist
stegtriage challenge.bmp --only steghide --wordlist /usr/share/wordlists/rockyou.txt

Verbosity

stegtriage challenge.png           # default: findings table
stegtriage challenge.png -v        # + module status lines with timing
stegtriage challenge.png -vv       # + full raw tool output per module
stegtriage challenge.png --quiet   # findings table only, no status lines

Machine-readable JSON

# Full JSON output (safe for --json | jq piping)
stegtriage challenge.png --json

# All HIGH findings across all modules
stegtriage challenge.png --json \
  | jq '[.[].findings[] | select(.severity=="high")]'

# Modules that found something
stegtriage challenge.png --json \
  | jq '.[] | select(.findings | length > 0) | {module: .name, count: (.findings | length)}'

# Feed into another tool
stegtriage challenge.png --json > report.json

All options

Option Default Description
IMAGE (required) Path to the image / file to analyse
--wordlist PATH bundled 65-entry list Wordlist for steghide passphrase brute-force
--password TEXT — Single known passphrase for steghide (skips wordlist)
--outdir PATH ./stegtriage_<name>/ Where artifacts are written
--only MODULES all Comma-separated list of modules to run
--skip MODULES none Comma-separated list of modules to skip
--min-str-len N 6 Minimum printable-string run length
--threads N CPU count Max parallel workers
--json off Emit JSON to stdout instead of the rich table
--quiet off Findings table only, suppress status lines
-v / -vv off Increase verbosity (-vv dumps raw tool output)

Exit code is always 0 on a completed run ("nothing found" is a valid result). Non-zero only on usage or I/O errors.


Modules

All modules run in parallel. Each writes its full raw output and any carved artifacts to --outdir; the terminal shows only matched findings.

fileinfo

Reads the first 32 bytes natively to identify the file type, cross-checks against the extension, then optionally runs file.

  • HIGH — extension/content mismatch (e.g. a .jpg that is actually a ZIP)
  • MEDIUM — suspicious aspect ratio (> 50:1), file larger than its raw uncompressed pixels
  • INFO — image dimensions and detected type

exif

Runs exiftool -a -G1 -s and parses every tag. Extracts embedded thumbnails to --outdir.

  • HIGH — flag pattern inside a comment/description field; flag decoded from a base64-encoded field
  • MEDIUM — GPS coordinates, comment/UserComment/ImageDescription, base64-looking values, ExifTool warnings
  • LOW — embedded thumbnail extracted (compare with main image), high-entropy fields
  • INFO — software/creator tags

Requires exiftool. Skipped gracefully if absent.

strings

Pure-Python printable-ASCII extractor — no external binary needed. Scans every raw byte of the file and runs the results through the pattern matcher.

  • HIGH — flag pattern (flag{…}, CTF{…}, picoCTF{…}, HTB{…}, etc.), PEM/private-key headers, flags decoded from base64
  • MEDIUM — URLs, onion addresses, base64 blobs that decode to binary magic
  • LOW — email addresses, hex blobs

Full strings dump saved to <outdir>/strings_dump.txt.

binwalk

Two independent checks — the native one always runs regardless of whether binwalk is installed:

  1. Native trailing-data check: walks PNG chunks forward to the IEND marker; for JPEG, parses the marker chain to the real FF D9 EOI (not rfind, so an appended JPEG is correctly detected); for GIF, seeks to the 0x3B trailer. Any bytes after the container's declared end are HIGH; the payload is identified by magic (ZIP, gzip, RAR, 7-Zip, PDF, JPEG, …) and saved as trailing_<fmt>.bin.

  2. Binwalk binary: runs binwalk -e -C <outdir> for signature detection and file extraction. Signatures at offsets past the container EOF are HIGH; signatures inside normal file structure (e.g. PNG IDAT zlib) are MEDIUM.

lsb

Native Pillow + NumPy — no external binary.

(a) Bit-plane export: Every bit plane (bits 0–7) for every channel (R, G, B, A) is saved to --outdir as a greyscale image. LSB stego often becomes visible immediately when you open bitplane_B_bit0.png.

(b) Bitstream extraction: LSB bits are extracted in multiple orderings (row-major / column-major × RGB / BGR × LSB-first / MSB-first, per-channel and combined) and each stream is scanned for flags, URLs, base64, and key material. A match is HIGH and reports the exact ordering used.

(c) Statistical analysis:

  • Shannon entropy — 0 = constant (solid colour), 1 = fully random. Near-constant channels (entropy < 0.05) flagged MEDIUM.
  • Structure score — long identical-bit runs + row-sum variance deviation. Non-trivial structure in a non-constant channel flagged MEDIUM/HIGH.
  • Chi-square heuristic — Westfeld-Pfitzmann adjacent-pair test. High embedding probability flagged MEDIUM. Most useful on natural photos; not meaningful on solid-colour images. Cited as a heuristic.

zsteg

Runs zsteg -a (all orderings) against PNG and BMP files. Parses every output line:

  • HIGH — flag pattern in text content; recognised file-type signature (JPEG, ZIP, ELF, …)
  • MEDIUM — any other readable text content

Requires zsteg (gem install zsteg). Automatically skipped for non-PNG/BMP inputs.

steghide

Attempts extraction from JPEG, BMP, WAV, and AU files.

  1. Tries the empty passphrase and the filename stem first.
  2. With --password TEXT, tries that single passphrase and stops.
  3. Otherwise iterates the wordlist (--wordlist PATH; default: bundled 65-entry CTF list), stopping on first success.

Capped at 5000 attempts. Progress bar shown during brute-force (written to stderr so --json stdout stays clean).

  • HIGH — extraction succeeded; extracted file saved as artifact
  • INFO — no passphrase cracked (suggests a larger wordlist)

Requires steghide. Automatically skipped for unsupported formats.


Output

Terminal (default)

Analyzing: challenge.png
Artifacts → stegtriage_challenge/

  fileinfo       ok       0.08s  0 findings
  exif           ok       0.31s  1 finding
  strings        ok       0.01s  0 findings
  binwalk        ok       0.94s  5 findings
  lsb            ok       0.09s  1 finding
  zsteg          skipped           (Required tool 'zsteg' not found…)
  steghide       skipped           (steghide only supports JPEG/BMP…)

╭──────────┬─────────┬──────────────────────────┬──────────────────────────────┐
│ Severity │ Module  │ Label                    │ Detail                       │
├──────────┼─────────┼──────────────────────────┼──────────────────────────────┤
│ HIGH     │ binwalk │ Trailing data after PNG  │ 149 bytes after PNG end …    │
│ HIGH     │ lsb     │ Flag in LSB bitstream    │ Ordering B/row-lsb: flag{…}  │
│ MEDIUM   │ exif    │ ExifTool warning         │ Trailer data after PNG IEND  │
╰──────────┴─────────┴──────────────────────────┴──────────────────────────────┘

Next steps:
  ▶ Trailing data after container EOF → inspect trailing_png.bin
  ▶ Flag pattern found → Ordering B/row-lsb: flag{lsb_blue_channel}

JSON (--json)

All console output is suppressed; only valid JSON is written to stdout.

stegtriage challenge.png --json | jq .
[
  {
    "name": "binwalk",
    "status": "ok",
    "findings": [
      {
        "severity": "high",
        "label": "Trailing data after PNG EOF (ZIP archive)",
        "detail": "149 bytes after PNG end at offset 0x1ad. First bytes: 504b0304…",
        "artifact": "stegtriage_challenge/trailing_png.bin"
      }
    ],
    "raw_output": "…",
    "duration_s": 0.94
  },
  {
    "name": "lsb",
    "status": "ok",
    "findings": [
      {
        "severity": "high",
        "label": "Flag in LSB bitstream",
        "detail": "Ordering B/row-lsb: flag{lsb_blue_channel}",
        "artifact": null
      }
    ],
    "raw_output": "…",
    "duration_s": 0.09
  }
]

Artifacts directory

Every module's raw output and every carved/extracted file lands in --outdir:

stegtriage_challenge/
  fileinfo_raw.txt          ← magic bytes, file command output, image info
  exif_raw.txt              ← full exiftool output
  strings_dump.txt          ← every printable string extracted from the file
  strings_raw.txt           ← strings module summary
  binwalk_raw.txt           ← binwalk signature table
  lsb_raw.txt               ← per-channel entropy / structure / chi-square stats
  trailing_png.bin          ← bytes appended after container EOF
  bitplane_R_bit0.png       ← LSB plane images (channels × bits 0–7 = up to 32 files)
  bitplane_G_bit0.png
  bitplane_B_bit0.png
  binwalk_extracted/
    _challenge.extracted/
      1AD.zip
      secret.txt

Severity levels

Level Meaning
HIGH Strong signal — flag found, known-malicious signature, extension mismatch, data past container EOF, steghide extraction succeeded
MEDIUM Worth investigating — GPS data, comment fields, base64 blobs, ExifTool warnings, near-constant or structured LSB planes, text content in zsteg
LOW Informational — email addresses, hex blobs, embedded thumbnail, unusual aspect ratio
INFO Context only — software tags, steghide wordlist exhausted

Supported formats

Format fileinfo exif strings binwalk lsb zsteg steghide
PNG ✓ ✓ ✓ ✓ native EOF ✓ ✓ ✗
JPEG ✓ ✓ ✓ ✓ native EOF ✓ ✗ ✓
BMP ✓ ✓ ✓ ✓ ✓ ✓ ✓
GIF ✓ ✓ ✓ ✓ native EOF ✓ ✗ ✗
WAV / AU ✓ ✓ ✓ ✓ ✗ ✗ ✓
Any file ✓ ✓ ✓ ✓ ✗ ✗ ✗

Project layout

stegtriage/
  pyproject.toml
  README.md
  TESTING.md
  stegtriage/
    cli.py            ← click entry point
    orchestrator.py   ← tool probing, thread pool, summary renderer
    models.py         ← Finding and ModuleResult dataclasses
    patterns.py       ← shared regexes, base64/hex decoders, extract_strings
    modules/
      fileinfo.py
      exif.py
      strings_mod.py
      binwalk_mod.py
      lsb.py
      zsteg.py
      steghide.py
    data/
      default_wordlist.txt
  tests/
    conftest.py
    make_fixtures.py
    fixtures/           ← generated at test time, not committed
    test_patterns.py
    test_lsb.py
    test_orchestrator.py

Metadata

Release files for stegtriage 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stegtriage 0.1.0
File Size Uploaded Provenance Actions
stegtriage-0.1.0.tar.gz 36.1 kB With provenance Details for stegtriage-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for stegtriage 0.1.0
File Interpreter ABI Platform Actions
stegtriage-0.1.0-py3-none-any.whl Python 3 none any Details for stegtriage-0.1.0-py3-none-any.whl

Total release size: 69.0 kB

Release files / stegtriage-0.1.0.tar.gz

Download URL stegtriage-0.1.0.tar.gz
Size 36.1 kB
Tags Source
SHA-256 checksum
How to use checksums
0d87a4cf0cda2fdada3180dfba493fe8744c8e54ec7d379d16987adeec844f61
BLAKE2b-256 checksum
How to use checksums
07a2bd2b4c51939902cebef96d6da1293fd0c3c0874a698ac313f96c8b0fd906
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.

Transparency log

Release files / stegtriage-0.1.0-py3-none-any.whl

Download URL stegtriage-0.1.0-py3-none-any.whl
Size 32.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
810f51a2084e4674aaf6de7038e64f4f72e78cd6623e0db6959c215149455db3
BLAKE2b-256 checksum
How to use checksums
e09c71464def022941bb39c6d4ddb25feedb351a7381da7935914d93e1d14abc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.

Transparency log

Security for stegtriage 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for stegtriage-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page