1
$\begingroup$

I have observed an RNG implementation using an HMAC with SHA-256 that takes serverSeed, clientSeed, nonce, and cursor as inputs. For the specific scenario, the cursor remains zero. The key detail is that the host provides the SHA-256 hash of the serverSeed to the client before the client chooses their clientSeed. After games are played, the user may request the original serverSeed from the host as soon as they change to a new client seed. Heres the RNG implementation:

function byteGenerator({ serverSeed, clientSeed, nonce, cursor }) {
    let currentRoundCursor = 0;
    let currentRound = 0;
    const hmac = createHmac('sha256', serverSeed);
    hmac.update(`${clientSeed}:${nonce}:${currentRound}`);
    const buffer = hmac.digest();

    // Yields 32 bytes of output
    while (currentRoundCursor < 32) {
      yield Number(buffer[currentRoundCursor]);
      currentRoundCursor += 1;
    } }

function generateFloats({ serverSeed, clientSeed, nonce, cursor, count }) {   const rng = byteGenerator({ serverSeed, clientSeed, nonce, cursor });   const bytes = [];

  // Collect `count * 4` bytes   while (bytes.length < count * 4) {
    bytes.push(rng.next().value);   }

  // Converts chunks of 4 bytes into floats   return _.chunk(bytes, 4).map(bytesChunk =>
    bytesChunk.reduce((result, value, i) => {
      const divider = 256 ** (i + 1);
      return result + (value / divider);
    }, 0)   ); }

Example:
For a game output in the range [0.00, 100.00], one set of 4 bytes (113, 141, 68, 253) leads to a value derived mostly from the first byte:

(113 / 256^1) ≈ 0.44140625
(141 / 256^2) ≈ 0.002151489258
(68 / 256^3) ≈ 0.000004053116
(253 / 256^4) ≈ 0.000000058906

Total ≈ 0.44356185128 * 10001 ≈ 4436.062074649
RNG output ≈ 44.36

Issues Identified:

1   Predictability: Once the client has chosen their seed, the host (knowing the serverSeed and incrementing the nonce) can predict all future outcomes for that seed pair.
2   Single Byte Dominance: Although the code claims to use 4 bytes from the HMAC output for each result, about 99.5% of each final number is determined by the first byte. The host only needs to influence one byte of the HMAC output to control results.
3   Hash Choice and Vulnerabilities: The host insists on using only SHA-256 or SHA-512. Both are known to be vulnerable to length extension attacks, suggesting this property could be leveraged to manipulate outcomes.

In tests with tens of millions of data points taken from the host’s platform, the results show severe statistical anomalies (e.g., outcomes with p-values < 1.0e-500), clearly indicating deliberate manipulation. Interestingly, when using truly random seeds in a controlled environment, the distribution appears uniform and fair. But with the host’s chosen seeds, the results heavily favor the host. When the results are actively being manipulated, they tend to bunch together. So for example, the first byte will be identical or near identical for three, four, sometimes five sequential output hashes. Lastly, it should be noted that before a user chooses a seed, they are provided suggested ones, which appear to facilitate even easier manipulation. the suggested seeds are always 10 characters in length and contain very few hex digits if any at all, examples: P3hmVYpIM7 l-XiEvH-4K H4UHHqM6N3. Perhaps encoding plays a role.

Question:


Given that the server provides the SHA-256 hash of the serverSeed before the client chooses the clientSeed, and eventually reveals the serverSeed so users can verify the outcomes, how can the host still manage to manipulate results efficiently? Considering that each result is overwhelmingly determined by the first output byte, and given the potential relevance of length extension attacks on SHA-256 and SHA-512, how could a bad actor use these properties to influence the RNG outputs in practice? Assume that the host has substantial financial and computational resources at their disposal.

$\endgroup$

1 Answer 1

3
$\begingroup$

Is the server seed longer than 32 bytes (256 bits)? The first step of HMAC is to hash the key if it is larger than a single block. In other words, if $K$ is too long, then $H(K)$ becomes the effective key for HMAC. If you proceed to give away $H(K)$ then you have given away the (effective) HMAC key, so the client can compute $\textsf{HMAC}(K,X)$ for any $X$ they like. They can try many $X$ until they find a favorable one. It takes only 256 attempts to find an $X$ that leads to a desired first output byte, etc.

$\endgroup$
1
  • 1
    $\begingroup$ Well spotted. The attack allows clients to cheat. I think the limit is >64 bytes, not 32, because the threshold for the first step of HMAC being to hash the key is based on the input block size of the hash (64 bytes for SHA-256), not it's output size (32 bytes). $\endgroup$
    – fgrieu
    Commented yesterday

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.