QNX 7 Password Hash Analysis and Writing a Hashcat Module

Thursday, September 14, 2023 🌐中文

Preface

Back in 2021, while attempting to crack QNX hashes, I discovered that Hashcat lacked support for QNX 6.6.0. Although there was an existing issue requesting this feature, I was too occupied to implement it at the time.

It wasn’t until the grueling “solo” phase of the MIIT vehicle-to-everything (V2X) offensive/defensive exercise in September 2023 that I encountered QNX 7 hashes. Surprisingly, after all these years, support was still missing.

To avoid facing this same roadblock in the future, I dedicated some time to studying the QNX libraries and Hashcat’s architecture. I also took a few detours along the way while developing the Hashcat module.

qnxhash.png

QNX /etc/shadow hash algorithm analysis

According to the official documentation QNX 700 docs: accounts_etc_shadow, the hash format is:

@digest@hash@salt
@digest,iterations@hash@salt

The hash uses @ as a delimiter. Uppercase S indicates SHA-512, while lowercase s refers to SHA-256. If a comma followed by a number is present, it specifies the iteration count. This is followed by the Base64-encoded hash and the Base64-encoded salt.

Reading the official documentation, it seemed deceptively simple. My initial thought, looking at Hashcat’s QNX 6 module (-m 19200), was whether I could simply adjust the iteration count and format.

I attempted to clone the QNX 6 module to minimize effort, but after adapting it to match the documented format, the Hashcat self-tests repeatedly failed. Analyzing /src/OpenCL/m19200.cl, I noticed it referenced code from John the Ripper and contained some inelegant hacks. Suspecting bugs in the OpenCL code, I decided to analyze how QNX 7 actually generates its hashes.

The hash generation logic is in /usr/lib/pam.qnx.so.

After analyzing the logic and comparing it with m19200.cl, I discovered the issue: the round logic in the old OpenCL code was incorrect. The first round performed a SHA calculation on salt || password, but the resulting digest wasn’t XORed with the previous result as expected. It wasn’t just a simple Base64 encoding issue.

pam_qnx.png

In reality, the new hash generation logic employs the standard PBKDF2 (Password-Based Key Derivation Function) algorithm, widely used by products like Adobe, macOS, and Cisco. While higher iteration counts reduce cracking efficiency, they improve security. QNX’s documentation vague reference to only “SHA-512” without explicitly mentioning PBKDF2 somewhat obscures the implementation details for security researchers.

PBKDF2

You can verify it with CyberChef:

https://gchq.github.io/CyberChef/#recipe=Derive_PBKDF2_key(%7B'option':'UTF8','string':'hashcat'%7D,512,4096,'SHA512',%7B'option':'Base64','string':'NDY2MDEwNjk3YjBjYzM2MzliMzc3Mzc0ZTNiMTAzNzE%3D'%7D)&input=dm0ybkJHSGVzNlFrWHJhMGY3NFhtb3VTaVJ6allEM3IvMHB5K3R4djBLcjhBNGhDUE1HRkhvWnFyNDFKRmlZY0pQUE9lSWhlcUZzZU15THl3LzE1UHc9PQ

Writing a Hashcat module

Refer to the official documentation:

https://github.com/hashcat/hashcat/blob/master/docs/hashcat-plugin-development-guide.md

Initially, I considered writing new OpenCL code. However, once I realized it was a standard algorithm (PBKDF2), I abandoned that idea and looked for a suitable existing module to modify.

./hashcat -hh | grep PBKDF2
  11900 | PBKDF2-HMAC-MD5                                            | Generic KDF
  12000 | PBKDF2-HMAC-SHA1                                           | Generic KDF
  10900 | PBKDF2-HMAC-SHA256                                         | Generic KDF
  12100 | PBKDF2-HMAC-SHA512                                         | Generic KDF
   2500 | WPA-EAPOL-PBKDF2                                           | Network Protocol
  22000 | WPA-PBKDF2-PMKID+EAPOL                                     | Network Protocol
  16800 | WPA-PMKID-PBKDF2                                           | Network Protocol
  12800 | MS-AzureSync PBKDF2-HMAC-SHA256                            | Operating System
   9200 | Cisco-IOS $8$ (PBKDF2-SHA256)                              | Operating System
   7100 | macOS v10.8+ (PBKDF2-SHA512)                               | Operating System

Hashcat runs self-tests on every module at startup, using ST_HASH and ST_PASS as test vectors. I set KERN_TYPE to 7100 and prepared to debug iteratively—but to my surprise, it worked on the first attempt.

Since Hashcat is open-source and the pull request merge process can be lengthy, I have included the source code below.

/**
 * Author......: See docs/credits.txt
 * License.....: MIT
 */

#include "common.h"
#include "types.h"
#include "modules.h"
#include "bitops.h"
#include "convert.h"
#include "shared.h"
#include "emu_inc_hash_sha512.h"
#include "memory.h"

static const u32   ATTACK_EXEC    = ATTACK_EXEC_OUTSIDE_KERNEL;
static const u32   DGST_POS0      = 0;
static const u32   DGST_POS1      = 1;
static const u32   DGST_POS2      = 2;
static const u32   DGST_POS3      = 3;
static const u32   DGST_SIZE      = DGST_SIZE_8_16;
static const u32   HASH_CATEGORY  = HASH_CATEGORY_OS;
static const char *HASH_NAME      = "QNX 7 /etc/shadow (SHA512)";
static const u64   KERN_TYPE      = 7100;
static const u32   OPTI_TYPE      = OPTI_TYPE_ZERO_BYTE
                                  | OPTI_TYPE_USES_BITS_64
                                  | OPTI_TYPE_SLOW_HASH_SIMD_LOOP;
static const u64   OPTS_TYPE      = OPTS_TYPE_STOCK_MODULE
                                  | OPTS_TYPE_PT_GENERATE_LE
                                  | OPTS_TYPE_ST_BASE64
                                  | OPTS_TYPE_HASH_COPY;
static const u32   SALT_TYPE      = SALT_TYPE_EMBEDDED;
static const char *ST_PASS        = "hashcat";
static const char *ST_HASH        = "@S@vm2nBGHes6QkXra0f74XmouSiRzjYD3r/0py+txv0Kr8A4hCPMGFHoZqr41JFiYcJPPOeIheqFseMyLyw/15Pw==@NDY2MDEwNjk3YjBjYzM2MzliMzc3Mzc0ZTNiMTAzNzE=";

u32         module_attack_exec    (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return ATTACK_EXEC;     }
u32         module_dgst_pos0      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_POS0;       }
u32         module_dgst_pos1      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_POS1;       }
u32         module_dgst_pos2      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_POS2;       }
u32         module_dgst_pos3      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_POS3;       }
u32         module_dgst_size      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_SIZE;       }
u32         module_hash_category  (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return HASH_CATEGORY;   }
const char *module_hash_name      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return HASH_NAME;       }
u64         module_kern_type      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return KERN_TYPE;       }
u32         module_opti_type      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return OPTI_TYPE;       }
u64         module_opts_type      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return OPTS_TYPE;       }
u32         module_salt_type      (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return SALT_TYPE;       }
const char *module_st_hash        (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return ST_HASH;         }
const char *module_st_pass        (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return ST_PASS;         }

typedef struct pbkdf2_sha512
{
  u32 salt_buf[64];

} pbkdf2_sha512_t;

typedef struct pbkdf2_sha512_tmp
{
  u64  ipad[8];
  u64  opad[8];

  u64  dgst[16];
  u64  out[16];

} pbkdf2_sha512_tmp_t;

u64 module_esalt_size (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra)
{
  const u64 esalt_size = (const u64) sizeof (pbkdf2_sha512_t);

  return esalt_size;
}

u64 module_tmp_size (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra)
{
  const u64 tmp_size = (const u64) sizeof (pbkdf2_sha512_tmp_t);

  return tmp_size;
}

static const int ROUNDS_QNX = 4096;
static const int HASH_SIZE = 64;

int module_hash_decode (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED void *digest_buf, MAYBE_UNUSED salt_t *salt, MAYBE_UNUSED void *esalt_buf, MAYBE_UNUSED void *hook_salt_buf, MAYBE_UNUSED hashinfo_t *hash_info, const char *line_buf, MAYBE_UNUSED const int line_len)
{
  u64 *digest = (u64 *) digest_buf;
    
  pbkdf2_sha512_t *pbkdf2_sha512 = (pbkdf2_sha512_t *) esalt_buf;

  hc_token_t token;

  memset (&token, 0, sizeof (hc_token_t));

  token.token_cnt  = 4;

  // @digest@hash@salt
  // @digest,iterations@hash@salt
  
  token.sep[0]     = '@';
  token.len[0]     = 0;
  token.attr[0]    = TOKEN_ATTR_FIXED_LENGTH;

  token.sep[1]     = '@';
  token.len_min[1] = 1;
  token.len_max[1] = 8;
  token.attr[1]    = TOKEN_ATTR_VERIFY_LENGTH;

  token.sep[2]     = '@';
  token.len_min[2] = 64;
  token.len_max[2] = 100;
  token.attr[2]    = TOKEN_ATTR_VERIFY_LENGTH | TOKEN_ATTR_VERIFY_BASE64A;

  token.sep[3]     = '@';
  token.len_min[3] = 32;
  token.len_max[3] = 60;
  token.attr[3]    = TOKEN_ATTR_VERIFY_LENGTH | TOKEN_ATTR_VERIFY_BASE64A;

  const int rc_tokenizer = input_tokenizer ((const u8 *) line_buf, line_len, &token);

  if (rc_tokenizer != PARSER_OK) return (rc_tokenizer);

  // check hash type

  if (token.buf[1][0] != 'S') return (PARSER_SIGNATURE_UNMATCHED);

  // check iter

  u32 iter = ROUNDS_QNX;

  if (token.len[1] > 1)
  {
    if (token.buf[1][1] != ',') return (PARSER_SEPARATOR_UNMATCHED);

    iter = hc_strtoul ((const char *) token.buf[1] + 2, NULL, 10);
  }

  // iter++; the additional round is added in the init kernel

  salt->salt_iter = iter - 1;

  const u8 *hash_pos = token.buf[2];
  const int hash_len = token.len[2];

  int decoded_len;
  u8 tmp_buf[512];
  memset (tmp_buf, 0, sizeof (tmp_buf));

  decoded_len = base64_decode (base64_to_int, hash_pos, hash_len, tmp_buf);
  if (decoded_len != HASH_SIZE) {
    return (PARSER_SALT_LENGTH);
  }
  memcpy (digest, tmp_buf, 64);
  digest[0] = byte_swap_64 (digest[0]);
  digest[1] = byte_swap_64 (digest[1]);
  digest[2] = byte_swap_64 (digest[2]);
  digest[3] = byte_swap_64 (digest[3]);
  digest[4] = byte_swap_64 (digest[4]);
  digest[5] = byte_swap_64 (digest[5]);
  digest[6] = byte_swap_64 (digest[6]);
  digest[7] = byte_swap_64 (digest[7]);

  // salt

  const u8 *salt_pos = token.buf[3];
  const int salt_len = token.len[3];

  memset (tmp_buf, 0, sizeof (tmp_buf));

  decoded_len = base64_decode (base64_to_int, salt_pos, salt_len, tmp_buf);

  if (decoded_len < 1) {
    return (PARSER_SALT_LENGTH);
  }
  else
  {
    memcpy (pbkdf2_sha512->salt_buf, tmp_buf, decoded_len);
    salt->salt_buf[0] = pbkdf2_sha512->salt_buf[0];
    salt->salt_buf[1] = pbkdf2_sha512->salt_buf[1];
    salt->salt_buf[2] = pbkdf2_sha512->salt_buf[2];
    salt->salt_buf[3] = pbkdf2_sha512->salt_buf[3];
    salt->salt_buf[4] = pbkdf2_sha512->salt_buf[4];
    salt->salt_buf[5] = pbkdf2_sha512->salt_buf[5];
    salt->salt_buf[6] = pbkdf2_sha512->salt_buf[6];
    salt->salt_buf[7] = pbkdf2_sha512->salt_buf[7];
    salt->salt_len = decoded_len;
  }
  return (PARSER_OK);
}

int module_hash_encode (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const void *digest_buf, MAYBE_UNUSED const salt_t *salt, MAYBE_UNUSED const void *esalt_buf, MAYBE_UNUSED const void *hook_salt_buf, MAYBE_UNUSED const hashinfo_t *hash_info, char *line_buf, MAYBE_UNUSED const int line_size)
{
  return snprintf (line_buf, line_size, "%s", hash_info->orighash);
}

void module_init (module_ctx_t *module_ctx)
{
  module_ctx->module_context_size             = MODULE_CONTEXT_SIZE_CURRENT;
  module_ctx->module_interface_version        = MODULE_INTERFACE_VERSION_CURRENT;

  module_ctx->module_attack_exec              = module_attack_exec;
  module_ctx->module_benchmark_esalt          = MODULE_DEFAULT;
  module_ctx->module_benchmark_hook_salt      = MODULE_DEFAULT;
  module_ctx->module_benchmark_mask           = MODULE_DEFAULT;
  module_ctx->module_benchmark_charset        = MODULE_DEFAULT;
  module_ctx->module_benchmark_salt           = MODULE_DEFAULT;
  module_ctx->module_build_plain_postprocess  = MODULE_DEFAULT;
  module_ctx->module_deep_comp_kernel         = MODULE_DEFAULT;
  module_ctx->module_deprecated_notice        = MODULE_DEFAULT;
  module_ctx->module_dgst_pos0                = module_dgst_pos0;
  module_ctx->module_dgst_pos1                = module_dgst_pos1;
  module_ctx->module_dgst_pos2                = module_dgst_pos2;
  module_ctx->module_dgst_pos3                = module_dgst_pos3;
  module_ctx->module_dgst_size                = module_dgst_size;
  module_ctx->module_dictstat_disable         = MODULE_DEFAULT;
  module_ctx->module_esalt_size               = module_esalt_size;
  module_ctx->module_extra_buffer_size        = MODULE_DEFAULT;
  module_ctx->module_extra_tmp_size           = MODULE_DEFAULT;
  module_ctx->module_extra_tuningdb_block     = MODULE_DEFAULT;
  module_ctx->module_forced_outfile_format    = MODULE_DEFAULT;
  module_ctx->module_hash_binary_count        = MODULE_DEFAULT;
  module_ctx->module_hash_binary_parse        = MODULE_DEFAULT;
  module_ctx->module_hash_binary_save         = MODULE_DEFAULT;
  module_ctx->module_hash_decode_postprocess  = MODULE_DEFAULT;
  module_ctx->module_hash_decode_potfile      = MODULE_DEFAULT;
  module_ctx->module_hash_decode_zero_hash    = MODULE_DEFAULT;
  module_ctx->module_hash_decode              = module_hash_decode;
  module_ctx->module_hash_encode_status       = MODULE_DEFAULT;
  module_ctx->module_hash_encode_potfile      = MODULE_DEFAULT;
  module_ctx->module_hash_encode              = module_hash_encode;
  module_ctx->module_hash_init_selftest       = MODULE_DEFAULT;
  module_ctx->module_hash_mode                = MODULE_DEFAULT;
  module_ctx->module_hash_category            = module_hash_category;
  module_ctx->module_hash_name                = module_hash_name;
  module_ctx->module_hashes_count_min         = MODULE_DEFAULT;
  module_ctx->module_hashes_count_max         = MODULE_DEFAULT;
  module_ctx->module_hlfmt_disable            = MODULE_DEFAULT;
  module_ctx->module_hook_extra_param_size    = MODULE_DEFAULT;
  module_ctx->module_hook_extra_param_init    = MODULE_DEFAULT;
  module_ctx->module_hook_extra_param_term    = MODULE_DEFAULT;
  module_ctx->module_hook12                   = MODULE_DEFAULT;
  module_ctx->module_hook23                   = MODULE_DEFAULT;
  module_ctx->module_hook_salt_size           = MODULE_DEFAULT;
  module_ctx->module_hook_size                = MODULE_DEFAULT;
  module_ctx->module_jit_build_options        = MODULE_DEFAULT;
  module_ctx->module_jit_cache_disable        = MODULE_DEFAULT;
  module_ctx->module_kernel_accel_max         = MODULE_DEFAULT;
  module_ctx->module_kernel_accel_min         = MODULE_DEFAULT;
  module_ctx->module_kernel_loops_max         = MODULE_DEFAULT;
  module_ctx->module_kernel_loops_min         = MODULE_DEFAULT;
  module_ctx->module_kernel_threads_max       = MODULE_DEFAULT;
  module_ctx->module_kernel_threads_min       = MODULE_DEFAULT;
  module_ctx->module_kern_type                = module_kern_type;
  module_ctx->module_kern_type_dynamic        = MODULE_DEFAULT;
  module_ctx->module_opti_type                = module_opti_type;
  module_ctx->module_opts_type                = module_opts_type;
  module_ctx->module_outfile_check_disable    = MODULE_DEFAULT;
  module_ctx->module_outfile_check_nocomp     = MODULE_DEFAULT;
  module_ctx->module_potfile_custom_check     = MODULE_DEFAULT;
  module_ctx->module_potfile_disable          = MODULE_DEFAULT;
  module_ctx->module_potfile_keep_all_hashes  = MODULE_DEFAULT;
  module_ctx->module_pwdump_column            = MODULE_DEFAULT;
  module_ctx->module_pw_max                   = MODULE_DEFAULT;
  module_ctx->module_pw_min                   = MODULE_DEFAULT;
  module_ctx->module_salt_max                 = MODULE_DEFAULT;
  module_ctx->module_salt_min                 = MODULE_DEFAULT;
  module_ctx->module_salt_type                = module_salt_type;
  module_ctx->module_separator                = MODULE_DEFAULT;
  module_ctx->module_st_hash                  = module_st_hash;
  module_ctx->module_st_pass                  = module_st_pass;
  module_ctx->module_tmp_size                 = module_tmp_size;
  module_ctx->module_unstable_warning         = MODULE_DEFAULT;
  module_ctx->module_warmup_disable           = MODULE_DEFAULT;
}

References

Serious Security : stocker vos mots de passe en toute sécurité

https://github.com/openwall/john/blob/bleeding-jumbo/src/sha2.c#L578-L595

https://github.com/hashcat/hashcat

Automotive SecurityQNXHashcatPBKDF2

VW ID.4 ICAS1 Vehicle Control Analysis

General Tips for Firmware Reverse Engineering