【问题标题】:Inverse function of Java's Random functionJava的Random函数的反函数
【发布时间】:2013-02-20 13:59:38
【问题描述】:

Java 的 Random 函数接受一个种子并产生一个“伪随机”数字序列。 (它是基于Donald Knuth, The Art of Computer Programming, Volume 3, Section 3.2.1.)中讨论的一些算法实现的,但是这篇文章太技术了,我看不懂)

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

[编辑] 这里似乎有相当多的 cmets...我想我会澄清我在寻找什么。

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

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

现在回到我的问题,如果你说函数不可逆,请解释原因。

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

【问题讨论】:

  • 什么意思? (不要冒犯,但我不确定你知道reverse-function的含义)
  • @OneTwoThree 它的正确名称是反函数——我以前从未听说过reverse。这可能就是为什么@LukasKnuth 有一些困惑
  • @Cheezey:我不确定线性同余生成器是否有任何可以利用的弱点。但是维基百科中提到了They should also not be used for cryptographic applications,所以这个方法应该有一些漏洞,使它在密码学上不安全。
  • reteam.org/papers/e59.pdf 用于基本的线性同余生成器。

标签: java random


【解决方案1】:

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

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

Random的种子更新如下:

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

这是一个非常简单的函数,如果你通过计算知道种子的所有位,就可以将其倒置

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

因为0x5DEECE66DL * 0xdfe05bcb1365L = 1 mod 248。这样,任何时间点的单个种子值都足以恢复所有过去和未来的种子

Random 没有显示整个种子的函数,所以我们必须要聪明一点。

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

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

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

我们有(b &lt;&lt; 16) + d = ((a &lt;&lt; 16) + c) * 0x5DEECE66DL + 11。因此,做一些代数,我们得到(b &lt;&lt; 16) - 11 - (a &lt;&lt; 16)*0x5DEECE66DL = c*0x5DEECE66DL - d, mod 248。由于cd 都是16 位量,所以c*0x5DEECE66DL 最多有51 位。这很有用地意味着

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

对于某些k,最多等于6 个c*0x5DEECE66DL - d。(有更复杂的方法来计算cd,但是因为k 上的界限非常小,所以更容易计算蛮力)。

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

演示方法的代码:

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());
    }
}

【讨论】:

  • 这是否意味着r.nextLong() 永远不会生成一些long?
  • +1 不错的方法。这也可以用来减少nextInt() 生成的 2 个整数的暴力破解。
  • @Tom:正确。只有 48 位状态,r.nextLong 最多可以生成 48 个唯一的longs。
  • @nneonneo 当然你的意思是最多 2^48 个独特的 long ,而不仅仅是 48 个 long ......
  • 啊,是的,我注意到我的评论中有这个错误,但来不及编辑它。我的帖子包含正确的号码。也是七年了……
【解决方案2】:

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

程序将暴力破解nextInt()丢弃的低16位,使用James Roper博客中提供的算法找到之前的种子,然后检查48位种子的高32位是否相同作为之前的号码。我们至少需要 2 个整数来推导出前一个种子。否则,前一个种子将有 216 种可能性,并且在我们至少有一个数字之前,它们都同样有效。

它可以很容易地扩展为nextLong(),并且1 long 数量足以找到种子,因为我们在一个@987654327 中有2 个种子的高32 位@, due to the way it is generated.

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

我想把大部分功劳归功于 this blog article 的作者 James Roper,这使得下面的示例代码成为可能:

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);
  }
}

【讨论】:

  • 你就是那个男人!请注意,在许多情况下,您不会获得相同的种子,而是生成相同数字序列的等效种子值。例如,尝试反转 -26691 或任何其他负值和/或小值。这意味着该函数并不是真正可逆的,但它(相当)容易找到碰撞。
  • @Slanec:在内部,Java 使用 48 位种子。小的负数会超过 48 位,所以不会取回原来的数。但是,我认为低 48 位应该是相同的。
【解决方案3】:

我通常不会只链接文章...但我发现了一个网站,有人对此进行了深入研究并认为值得发布。 http://jazzy.id.au/default/2010/09/20/cracking_random_number_generators_part_1.html

看来你可以这样计算种子:

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

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

编辑:正如 nhahtdh 所说,在第 2 部分中,他深入研究了种子背后的更多数学。

【讨论】:

  • 我的意思是,第 1 部分演示了他如何从 2 个数字中找到未来的数字。第 2 部分演示了他如何从当前号码中找到过去的号码。
  • @nhahtdh 嗯,从我阅读的方式来看,第 2 部分演示了如何找到以前的“随机”数字/种子,但您必须从已知种子开始该过程。如果这是真的,我认为说你需要两个“随机”数字来确定种子仍然是正确的。
  • 通常的情况是我们生成了大量的随机数,我们想找到种子,所以我认为这个假设通常是成立的。如有必要,我们可以暴力破解。
猜你喜欢
  • 2017-01-18
  • 1970-01-01
  • 1970-01-01
  • 2017-09-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多