SKIP TO MAIN CONTENT

[ WIKI NODE / INTEL DISPATCH ]

KNOWLEDGE BASE & TECHNIQUE REGISTRY

SECURITY RESEARCH KNOWLEDGE TECHNIQUES
CATEGORY: InternalREVISIONS: 2

Hashendra Tool

LATEST CONTRIBUTIONS BY:
@bealthguy
LAST MODIFIED: 9/19/2026

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 | bash

Useful 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 md5

If 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.

graph TB CLI[CLI Entry: src/main.rs<br/>+ cli.rs, handlers/] LIB[lib.rs<br/>4 modules] subgraph core["core/"] SCANNER[scanner/<br/>mod + score,<br/>decode, codecs] PATTERNS[patterns.rs<br/>Signature scanning,<br/>confidence scoring] RECURSIVE[recursive_engine.rs<br/>+ engine_rules.rs<br/>Multi-layer unwrap] CRYPTANALYSIS[cryptanalysis.rs<br/>Chi-squared, freq analysis] ENCODER[encoder.rs<br/>+ basecodecs.rs<br/>Base64/32/58/85/91,<br/>hex, URL, HTML, morse] HASHER[hasher.rs<br/>MD5, SHA1/2/3, BLAKE3] ENTROPY[entropy.rs<br/>Rolling entropy,<br/>boundary detection] end subgraph detectors["detectors/"] HASHES[hashes/<br/>85 sigs: digests,<br/>kdf, apps, keys] ENCODINGS[encodings.rs<br/>28 encoding sigs] CIPHERS[ciphers.rs<br/>Classical cipher sigs] CLASSIC[classic_ciphers.rs<br/>+ classic_transposition.rs<br/>Auto-crack ciphers] STEGO[stego.rs<br/>File signatures,<br/>magic bytes, carving] end subgraph forensics["forensics/"] CARVE[carve/<br/>profiles, scan,<br/>containers, config] DISK[disk/<br/>layout, fingerprint] NTFS[ntfs/<br/>parse, recover, report] FAT[fat/<br/>parse, recover] EXT[ext/<br/>parse] INSPECT[inspect/<br/>images, media, audio,<br/>docs, exec, data] REPORT[report.rs<br/>Forensic report builder] DIRECTORY[directory.rs<br/>Recursive dir scan] STRINGS[strings.rs<br/>ASCII / UTF-16<br/>string extraction] FILETYPES[filetypes.rs<br/>Magic byte detection] end CLI --> LIB LIB --> core LIB --> detectors LIB --> forensics PATTERNS -.-> HASHES PATTERNS -.-> ENCODINGS PATTERNS -.-> CIPHERS SCANNER -.-> CLASSIC INSPECT -.-> STEGO CARVE -.-> STEGO CARVE -.-> INSPECT

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.

flowchart LR INPUT[Input Text / File] subgraph Analysis["Analysis Pipeline"] SCAN[Signature Scanner] DECODE[Decoders] CRACK[Cipher Crackers] RECURSE[Recursive Engine] end subgraph Forensic["Forensic Pipeline"] IDENTIFY[File Type ID] INSPECT[Metadata Inspect] CARVE[File Carving] DISK_ANALYSIS[Disk Analysis] end INPUT --> SCAN SCAN --> DECODE DECODE --> CRACK CRACK --> RECURSE FILE[File Input] --> IDENTIFY IDENTIFY --> INSPECT IDENTIFY --> CARVE IDENTIFY --> DISK_ANALYSIS DISK_ANALYSIS --> CARVE

Detection engine

Signatures fall into four categories: hashes, encodings, ciphers, and file magic bytes.

mindmap root((Detection Engine)) Hashes MD2/4/5 SHA-0/1/2/3 NTLM / LM bcrypt / scrypt PBKDF2 / Argon2 CRC / Adler Bitcoin / Ethereum HMAC / PBKDF2 Encodings Base16/32/58/64/85 Hex / Hex+ASCII URL Encoding JWT Binary / Octal Morse Code Punycode Ciphers Caesar / ROT Vigenere Affine Atbash XOR Stego / File JPEG / PNG / GIF PDF / ZIP / RAR ELF / PE / Mach-O NTFS / OLE2 SQLite

A single input passes through preprocessing, pattern matching, scoring, and disambiguation before results are printed in rank order:

sequenceDiagram participant U as User participant CLI as CLI participant P as patterns.rs participant S as scanner/ participant D as detectors/ U->>CLI: hashendra <input> CLI->>P: scan_input(input, context) P->>D: match signatures D-->>P: detection results P->>S: analyze context, entropy, charset S-->>CLI: ranked results CLI->>U: formatted output

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:

flowchart TB DATA[Raw Bytes] subgraph Scan["Concurrent Signature Scan (Rayon)"] PROFILES[19 CarveProfiles] HEADER[Header matching] end subgraph Slice["Slice Determination"] KNOWN[Known end marker] LENGTH[Length-hint from header] FOOTER[Footer pattern] NEXT[Next match boundary] MAX[Max size cap] end subgraph Output["Output Pipeline"] DEDUP[BLAKE3 Deduplication] WRITE[Write to disk] INSPECT_META[Metadata inspection] QUOTA[10 GiB hard quota] end DATA --> PROFILES PROFILES --> HEADER HEADER --> KNOWN KNOWN --> LENGTH LENGTH --> FOOTER FOOTER --> NEXT NEXT --> MAX MAX --> DEDUP DEDUP --> INSPECT_META INSPECT_META --> WRITE WRITE --> 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 Image

The 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: 7276

The 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:   b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

Available 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:   700314a82871cac61d028f13a0d560b0d028e1e59698f9bf4ccf8853e2fb38e3

Classical 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 --
AABBBABAAA

Key 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 = 0f4243

Unsuccessful 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: hello

The 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.dd

Supported 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 default

EVM function selectors resolve from a bare selector or full calldata:

$ hashendra evm a9059cbb
0xa9059cbb: transfer(address,uint256)
  area: ERC20

Interactive 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 = 8319414034808493011642047483779199793435606745157396495032057828192084358043402721956218172822021893118082880857855102214059864427069040325574313107855927

The 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:   700314a82871cac61d028f13a0d560b0d028e1e59698f9bf4ccf8853e2fb38e3

The 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 = 0f4243

The 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 125

The 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.

REVISION JOURNAL

@bealthguyEdited article version increment
9/19/2026 - 9:28:22 AM
@bealthguyTooling
9/19/2026 - 9:08:47 AM