HashEndra takes an unknown string, file, or disk image and reports what it most likely is, with a confidence score and a suggested next step. It identifies hash types, decodes layered encodings, encrypts and decrypts with classical and modern ciphers, runs standard RSA attacks for CTF work, cracks wordlist hashes, and performs forensic triage including carving and filesystem inspection. It runs fully offline.
Repository: https://github.com/meshackbahati/HashEndra
This page is self-contained. Everything it describes is explained or diagrammed below, including the challenge solves in full.
What the tool is
Most security work starts with an unidentified blob: a hash in a leak, a base64 string in a config file, a disk image on a desk. The first question is always the same, and it is also the most expensive one to get wrong, because every later step depends on the answer. HashEndra exists to answer that first question quickly and to say how sure it is.
The tool has two halves that share one engine. The first half looks at a string and tells you what it probably is: which hash, which encoding, which cipher, with a score and a pointer to the next tool (hashcat modes and John formats are printed for exactly this reason). The second half acts on files and disk images: metadata, carving, string tables, filesystem parsing. Both halves work offline, because triage machines are often air-gapped and CTF networks are often hostile.
Five design rules shape every behavior described below. First, uncertainty is scored, never hidden: every verdict carries a percentage, and low scores mean the tool does not know. Second, strict input wins over guesses: odd-length hex, out-of-range decimal bytes, and missing keys are errors, not invitations to coerce. Third, cryptography fails closed: a wrong key or a bad tag produces an error, never plausible-looking garbage. Fourth, large inputs are streamed or memory mapped, never loaded whole, so memory usage stays flat as inputs grow. Fifth, the exit code is 0 only when the requested work actually succeeded, so scripts and pipelines can trust it.
What it is not: a replacement for dedicated crackers on expensive hashes, a factoring engine for properly generated moduli, or a cryptographic library for production systems. The RSA pieces are textbook implementations for learning and competition use.
Installation
Install with the installer. It fetches a prebuilt binary for your platform when one exists (Linux x86_64 and aarch64, macOS Intel and Apple Silicon, Windows x86_64), and builds from source otherwise:
curl -sSL https://raw.githubusercontent.com/meshackbahati/HashEndra/main/install.sh | bashUseful flags: --version 2.0.0 to pin a release, --prefix DIR to
choose the install location, --from-source to always build,
--uninstall to remove. The current version is 2.0.0.
Concepts in plain terms
If the vocabulary below is new, this section covers everything the rest of the page assumes.
A hash is a one-way fingerprint of data. MD5 turns any input into 32 hex characters, SHA-256 into 64. One-way means you cannot reverse it; to recover a password from its hash, you guess candidates until one matches, which is what wordlist cracking does.
An encoding is a reversible change of representation, not protection. Base64 turns bytes into printable text so they survive email and JSON. Hex writes each byte as two characters. Anyone can decode these without a key, which is precisely why identifying them matters: an "encrypted" string is often just encoded.
Encryption does need a key. Classical ciphers (Caesar shifts, Vigenere keywords, single-byte XOR) use short keys and fall to statistical analysis. Modern ciphers (AES) use 16, 24, or 32 byte keys and do not. A nonce (number used once) and an IV (initialization vector) are random starting values that keep identical messages from encrypting identically. A tag is a checksum over the ciphertext that detects tampering; checking it before decrypting is what "fails closed" means.
A KDF (key derivation function) turns some secret value into a
fixed-size key, usually by hashing it. When a challenge says the key
is sha256(secret)[:16], it means exactly that: hash the secret,
keep the first 16 bytes.
Three measurements appear in every analysis. Length narrows the field because hash outputs have fixed sizes. Entropy measures randomness in bits per character: English text sits low, hashes and ciphertext sit high. Charset records which symbols appear (hex digits only, Base64 alphabet, and so on). Confidence is the combined score as a percentage. Treat anything under roughly half as the tool thinking aloud rather than concluding.
First five minutes
Identify something. Any 32-character hex string works:
hashendra "5d41402abc4b2a76b9719d911017c592"Read the output top to bottom: what you gave it, how long it is, how random it looks, which character set it uses, then the ranked guesses. The top guess here is MD5 at 72 percent with its hashcat mode (0) and John format (raw-md5) attached.
Decode something. The string aGVsbG8= is Base64 for hello:
hashendra --decode "aGVsbG8="Hash something. The quotes keep the shell from splitting the words:
hashendra --hash sha256 "hello world"Crack something. Make a file called wordlist.txt with a few words
in it, one per line, then:
hashendra crack 5d41402abc4b2a76b9719d911017c592 -w wordlist.txt --format md5If one of your words is hello, the tool reports the password and
exits 0. If none match, it exits nonzero, which is how a script
tells the difference.
System map
The binary (main.rs, cli.rs, handlers/) dispatches to a library
of four module groups. Detection input flows through signatures and
scoring; files flow through inspection, carving, and disk parsing.
String input moves through scan, decode, crack, and recurse in order. File input is identified by type first, then inspected, carved, or disk-analyzed depending on what it is.
Detection engine
Signatures fall into four categories: hashes, encodings, ciphers, and file magic bytes.
A single input passes through preprocessing, pattern matching, scoring, and disambiguation before results are printed in rank order:
Each signature carries a regex and a confidence weight. The raw score
is the match multiplied by the weight, then adjusted: entropy fit,
charset consistency, semantic checks (does the Base64 actually
decode, is the JWT payload valid JSON), a penalty for repeated
characters, and context weighting. Known ambiguities carry explicit
penalties: 32-char hex could be MD5, NTLM, or MD4, so those share a
0.80 multiplier in the generic context; Base64 variants share 0.90
when co-detected; plain hex detection caps at 35 percent. A custom
signature file at ~/.hashendra/signatures.json merges with the
built-ins, and reusing a built-in name overrides it.
Security labels use four levels: Secure (Argon2, SHA-3, bcrypt with cost of 10 or more), Weak (SHA-1, low-cost PBKDF2), Broken (MD5, SHA-0), Insecure (DES, 40-bit RC4).
Carving pipeline
Carving scans a byte stream for file headers, determines slice boundaries, deduplicates, and writes results under a hard quota:
Boundary strategies in order of preference: a known end marker (PNG
IEND, JPEG EOI, ZIP central directory end), a length embedded in the
header (BMP, WAV, TIFF), a footer pattern, the next header of the same
type for streaming formats, and a configured maximum size for formats
with no boundaries. Custom profiles use a foremost-style config line
loaded with --config:
<extension> <needs_footer y/n> <max_size> <header_hex> [footer_hex] <description>Example:
jpg y 0 FFD8FF FFD9 JPEG Image
png y 0 89504E47 49454E44 PNG ImageThe 10 GiB extraction quota is compiled in and cannot be raised with flags.
How it works inside
The binary is a thin dispatch layer. main.rs parses arguments with
cli.rs, calls one handler per command group, and returns a process
exit code that is 0 only when every requested operation succeeded.
The handlers live in src/handlers/: analyze for single and batch
identification, decode for decoding and ROT/XOR cracking,
compute with compute_encrypt and compute_decrypt for hashing,
encoding, and ciphers, crack for wordlists, rsa for the four RSA
attacks, lookup for the TLS and EVM tables, scan, carve,
disk, and inspect for forensics, and workshop for the REPL.
All output goes through a thread-safe print macro in src/utils/
because scans run on multiple threads.
The shared library has three groups. src/core/ holds the engines:
the scanner measures entropy and charset and scores candidates, the
patterns module matches the compiled signature set, the recursive
engine unwraps layered encodings under rules that decide what counts
as plaintext and when to stop, and the cryptanalysis module scores
Caesar shifts and XOR candidates with chi-squared statistics against
English letter frequencies. Beside them sit the codec libraries (the
general encoder plus base32hex, base58check, base62, base91, Z85,
Crockford, UU, and XX), the hash implementations, the AES/RSA/HMAC
primitives, the streaming wordlist cracker, and the TLS and EVM
tables. src/detectors/ holds the signature data itself: hash
signatures split into digests, KDFs, application formats, and keys,
plus encoding, cipher, classical-cipher, and file-magic signatures.
src/forensics/ holds carving (profiles, scanning, container
expansion, config), disk layout and fingerprinting, NTFS/FAT/ext
parsers with recovery, per-format metadata inspectors, string
extraction, file typing, and report building.
Two properties fall out of this layout. Memory stays flat because files are memory mapped and streamed: wordlists flow through the cracker without loading, carve scans slide across mapped regions, and the rayon thread pools (eco, normal, turbo tiers) parallelize signature scanning without duplicating input. And nothing exceeds a small size budget: no source file runs over 500 lines, with test modules colocated next to their code so unit tests keep access to private items. The suite holds over 140 tests with zero clippy warnings as a standing gate, which is also enforced by the release pipeline before any binary is published.
Identifying input
$ hashendra "5d41402abc4b2a76b9719d911017c592"
[INPUT] : 5d41402abc4b2a76b9719d911017c592
[CONTEXT] : Generic
[LENGTH] : 32 characters
[ENTROPY] : 3.4803 bits/char
[CHARSET] : Hex
[CONFIDENCE] : [#######---] 72%
[SECURITY] : BROKEN
+-- DETECTION RESULTS -------------------------------------------+
| [i] MD5 72% [hashcat: 0] [john: raw-md5] |Input measurements (length, entropy, charset) come first, then ranked
candidates with hashcat modes and John formats for handoff to
dedicated crackers. The -j flag emits the same result as flat JSON
with confidence as a 0-1 float, and -f processes a file of inputs
line by line.
Decoding
Single-layer decoding reports each layer it unwraps:
$ hashendra --decode "aGVsbG8="
Layer 1: Decoded Base64 -> hello
[OK] Decoded 1 layers to: hello--deep-decrypt recurses until output stabilizes, a state repeats, or
the layer limit is reached. When plaintext is never reached, it
reports the stopping condition and the best candidate:
$ hashendra --deep-decrypt "NzI3Ng=="
[i] Stopped after 1 layer(s) without reaching clear plaintext.
[i] Best candidate so far: 7276The decimal codec accepts space, comma, or newline separated byte values from 0 to 255 and rejects anything outside that range, which keeps timestamps and version numbers from decoding into spurious output.
Computing hashes
$ hashendra --hash sha256 "hello world"
-- Hash Results --
SHA-256: b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9Available algorithms: MD5, SHA-1, the SHA-2 family, BLAKE3, and
HMAC-SHA-256/512 with a required --key. The --hex-input flag
hashes the hex-decoded bytes of the input instead of its text, for
cases where the hashed value is key material or a KDF input:
$ hashendra --hash sha256 --hex-input 29b035d9...3b041
-- Hash Results --
SHA-256: 700314a82871cac61d028f13a0d560b0d028e1e59698f9bf4ccf8853e2fb38e3Classical ciphers
Encryption and decryption share 27 cipher names and key formats:
caesar, atbash, vigenere, beaufort, autokey, gronsfeld, porta, affine, rail-fence, columnar, polybius, tap, adfgx, adfgvx, four-square, two-square, trifid, playfair, bifid, bacon, substitution, xor, aes-cbc, aes-ecb, aes-gcm, rsa, rsa-keygen.
$ hashendra --encrypt caesar --key 13 "hello"
-- Encrypted Output --
uryyb$ hashendra --encrypt affine --key "5,8" "hello"
-- Encrypted Output --
rclla$ hashendra --encrypt vigenere --key LEMON "ATTACKATDAWN"
-- Encrypted Output --
LXFOPVEFRNHR$ hashendra --encrypt beaufort --key LEMON "ATTACKATDAWN"
-- Encrypted Output --
LLTOLBETLNPR$ hashendra --encrypt polybius "HELLO"
-- Encrypted Output --
2315313134$ hashendra --encrypt bacon --key AB "HI"
-- Encrypted Output --
AABBBABAAAKey formats: shift numbers for Caesar, keywords for the Vigenere
family, a,b pairs for Affine, square and column keys for ADFGX,
K1,K2 for the four-square family, a period via --cipher-param
for Trifid and Bifid. Case is preserved and non-letters pass through
for the letter ciphers; digraph ciphers drop non-letters, merge J
into I, and pad odd lengths with X. ADFGX covers A-Z while ADFGVX
keeps A-Z0-9. --encrypt xor emits hex and --decrypt xor accepts
hex or raw bytes.
Keyless cracking covers Caesar shifts and single-byte XOR.
--rot ranks all shifts with chi-squared statistics:
$ hashendra --rot "Uryyb Jbeyq"
[ROT] Brute-forcing ROT for: Uryyb Jbeyq
* 16: Ebiil Tloia (chi2=24.0)
+ 24: Wtaad Ldgas (chi2=24.8)
+ 13: Hello World (chi2=28.4)
...--xor cracks single-byte XOR over hex input with flag-shaped
candidates ordered first:
$ hashendra --xor e39690dff69190fb90c6fd9191fbc890fd97d697e0fbcf97fdfbc097f695d290939594eafb9194c8f297c0d9
[XOR] Attempting single-byte XOR crack...
[as hex-decoded bytes]:
Key 0xa4 (Score 1.00): G24{R54_4bY55_l4Y3r3D_k3Y_d3R1v4710N_50lV3d}
Key 0xa1 (Score 1.00): B71~W01Z1g\00Zi1\6w6AZn6\Za6W4s1245KZ05iS6ax
...Modern cryptography
Symmetric keys, IVs, nonces, and RSA components are accepted as hex. AES-CBC and AES-ECB accept 16, 24, or 32 byte keys with a 16 byte IV for CBC. AES-GCM uses a 16 byte key with the 16 byte tag appended to the ciphertext. Decryption verifies the tag and reports an error on any mismatch; it never outputs unauthenticated plaintext.
GCM nonces are not restricted to 12 bytes. The standard length uses
the crate implementation and other lengths use the GHASH J0
derivation from NIST SP 800-38D, matching PyCryptodome behavior. Unit
tests carry PyCryptodome vectors for 8 and 16 byte nonces, including
the empty-plaintext tag a92142af17533472bd1b934c101379d2 under the
test key.
Textbook RSA encrypts small hex messages from n,e pairs, and
rsa-keygen generates practice keypairs. These implement raw modular
exponentiation for CTF mathematics. They are not padded encryption
and are unsuitable for protecting real data.
RSA attacks
hashendra rsa gcd <n1hex> <n2hex>
hashendra rsa wiener <nhex> <ehex>
hashendra rsa hastad <c1> <n1> <c2> <n2> <c3> <n3>
hashendra rsa fermat <nhex>The four attacks address shared primes across two moduli, small private exponents via continued fractions, e=3 broadcast across three moduli resolved with CRT and an integer cube root, and close primes factored outward from the integer square root:
$ hashendra rsa fermat E8D6CA6163
[OK] Fermat factored n:
p = 0f4261
q = 0f4243Unsuccessful attacks report failure explicitly instead of emitting plausible-looking factors.
Wordlist cracking
$ hashendra crack 5d41402abc4b2a76b9719d911017c592 -w wordlist.txt --format md5
[CRACK] MD5 against wordlist.txt (light rules, Normal speed)
[OK] Cracked in 0.0s (3 candidates, 3000 H/s)
password: helloThe wordlist is memory mapped and streamed, so candidate files of any
size process with flat memory usage. Mutation rules (--rules),
thread count (--jobs), candidate limits (--max-candidates), and
rayon speed tiers (eco, normal, turbo) are configurable. The
exit code is 0 only when a password is found.
Forensics
forensic scan reports file metadata, EXIF and format-specific
fields where applicable, string tables, and matched signatures:
$ hashendra forensic scan image.jpg
Running forensic scan on image.jpg...
[METADATA]
Path : image.jpg
...
[TRIAGE] entropy ... | ascii strings ... | hits ... | embedded ...forensic carve extracts embedded files (-t filters types,
--quick takes the first hit per profile, --dry-run reports
without writing, -M recurses into carved files):
hashendra forensic carve disk_image.dd -o carved/
hashendra forensic carve -t jpg,png,zip file.bin -o images/
hashendra forensic carve --matryoshka --depth 5 malware.bin -o extracted/forensic disk parses filesystems with deleted entry recovery:
hashendra forensic disk disk_image.dd
hashendra forensic disk --fs ntfs --deleted-only disk.dd
hashendra forensic disk --extract-data recovered/ --ntfs disk.dd
hashendra forensic disk --offset 1048576 --fs ext4 disk.ddSupported filesystems: NTFS (MFT parsing, resident and non-resident
attributes, data runs, deleted entry recovery, $Bitmap,
$LogFile), FAT12/16/32 (BPB parsing, FAT chain walking, VFAT long
names, deleted entry recovery), ext2/3/4 (superblock, block groups,
inode tables, extent trees, deleted inode recovery), and Btrfs
superblock parsing. Supported partition schemes: MBR (55 AA at
offset 510), GPT (EFI PART at 512), and APM. String extraction
covers ASCII and UTF-16LE with offsets, and --json produces
structured reports.
Metadata inspection covers JPEG EXIF (camera, exposure, GPS), PNG chunks, MP3 ID3 tags, FLAC comments, WAV format data, AVI and MP4 structure, OOXML document properties, ELF/PE/Mach-O headers, ZIP entries, GZIP fields, and SQLite headers.
Offline lookup tables
TLS cipher suite codes resolve with a security rating:
$ hashendra tls 1301
TLS suite 0x1301: TLS_AES_128_GCM_SHA256
rating: secure
note: TLS 1.3 defaultEVM function selectors resolve from a bare selector or full calldata:
$ hashendra evm a9059cbb
0xa9059cbb: transfer(address,uint256)
area: ERC20Interactive workshop
The workshop subcommand opens a REPL with slash-prefixed commands
for setting working text, loading files, running detection, decoding
one layer in 20-plus formats, shifting classical ciphers, running the
auto-unwrapper with /deep, and managing buffer state and history.
Type /help inside for the full table.
Solved case studies
Each case below is a G24 challenge solved with this tool, shown with
its artifacts and the exact commands. Flags are formatted G24{...}.
borrowed-bits: 72 lost prime bits
The bundle export lost the final 72 bits of one RSA prime. The surviving bundle:
N = 103037873330415603278106830450864635433844967643289606717227881310428721625182352763726356032198281354099470139861585416249604511120447664326371608216421246150945433507165864388685454437940043466469060477924053809238171164356522241742968567555347690826411636722922957955611303450334834450046295327453067160591
e = 65537
p_hi = 2622674901162262012341820645490501658910987916494391528788306130342626394660101422867229054367995691163717782379578367420828962420899
nonce = c75d81ff16a33faef8369b14fb1179cc
ciphertext = b447981e08d66be24bad4d8cfeddcb09ab828baeb44f
tag = 2d7808dc6bfc4a1002d2d165b5de36af
kdf = sha256(long_to_bytes(secret)).digest()[:16]N is 1024 bits and p_hi is 440 bits, so the prime has the form
p = p_hi * 2^72 + r with r < 2^72. The Coppersmith bound for a
linear polynomial admits unknowns below N^0.25 = 2^256, and 2^72 is
far inside it. Sage small_roots on f(x) = p_hi * 2^72 + x over
Zmod(N) returned one root:
[*] running small_roots...
[*] roots: [1668331181625054212329]
[OK] p = 12385232048712125645261275169613695088051937932262018085816873867723740682659046902750784757738383617493200045081969653379727133513358289624488289405645033
[OK] q = 8319414034808493011642047483779199793435606745157396495032057828192084358043402721956218172822021893118082880857855102214059864427069040325574313107855927The private exponent follows from d = pow(e, -1, (p-1)*(q-1)),
giving d starting 29b035d9 and ending 3b041. Trying d as the
KDF secret, hashing its decoded bytes:
$ hashendra --hash sha256 --hex-input 29b035d9...3b041
-- Hash Results --
SHA-256: 700314a82871cac61d028f13a0d560b0d028e1e59698f9bf4ccf8853e2fb38e3The first 16 bytes (700314a82871cac61d028f13a0d560b0) are the AES
key. Trying p or q as the secret produces keys the ciphertext
rejects at tag verification. Decrypting with the 16-byte nonce and
the concatenated ciphertext and tag:
$ hashendra --decrypt aes-gcm --key 700314a82871cac61d028f13a0d560b0 \
--cipher-param c75d81ff16a33faef8369b14fb1179cc \
b447981e08d66be24bad4d8cfeddcb09ab828baeb44f2d7808dc6bfc4a1002d2d165b5de36af
-- Decrypted Output --
G24{R5A2026BR0K3NB1T5}Flag: G24{R5A2026BR0K3NB1T5}. This challenge motivated the general
GCM nonce support and the --hex-input flag.
The ripple family: RSA as decoration
Four challenges (rsa-ripple, rsa-abyss, prime-vault, prime-trickle) share one modulus with close primes and similar stories about weak key generation:
n = 1000036000099, "close prime factors"Factoring closes the RSA question in milliseconds:
$ hashendra rsa fermat E8D6CA6163
[OK] Fermat factored n:
p = 0f4261
q = 0f4243The primes are 1000033 and 1000003, differing by 30. But every blob is far longer than the 5-byte modulus (38, 44, 34, and 40 bytes respectively), and RSA output cannot exceed its modulus, so none of the blobs are RSA ciphertext. The factored primes unlock nothing, which the size comparison predicts before any number theory.
All four blobs open with the same four bytes, e39690df, so they
share a keystream start. With flags shaped G24{...}, the opening
is a free crib: blob[:4] XOR G24{ gives a4a4a4a4, one repeating
byte. The blobs and their flags:
rsa-ripple (38 bytes):
e39690dff69190fbd695f4d4e897fbc290e79394d691fb90eac0fbef97ddfb9194e8d297e0d9
G24{R54_r1PpL3_f4C70r5_4Nd_K3y_50Lv3D}rsa-abyss (44 bytes):
e39690dff69190fb90c6fd9191fbc890fd97d697e0fbcf97fdfbc097f695d290939594eafb9194c8f297c0d9
G24{R54_4bY55_l4Y3r3D_k3Y_d3R1v4710N_50lV3d}prime-vault (34 bytes):
e39690dff4d695e997fbd290f1c893fbe290c79394f691fbd697e794d297f697c0d9
G24{Pr1M3_v4Ul7_F4c70R5_r3C0v3R3d}prime-trickle (40 bytes):
e39690dff4d695e997fb93d695e7cfe897fbcf97fdfbd697e794d297f6ddfbe794c9f4c8979397d9
G24{Pr1M3_7r1CkL3_k3Y_r3C0v3Ry_C0mPl373}Each decrypts under --xor with key 0xA4, listed first because
the cracker orders flag-shaped candidates at the top. The routine
that solves the set: compare ciphertext length against modulus size
first, compare blob prefixes across related inputs second, drag the
known-plaintext crib third.
number-drift: a missing codec
A damaged office note held one suspicious line:
Archive Note: batch-seventeen
Parsed values:
71 50 52 123 65 53 67 49 49 68 51 67 49 77 65 76 50 48 50 54 125The detector had no decimal ASCII codec and offered low-confidence
Base64 instead, while 71 50 52 123 reads G 2 4 { on sight. The
decimal codec was added with strict 0-255 validation and wired into
auto-detection behind a numeric-tokens gate:
$ hashendra --decode "71 50 52 123 65 53 67 49 49 68 51 67 49 77 65 76 50 48 50 54 125"
Layer 1: Decoded Decimal -> G24{A5C11D3C1MAL2026}
[OK] Decoded 1 layers to: G24{A5C11D3C1MAL2026}Flag: G24{A5C11D3C1MAL2026}. The challenge input is now a unit
test, so the gap cannot regress.
Limitations
HashEndra does not replace dedicated password crackers for expensive hashes, does not factor properly generated large moduli, and does not report conclusions beyond what the evidence supports. Low confidence scores and informational markers mean the result is uncertain and should be treated accordingly.