├── .gitignore ├── Makefile ├── test.sh ├── random.js ├── README.md └── random.c /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .*.sw* 3 | /random 4 | node_modules 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | random: random.c 2 | cc random.c -W -Wall -O3 -o random 3 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | randi32() { 5 | dd if=/dev/random bs=4 count=1 2>/dev/null | hexdump -e '/1 "%02X"' 6 | } 7 | 8 | for i in $(seq 1 10); do 9 | MSB=$(randi32) 10 | LSB=$(randi32) 11 | node random.js test $MSB $LSB > node.out 12 | ./random test $MSB $LSB > c.out 13 | if diff node.out c.out; then 14 | echo "Passed test ${i}" 15 | else 16 | echo "Failed test ${i}: ${MSB} ${LSB}" 17 | fi 18 | done 19 | 20 | rm -f node.out 21 | rm -f c.out -------------------------------------------------------------------------------- /random.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | function nextState(rngstate) { 4 | var r0 = (Math.imul(18273, rngstate[0] & 0xFFFF) + (rngstate[0] >>> 16)) | 0 5 | var r1 = (Math.imul(36969, rngstate[1] & 0xFFFF) + (rngstate[1] >>> 16)) | 0 6 | return [r0, r1] 7 | } 8 | 9 | function randomInt32(rngstate) { 10 | var r0 = rngstate[0] 11 | var r1 = rngstate[1] 12 | var x = ((r0 << 16) + (r1 & 0xFFFF)) | 0 13 | return x 14 | } 15 | 16 | function int32ToDouble(x) { 17 | return (x < 0 ? (x + 0x100000000) : x) * 2.3283064365386962890625e-10 18 | } 19 | 20 | function toHex(x) { 21 | var s = (x < 0 ? (x + 0x100000000) : x).toString(16) 22 | return new Array(8 - s.length + 1).join('0') + s 23 | } 24 | 25 | function test(rngstate) { 26 | rngstate = nextState(rngstate) 27 | console.log("Next state: " + toHex(rngstate[0]) + toHex(rngstate[1])) 28 | var x = randomInt32(rngstate) 29 | console.log("Random u32: " + toHex(x)) 30 | console.log("Random double: " + int32ToDouble(x)) 31 | } 32 | 33 | function usage() { 34 | console.log("Usage: ./random.js <\"generate\"|\"test\"> ") 35 | process.exit(1) 36 | } 37 | 38 | if (process.argv.length < 5) { 39 | usage() 40 | } 41 | 42 | var msb = parseInt(process.argv[3], 16) 43 | var lsb = parseInt(process.argv[4], 16) 44 | var rngstate = [msb, lsb] 45 | 46 | switch (process.argv[2]) { 47 | case 'test': 48 | test(rngstate) 49 | break 50 | case 'generate': 51 | for (var i = 0; i < 50; i++) { 52 | rngstate = nextState(rngstate) 53 | console.log(int32ToDouble(randomInt32(rngstate))) 54 | } 55 | break 56 | default: 57 | usage() 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Breaking Node.js 0.12's RNG 2 | > Marius Petcu - 343C4 3 | 4 | An implementation of [Exploiting CSGOJackpot's Weak RNG](https://jonasnick.github.io/blog/2015/07/08/exploiting-csgojackpots-weak-rng/). 5 | 6 | [Another good article explaining how broken Node's RNG was](https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.vj8favdol). 7 | 8 | This was fixed in [V8 4.9.41.0](http://v8project.blogspot.ro/2015/12/theres-mathrandom-and-then-theres.html) 9 | 10 | ## Deconstructing the problem 11 | 12 | Old versions of Node.js used the MWC1616 PRNG for `Math.random()`. This uses a 13 | 64bit state that gets mutated each iteration, then a 32bit integer is derived 14 | from that state, which is then converted to a double by dividing it by `2^32`. 15 | 16 | The article above provided us with a JS implementation of the RNG. We start 17 | by splitting it in 3 standalone stages: 18 | 19 | ```js 20 | nextState(rngstate) 21 | randomInt32(rngstate) 22 | int32ToDouble(x) 23 | ``` 24 | 25 | This allows us to test each function separately as we implement the C versions 26 | of these functions. 27 | 28 | The following invocations should output the results after each of these 3 steps 29 | from both the JS and the C implementations with `0x0123456789ABCDEF` as its 30 | 64bit RNG state: 31 | 32 | ```bash 33 | ./random.js test 01234567 89ABCDEF 34 | ./random test 01234567 89ABCDEF 35 | ``` 36 | 37 | I wrote `./test.sh` to diff the outputs of the 2 implementations on a set of 38 | random RNG states. There are small differences in how C and JS print doubles, 39 | so the outputs are not always exact, but the differences don't seem to be felt 40 | internally. 41 | 42 | ## Cracking the RNG 43 | 44 | We can easily turn a double generated with this RNG back to its original 32bit 45 | integer by multiplying the double with 2^32. The 32bit random integer directly 46 | contains 32bits of the 64bit RNG state, so in order to get the full RNG state, 47 | we need to brute force the remaining 32bits. 48 | 49 | If we have two consecutive random numbers, we can easily check if our guess is 50 | good by simply computing the next RNG with our guessed seed and checking it 51 | against our known number. 52 | 53 | If the two random numbers are an unknown (but known small) number of RNG 54 | iterations apart, we can apply the same tactic and generate random numbers ahead 55 | starting with our seed and see if one of them is our known number. But we run 56 | into problems when we discover that two different seeds can generate the same 57 | two random numbers in sequence (though maybe not the exact same number of 58 | iterations apart). We can eliminate these false positives if we know a third 59 | random number from the sequence. 60 | 61 | Usage: 62 | 63 | ```bash 64 | # For 2 consecutive known numbers: 65 | ./random crack 0.7211675397120416 0.051753338193520904 1 66 | # For 3 known numbers spread over at most 10 iterations: 67 | ./random crack 0.7211675397120416 0.5180133141111583 0.308838497614488 10 68 | ``` 69 | 70 | ## Verification 71 | 72 | We just run the RNG over the seed we found: 73 | 74 | ```bash 75 | ./random.js generate 01234567 89ABCDEF 76 | ./random generate 01234567 89ABCDEF 77 | ``` 78 | -------------------------------------------------------------------------------- /random.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | inline 8 | uint64_t nextState(uint64_t rngState) { 9 | uint32_t r0 = 18273 * (uint32_t)((rngState >> 32) & 0xffff) + (uint32_t)(rngState >> 48); 10 | uint32_t r1 = 36969 * (uint32_t)(rngState & 0xffff) + (uint32_t)((rngState >> 16) & 0xffff); 11 | return (((uint64_t)r0) << 32) | ((uint64_t)r1); 12 | } 13 | 14 | inline 15 | uint32_t randomInt32(uint64_t rngState) { 16 | return (((uint32_t)(rngState >> 16)) & 0xffff0000) | (((uint32_t)rngState) & 0xffff); 17 | } 18 | 19 | inline 20 | double int32ToDouble(uint32_t x) { 21 | return x * 2.3283064365386962890625e-10; 22 | } 23 | 24 | inline 25 | uint32_t doubleToInt32(double x) { 26 | return (uint32_t)round(x * (double)0x100000000LL); 27 | } 28 | 29 | void test(uint64_t rngState) { 30 | rngState = nextState(rngState); 31 | printf("Next state: %016llx\n", rngState); 32 | uint32_t x = randomInt32(rngState); 33 | printf("Random u32: %08x\n", x); 34 | double d = int32ToDouble(x); 35 | printf("Random double: %.17g\n", d); 36 | uint32_t xx = doubleToInt32(d); 37 | if (x != xx) { 38 | printf("Double converted back to u32: %08x\n", xx); 39 | } 40 | } 41 | 42 | int testState(uint64_t rngState, uint32_t *knownInts, int knownIntCount, int maxIterApart) { 43 | for (; maxIterApart; maxIterApart--) { 44 | rngState = nextState(rngState); 45 | if (randomInt32(rngState) == *knownInts) { 46 | knownInts++; 47 | if (!--knownIntCount) { return 1; } 48 | } 49 | } 50 | return 0; 51 | } 52 | 53 | void crack(double firstDouble, double knownDoubles[], int knownDoubleCount, int maxIterApart) { 54 | int found = 0; 55 | uint32_t firstInt = doubleToInt32(firstDouble); 56 | uint64_t knownMask = (((uint64_t)(firstInt >> 16)) << 32) | ((uint64_t)(firstInt & 0xffff)); 57 | 58 | uint32_t knownInts[knownDoubleCount]; 59 | for (int i = 0; i < knownDoubleCount; i++) { 60 | knownInts[i] = doubleToInt32(knownDoubles[i]); 61 | } 62 | 63 | for (uint64_t i = 0; i < 0x100000000LL; i++) { 64 | uint64_t rngState = knownMask | ((i & 0xffff) << 16) | ((i & 0xffff0000) << 32); 65 | if (testState(rngState, knownInts, knownDoubleCount, maxIterApart)) { 66 | printf("Found matching RNG state: %016llx\n", rngState); 67 | found++; 68 | } 69 | } 70 | if (!found) { 71 | printf("Could not brute force RNG state\n"); 72 | } 73 | } 74 | 75 | void usage() { 76 | printf("Usage: ./random <\"generate\"|\"test\"> \n"); 77 | printf(" or: ./random crack \n"); 78 | } 79 | 80 | int main(int argc, const char * argv[]) { 81 | if (argc < 4) { usage(); return 1; } 82 | 83 | if (!strcmp(argv[1], "crack")) { 84 | if (argc < 5) { usage(); return 1; } 85 | double firstDouble; 86 | assert(sscanf(argv[2], "%lf", &firstDouble) == 1); 87 | 88 | int knownDoubleCount = argc - 4; 89 | double knownDoubles[knownDoubleCount]; 90 | for (int i = 0; i < knownDoubleCount; i++) { 91 | assert(sscanf(argv[3 + i], "%lf", knownDoubles + i) == 1); 92 | } 93 | 94 | int maxIterApart; 95 | assert(sscanf(argv[argc - 1], "%d", &maxIterApart) == 1); 96 | 97 | crack(firstDouble, knownDoubles, knownDoubleCount, maxIterApart); 98 | return 0; 99 | } 100 | 101 | uint32_t msb, lsb; 102 | assert(sscanf(argv[2], "%x", &msb) == 1); 103 | assert(sscanf(argv[3], "%x", &lsb) == 1); 104 | uint64_t rngState = (((uint64_t)msb) << 32) | ((uint64_t)lsb); 105 | 106 | if (!strcmp(argv[1], "test")) { 107 | test(rngState); 108 | return 0; 109 | } 110 | 111 | if (!strcmp(argv[1], "generate")) { 112 | for (int i = 0; i < 50; i++) { 113 | rngState = nextState(rngState); 114 | printf("%lf\n", int32ToDouble(randomInt32(rngState))); 115 | } 116 | return 0; 117 | } 118 | 119 | usage(); 120 | return 1; 121 | } --------------------------------------------------------------------------------