Java随机函数的反函数

Inverse function of Java's Random function

Java的随机函数取一个种子并产生一个"pSuedo-No"序列的序列。(这是根据Donald Knuth, The Art of Computer Programming, Volume 3, Section 3.2.1.)中讨论的一些算法实现的,但这篇文章太过技术性,我无法理解)

它有反函数吗?也就是说,给定一个数字序列,是否可以用数学方法确定种子是什么?(也就是说,暴力强迫不算是有效的方法)

[编辑]这里似乎有很多评论…我想我会澄清我在找什么。

例如,函数y = f(x) = 3x有一个反函数,就是y = g(x) = x/3

但是函数z = f(x, y) = x * y没有反函数,因为(我可以在这里给出一个完整的数学证明,但我不想偏离我的主要问题),直观地说,有不止一对(x, y),所以(x * y) == z

现在回到我的问题上,如果你说函数是不可逆的,请解释为什么。

(我希望能从那些真正读过这篇文章并理解它的人那里得到答案。像"这是不可能的"这样的回答真的没有帮助)


如果我们讨论的是java.util.Random的Oracle(n_e Sun)实现,那么是的,只要您知道足够的位,就有可能实现。

Random使用48位种子和线性同余生成器。这些不是加密安全的生成器,因为它们的状态大小很小(不可破解!)事实上,输出并不是随机的(许多生成器在某些位中会显示出很小的循环长度,这意味着即使其他位看起来是随机的,也可以很容易地预测这些位)。

Random的种子更新如下:

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

这是一个非常简单的函数,如果通过计算知道种子的所有位,它就可以被反转。

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

0x5DEECE66DL * 0xdfe05bcb1365L = 1248型起。有了这个,在任何时间点上的单个种子值都足以恢复所有过去和将来的种子。

不过,Random没有显示整个种子的功能,所以我们必须有点聪明。

显然,对于48位种子,您必须观察至少48位的输出,或者您显然没有要处理的内射函数(因此是可逆的)。我们很幸运:nextLong返回((long)(next(32)) << 32) + next(32);,因此它产生64位输出(比我们需要的多)。实际上,我们可以使用nextDouble(产生53位),或者只是重复调用任何其他函数。请注意,由于种子的大小有限,这些函数不能输出超过248个唯一值(例如,nextLong永远不会生成264-248个long)。

让我们具体看看nextLong。它返回一个数字(a << 32) + b,其中ab都是32位的数量。在调用nextLong之前,让s作为种子。然后,让t = s * 0x5DEECE66DL + 0xBL使at的高位32位,让u = t * 0x5DEECE66DL + 0xBL使bu的高位32位。让cd分别为tu的低16位。

请注意,由于cd是16位的数量,我们可以对它们进行野蛮处理(因为我们只需要一个)并完成它。这相当便宜,因为216只有65536台——对于计算机来说很小。但让我们更聪明一点,看看有没有更快的方法。

我们有(b << 16) + d = ((a << 16) + c) * 0x5DEECE66DL + 11。因此,在做一些代数运算时,我们得到了(b << 16) - 11 - (a << 16)*0x5DEECE66DL = c*0x5DEECE66DL - d,mod 248。由于cd都是16位的数量,所以c*0x5DEECE66DL最多有51位。这就意味着

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

对于某些k至多6个,等于c*0x5DEECE66DL - d。(计算cd有更复杂的方法,但由于k上的界限太小,所以更容易进行野蛮的处理)。

我们只需测试k的所有可能值,直到我们得到一个求反的余数mod 0x5DEECE66DL是16位(mod 248再次),这样我们就可以恢复tu的低16位。在这一点上,我们有一个完整的种子,所以我们可以用第一个方程找到未来的种子,或者用第二个方程找到过去的种子。

演示方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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());
    }
}


我通常不会只是链接文章…但我找到了一个网站,有人深入调查了这个网站,认为它值得张贴。http://jazzy.id.au/default/2010/09/20/cracking_random_number_generators_part_1.html

您似乎可以这样计算种子:

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

其中乘数为25214903917,加数为11,精度为48(位)。你不能计算只有1个数字的种子是什么,但是你可以用2。

编辑:正如恩哈德所说,有一个第2部分,他深入研究种子背后的数学。


我想介绍一个实现来反转由nextInt()生成的整数序列。

该程序将对nextInt()丢弃的16位下进行强力强制,使用james roper在blog中提供的算法查找前一个种子,然后检查48位种子的32位上是否与前一个数字相同。我们需要至少2个整数来派生前面的种子。否则,前一个种子将有216种可能,并且在我们至少有一个以上的数字之前,它们都是同样有效的。

它可以很容易地扩展到nextLong(),1个long数字足以找到种子,因为一个long中有2个32位以上的种子,这是由于它的生成方式。

注意,有些情况下,结果与您在SEED变量中设置的秘密种子不同。如果您设置为秘密种子的数字占用超过48位(这是用于内部生成随机数的位的数目),那么在setSeed()方法中将删除long的64位的上16位。在这种情况下,返回的结果将与您最初设置的结果不同,较低的48位可能是相同的。

我想感谢本文作者JamesRoper,他使下面的示例代码成为可能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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);
  }
}