Skip to content

Silent-failure bugs across statsgen, maskgen, policygen and rulegen (wrong coverage numbers, dropped rules, no-op options) #31

Description

@bandrel

Silent-failure bugs in all four tools (with line references)

These are bugs where PACK produces a wrong number, drops output, or ignores an
option without reporting an error — so the operator has no signal that
anything went wrong. Line numbers are against master (622dd92).

I've fixed all of these in a maintained Python 3 fork
(bandrel/pack) with regression tests, and
I'm happy to split any of them into individual PRs here if that's useful.

maskgen

  1. loadmasks() ignores its own argument (maskgen.py:72). The signature
    takes filename, but the body opens args[0]. Since maskgen.py:271-272
    loops for arg in args: maskgen.loadmasks(arg), running
    maskgen.py a.masks b.masks c.masks parses a.masks three times and never
    reads b or c. total_occurrence is inflated N× while self.masks is
    overwritten, so the reported "Masks coverage" comes out around 1/N of the
    truth and the other files' masks are missing from the output.

  2. Masks repeated across files are not aggregated (maskgen.py:95).
    self.masks[mask] = dict() is last-write-wins, but :83 adds every
    occurrence to the coverage denominator, so merging statsgen outputs that
    share masks understates coverage. (Masked today by bug 1; fixing 1 exposes 2.)

  3. Runtime truncates to zero (maskgen.py:81, policygen.py:110,124,128).
    mask_complexity / self.pps is integer division, so at the default 1e9 pps
    any mask under a billion candidates reports 0 seconds: ?d?d?d?d?d?d?d?d
    (1e8) and ?l?l?l?l?l?l (3.1e8) both show 0s. --mintime/--maxtime can't
    distinguish them and --targettime accumulates zeros and never trips. This
    is also the cause of the # TODO: Something wrong here at maskgen.py:166:
    getmaskscoverage sums complexity and divides once, while loadmasks
    divides per mask, so the two paths genuinely disagree.

  4. Unrecognised mask tokens are costed as 1 (maskgen.py:66). The else
    branch prints a warning but leaves count unmultiplied, so the mask stays in
    the run with an understated complexity and runtime. Two realistic triggers:
    ?1 without --custom-charset1-len (the and self.customcharset1len guard
    at :62 falls through), and literal characters in a .hcmask, since
    mask[1:].split("?") turns ?u?l?lpass into a token lpass. Literals also
    break mask_length = len(mask)/2 at :79.

  5. --targettime overshoots by one mask (maskgen.py:119-128). The mask is
    written to the output file before the budget check breaks the loop.

  6. Zero-valued options are silently discarded (maskgen.py:243,249-256,265).
    These use if options.x: rather than a None check, so --minoccurrence 0,
    --mintime 0, --targettime 0 and --pps 0 are ignored. statsgen (:248)
    already uses the == None form, so it's inconsistent within the codebase.

statsgen

  1. The Advanced Masks section ignores --hiderare (statsgen.py:203). The
    other three sections gate on if self.hiderare and not ... > 0, but this one
    hardcodes if count*100/self.filter_counter > 0, so sub-1% advanced masks are
    always hidden from the display whether or not you asked for it. The file
    write at :206 is correctly outside the if, so only the terminal view is
    affected.

rulegen

  1. Worker processes die on words with no rules (rulegen.py:789).
    sorted(words, key=lambda word: len(word["hashcat_rules"][0])) indexes [0]
    on a list that generate_hashcat_rules returns empty whenever nothing
    survives the max_rule_len filter at :461 — easy to hit with a small
    --maxrulelen. password_worker's except only catches KeyboardInterrupt
    and SystemExit, so the worker dies, and since passwords_queue is bounded
    to the thread count (:882) the parent eventually blocks forever on put().
    Observable symptom: the run stops making progress with no error summary.

  2. The last rules are dropped (rulegen.py:926).
    while not passwords_queue.empty() treats an empty input queue as "work
    finished", but it only means the last password was taken, not analyzed. The
    parent then pushes the None death pill onto rules_queue, which
    rule_worker can consume before the still-running workers finish enqueuing.
    Joining the worker processes before pilling the writers fixes it.

  3. Ctrl-C skips shutdown entirely (rulegen.py:917-931). :879 advertises
    Ctrl-C as the way to finish early and get statistics, but the
    try/except/else means an interrupt skips the else block, so no death
    pills are sent. Workers and both writer processes are left running while the
    parent reads the partially written .rule/.word files and reports
    statistics over them.

  4. Top 10 words percentages use the rule total (rulegen.py:967).
    word_counter_total = sum(rules_counter.values()) should be
    words_counter. The shares at :973 are scaled by the wrong denominator
    and don't sum to 100%.

  5. --maxrules does nothing. self.max_rules is set at :58 and assigned
    from the option at :1068, then never read. Only max_rule_len is
    enforced, despite the help text promising "Maximum number of rules to
    consider".

  6. --hashcat does nothing. verify_hashcat_rules (:980) has no caller,
    and rulegen.hashcat at :1078 sets an attribute that isn't in __init__
    and is never read. Users pass the flag expecting rules to be validated
    against hashcat --stdout and silently get no verification.

  7. Hashcat offsets past Z produce malformed rules (rulegen.py:410).
    int_to_hashcat returns chr(65+N-10), which walks into [, \, ] for
    positions above 35 instead of refusing.

  8. The progress line reports the wrong elapsed time (rulegen.py:908).
    It prints segment_start - analysis_start, the previous segment's start
    offset, rather than elapsed time, so it under-reports by one segment.

Lower severity

  1. Division by zero on empty or fully-filtered input: statsgen.py:178,
    maskgen.py:132,171, rulegen.py:936,940. These crash rather than
    mislead, but empty input is a normal thing to hand these tools.

  2. Output files are never closed (statsgen.py:257, maskgen.py:246,
    policygen.py:178, and the csv.reader(open(...)) at maskgen.py:72).
    CPython flushes at normal exit, so this only bites on a kill — but a
    Ctrl-C'd maskgen run can leave a truncated .hcmask.

  3. hashcat_rule["p"] = lambda x,y: x*y (rulegen.py:106) yields y copies,
    whereas hashcat's pN yields N+1. Latent only — nothing emits p.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions