├── README.md ├── main.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # v8-randomness-predictor 2 | 3 | Using [z3](https://github.com/Z3Prover/z3) to predict `Math.random` in [v8](https://v8.dev) 4 | 5 | ## YouTube video 6 | 7 |

8 | 9 | PwnFunction YouTube Video 10 | 11 |

12 | 13 | Watch the [✨ YouTube Video](https://www.youtube.com/watch?v=-h_rj2-HP2E) 14 | 15 | ## Run Instructions 16 | 17 | Get a few Random numbers from v8, run to following code in [d8](https://v8.dev/docs/d8), [nodejs](https://nodejs.org/) or [chrome](https://www.google.com/chrome/). 18 | 19 | ```js 20 | Array.from(Array(5), Math.random) 21 | ``` 22 | 23 | Optionally you can set the random seed in nodejs so you'd get the same numbers as shown below. 24 | ```js 25 | /* 26 | * Run nodejs with `--random_seed` flag like 27 | * node --random_seed=1337 28 | */ 29 | Array.from(Array(5), Math.random) 30 | // [0.9311600617849973, 0.3551442693830502, 0.7923158995678377, 0.787777942408997, 0.376372264303491] 31 | ``` 32 | 33 | Next we feed these random numbers into the python script (line 23). 34 | 35 | ```py 36 | sequence = [ 37 | 0.9311600617849973, 38 | 0.3551442693830502, 39 | 0.7923158995678377, 40 | 0.787777942408997, 41 | 0.376372264303491, 42 | ][::-1] 43 | ``` 44 | 45 | Run the script. 46 | 47 | ```sh 48 | $ python3 main.py 49 | 50 | # Outputs 51 | # {'se_state1': 6942842836049070467, 'se_state0': 4268050313212552111} 52 | # 0.23137147109312428 53 | ``` 54 | 55 | ## Resources 56 | - [Learn z3 by solving simple challenges](https://github.com/PwnFunction/learn-z3) 57 | - [There’s Math.random(), and then there’s Math.random()](https://v8.dev/blog/math-random) 58 | - [Further scramblings of Marsaglia’s xorshift generators](https://vigna.di.unimi.it/ftp/papers/xorshiftplus.pdf) 59 | - [(V8 Deep Dives) Random Thoughts on Math.random()](https://apechkurov.medium.com/v8-deep-dives-random-thoughts-on-math-random-fb155075e9e5) 60 | - [Hacking the JavaScript Lottery](https://blog.securityevaluators.com/hacking-the-javascript-lottery-80cc437e3b7f) 61 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import z3 3 | import struct 4 | import sys 5 | 6 | 7 | """ 8 | Solving for seed states in XorShift128+ used in V8 9 | > https://v8.dev/blog/math-random 10 | > https://apechkurov.medium.com/v8-deep-dives-random-thoughts-on-math-random-fb155075e9e5 11 | > https://blog.securityevaluators.com/hacking-the-javascript-lottery-80cc437e3b7f 12 | 13 | > Tested on Chrome(102.0.5005.61) or Nodejs(v18.2.0.) 14 | """ 15 | 16 | """ 17 | Plug in a handful random number sequences from node/chrome 18 | > Array.from(Array(5), Math.random) 19 | 20 | (Optional) In node, we can specify the seed 21 | > node --random_seed=1337 22 | """ 23 | sequence = [ 24 | 0.9311600617849973, 25 | 0.3551442693830502, 26 | 0.7923158995678377, 27 | 0.787777942408997, 28 | 0.376372264303491, 29 | # 0.23137147109312428 30 | ] 31 | 32 | """ 33 | Random numbers generated from xorshift128+ is used to fill an internal entropy pool of size 64 34 | > https://github.com/v8/v8/blob/7a4a6cc6a85650ee91344d0dbd2c53a8fa8dce04/src/numbers/math-random.cc#L35 35 | 36 | Numbers are popped out in LIFO(Last-In First-Out) manner, hence the numbers presented from the entropy pool are reveresed. 37 | """ 38 | sequence = sequence[::-1] 39 | 40 | solver = z3.Solver() 41 | 42 | """ 43 | Create 64 bit states, BitVec (uint64_t) 44 | > static inline void XorShift128(uint64_t* state0, uint64_t* state1); 45 | > https://github.com/v8/v8/blob/a9f802859bc31e57037b7c293ce8008542ca03d8/src/base/utils/random-number-generator.h#L119 46 | """ 47 | se_state0, se_state1 = z3.BitVecs("se_state0 se_state1", 64) 48 | 49 | for i in range(len(sequence)): 50 | """ 51 | XorShift128+ 52 | > https://vigna.di.unimi.it/ftp/papers/xorshiftplus.pdf 53 | > https://github.com/v8/v8/blob/a9f802859bc31e57037b7c293ce8008542ca03d8/src/base/utils/random-number-generator.h#L119 54 | 55 | class V8_BASE_EXPORT RandomNumberGenerator final { 56 | ... 57 | static inline void XorShift128(uint64_t* state0, uint64_t* state1) { 58 | uint64_t s1 = *state0; 59 | uint64_t s0 = *state1; 60 | *state0 = s0; 61 | s1 ^= s1 << 23; 62 | s1 ^= s1 >> 17; 63 | s1 ^= s0; 64 | s1 ^= s0 >> 26; 65 | *state1 = s1; 66 | } 67 | ... 68 | } 69 | """ 70 | se_s1 = se_state0 71 | se_s0 = se_state1 72 | se_state0 = se_s0 73 | se_s1 ^= se_s1 << 23 74 | se_s1 ^= z3.LShR(se_s1, 17) # Logical shift instead of Arthmetric shift 75 | se_s1 ^= se_s0 76 | se_s1 ^= z3.LShR(se_s0, 26) 77 | se_state1 = se_s1 78 | 79 | """ 80 | IEEE 754 double-precision binary floating-point format 81 | > https://en.wikipedia.org/wiki/Double-precision_floating-point_format 82 | > https://www.youtube.com/watch?v=p8u_k2LIZyo&t=257s 83 | 84 | Sign (1) Exponent (11) Mantissa (52) 85 | [#] [###########] [####################################################] 86 | """ 87 | 88 | """ 89 | Pack as `double` and re-interpret as unsigned `long long` (little endian) 90 | > https://stackoverflow.com/a/65377273 91 | """ 92 | float_64 = struct.pack("d", sequence[i] + 1) 93 | u_long_long_64 = struct.unpack(" https://github.com/v8/v8/blob/a9f802859bc31e57037b7c293ce8008542ca03d8/src/base/utils/random-number-generator.h#L111 126 | 127 | static inline double ToDouble(uint64_t state0) { 128 | // Exponent for double values for [1.0 .. 2.0) 129 | static const uint64_t kExponentBits = uint64_t{0x3FF0000000000000}; 130 | uint64_t random = (state0 >> 12) | kExponentBits; 131 | return base::bit_cast(random) - 1; 132 | } 133 | """ 134 | u_long_long_64 = (state0 >> 12) | 0x3FF0000000000000 135 | float_64 = struct.pack("