agentsclimarketplace

Time window seed bruteforce

Skill abedegno/reverse-engineering-companion/plugins/reverse-engineering-companion/skills/time-window-seed-bruteforce

Companion skills for mobile/web reverse engineering — pairs with android-reverse-engineering-skill

Install
npx -y skills add abedegno/reverse-engineering-companion --skill time-window-seed-bruteforce

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Recover a low-entropy PRNG seed from observed state plus a known time window. Common when a system uses Date.now() or time(NULL) as its default seed — that's ~41 bits of entropy, brute-forceable in seconds when you know within ~10s when the seed was minted. Use when porting a deterministic system that won't tell you its seed, or to validate a leaked-seed claim.

SKILL.md

9.2 KB, ~2.1k tokens by cl100k_base, as published. Nobody here has run it

Time-Window Seed Brute Force

When a deterministic system uses Date.now() (JavaScript) or time(NULL) (C) or time.time() (Python) as its default PRNG seed, the seed has only ~41 bits of useful entropy — the high bits are a predictable millisecond timestamp. If you can observe a few values the PRNG produced (or any state derived from it), you can brute-force the seed in seconds.

This isn't an attack on the PRNG algorithm. xorshift32, mt19937, and similar are computationally strong once seeded. The weakness is in the seed entropy, not the algorithm.

When this skill applies

  • The system uses a PRNG with a low-entropy default seed (millisecond timestamp, process PID, etc.).
  • You know roughly when the seed was minted (within seconds, ideally — but even within minutes is tractable).
  • You can observe some output of the PRNG — initial state, first N draws, anything reproducible.
  • You have a port of the exact PRNG algorithm (see find-production-sourcemap — the algorithm is often right there in the source).

If the seed is shipped to the client (e.g. in a prepare_game message), you don't need to brute-force — but knowing the technique is the fallback when it isn't.

The math

A 64-bit JavaScript timestamp is Date.now(), which is "milliseconds since 1970-01-01". Today's values are around 1.78e12, which is around 2^40.7. That's ~41 bits.

For any specific seed event:

  • If you know it's "today, within the last 24 hours" → 86,400,000 candidate ms values → ~27 bits.
  • If you know it's "within the last hour" → 3,600,000 candidates → ~22 bits.
  • If you know it's "within the last 10 seconds" → 10,000 candidates → ~14 bits.
  • If you triggered the event and timed it yourself → ~1000 candidates → ~10 bits.

Per-candidate verification is "seed the PRNG, run the system's initialisation logic, compare against observed state". Each verification is ~microseconds. Brute-forcing 10,000 candidates takes ~10ms.

The procedure

Step 1: Port the PRNG bit-exactly

You need a Python (or whatever) implementation that produces the same output as the original given the same seed. The most common case is porting a JavaScript PRNG.

Standard JS PRNG implementations you'll encounter:

  • xorshift32 — three lines, trivial to port.
  • mulberry32 — four lines, popular for game state.
  • sfc32 — small fast counter, four state words.
  • xxHash32 finaliser combined with one of the above.
  • mt19937 — Mersenne Twister, often imported from a library; port via numpy.random.MT19937 for verification.

Port carefully — JS integer semantics differ from Python's. See references/js-prng-port-traps.md.

Step 2: Validate the port against captured output

You need at least one (seed, output) pair from the original to confirm your port is correct. Get this either:

  • From a captured session where the seed is visible on the wire and you can also see the first few PRNG-derived values.
  • By logging the original (if you have a debug build).
  • By comparing predicted vs observed initial state for a known seed.
def test_prng_port_against_known_seed():
    """If the original system says seed=12345 produces sequence [a, b, c, ...],
    my port must produce the same sequence."""
    prng = PortedPRNG(seed=12345)
    out = [prng.next_uint32() for _ in range(20)]
    assert out == [0xC18E0DDD, 0x4DE82C7D, ...], "port diverges from original"

Without this validation, brute-force will find seeds that produce your port's output for a given session, not the original's.

Step 3: Brute-force the window

def recover_seed(t_start_ms, t_end_ms, observed_initial_state, init_fn):
    """
    Try every millisecond candidate in [t_start_ms, t_end_ms]. For each:
    seed the PRNG, run the system's initialisation, compare against
    observed_initial_state. Return the first match.

    Args:
        t_start_ms, t_end_ms: bounds of the candidate window (ms-epoch).
        observed_initial_state: the state derived from the unknown seed.
        init_fn: function (prng) -> state, mimicking the original's init logic.

    Returns:
        The recovered seed, or None.
    """
    for candidate in range(t_start_ms, t_end_ms + 1):
        prng = PortedPRNG(seed=candidate)
        predicted = init_fn(prng)
        if predicted == observed_initial_state:
            return candidate
    return None

For an 8x8 grid of colours, init_fn typically draws 64 values mod-7 plus some re-roll logic to avoid immediate matches. Reproduce that exact init logic in your port; otherwise you'll never match.

Step 4: Verify on independent state

A 64-cell match could be coincidence (very unlikely, but worth ruling out). Confirm by drawing more state from the seeded PRNG and comparing against more observed values:

def find_seed_with_verification(t_start, t_end, primary_observation, secondary_observations, init_fn, secondary_fn):
    candidate = recover_seed(t_start, t_end, primary_observation, init_fn)
    if candidate is None:
        return None
    # Re-seed and verify secondary
    prng = PortedPRNG(seed=candidate)
    init_fn(prng)  # advance through init
    for observation in secondary_observations:
        if secondary_fn(prng) != observation:
            # Match was a coincidence
            return None
    return candidate

For 64-cell match plus a handful of secondary draws, false-positive probability is essentially zero.

Narrowing the window

The smaller the window, the faster. Sources of timing info:

  • You triggered the event. Capture time.time() * 1000 immediately before triggering; you know within ~100ms when the seed was minted.
  • A wire timestamp. Some servers stamp messages with epoch ms. If a message close to seed creation has one, that bounds the window.
  • The client logs a Date.now() value. Inspect the JS bundle (find-production-sourcemap) for client-side timestamps that get sent to the server.
  • An HTTP Date: header on a related response. Second-resolution; tightens the window to 1,000 ms.

If you have no timing info, you can still brute-force a "today" window (86M candidates, ~minutes) or "this hour" (3.6M, seconds).

When this skill isn't enough

  • High-entropy seeds. If the system uses crypto.getRandomValues() or seeds from /dev/urandom, brute-force is infeasible. You need either to find a way to observe the seed directly, or to give up on knowing it.
  • Per-session re-seeding from cryptographic source. Even worse — each session has independent strong entropy.
  • Sealed sub-state. Some games re-seed sub-systems with cryptographic values mid-session. Even if the primary seed is recoverable, downstream state may not be.

If brute-force isn't tractable, look harder for a leak point — does the seed appear in any client-side log, any HTTP header, any state that's later echoed back?

Common failure modes

SymptomLikely cause
No candidate in the window matchesPRNG port is wrong; init logic doesn't match exactly; or the window doesn't actually contain the seed.
Multiple candidates match the initial stateInitial state has low entropy (e.g. only 16 distinct colours of 64 cells); add secondary verification.
Match works in dev, fails in prodDifferent PRNG, or different init logic between dev and prod.
Match for one session, fails for anotherThe system has multiple seed sources, or you're brute-forcing the wrong one (e.g. the session-level seed when the relevant one is the player-level seed).

Defensive perspective

The mitigation, if you operate a system like this: don't seed your PRNG from a millisecond timestamp. Use a cryptographically secure source. crypto.getRandomValues(new Uint32Array(1))[0] in JavaScript, secrets.randbits(32) in Python.

If you must use a timestamp seed (compatibility, reproducibility), the seed is public information — design the rest of the system to not depend on the seed being secret. If state derived from the seed is sensitive (e.g. determines a game outcome), the seed itself is sensitive and shouldn't have ~41 bits of entropy.

References

Pairs with

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.