	What is php_mt_seed?

php_mt_seed is a PHP mt_rand() seed cracker.  In the most trivial
invocation mode, it finds possible seeds given the very first mt_rand()
output after possible seeding with mt_srand().  With advanced invocation
modes, it is also able to match multiple, non-first, and/or inexact
mt_rand() outputs to possible seed values.

PHP's mt_rand() algorithm changed over the years since its introduction
in PHP 3.0.6.  php_mt_seed 4.0 supports 3 major revisions of the
algorithm: PHP 3.0.7 to 5.2.0, PHP 5.2.1 to 7.0.x, and PHP 7.1.0+ (at
least up to the latest as of this writing, which is PHP 7.2.0beta3).

php_mt_seed uses attack-optimized reimplementations of PHP's mt_rand()
algorithms.  It is written in C with optional SIMD intrinsics (SSE2,
SSE4.1/AVX, XOP, AVX2, AVX-512, as well as MIC) and OpenMP.  On a modern
quad-core CPU, it is able to search the full 32-bit seed space in under
a minute.  On second generation Xeon Phi, it does the same in 3 seconds.


	Why crack mt_rand() seeds?

It is well-known that mt_rand() is a non-cryptographic PRNG and that its
32-bit seed space would be too small for cryptographic applications.
Yet many PHP applications misuse mt_rand() for purposes where a CSPRNG
would be needed.  Thus, a use case of php_mt_seed is to demonstrate to
developers and users of those applications just how very practical it is
to attack mt_rand() and how vulnerable those applications are, so that
the misuses of mt_rand() would decline.  Specific opportunities for such
demonstration include source code audits and network/system penetration
tests.  In the latter, the cracked seeds may allow the penetration test
to proceed further into the network or system, potentially exposing
other vulnerabilities there may be.  Other opportunities to practice
with php_mt_seed include CTFs (capture the flag competitions).

Common misuses of mt_rand() include generation of anti-CSRF tokens,
custom session tokens (not relying on PHP's builtin sessions support,
which uses a different PRNG yet was also vulnerable until recently),
password reset tokens, passwords, database backup filenames, etc.  If
one of these items is exposed and another is generated later without the
web application or server reseeding the PRNG, then an attack is possible
where the seed is cracked from the item generated earlier and is then
used to infer the unknown item generated later.  For example, if an
application generates (and necessarily exposes) an anti-CSRF token or a
custom session token and then generates (and sends to the target user's
registered e-mail address) a password reset token, then the latter may
be inferred from the former, resulting in compromise of the target
account (such as an admin account).  On web servers that do not
reinitialize PHP (and thus do not reseed the PRNG) across PHP script
invocations, inferring of another user's token or generated password
from the attacker's own token or generated password may be possible as
well, without needing more uses of mt_rand() in the application itself.

As a curiosity, certain website encrypting ransomware misused mt_rand()
as well, and seed cracking enabled quick recovery of the encryption key.


	How to build php_mt_seed.

To build php_mt_seed from source on a system that has GCC and (GNU) make
installed, simply type "make" in its directory.  For example, here's
what this looks like on CentOS 7 running on a i7-4770K CPU (supporting
SIMD instruction sets up to AVX2, so that's what php_mt_seed will use):

    $ make
    gcc -Wall -march=native -mtune=generic -O2 -fomit-frame-pointer -funroll-loops -fopenmp php_mt_seed.c -o php_mt_seed
    php_mt_seed.c:47:2: warning: #warning AVX-512 not enabled. Try gcc -mavx512f (on Intel Knights Landing, Skylake-X, or some newer). [-Wcpp]
     #warning AVX-512 not enabled. Try gcc -mavx512f (on Intel Knights Landing, Skylake-X, or some newer).
      ^

(The warning tells us that a more advanced SIMD instruction set is not
enabled in the build, in this case because the CPU actually lacks it.
These warnings are safe to ignore, but they're sometimes useful in case
non-default compiler flags are used and a SIMD instruction set would be
left disabled inadvertently.)


	How to use php_mt_seed.

php_mt_seed should be run from the command line, with command-line
arguments given to it according to the syntax described below.

Usage of php_mt_seed can be trivial or complex, depending on use case
details.  Here's a trivial usage example:

First generate a "random" number using PHP, e.g. with:

    $ php5 -r 'mt_srand(1234567890); echo mt_rand(), "\n";'
    1328851649

Then run the cracker (in this example, on the same system as we used for
the build above):

    $ time ./php_mt_seed 1328851649
    Pattern: EXACT
    Version: 3.0.7 to 5.2.0
    Found 0, trying 0xfc000000 - 0xffffffff, speed 16261.0 Mseeds/s 
    Version: 5.2.1+
    Found 0, trying 0x1e000000 - 0x1fffffff, speed 91.8 Mseeds/s 
    seed = 0x1fd65f9a = 534142874 (PHP 7.1.0+)
    Found 1, trying 0x26000000 - 0x27ffffff, speed 91.9 Mseeds/s 
    seed = 0x273a3517 = 658126103 (PHP 5.2.1 to 7.0.x; HHVM)
    Found 2, trying 0x48000000 - 0x49ffffff, speed 91.9 Mseeds/s 
    seed = 0x499602d2 = 1234567890 (PHP 5.2.1 to 7.0.x; HHVM)
    seed = 0x499602d2 = 1234567890 (PHP 7.1.0+)
    Found 4, trying 0xfe000000 - 0xffffffff, speed 91.9 Mseeds/s 
    Found 4
    
    real    0m47.028s
    user    6m15.211s
    sys     0m0.015s

php_mt_seed first searches for seeds for the legacy PHP 3.0.7 to 5.2.0,
which it typically completes in a fraction of a second.  Then it
proceeds to search for seeds for PHP 5.2.1 to 7.0.x and for PHP 7.1.0+
simultaneously, which takes a while.

In 47 seconds, it found the original seed (which in this specific case
happens to produce this same mt_rand() output both with PHP 5.2.1 to
7.0.x and with PHP 7.1.0+ due to similarities in their algorithms) and
two other seeds that also produce the same mt_rand output (albeit one
only with PHP 5.2.1 to 7.0.x and the other only with PHP 7.1.0+), and it
searched the rest of the 32-bit seed space (not finding other matches).

For reference, on a 16-core server with two E5-2670 v1 CPUs (supporting
AVX, but not yet AVX2) the same trivial attack completes in 18 seconds,
on Xeon Phi 5110P in 8 seconds, and on Xeon Phi 7290 in 3 seconds.

You'll find a complex usage example further below.


	Command-line syntax.

php_mt_seed expects 1, 2, 4, or more numbers on its command line.  The
numbers specify constraints on mt_rand() outputs.

When invoked with only 1 number, that's the first mt_rand() output to
find seeds for.

When invoked with 2 numbers, those are the bounds (minimum and maximum,
in that order) that the first mt_rand() output should fall within.

When invoked with 4 numbers, the first 2 give the bounds for the first
mt_rand() output and the second 2 give the range passed into mt_rand().

When invoked with 5 or more numbers, each group of 4 and then the last
group of 1, 2, or (usually) 4 are processed as above, where each group
refers to a corresponding mt_rand() output.

Although the syntax above technically requires specification of ranges
when matching multiple mt_rand() outputs, it is also possible to match
exact outputs and/or outputs from mt_rand() without a range specified by
listing the value to match twice (same minimum and maximum) and/or by
listing the range "passed into" mt_rand() as "0 2147483647".  The latter
is assumed to be equivalent to mt_rand() called without a range.  For
example, this matches first mt_rand() output of 1328851649 followed by
second mt_rand() output of 1423851145:

    $ time ./php_mt_seed 1328851649 1328851649 0 2147483647  1423851145
    Pattern: EXACT EXACT
    Version: 3.0.7 to 5.2.0
    Found 0, trying 0xfc000000 - 0xffffffff, speed 15658.7 Mseeds/s 
    Version: 5.2.1+
    Found 0, trying 0x48000000 - 0x49ffffff, speed 91.9 Mseeds/s 
    seed = 0x499602d2 = 1234567890 (PHP 5.2.1 to 7.0.x; HHVM)
    Found 1, trying 0xfe000000 - 0xffffffff, speed 91.9 Mseeds/s 
    Found 1
    
    real    0m47.035s
    user    6m15.273s
    sys     0m0.004s

This is on the same machine as above.  The additional constraint (on the
second mt_rand() output) caused no slowdown, but removed extra seeds
from the output.

It is possible to have php_mt_seed skip (ignore) some mt_rand() outputs
by listing for them 4 numbers that would match any output value.  By
convention, this is typically done by listing "0 0 0 0", which literally
means "the output must be from 0 to 0 as returned by mt_rand(0, 0)", a
condition that is always true.  This is illustrated further below.


	Complex usage example.

Here's a script embedding the vulnerable password generation function
from old versions of Drupal:

    <?php
    function user_password($length = 10) {
      // This variable contains the list of allowable characters for the
      // password. Note that the number 0 and the letter 'O' have been
      // removed to avoid confusion between the two. The same is true
      // of 'I', 1, and l.
      $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    
      // Zero-based count of characters in the allowable list:
      $len = strlen($allowable_characters) - 1;
    
      // Declare the password as a blank string.
      $pass = '';
    
      // Loop the number of times specified by $length.
      for ($i = 0; $i < $length; $i++) {
    
        // Each iteration, pick a random character from the
        // allowable string and append it to the password:
        $pass .= $allowable_characters[mt_rand(0, $len)];
      }
    
      return $pass;
    }
    
    if ($argc === 2) {
      mt_srand($argv[1]);
    }
    
    echo user_password(), "\n";
    echo user_password(), "\n";
    echo user_password(), "\n";
    ?>

Given a password generated by that function, let's try to predict what
password it'd generate next assuming no PRNG seed reset inbetween.
First generate a bunch of passwords for an unknown seed (we let PHP seed
the PRNG automatically, as non-ancient versions do):

    $ php drupal.php
    pAiwtk6Yed
    HW9UPrqKWC
    57CN74bkzL

Let's pretend to know only the first password.  We need to convert it to
inputs to php_mt_seed, which we may do with this script:

    <?php
    $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    $len = strlen($allowable_characters) - 1;
    $pass = $argv[1];
    for ($i = 0; $i < strlen($pass); $i++) {
      $number = strpos($allowable_characters, $pass[$i]);
      echo "$number $number 0 $len  ";
    }
    echo "\n";
    ?>

We don't actually need to look at its output ourselves, but for the sake
of illustration here it is:

    $ php pw2args.php pAiwtk6Yed
    14 14 0 56  25 25 0 56  8 8 0 56  21 21 0 56  18 18 0 56  10 10 0 56  53 53 0 56  47 47 0 56  4 4 0 56  3 3 0 56  

What we actually need is to pass that output to php_mt_seed:

    $ time ./php_mt_seed `php pw2args.php pAiwtk6Yed`
    Pattern: EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57
    Version: 3.0.7 to 5.2.0
    Found 0, trying 0xfc000000 - 0xffffffff, speed 4404.0 Mseeds/s 
    Version: 5.2.1+
    Found 0, trying 0xa0000000 - 0xa1ffffff, speed 210.0 Mseeds/s 
    seed = 0xa1872e34 = 2709990964 (PHP 5.2.1 to 7.0.x; HHVM)
    Found 1, trying 0xfe000000 - 0xffffffff, speed 210.1 Mseeds/s 
    Found 1
    
    real    0m21.441s
    user    11m21.957s
    sys     0m0.034s

This is 21 seconds on the 2x E5-2670 v1 server mentioned above, on
which the trivial attack would run in 18 seconds.   There's some
performance cost from the more advanced constraints on mt_rand() outputs
(and especially from not rejecting unsuitable outputs as quickly as we
would when searching for an exact output value), but in this case it's
not very high.

Now let's recompute this and further passwords from the cracked seed:

    $ php drupal.php 2709990964
    pAiwtk6Yed
    HW9UPrqKWC
    57CN74bkzL

Here we are: it's the same three passwords we had above, out of which we
used only the first one to infer the remaining two.

For even more real-world complexity, what if someone else had already
generated a password using the same instance of PHP (e.g., same mod_php
child process), and the password we know is the second one after
(automatic) seeding?  Let's convert that one to php_mt_seed outputs as
well, and let's also tell php_mt_seed to skip 10 mt_rand() outputs (as
the application would have spent them on the first generated password,
presumably unknown to us) before attempting a match:

    $ time ./php_mt_seed 0 0 0 0  0 0 0 0  0 0 0 0  0 0 0 0  0 0 0 0  0 0 0 0  0 0 0 0  0 0 0 0  0 0 0 0  0 0 0 0  `php pw2args.php HW9UPrqKWC`
    Pattern: SKIP SKIP SKIP SKIP SKIP SKIP SKIP SKIP SKIP SKIP EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57
    Version: 3.0.7 to 5.2.0
    Found 0, trying 0xfc000000 - 0xffffffff, speed 1457.9 Mseeds/s 
    Version: 5.2.1+
    Found 0, trying 0xa0000000 - 0xa1ffffff, speed 128.9 Mseeds/s 
    seed = 0xa1872e34 = 2709990964 (PHP 5.2.1 to 7.0.x; HHVM)
    Found 1, trying 0xfe000000 - 0xffffffff, speed 128.9 Mseeds/s 
    Found 1
    
    real    0m36.288s
    user    19m14.502s
    sys     0m0.031s

We found the same seed as above, and would be able to infer the same
passwords from it just like we did above (both the preceding and the
next password relative to the only one we pretended to know), but now it
took 36 seconds, which is double the time the trivial attack used to
take.  There's much room for optimization here for future versions of
php_mt_seed (and in fact the older php_mt_seed 3.4 would process this
much faster due to it lacking support for PHP 7.1.0+).  Meanwhile, high
and especially unknown SKIP counts are best avoided, which typically can
be done through forcing the web server to allocate a fresh instance of
PHP by setting up many connections (so that more instances of PHP would
be created than the server had running previously) or by crashing an
instance (so that it'd be restarted) via one of many non-security bugs
or resource exhaustion in PHP (but first make sure you're authorized to
do things like that).

The above attack might not have worked against old versions of Drupal
as-is.  There could be reseeding and/or other uses of mt_rand() getting
in the way.  It is just an illustration of how to approach applying
mt_rand() seed cracking in a real-world'ish scenario.


	When extra tools or php_mt_seed changes are needed.

Sometimes applications post-process mt_rand() outputs in ways very
different from what was illustrated above.  It isn't always practical to
write and use tiny scripts like we did above to reverse those tokens,
generated passwords, etc. to mt_rand() output constraints that can be
passed to php_mt_seed.

In simpler ones of those other cases, a pre-existing extra tool can be
used.  For example, if a PHP application exposes md5(mt_rand()) as a
token, then a password hash cracker such as John the Ripper -jumbo or
Hashcat can be used to crack the MD5 hash, retrieving the mt_rand()
output value that can be passed to php_mt_seed.  For example:

    $ php -r 'echo md5(mt_rand()), "\n";' | tee hashfile
    a67d0e9f38d578eefb1720d611211a26
    $ time ./john --format=raw-md5 --incremental=digits --max-length=10 --fork=32 hashfile 2>/dev/null
    Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
    1871584565       (?)
    
    real    0m40.922s
    user    6m41.117s
    sys     0m1.739s
    $ time ./php_mt_seed 1871584565
    Pattern: EXACT
    Version: 3.0.7 to 5.2.0
    Found 0, trying 0x48000000 - 0x4bffffff, speed 24159.2 Mseeds/s 
    seed = 0x4be01ac0 = 1272978112 (PHP 3.0.7 to 5.2.0)
    seed = 0x4be01ac1 = 1272978113 (PHP 3.0.7 to 5.2.0)
    Found 2, trying 0x5c000000 - 0x5fffffff, speed 25725.1 Mseeds/s 
    seed = 0x5fe49e4e = 1608818254 (PHP 3.0.7 to 5.2.0)
    seed = 0x5fe49e4f = 1608818255 (PHP 3.0.7 to 5.2.0)
    Found 4, trying 0xfc000000 - 0xffffffff, speed 28185.7 Mseeds/s 
    Version: 5.2.1+
    Found 4, trying 0x86000000 - 0x87ffffff, speed 234.4 Mseeds/s 
    seed = 0x86d2e002 = 2261966850 (PHP 7.1.0+)
    Found 5, trying 0xc2000000 - 0xc3ffffff, speed 234.5 Mseeds/s 
    seed = 0xc24768d7 = 3259459799 (PHP 5.2.1 to 7.0.x; HHVM)
    seed = 0xc24768d7 = 3259459799 (PHP 7.1.0+)
    Found 7, trying 0xc6000000 - 0xc7ffffff, speed 234.4 Mseeds/s 
    seed = 0xc6d8b812 = 3336091666 (PHP 5.2.1 to 7.0.x; HHVM)
    seed = 0xc6d8b812 = 3336091666 (PHP 7.1.0+)
    Found 9, trying 0xfe000000 - 0xffffffff, speed 234.5 Mseeds/s 
    Found 9
    
    real    0m18.478s
    user    9m48.751s
    sys     0m0.015s
    $ php -r 'mt_srand(3259459799); echo md5(mt_rand()), "\n";'
    a67d0e9f38d578eefb1720d611211a26
    $ php -r 'mt_srand(3336091666); echo md5(mt_rand()), "\n";'
    a67d0e9f38d578eefb1720d611211a26

We found two seeds that generate our observed md5(mt_rand()) token (and
some more that would do it with other versions of PHP).  While both are
correct given what we knew (assuming that we know the PHP version), in a
real-world scenario only one of those would likely allow us to correctly
infer prior and predict further mt_rand() outputs.  That's good enough.

The invocation of JtR is sub-optimal in that it'd search all strings of
up to 10 digits rather than numbers that fit in 31 bits and do not start
with a 0 (except for the number 0).  This can be partially corrected by
splitting the invocation in two:

    $ time ./john --format=raw-md5 --incremental=digits --max-length=9 --fork=32 hashfile 2>/dev/null
    Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
    
    real    0m4.540s
    user    0m43.320s
    sys     0m1.762s
    $ time ./john --format=raw-md5 --mask='[12]?d?d?d?d?d?d?d?d?d' --fork=32 hashfile 2>/dev/null
    Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
    1871584565       (?)
    
    real    0m4.092s
    user    1m58.155s
    sys     0m1.609s

As a slightly trickier example, old eZ Publish used:

    $time   = time();
    $userID = $user->id();
    $hashKey = md5( $userID . ':' . $time . ':' . mt_rand() );

yet this can be cracked similarly, by obtaining the timestamp from the
server itself (such as from the HTTP headers) or assuming synchronized
time and by knowing or cracking the target user ID as well.  The known
portions of the information may be specified in a JtR or Hashcat mask
as-is (e.g., as --mask='100:1503415769:[12]?d?d?d?d?d?d?d?d?d' in the
second invocation of JtR above) and then mt_rand() output extracted from
the cracked "password" and passed into php_mt_seed.

As a harder to handle example, old MediaWiki used:

    function generateToken( $salt = '' ) {
    	$token = dechex( mt_rand() ) . dechex( mt_rand() );
    	return md5( $token . $salt );
    }

Two mt_rand() outputs at once are unlikely to be quickly cracked by a
program not aware of mt_rand() specifics.  This is a case where we'd
need to modify php_mt_seed internals - specifically, introduce recording
of two mt_rand() outputs in php_mt_seed's diff() function and have it
compute MD5 and compare the result against our token value.  Then we'd
invoke php_mt_seed with dummy command-line arguments, but not exactly
arbitrary ones: e.g., "0 1" is non-trivial enough for php_mt_seed to
always call diff() and thus let our added code take over the comparison.

Cracking seeds from old MediaWiki tokens as above is readily supported
as an example exploit in Snowflake, an alternative to php_mt_seed.
However, in general either php_mt_seed or Snowflake would need custom
code written for new cases like this.


	Xeon Phi specifics.

To build php_mt_seed for first generation Xeon Phi (Knights Corner),
install Intel's C compiler and run "make mic".  To run php_mt_seed on
Xeon Phi, copy Intel C compiler's OpenMP runtime library (such as
libiomp5.so) to the Xeon Phi card, e.g. using scp.  Then SSH in to the
card and run a command like:

    $ LD_LIBRARY_PATH=. time ./php_mt_seed 1328851649
    Pattern: EXACT
    Version: 3.0.7 to 5.2.0
    Found 0, trying 0xe0000000 - 0xffffffff, speed 8947.8 Mseeds/s 
    Version: 5.2.1+
    Found 0, trying 0x10000000 - 0x1fffffff, speed 583.6 Mseeds/s 
    seed = 0x1fd65f9a = 534142874 (PHP 7.1.0+)
    Found 1, trying 0x20000000 - 0x2fffffff, speed 583.6 Mseeds/s 
    seed = 0x273a3517 = 658126103 (PHP 5.2.1 to 7.0.x; HHVM)
    Found 2, trying 0x40000000 - 0x4fffffff, speed 586.7 Mseeds/s 
    seed = 0x499602d2 = 1234567890 (PHP 5.2.1 to 7.0.x; HHVM)
    seed = 0x499602d2 = 1234567890 (PHP 7.1.0+)
    Found 4, trying 0xf0000000 - 0xffffffff, speed 586.1 Mseeds/s 
    Found 4
    real    0m 7.82s
    user    30m 17.88s
    sys     0m 1.78s

This is on a Xeon Phi 5110P.

Advanced invocation modes work too, but the performance impact of the
non-vectorized portions of code is higher than it is on regular CPUs:

    $ LD_LIBRARY_PATH=. time ./php_mt_seed 14 14 0 56  25 25 0 56  8 8 0 56  21 21 0 56  18 18 0 56  10 10 0 56  53 53 0 56  47 47 0 56  4 4 0 56  3 3 0 56
    Pattern: EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57 EXACT-FROM-57
    Version: 3.0.7 to 5.2.0
    Found 0, trying 0xe0000000 - 0xffffffff, speed 1824.3 Mseeds/s 
    Version: 5.2.1+
    Found 0, trying 0xa0000000 - 0xafffffff, speed 247.0 Mseeds/s 
    seed = 0xa1872e34 = 2709990964 (PHP 5.2.1 to 7.0.x; HHVM)
    Found 1, trying 0xf0000000 - 0xffffffff, speed 246.9 Mseeds/s 
    Found 1
    real    0m 19.78s
    user    1h 17m 56s
    sys     0m 9.54s

Building and running on second generation Xeon Phi (Knights Landing) and
later doesn't necessarily require a special make target since those
support AVX-512 (which recent gcc supports too) and doesn't necessarily
require any runtime library tricks (perhaps not when Xeon Phi is the
host CPU rather than a coprocessor).  Also, the performance impact of
the non-vectorized portions of code is lower than on first generation.


	PHP version curiosities (mostly unimportant).

While php_mt_seed supports 3 major revisions of PHP's mt_rand()
algorithm and that sort of covers PHP 3.0.7 through 7.1.0+ (up to the
latest as of this writing and probably beyond), the reality is somewhat
trickier than that.  From older versions to newer:

As a mere historical curiosity, php_mt_seed is in fact able to crack
seeds of PHP 3.0.6, which is the very first version that introduced
mt_rand(), but only as long as no range was passed into mt_rand().  That
version had broken support for ranges, and indeed there's no point in
supporting that short-lived breakage in php_mt_seed now.  With this
detail, php_mt_seed has some support for all mt_rand() capable versions
of PHP released so far.

Then there's PHP 3.0.7 through 5.2.0, where Mersenne Twister's state
initialization is with multiples of 69069.  This enables our stateless
implementation to quickly jump to the state array element needed to
compute the first mt_rand() output by using a precomputed value for
69069 raised to the power 396 (mod 2**32), which is MT's M-1.  Another
curiosity of those versions, which we take advantage of too, is that
they treat adjacent even and odd seeds the same, so the effective seed
space is 31-bit.

PHP 3.0.6 to 4.1.2 used a default seed of 4357 (and thus also 4356) if
mt_srand() was not called.  PHP 4.2.0 changed that to automatic seeding
using system time and PHP process ID (still predictable and now also
leaky, but no longer a constant), but there was "Bug #25007 rand &
mt_rand seed RNG every call" until 4.3.3, which presumably affected how
cracked seeds could (not) be used.

PHP 5.2.1 changed MT state initialization to MT authors' new recommended
algorithm, which is no longer linear so we have to compute the first 397
state elements (out of 624) even though in the simplest case we only
need (and only store) the first and last one of those (or we could use a
time-memory trade-off, which we currently don't).

PHP 5.2.1 also introduced a bug into its implementation of MT (use of a
wrong variable, whereas pre-5.2.1 code was correct in that respect).
This bug lets us skip a few operations for every other seed, which we
do, although this optimization is so minor that we could as well not
bother.  PHP 7.1.0 fixed this bug (reverting to pre-5.2.1 code in that
respect, so we use the same logic for pre-5.2.1 and 7.1.0+ there).

In PHP versions from 3.0.7 to 7.0.x, if mt_rand() was called with its
optional output range specified, a 31-bit (0 to 2147483647) MT PRNG
output was scaled to that range using floating-point math.  This meant
that if a range wider than 31-bit was requested on a 64-bit build of
PHP, some values would never occur.  This also meant that even for most
ranges smaller than 31-bit a bias was introduced (some output values
became more likely than others), as compared to MT's raw output (which
was relatively unbiased).

PHP 7.1.0 tried to fix those biases by dropping the floating-point math
and instead mapping the raw 32-bit MT PRNG outputs to the target range
using integer modulo division.  To avoid inherent bias when the target
range isn't a whole power of 2 of possible integer values, a loop was
introduced to skip raw 32-bit PRNG outputs (until a suitable one is
seen) that would result in such bias.  A bug in that code was found and
reported due to work on php_mt_seed.  As it turned out, the loop only
works right in 32-bit builds of PHP, and is ineffective on 64-bit
(except with 64-bit ranges, see below).  Luckily, this actually makes
things simpler for php_mt_seed, and currently php_mt_seed fully supports
the behavior of 64-bit builds only (for ranges up to 0 to 2147483646).

There's currently no intent to add to php_mt_seed the complication of
bias-avoidance of 32-bit builds of PHP 7.1.0+, as well as of 64-bit
builds of future versions where the bug will presumably get fixed.  What
this means in practice is that for 32-bit builds of PHP and future
versions of PHP, php_mt_seed may occasionally find wrong and miss
correct seeds for mt_rand() invoked with a range, but the probability of
this happening is very low except for very wide ranges that are not a
whole power of 2 of possible integer values.  For example, mt_rand(0,
61) or mt_rand(111, 222) are very unlikely to trigger the problem,
mt_rand(0, 255) can't trigger the problem, whereas mt_rand(1000000000,
2000000000) is somewhat likely to trigger it.  Such likely problematic
ranges are probably rarely used and are of little relevance to uses of
php_mt_seed.  Also, supporting this buggy vs. correct behavior would
require treating 32- and 64-bit builds of PHP separately and reporting
on them differently.

PHP 7.1.0 also tried to introduce proper support for 64-bit ranges in
64-bit builds.  It generates two raw 32-bit PRNG outputs to derive one
mt_rand() output when the target range spans more than a 32-bit space.
Unfortunately, the implementation is buggy in a way where it'd introduce
biases into such mt_rand() outputs.  The bug will presumably get fixed
as well, but regardless there's currently no intent to support wider
than 31-bit ranges in php_mt_seed.  This is obscure functionality
(arguably, originally an accidental misfeature, which the PHP developers
didn't really have to make official) that is only available on 64-bit
builds of PHP.  Currently, php_mt_seed does not allow specifying larger
than 31-bit integers on its command line (it will report an error when a
larger value is specified).

Prior to PHP 7.1.0, mt_rand(0, 2147483647) was equivalent to mt_rand()
without a range, and php_mt_seed still assumes so.  This assumption is
no longer valid for PHP 7.1.0+, which means that when searching for
seeds for PHP 7.1.0+ for mt_rand() called with a range specified, you
can specify at most a range one smaller than that, thus "0 2147483646"
being the maximum that php_mt_seed supports for those versions.  This
minor limitation shouldn't matter in practice, except that you might
need to be aware you can continue to specify a range of "0 2147483647"
to indicate that no range was passed into mt_rand().

PHP 7.1.0 also aliased rand() to mt_rand() and srand() to mt_srand().
This means that on one hand you can use php_mt_seed to crack rand()
seeds for PHP 7.1.0+ (since those are also mt_rand() seeds), but on the
other hand this cross-seeding and cross-consumption of random numbers
can affect which attacks work or don't work, and exactly how, against
specific applications that make use of both sets of PHP functions.

PHP 7.1.0 also introduced MT_RAND_PHP as optional second parameter to
mt_srand().  When specified, it correctly enables behavior identical to
that of PHP versions 5.2.1 to 7.0.x.  Thus, seeds that php_mt_seed
reports as valid for 5.2.1 to 7.0.x are always also valid for 7.1.0+
with MT_RAND_PHP, and conversely seeds that php_mt_seed reports as valid
for 7.1.0+ are often invalid for 7.1.0+ with MT_RAND_PHP (except when
the same seeds are also valid for 5.2.1 to 7.0.x, which is common).


	Contact info.

Please check the php_mt_seed homepage for new versions:

    http://www.openwall.com/php_mt_seed/

If you have anything valuable to add or a non-trivial question to ask,
you may contact the author of php_mt_seed at:

    Solar Designer <solar at openwall.com>
