29

Java's Random function takes a seed and produces the a sequence of 'psuedo-random' numbers. (It is implemented based on some algorithm discussed in Donald Knuth, The Art of Computer Programming, Volume 3, Section 3.2.1.), but the article is too technical for me to understand)

Is there an inverse function of it? That is, given a sequence of numbers, would it be possible to mathematically determine what the seed would be? (, which means, brute-forcing doesn't count as a valid method)

[Edit] There seems to be quite a number of comments here... I thought I'd clarify what I am looking for.

So for instance, the function y = f(x) = 3x has an inverse function, which is y = g(x) = x/3.

But the function z = f(x, y) = x * y does not have an inverse function, because (I could give a full mathematical proof here, but I don't want to sidetrack my main question), intuitively speaking, there are more than one pair of (x, y) such that (x * y) == z.

Now back to my question, if you say the function is not inversible, please explain why.

(And I am hoping to get answers from those who have really read to article and understand it. Answers like "It's just not possible" aren't really helping)

Makoto
  • 104,088
  • 27
  • 192
  • 230
One Two Three
  • 22,327
  • 24
  • 73
  • 114
  • What do you mean? (Not to be offensive, but I'm not sure you know the meaning of `reverse-function`) – One Two Three Mar 05 '13 at 23:34
  • @OneTwoThree The correct name for it is inverse function - I've never heard `reverse` before. That may be why there is some confusion from @LukasKnuth – ddmps Mar 05 '13 at 23:36
  • this is related to printing "hello world" with random number seeds. http://stackoverflow.com/questions/15182496/why-does-this-code-print-hello-world – Kailua Bum Mar 05 '13 at 23:38
  • Related: http://en.wikipedia.org/wiki/Linear_congruential_generator – nhahtdh Mar 05 '13 at 23:38
  • It depends on the algorithm. Many seeds can generate the same sequence. This would be an interesting problem to solve, but I don't think anyone's really looked in to it before. – Cheezey Mar 05 '13 at 23:40
  • 2
    @Cheezey: I am not sure if there is any weakness of Linear congruential generator that can be exploited. But it is mentioned in Wikipedia that `They should also not be used for cryptographic applications`, so there should be some hole in this method that makes it cryptographically insecure. – nhahtdh Mar 05 '13 at 23:43
  • http://stackoverflow.com/questions/6001368/how-do-i-get-the-seed-from-a-random-in-java you can get inspiration from this question – dierre Mar 05 '13 at 23:44
  • 2
    http://jazzy.id.au/default/2010/09/20/cracking_random_number_generators_part_1.html and part 2: http://jazzy.id.au/default/2010/09/21/cracking_random_number_generators_part_2.html – nhahtdh Mar 05 '13 at 23:49
  • 2
    http://www.reteam.org/papers/e59.pdf for basic Linear congruential generators. – Argon Mar 05 '13 at 23:52
  • Yeah, I meant 'inverse' – One Two Three Mar 06 '13 at 00:05
  • @OneTwoThree - BTW - It's volume 2 in my copy - volume 3 (Sorting and Searching) starts with chapter 5. You have the correct section `3.2.1`. – OldCurmudgeon Mar 06 '13 at 09:17

3 Answers3

28

If we're talking about the Oracle (née Sun) implementation of java.util.Random, then yes, it is possible once you know enough bits.

Random uses a 48-bit seed and a linear congruential generator. These are not cryptographically safe generators, because of the tiny state size (bruteforceable!) and the fact that the output just isn't that random (many generators will exhibit small cycle length in certain bits, meaning that those bits can be easily predicted even if the other bits seem random).

Random's seed update is as follows:

nextseed = (seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1)

This is a very simple function, and it can be inverted if you know all the bits of the seed by calculating

seed = ((nextseed - 0xBL) * 0xdfe05bcb1365L) & ((1L << 48) - 1)

since 0x5DEECE66DL * 0xdfe05bcb1365L = 1 mod 248. With this, a single seed value at any point in time suffices to recover all past and future seeds.

Random has no functions that reveal the whole seed, though, so we'll have to be a bit clever.

Now, obviously, with a 48-bit seed, you have to observe at least 48 bits of output or you clearly don't have an injective (and thus invertible) function to work with. We're in luck: nextLong returns ((long)(next(32)) << 32) + next(32);, so it produces 64 bits of output (more than we need). Indeed, we could probably make do with nextDouble (which produces 53 bits), or just repeated calls of any other function. Note that these functions cannot output more than 248 unique values because of the seed's limited size (hence, for example, there are 264-248 longs that nextLong will never produce).

Let's specifically look at nextLong. It returns a number (a << 32) + b where a and b are both 32-bit quantities. Let s be the seed before nextLong is called. Then, let t = s * 0x5DEECE66DL + 0xBL, so that a is the high 32 bits of t, and let u = t * 0x5DEECE66DL + 0xBL so that b is the high 32 bits of u. Let c and d be the low 16 bits of t and u respectively.

Note that since c and d are 16-bit quantities, we can just bruteforce them (since we only need one) and be done with it. That's pretty cheap, since 216 is only 65536 -- tiny for a computer. But let's be a bit more clever and see if there's a faster way.

We have (b << 16) + d = ((a << 16) + c) * 0x5DEECE66DL + 11. Thus, doing some algebra, we obtain (b << 16) - 11 - (a << 16)*0x5DEECE66DL = c*0x5DEECE66DL - d, mod 248. Since c and d are both 16-bit quantities, c*0x5DEECE66DL has at most 51 bits. This usefully means that

(b << 16) - 11 - (a << 16)*0x5DEECE66DL + (k<<48)

is equal to c*0x5DEECE66DL - d for some k at most 6. (There are more sophisticated ways to compute c and d, but because the bound on k is so tiny, it's easier to just bruteforce).

We can just test all the possible values for k until we get a value whos negated remainder mod 0x5DEECE66DL is 16 bits (mod 248 again), so that we recover the lower 16 bits of both t and u. At that point, we have a full seed, so we can either find future seeds using the first equation, or past seeds using the second equation.

Code demonstrating the approach:

import java.util.Random;

public class randhack {
    public static long calcSeed(long nextLong) {
        final long x = 0x5DEECE66DL;
        final long xinv = 0xdfe05bcb1365L;
        final long y = 0xBL;
        final long mask = ((1L << 48)-1);

        long a = nextLong >>> 32;
        long b = nextLong & ((1L<<32)-1);
        if((b & 0x80000000) != 0)
            a++; // b had a sign bit, so we need to restore a
        long q = ((b << 16) - y - (a << 16)*x) & mask;
        for(long k=0; k<=5; k++) {
            long rem = (x - (q + (k<<48))) % x;
            long d = (rem + x)%x; // force positive
            if(d < 65536) {
                long c = ((q + d) * xinv) & mask;
                if(c < 65536) {
                    return ((((a << 16) + c) - y) * xinv) & mask;
                }
            }
        }
        throw new RuntimeException("Failed!!");
    }

    public static void main(String[] args) {
        Random r = new Random();
        long next = r.nextLong();
        System.out.println("Next long value: " + next);
        long seed = calcSeed(next);
        System.out.println("Seed " + seed);
        // setSeed mangles the input, so demangle it here to get the right output
        Random r2 = new Random((seed ^ 0x5DEECE66DL) & ((1L << 48)-1));
        System.out.println("Next long value from seed: " + r2.nextLong());
    }
}
nneonneo
  • 171,345
  • 36
  • 312
  • 383
  • Does this mean there are some longs that `r.nextLong()` will never generate? – Tom Mar 06 '13 at 09:03
  • +1 Nice approach. This can be used to reduce the brute force used on 2 integers generated by `nextInt()` also. – nhahtdh Mar 06 '13 at 09:10
  • 1
    @Tom: Correct. With only 48 bits of state, `r.nextLong` can generate at most 48 unique `long`s. – nneonneo Mar 06 '13 at 18:25
  • @nneonneo surely you mean at most 2^48 unique longs, not only 48 total longs... – Nacht Apr 16 '20 at 11:17
  • 1
    Ah yes I noticed that error in my comment but was too late to edit it. My post contains the right number. It’s also been seven years... – nneonneo Apr 16 '20 at 23:41
4

I normally wouldn't just link articles... But I found a site where someone looks into this in some depth and thought it was worth posting. http://jazzy.id.au/default/2010/09/20/cracking_random_number_generators_part_1.html

It seems that you can calculate a seed this way:

seed = (seed * multiplier + addend) mod (2 ^ precision)

where multiplier is 25214903917, addend is 11, and precision is 48 (bits). You can't calculate what the seed was with only 1 number, but you can with 2.

EDIT: As nhahtdh said there's a part 2 where he delves into more of the math behind the seeds.

Memento Mori
  • 3,327
  • 2
  • 22
  • 29
  • What I meant was that, part 1 demonstrates how he find the future numbers from 2 numbers. And part 2 demonstrates how he find the past numbers from the current number. – nhahtdh Mar 05 '13 at 23:59
  • @nhahtdh hmm from the way I'm reading it, part 2 demonstrates how to find previous 'random' numbers/seeds but you have to start that process with a known seed. If that's true, I think it's still correct to say you need two 'random' numbers to determine a seed. – Memento Mori Mar 06 '13 at 00:06
  • The usual case is that we have a tons of random number generated, and we want to find the seed, so I think that assumption is usually fulfilled. We can brute force if necessary. – nhahtdh Mar 06 '13 at 00:10
3

I would like to present an implementation to reverse a sequence of integers generated by nextInt().

The program will brute force on the lower 16-bit discarded by nextInt(), use the algorithm provided in the blog by James Roper to find previous seed, then check that upper 32 bit of the 48-bit seed are the same as the previous number. We need at least 2 integers to derive the previous seed. Otherwise, there will be 216 possibilities for the previous seed, and all of them are equally valid until we have at least one more number.

It can be extended for nextLong() easily, and 1 long number is enough to find the seed, since we have 2 pieces of upper 32-bit of the seed in one long, due to the way it is generated.

Note that there are cases where the result is not the same as what you set as secret seed in the SEED variable. If the number you set as secret seed occupies more than 48-bit (which is the number of bits used for generating random numbers internally), then the upper 16 bits of 64 bit of long will be removed in the setSeed() method. In such cases, the result returned will not be the same as what you have set initially, it is likely that the lower 48-bit will be the same.

I would like to give most the credit to James Roper, the author of this blog article which makes the sample code below possible:

import java.util.Random;
import java.util.Arrays;

class TestRandomReverse {
  // The secret seed that we want to find
  private static long SEED = 782634283105L;

  // Number of random numbers to be generated
  private static int NUM_GEN = 5;

  private static int[] genNum(long seed) {
    Random rand = new Random(seed);
    int arr[] = new int[NUM_GEN];
    for (int i = 0; i < arr.length; i++) {
      arr[i] = rand.nextInt();
    }

    return arr;
  }

  public static void main(String args[]) {

    int arr[] = genNum(SEED);
    System.out.println(Arrays.toString(arr));

    Long result = reverse(arr);

    if (result != null) {
      System.out.println(Arrays.toString(genNum(result)));
    } else {
      System.out.println("Seed not found");
    }
  }

  private static long combine(int rand, int suffix) {
    return (unsignedIntToLong(rand) << 16) | (suffix & ((1L << 16) - 1));
  }

  private static long unsignedIntToLong(int num) {
    return num & ((1L << 32) - 1);
  }

  // This function finds the seed of a sequence of integer, 
  // generated by nextInt()
  // Can be easily modified to find the seed of a sequence 
  // of long, generated by nextLong()
  private static Long reverse(int arr[]) {
    // Need at least 2 numbers.
    assert (arr.length > 1);

    int end = arr.length - 1;

    // Brute force lower 16 bits, then compare
    // upper 32 bit of the previous seed generated
    // to the previous number.
    for (int i = 0; i < (1 << 16); i++) {
      long candidateSeed = combine(arr[end], i);
      long previousSeed = getPreviousSeed(candidateSeed);

      if ((previousSeed >>> 16) == unsignedIntToLong(arr[end - 1])) {
        System.out.println("Testing seed: " + 
                            previousSeed + " --> " + candidateSeed);

        for (int j = end - 1; j >= 0; j--) {
          candidateSeed = previousSeed;
          previousSeed = getPreviousSeed(candidateSeed);

          if (j > 0 && 
             (previousSeed >>> 16) == unsignedIntToLong(arr[j - 1])) {
            System.out.println("Verifying: " + 
                                previousSeed + " --> " + candidateSeed);
          } else if (j == 0) {
            // The XOR is done when the seed is set, need to reverse it
            System.out.println("Seed found: " + (previousSeed ^ MULTIPLIER));
            return previousSeed ^ MULTIPLIER;
          } else {
            System.out.println("Failed");
            break;
          }
        }
      }
    }

    return null;
  }

  private static long ADDEND = 0xBL;
  private static long MULTIPLIER = 0x5DEECE66DL;

  // Credit to James Roper
  // http://jazzy.id.au/default/2010/09/21/cracking_random_number_generators_part_2.html
  private static long getPreviousSeed(long currentSeed) {
    long seed = currentSeed;
    // reverse the addend from the seed
    seed -= ADDEND; // reverse the addend
    long result = 0;
    // iterate through the seeds bits
    for (int i = 0; i < 48; i++)
    {
      long mask = 1L << i;
      // find the next bit
      long bit = seed & mask;
      // add it to the result
      result |= bit;
      if (bit == mask)
      {
        // if the bit was 1, subtract its effects from the seed
        seed -= MULTIPLIER << i;
      }
    }

    return result & ((1L << 48) - 1);
  }
}
nhahtdh
  • 55,989
  • 15
  • 126
  • 162
  • 1
    You're the man! Note that in many cases, you _won't_ get the same seed, but an equivalent seed value that generates the same number sequence. For example, try inverting `-26691` or any other negative and/or small value. This means that the function is not really invertible, but it's (fairly) easy to find a collision. – Petr Janeček Mar 06 '13 at 01:38
  • @Slanec: Internally, Java uses 48-bit seed. Small negative number will exceed 48-bit, so it will not get back the original number. However, I think the lower 48-bit should be identical. – nhahtdh Mar 06 '13 at 01:48