home intel cve-2026-42482-hashcat-hex-mangle-stack-overflow
CVE Analysis 2026-05-01 · 8 min read

CVE-2026-42482: Stack Overflow in hashcat's Hex Mangle Functions

A bounds check in hashcat's mangle_to_hex_lower/upper fails to account for 2x byte expansion during hex encoding, enabling stack corruption via crafted rules or long password candidates.

#buffer-overflow#stack-based#hashcat#rule-parsing#remote-code-execution
Technical mode — for security professionals
▶ Attack flow — CVE-2026-42482 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-42482Cross-platform · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-42482 is a stack-based buffer overflow in hashcat v7.1.2 affecting two rule functions: mangle_to_hex_lower() and mangle_to_hex_upper(), both implemented in src/rp_cpu.c. The functions convert raw password bytes into their two-character hexadecimal ASCII representation — a transformation that doubles the output length. The bug is a classic expansion overflow: the output buffer is allocated based on the input length, not the expanded output length, so any password candidate of 128 bytes or more will write beyond the end of a stack-allocated buffer.

The vulnerability is reachable via two distinct attack surfaces: a crafted rule file processed at startup, or the -j / -k rule options passed directly on the command line. Because hashcat is frequently deployed in automated pipeline tooling and cloud cracking infrastructure, a malicious wordlist combined with a rule file is a credible remote trigger. CVSS 9.8 reflects unauthenticated, network-adjacent exploitability in those deployment models.

Root cause: mangle_to_hex_lower() and mangle_to_hex_upper() allocate a fixed-size stack buffer sized to RP_PASSWORD_SIZE (128 bytes) without doubling the capacity to accommodate hexadecimal expansion, allowing a 128-character input to write 256 bytes and overflow the buffer by 128 bytes.

Affected Component

File: src/rp_cpu.c — the CPU-side rule processor. This module applies transformation rules to each password candidate before the hash comparison step. The mangle_to_hex_lower() rule corresponds to rule opcode B (lowercase hex encode) and mangle_to_hex_upper() to opcode X (uppercase hex encode) in hashcat's rule engine. Both share the same structural flaw.

The rule processing loop in rp_apply_rules_cpu() operates on a local stack buffer of RP_PASSWORD_SIZE bytes (defined as 128 in include/rp.h). This same buffer is passed into the mangle functions, which write their output back into it in-place.

Root Cause Analysis


// include/rp.h
#define RP_PASSWORD_SIZE 128

// src/rp_cpu.c — simplified decompiled pseudocode

int mangle_to_hex_lower(char *in, int in_len)
{
    // BUG: 'in' points to a RP_PASSWORD_SIZE (128-byte) stack buffer.
    // Each input byte expands to 2 hex chars: max safe input is 64 bytes.
    // No check that (in_len * 2) fits within RP_PASSWORD_SIZE.

    char tmp[RP_PASSWORD_SIZE];   // second 128-byte stack buffer for swap
    int  out_len = 0;

    for (int i = 0; i < in_len; i++)
    {
        unsigned char byte = (unsigned char) in[i];

        // Writes 2 bytes per iteration into tmp[out_len]
        tmp[out_len++] = hex_lower[(byte >> 4) & 0xf];  // high nibble
        tmp[out_len++] = hex_lower[(byte     ) & 0xf];  // low nibble

        // BUG: out_len is never bounds-checked against RP_PASSWORD_SIZE.
        // When in_len == 128, out_len reaches 256 — 128 bytes past tmp[].
    }

    memcpy(in, tmp, out_len);    // BUG: copies oversized out_len back into
                                  // the same 128-byte caller stack buffer.
    return out_len;
}

int mangle_to_hex_upper(char *in, int in_len)
{
    char tmp[RP_PASSWORD_SIZE];   // identical layout, same overflow
    int  out_len = 0;

    for (int i = 0; i < in_len; i++)
    {
        unsigned char byte = (unsigned char) in[i];

        tmp[out_len++] = hex_upper[(byte >> 4) & 0xf];
        tmp[out_len++] = hex_upper[(byte     ) & 0xf];

        // BUG: same missing bounds check — out_len unconstrained
    }

    memcpy(in, tmp, out_len);    // BUG: overflows caller's 128-byte buffer
    return out_len;
}

// Caller context — rp_apply_rules_cpu() (src/rp_cpu.c)
int rp_apply_rules_cpu(const char *rule_buf, const int rule_len,
                        char *in, const int in_len)
{
    char out[RP_PASSWORD_SIZE];          // 128 bytes on stack
    int  out_len = in_len;

    memcpy(out, in, in_len);

    for (int i = 0; i < rule_len; /* ... */)
    {
        char rule_cmd = rule_buf[i];

        switch (rule_cmd)
        {
            /* ... */
            case RULE_OP_MANGLE_TO_HEX_LOWER:   // opcode 'B'
                out_len = mangle_to_hex_lower(out, out_len);
                // 'out' is 128 bytes; if out_len was 128, mangle writes 256.
                break;

            case RULE_OP_MANGLE_TO_HEX_UPPER:   // opcode 'X'
                out_len = mangle_to_hex_upper(out, out_len);
                break;
            /* ... */
        }
    }
    return out_len;
}

The tmp[] buffer inside each mangle function overflows first (intra-function), then the oversized memcpy back into the caller's out[] buffer causes a second overflow in the caller's frame. On a 64-bit Linux build with GCC default stack layout, both overflows are live during a single rule application pass.

Memory Layout


// Approximate stack layout for mangle_to_hex_lower() frame (x86-64, no canary)
// Growing downward, higher addresses at top

struct mangle_frame {
    /* -0x000 */ char     tmp[128];         // RP_PASSWORD_SIZE — output buffer
    /* -0x080 */ int      out_len;          // expansion counter
    /* -0x084 */ int      i;               // loop counter
    /* -0x088 */ char    *in;              // pointer to caller's 'out[]'
    /* -0x090 */ int      in_len;
    /* -0x098 */ uint64_t saved_rbp;
    /* -0x0a0 */ uint64_t saved_rip;       // <-- corruption target
};

STACK STATE BEFORE OVERFLOW (in_len = 128):
  [ mangle_to_hex_lower frame                        ]
  [ tmp[0..127]     : 128-byte output buffer @ rbp-0x80 ]
  [ out_len         : 0x00000000              @ rbp-0x04 ]
  [ saved_rbp       : caller frame pointer    @ rbp+0x00 ]
  [ saved_rip       : return address          @ rbp+0x08 ]

STACK STATE DURING LOOP — iteration i=64 (out_len = 128):
  [ tmp[0..127]     : FULL — 128 hex chars written      ]
  [ out_len         : 0x00000080 (128)                  ]
  -- next write: tmp[128] = hex_lower[...] --           <-- OVERFLOW STARTS

STACK STATE AFTER LOOP — iteration i=128 (out_len = 256):
  [ tmp[0..127]     : valid hex output                  ]
  [ out_len         : CORRUPTED (overlaps tmp[128..131]) ]
  [ saved_rbp       : CORRUPTED (overlaps tmp[136..143]) ]
  [ saved_rip       : CORRUPTED (overlaps tmp[144..151]) ]
                       ^^ attacker controls in[] content
                          => controls hex nibble pairs
                          => controls every byte of overflow region

Because the overflow bytes are the hex-encoded representation of attacker-supplied input characters, each two-byte hex pair is constrained to printable ASCII in [0-9a-f] (lower) or [0-9A-F] (upper). The saved RIP overwrite at offset +0x90 from tmp[0] requires 9 full input bytes past the 64-byte safe boundary — trivially satisfiable with a 128-byte password candidate. A BROP-style info-leak or known binary base (no PIE) removes the constraint entirely.

Exploitation Mechanics


EXPLOIT CHAIN (rule file delivery, Linux x86-64, no PIE, no stack canary):

1. Craft password candidate of exactly 128 bytes.
   Bytes [0..63]  : arbitrary (populate valid hex region of tmp[])
   Bytes [64..71] : encode desired saved_rbp value as 8 input bytes
                    (each input byte B produces hex pair "XY" in tmp[])
                    => select B such that hex(B) encodes target nibbles
   Bytes [72..79] : encode desired saved_rip / ROP gadget address
                    (same nibble-selection technique)
   Bytes [80..127]: padding to reach in_len == 128

2. Write rule file containing single rule line: "B"  (or "X" for upper)
   Deliver via filesystem or stdin pipe in automated cracking pipeline.

3. hashcat loads rule, begins candidate processing.
   rp_apply_rules_cpu() copies candidate into stack 'out[128]'.
   Dispatches to mangle_to_hex_lower(out, 128).

4. Loop runs 128 iterations. tmp[128..255] written — overflows into
   saved_rbp and saved_rip on the mangle frame.
   memcpy(in, tmp, 256) then overflows the caller's 'out[128]' frame.

5. mangle_to_hex_lower() returns. Corrupted saved_rip is loaded into RIP.

6. Execution redirected to:
   - DoS: NULL / invalid address => SIGSEGV, process crash
   - RCE: ROP chain pivot => mprotect() => shellcode (no-PIE scenario)
       or ret2libc => system("/bin/sh") via gadget chain

CONSTRAINTS:
  - Output bytes restricted to [0-9a-f] / [0-9A-F] — partial address control
  - Stack canary (default GCC/Clang builds) mitigates saved_rip overwrite;
    canary leak required for full RCE on hardened builds
  - ASLR + PIE: info-leak primitive required for gadget address resolution
  - DoS (crash) is unconditional and requires no bypass

Patch Analysis


// BEFORE (vulnerable — src/rp_cpu.c, hashcat v7.1.2):

#define RP_PASSWORD_SIZE 128

int mangle_to_hex_lower(char *in, int in_len)
{
    char tmp[RP_PASSWORD_SIZE];     // fixed 128-byte buffer
    int  out_len = 0;

    for (int i = 0; i < in_len; i++)
    {
        unsigned char byte = (unsigned char) in[i];
        tmp[out_len++] = hex_lower[(byte >> 4) & 0xf];
        tmp[out_len++] = hex_lower[(byte     ) & 0xf];
        // BUG: no check: if (out_len + 2 > RP_PASSWORD_SIZE) break;
    }

    memcpy(in, tmp, out_len);   // BUG: out_len may be up to 256
    return out_len;
}


// AFTER (patched):

// Option A — enlarge buffer to hold maximum expanded output:
#define RP_PASSWORD_SIZE      128
#define RP_PASSWORD_SIZE_HEX  (RP_PASSWORD_SIZE * 2)   // 256

int mangle_to_hex_lower(char *in, int in_len)
{
    char tmp[RP_PASSWORD_SIZE_HEX];  // 256-byte buffer — accommodates 2x expansion
    int  out_len = 0;

    for (int i = 0; i < in_len; i++)
    {
        // Bounds check: stop if next two bytes would overflow
        if (out_len + 2 > RP_PASSWORD_SIZE_HEX) break;   // FIX: explicit cap

        unsigned char byte = (unsigned char) in[i];
        tmp[out_len++] = hex_lower[(byte >> 4) & 0xf];
        tmp[out_len++] = hex_lower[(byte     ) & 0xf];
    }

    // out_len now guaranteed <= RP_PASSWORD_SIZE_HEX
    // caller's buffer must also be enlarged to RP_PASSWORD_SIZE_HEX
    memcpy(in, tmp, out_len);
    return out_len;
}

// Option B — truncate input at half capacity before processing:
int mangle_to_hex_lower(char *in, int in_len)
{
    char tmp[RP_PASSWORD_SIZE];
    int  safe_len = in_len > (RP_PASSWORD_SIZE / 2)   // FIX: cap input at 64
                  ? (RP_PASSWORD_SIZE / 2)
                  : in_len;
    int  out_len  = 0;

    for (int i = 0; i < safe_len; i++)
    {
        unsigned char byte = (unsigned char) in[i];
        tmp[out_len++] = hex_lower[(byte >> 4) & 0xf];
        tmp[out_len++] = hex_lower[(byte     ) & 0xf];
    }

    memcpy(in, tmp, out_len);
    return out_len;
}

// NOTE: Option A preserves rule semantics for long passwords.
// Option B silently truncates — functionally incorrect for cracking.
// Correct fix also requires rp_apply_rules_cpu()'s local 'out[]' buffer
// to be enlarged to RP_PASSWORD_SIZE_HEX to prevent the second overflow.

Detection and Indicators

Crash signature: SIGSEGV or SIGABRT in mangle_to_hex_lower / mangle_to_hex_upper with rsp pointing into stack red zone or a stack corruption detected by GCC's __stack_chk_fail symbol.


INDICATIVE CRASH OUTPUT (AddressSanitizer build):

==ASAN: stack-buffer-overflow on address 0x[...] at pc 0x[...]
WRITE of size 1 at offset 128 in frame:
  #0  mangle_to_hex_lower  src/rp_cpu.c:1042
  #1  rp_apply_rules_cpu   src/rp_cpu.c:1387
  #2  process_rule_main    src/rp_cpu.c:1621

'tmp' (128 bytes) is located at frame offset 0 in frame for mangle_to_hex_lower
  [0, 128) 'tmp'   <== Memory access at offset 128 overflows this variable

GCC stack protector (hardened build):
*** stack smashing detected ***: hashcat terminated
Aborted (core dumped)

Static detection: Grep src/rp_cpu.c for char tmp[RP_PASSWORD_SIZE] inside mangle_to_hex functions combined with an unbounded write loop incrementing by 2. Any memcpy(in, tmp, out_len) where out_len is not capped to the destination size is the finding.

Rule-based detection: Monitor hashcat invocations where -j or -k arguments contain the rule characters B or X and the wordlist contains entries of 128 bytes or longer. Log and alert on --rule-buf-l / --rule-buf-r with those opcodes in automated infrastructure.

Remediation

Immediate: Apply the upstream patch when released. Until then, build hashcat with -fstack-protector-strong and -fsanitize=address in test environments to convert silent corruption into loud crashes. In production pipelines, pre-filter wordlists to reject candidates exceeding 63 bytes before applying B or X rules, or strip those rule opcodes from all rule files.

Structural fix required in two places:

  • Enlarge tmp[] inside mangle_to_hex_lower() and mangle_to_hex_upper() to RP_PASSWORD_SIZE * 2.
  • Enlarge the out[] buffer in rp_apply_rules_cpu() to the same doubled size, since it receives the expanded result via memcpy.
  • Add an explicit cap: if (out_len + 2 > buf_capacity) break; inside each mangle loop as a defense-in-depth guard.

Deployment hardening: Enable RELRO, PIE, and stack canaries in all distributed hashcat binaries. These mitigations raise the exploitation bar from trivial stack smash to requiring an independent info-leak primitive.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →