├── mathrandomcrack ├── __init__.py ├── xs128.py ├── xs128crack.py ├── mathrandom.py ├── __main__.py └── mathrandomcrack.py ├── samples ├── scaled_values.txt ├── doubles.txt └── bounds.txt ├── LICENSE ├── tests ├── test_xs128.py ├── test_xs128crack.py ├── test_mathrandom.py └── test_mathrandomcrack.py ├── .gitignore └── README.md /mathrandomcrack/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/scaled_values.txt: -------------------------------------------------------------------------------- 1 | # Generated with Math.floor(Math.random() * 36) 2 | #21 3 | #1 4 | #22 5 | #4 6 | #5 7 | 15 8 | 30 9 | 23 10 | 22 11 | 9 12 | 14 13 | 29 14 | 26 15 | 28 16 | 28 17 | 1 18 | 25 19 | 24 20 | 2 21 | 35 22 | 27 23 | 26 24 | 13 25 | 32 26 | 27 27 | 18 28 | 5 29 | 15 30 | 14 31 | 1 32 | 34 33 | 18 34 | 29 35 | 31 36 | 31 37 | 32 38 | 0 39 | 26 40 | 31 41 | 10 42 | 4 43 | 15 44 | 25 45 | 31 46 | 2 47 | 17 48 | 1 49 | 33 50 | 21 51 | 8 52 | 26 53 | 34 54 | 18 55 | 31 56 | 30 57 | 35 58 | 7 59 | 23 60 | 7 61 | 9 62 | 22 63 | 5 64 | 13 65 | 24 66 | 30 67 | 22 68 | 28 69 | 0 70 | 20 71 | 6 72 | 35 73 | 19 74 | 4 75 | 26 76 | 23 77 | 15 78 | 1 79 | 28 80 | 31 81 | 23 82 | 12 83 | 25 84 | 33 85 | 33 86 | 13 87 | 1 88 | 30 89 | 12 90 | 0 91 | 25 92 | 33 93 | 11 94 | 24 95 | 29 96 | 3 97 | #20 98 | #29 99 | #1 100 | #22 101 | #20 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nicolas Stroppa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_xs128.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mathrandomcrack.xs128 import * 4 | 5 | class TestXS128(unittest.TestCase): 6 | 7 | def test_xs128(self): 8 | state0, state1 = 12092933408070727569, 7218780437263453395 9 | for _ in range(100): 10 | state0, state1 = xs128(state0, state1) 11 | self.assertEqual(state0, 5753612509715215338) 12 | self.assertEqual(state1, 17782382993159823008) 13 | 14 | def test_reverse_xs128(self): 15 | state0, state1 = 5753612509715215338, 17782382993159823008 16 | for _ in range(100): 17 | state0, state1 = reverse_xs128(state0, state1) 18 | self.assertEqual(state0, 12092933408070727569) 19 | self.assertEqual(state1, 7218780437263453395) 20 | 21 | def test_xs128_both(self): 22 | init_state0, init_state1 = 5753612509715215338, 17782382993159823008 23 | state0, state1 = init_state0, init_state1 24 | for _ in range(1000): 25 | state0, state1 = xs128(state0, state1) 26 | for _ in range(1000): 27 | state0, state1 = reverse_xs128(state0, state1) 28 | self.assertEqual(state0, init_state0) 29 | self.assertEqual(state1, init_state1) 30 | -------------------------------------------------------------------------------- /samples/doubles.txt: -------------------------------------------------------------------------------- 1 | 0.19183671868484475 2 | 0.858811120014111 3 | 0.9063805354813248 4 | 0.48676256058369116 5 | 0.691663878744965 6 | 7 | 0.715675498961374 8 | 0.06677992235138475 9 | 0.08965941558795398 10 | 0.759974094759373 11 | 0.14480399407323585 12 | 0.740011138341935 13 | 0.8696151110340634 14 | 15 | 0.1866023087428561 16 | 0.47688057750922963 17 | 0.7526524763016808 18 | 0.3632219910019642 19 | 0.6563598161277802 20 | 0.9920953554149312 21 | 0.5858598649575073 22 | 23 | 24 | 25 | 0.22046365366943022 26 | 0.3793138794574411 27 | 0.5142882888952846 28 | 0.426654286470844 29 | 0.019097914196636423 30 | 0.5995260405472022 31 | 0.4492697634742129 32 | 0.9683244366273637 33 | 0.5611170582748722 34 | 0.1239623315532361 35 | 0.9237120171060085 36 | 0.7359548312042391 37 | 0.44031073183567493 38 | #0.3826651187438035 39 | #0.7375710819016986 40 | #0.21863364189462264 41 | #0.8093784172561101 42 | #0.6964292873208223 43 | #0.6272218068697356 44 | #0.6100738714256938 45 | #0.25258737620214433 46 | #0.3696190724647912 47 | #0.5292798994490209 48 | #0.8268698501859357 49 | #0.9828846078837908 50 | #0.3868049733398491 51 | #0.4266558837314879 52 | #0.9122650510230678 53 | #0.5003907702243587 54 | #0.23584155270424045 55 | #0.9514123573719363 56 | #0.22782742499359243 57 | #0.7664600675561356 58 | #0.6154774892707892 59 | #0.5012624086444047 60 | #0.9869108057963876 61 | -------------------------------------------------------------------------------- /tests/test_xs128crack.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mathrandomcrack.xs128crack import * 4 | 5 | class TestXS128Crack(unittest.TestCase): 6 | 7 | def test_recover_seed_from_known_bits(self): 8 | # Pick a random seed 9 | seed0, seed1 = 12092933408070727569, 7218780437263453395 10 | state0, state1 = seed0, seed1 11 | 12 | # Generate random integers using xs128 13 | random_integers = [] 14 | for i in range(8): 15 | state0, state1 = xs128(state0, state1) 16 | random_integers.append(state0) 17 | 18 | # Only keep a few bits of some integers 19 | known_states_bits = [] 20 | for i, n in enumerate(random_integers): 21 | if i == 3: 22 | # Skip unknown integer 23 | state = [None for _ in range(64)] 24 | else: 25 | # Keep only 18 bits 26 | state = [None for _ in range(20)] # Unknown LSBs 27 | state.extend([(n >> j) & 1 for j in range(20, 38)]) # Known bits 28 | state.extend([None for _ in range(38, 64)]) # Unknown MSBs 29 | known_states_bits.append(state) 30 | 31 | # Try to recover the right seed 32 | found_correct_seed = False 33 | for rec_seed0, rec_seed1 in recover_seed_from_known_bits(known_states_bits): 34 | found_correct_seed = rec_seed0 == seed0 and rec_seed1 == seed1 35 | if found_correct_seed: 36 | break 37 | self.assertTrue(found_correct_seed) 38 | 39 | -------------------------------------------------------------------------------- /mathrandomcrack/xs128.py: -------------------------------------------------------------------------------- 1 | STATE_SIZE = 128 2 | HALF_STATE_SIZE = STATE_SIZE // 2 3 | 4 | def xs128(state0, state1): 5 | """ 6 | Execute XorShift128 and return the new 128-bit state. 7 | 8 | Arguments: 9 | state0, state1: integers or objects that can represent 64-bit integers. 10 | 11 | See also: https://github.com/v8/v8/blob/14.3.21/src/base/utils/random-number-generator.h#L121 12 | """ 13 | mask = (1 << HALF_STATE_SIZE) - 1 14 | s1 = state0 & mask 15 | s0 = state1 & mask 16 | s1 ^= (s1 << 23) & mask 17 | s1 ^= (s1 >> 17) & mask 18 | s1 ^= s0 19 | s1 ^= (s0 >> 26) & mask 20 | return s0, s1 21 | 22 | def reverse_xs128(state0, state1): 23 | """ 24 | Reverse the execution of XorShift128 and return the previous 128-bit state. 25 | 26 | Arguments: 27 | state0, state1: 64-bit integers. 28 | """ 29 | mask = (1 << HALF_STATE_SIZE) - 1 30 | s0 = state1 31 | s1 = state0 32 | s0 ^= s1 33 | s0 ^= (s1 >> 26) & mask 34 | s0 = reverse_xor_rshift(s0, 17) 35 | s0 = reverse_xor_lshift(s0, 23) 36 | return s0, s1 37 | 38 | # Helper functions to reverse operations used in XorShift128 39 | # https://stackoverflow.com/questions/31513168/finding-inverse-operation-to-george-marsaglias-xorshift-rng/31515396#31515396 40 | def reverse_xor_lshift(y, shift): 41 | x = y & ((1 << shift) - 1) 42 | for i in range(HALF_STATE_SIZE - shift): 43 | x |= (1 if bool(x & (1 << i)) ^ bool(y & (1 << (shift + i))) else 0) << (shift + i) 44 | return x 45 | def reverse_bin(x): 46 | return int(bin(x)[2:].rjust(HALF_STATE_SIZE, '0')[::-1], 2) 47 | def reverse_xor_rshift(y, shift): 48 | return reverse_bin(reverse_xor_lshift(reverse_bin(y), shift)) 49 | 50 | -------------------------------------------------------------------------------- /samples/bounds.txt: -------------------------------------------------------------------------------- 1 | 0.1550547269856864 0.1550657269856864 2 | 0.36076808722166837 0.36077908722166835 3 | 0.2750934784681825 0.2751044784681825 4 | 0.7467877577342982 0.7467987577342982 5 | 0.12168431447576385 0.12169531447576384 6 | 0.23599740562656396 0.23600840562656397 7 | 0.37857966162993 0.37859066162993 8 | 0.48420325136760384 0.4842142513676038 9 | 0.889654885062705 0.8896658850627049 10 | 0.36964129085966496 0.36965229085966494 11 | 0.5650585046720021 0.5650695046720021 12 | 0.4960353142222687 0.4960463142222687 13 | 0.6395173571486127 0.6395283571486127 14 | 0.984979420243275 0.984990420243275 15 | 0.5988660920582382 0.5988770920582381 16 | 0.27721622622931036 0.27722722622931034 17 | 0.8690863991880949 0.8690973991880949 18 | 0.5944565673522152 0.5944675673522152 19 | 0.11243527744379496 0.11244627744379496 20 | 0.3501869414083097 0.3501979414083097 21 | 0.16542322019834815 0.16543422019834816 22 | 0.289486448769774 0.28949744876977396 23 | 0.360576194341443 0.360587194341443 24 | 0.12980524663434628 0.1298162466343463 25 | 0.294197240523795 0.29420824052379496 26 | 0.21372455080495484 0.21373555080495485 27 | 0.2807228972126466 0.2807338972126466 28 | 0.4473236953326197 0.44733469533261966 29 | 0.41574364731514574 0.4157546473151457 30 | 0.9931956547406141 0.9932066547406141 31 | 0.2551487622114293 0.2551597622114293 32 | 0.12470647606725412 0.12471747606725411 33 | 0.49435153480414146 0.49436253480414144 34 | 0.25517359252708866 0.25518459252708864 35 | 0.4891165942327825 0.48912759423278246 36 | 0.4764911396361016 0.4765021396361016 37 | 0.750710831048312 0.750721831048312 38 | 0.330684070778506 0.33069507077850596 39 | 0.021444163814603966 0.021455163814603967 40 | 0.9601739352852533 0.9601849352852533 41 | 0.7724577573143195 0.7724687573143195 42 | 0.5941979611431002 0.5942089611431002 43 | 0.9856356078361667 0.9856466078361666 44 | 0.11552102446126906 0.11553202446126906 45 | 0.6169200657627848 0.6169310657627848 46 | 0.49942268282869656 0.49943368282869655 47 | 0.5990690703376585 0.5990800703376585 48 | 0.20233105982492955 0.20234205982492956 49 | 0.800376238129994 0.800387238129994 50 | 0.8638227280329499 0.8638337280329499 51 | 0.440821177496958 0.44083217749695797 52 | 0.07187783112171038 0.07188883112171038 53 | 0.35910360072280284 0.3591146007228028 54 | 0.227551408553256 0.22756240855325602 55 | 0.8839512621319566 0.8839622621319566 56 | 0.2868045189258866 0.2868155189258866 57 | 0.8827206380791469 0.8827316380791469 58 | 0.0010325638502664647 0.0010435638502664646 59 | 0.8853711040004807 0.8853821040004807 60 | 0.4112433981307467 0.41125439813074666 61 | 0.09089728902498136 0.09090828902498135 62 | 0.8824822591036857 0.8824932591036857 63 | 0.6675028174480692 0.6675138174480691 64 | 0.4836447602207324 0.48365576022073237 65 | 0.14424026898822812 0.14425126898822813 66 | 0.2501631064224808 0.2501741064224808 67 | 0.1361017690080904 0.1361127690080904 68 | 0.9332792355730194 0.9332902355730194 69 | 0.9295423323879844 0.9295533323879844 70 | 0.051837106949856944 0.05184810694985695 71 | 0.39692743231918454 0.3969384323191845 72 | 0.2643863300927594 0.2643973300927594 73 | 0.07086662261278794 0.07087762261278793 74 | 0.8715767468488759 0.8715877468488759 75 | 0.15712773912099803 0.15713873912099804 76 | 0.44768770164254185 0.44769870164254183 77 | #0.4784987261586638 0.4785097261586638 78 | #0.1782348465286095 0.1782458465286095 79 | #0.32040108880484525 0.32041208880484523 80 | #0.3614379466939979 0.3614489466939979 81 | #0.45723993737530827 0.45725093737530825 82 | #0.36807508248083526 0.36808608248083524 83 | #0.8028671759669009 0.8028781759669009 84 | #0.21660909147151092 0.21662009147151093 85 | #0.5451193519889125 0.5451303519889125 86 | #0.16419645589994963 0.16420745589994964 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | *.swp 163 | -------------------------------------------------------------------------------- /tests/test_mathrandom.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mathrandomcrack.mathrandom import * 4 | 5 | class TestMathRandom(unittest.TestCase): 6 | 7 | def test_v8_conversion(self): 8 | integer = 12092933408070727569 9 | expected = (integer >> 11) << 11 10 | for _ in range(10): 11 | double = v8_to_double(integer) 12 | integer = v8_from_double(double) 13 | self.assertEqual(integer, expected) 14 | 15 | def test_math_random_generation(self): 16 | math_random = MathRandom(6770692079143846949, 12009346246601641483) 17 | 18 | expected_doubles = [0.9742385602746879, 0.1481511886521042, 0.543673556132151, 0.776554156122268, 0.4070774492946395, 0.7285329198840588, 0.13973926820201366, 0.5736421400773356, 0.41667400100545327, 0.8912806642976514, 0.11204285276345027, 0.18404040839430247, 0.4782971487186798, 0.3796521669067089, 0.9892012954170081, 0.9697552194357352, 0.6966167349179451, 0.2634146257689207, 0.3489983866126112, 0.4393039244730844, 0.15723657672322866, 0.5732697001502722, 0.3363608031550396, 0.7090700200634418, 0.5286959878991273, 0.5078287948200143, 0.022943616157470648, 0.6551553161820237, 0.6284896147026541, 0.49998694315552816, 0.8649034511502789, 0.2629120209514204, 0.9002783858969124, 0.1397501858599427, 0.9039929075269798, 0.4576989046783827, 0.0947956489455567, 0.0595068197374502, 0.32746163228123415, 0.525262815224562, 0.9983071285287053, 0.7167870768352507, 0.8539612975205607, 0.7832119042919544, 0.08080137087888317, 0.5429897714329568, 0.7182961889994531, 0.3845317804144778, 0.002689699239920018, 0.13849809845172667, 0.287211348423441, 0.9750709028089097, 0.6836490775957172, 0.15267651599511545, 0.2787737619575896, 0.36268678147691835, 0.7738687831896665, 0.5913748804621017, 0.9831558069129928, 0.2777178491468483, 0.24657982760817454, 0.44016862318954464, 0.6962575901069894, 0.6510279645348068, 0.18836880033300052, 0.0748307177793821, 0.4195148090178126, 0.9893147372910016, 0.9724940577652632, 0.27999797561476003, 0.045785749739355786, 0.25908804604627256, 0.7005048965791644, 0.8650250641910667, 0.15841797304189253, 0.0693845619662874, 0.22354602835451864, 0.749507020763947, 0.3942969898144476, 0.21970616423971467, 0.11505475720713965, 0.6983446588496263, 0.8421662413364761, 0.023837970228779426, 0.12094459657302115, 0.3560306149463036, 0.15326170759886626, 0.4111046889573545, 0.03172794269168455, 0.26407934852612724, 0.43596030963609644, 0.9878040332714854, 0.1598127761728073, 0.23412524904062348, 0.24474634864855804, 0.43116407616041363, 0.5003629703143654, 0.6161629500777546, 0.11286711753883838, 0.23012062706659164, 0.12049784922614071, 0.1502940665384579, 0.9016579233326607, 0.1398769483861888, 0.393467024739662, 0.7898019017974195, 0.4022731522295312, 0.636409289263191, 0.19863712358599805, 0.6429054982279861, 0.006428908515869414, 0.7102661211473985, 0.41978405370899907, 0.9091906800788416, 0.4284484303075681, 0.5532118123691919, 0.01110623911911679, 0.5393619562300278, 0.5910370496750068, 0.19992532509028305, 0.13646179743046583, 0.7320738633431952, 0.7444229307963395, 0.8067643397728855, 0.6872848122935007, 0.6398155403789845, 0.48270362227304187, 0.8887545149146033, 0.4141369758905703, 0.9334605370967346, 0.6860794030671996, 0.13833765790260422, 0.0335521122295287, 0.6528964698535102, 0.5726281689229864, 0.3201584090821209, 0.6953015861164284, 0.666293894218843, 0.09316123123440401, 0.24139207624983194, 0.43899301926189094, 0.1509196064590388, 0.6271246626427348, 0.20570167270677597, 0.405849093465942, 0.879649126199882, 0.24834474608144763, 0.3380864300698956, 0.13176730677676862, 0.21226397221017068, 0.3862976380787799, 0.7537788843128865, 0.7542818053826482, 0.9364917195977182, 0.7003610869573811, 0.05161301128391971, 0.7859057743573763, 0.6792454020559878, 0.6397087308902907, 0.02828526921307495] 19 | 20 | # Test forward generation 21 | for d in expected_doubles: 22 | self.assertEqual(d, math_random.next()) 23 | 24 | # Test backward generation 25 | for d in expected_doubles[::-1]: 26 | self.assertEqual(d, math_random.previous()) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mathrandomcrack 2 | 3 | Cracking JavaScript V8 `Math.random()` and xs128 from arbitrary bit leaks. 4 | 5 | This is largely inspired by https://imp.ress.me/blog/2023-04-17/plaidctf-2023#fastrology. 6 | 7 | *Disclaimer: This tool is made for educational purposes only. Please don't use it to break into stuff.* 8 | 9 | ## What is this? 10 | 11 | This tool can be used to recover the internal state of the V8 implementation of `Math.random()` knowing enough bits of randomly generated values. The values don't have to be successive and the position of the known bits can vary. 12 | 13 | You can use the CLI to recover all the possibles internal states of `Math.random()` in one of the following situations: 14 | 15 | - You know more than 4 values generated by `Math.random()`. 16 | - You know *enough* integer values generated by `Math.floor(Math.random() * k + b)`. The larger k, the less values you need. 17 | - You know *enough* approximation of values generated by `Math.random()`, meaning you know bounds for each value and therefore know a few bits of each value. The better the bounds, the less values you need. 18 | 19 | After the internal state is recovered, this tool is also able to predict all the next and previous returned values of `Math.random()`. 20 | 21 | ## How do I protect my applications against this? 22 | 23 | Never use `Math.random()` for anything related to security. If someone recovering randomly generated values is bad for your application, use the cryptographically secure [Crypto.getRandomValues()](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) method instead. 24 | 25 | ## I just want to run it 26 | 27 | You should have Python3 and [Sage](https://doc.sagemath.org/html/en/installation/index.html) installed. 28 | 29 | Example usages: 30 | 31 | ```console 32 | $ # Generate the 10 next values from known consecutive outputs of Math.random() 33 | $ python3 -m mathrandomcrack --method doubles --next 10 ./samples/doubles.txt 34 | Found a possible Math.random internal state 35 | Predicted next 10 values: [0.3826651187438035, 0.7375710819016986, 0.21863364189462264, 0.8093784172561101, 0.6964292873208223, 0.6272218068697356, 0.6100738714256938, 0.25258737620214433, 0.3696190724647912, 0.5292798994490209] 36 | 37 | $ # Generate the 5 previous and 5 next integers from known consecutive values of Math.floor(Math.random() * 36) 38 | $ python3 -m mathrandomcrack --method scaled --next 5 --previous 5 --factor 36 --output-fmt scaled ./samples/scaled_values.txt 39 | Found a possible Math.random internal state 40 | Predicted previous 5 values: [21, 1, 22, 4, 5] 41 | Predicted next 5 values: [20, 29, 1, 22, 20] 42 | ``` 43 | 44 | The `samples` directory contains example files for various use cases. There should be one leaked value of `Math.random()` per line and it is possible to use an empty line to represent an unknown output of `Math.random()`. 45 | 46 | For more information about the CLI, you can run `python3 -m mathrandomcrack --help`. 47 | 48 | ## I have a more complex use case 49 | 50 | If you manage to leak enough bits (> 120) from multiple `Math.random()` outputs, you can directly use the `recover_state_from_math_random_known_bits` function in `mathrandomcrack.py` to recover the initial internal state of `Math.random()`. 51 | 52 | You can also try to use `recover_seed_from_known_bits` in `xs128crack.py` if you just want the XorShift128 state and don't care about `Math.random()` stuff. 53 | 54 | ## How does it work? 55 | 56 | `Math.random()` is defined as a function that returns pseudo-random numbers between 0 and 1 and does not provide cryptographically secure random numbers. Under the hood, in V8 (the JavaScript engine used by Chrome and NodeJS), random numbers are generated using the fast, reversible, seed-based, deterministic PRNG called [XorShift128](https://github.com/v8/v8/blob/14.3.21/src/base/utils/random-number-generator.h#L121). 57 | 58 | ### Cracking XorShift128 initial state 59 | 60 | XorShift128 is a simple PRNG with a 128-bit internal state. It uses only XOR and shift operations and the transition from a state to the next is linear. Each bit of a XorShift128 state can be expressed as a linear combination of pre-determined bits of the initial state. Knowing enough total bits from arbitrary states, the initial state can be recovered through basic linear algebra in GF(2). 61 | 62 | Initial XorShift128 state recovery from known bits is implemented in `xs128crack.py`. 63 | 64 | *Note: many other projects like [v8_rand_buster](https://github.com/d0nutptr/v8_rand_buster/tree/master) use symbolic execution with z3 to recover the initial state but this is not viable if your known bits are scattered over too many states.* 65 | 66 | ### Recovering Math.random() internal state 67 | 68 | `Math.random()` doesn't directly output XorShift128 random values. It generates [a cache](https://github.com/v8/v8/blob/14.3.21/src/numbers/math-random.cc#L35) of 64 values at a time and returns them one by one in reverse order. This makes the internal state recovery a little tricky because we have to account for the initial position of the cache index for the first known state. If only a few outputs are known (< 64), there will be up to 64 possible internal states due to the unknown position of the cache index. 69 | 70 | Internal state recovery is implemented in `mathrandomcrack.py`. 71 | 72 | ## Tests 73 | 74 | You can run the tests to make sure everything works using [unittest](https://docs.python.org/3/library/unittest.html). 75 | 76 | ```console 77 | $ python3 -m unittest discover -s tests 78 | ``` 79 | 80 | ## References 81 | 82 | - https://blog.securityevaluators.com/hacking-the-javascript-lottery-80cc437e3b7f - "Hacking the JavaScript Lottery" article from 2016. 83 | - https://security.stackexchange.com/a/123554 - "Predicting Math.random() numbers?" question on StackExchange. 84 | - https://github.com/d0nutptr/v8_rand_buster/tree/master - Another implementation to crack XorShift128 using z3. 85 | - https://imp.ress.me/blog/2023-04-17/plaidctf-2023#fastrology - The writeups of 2023 PlaidCTF challenges by @y4n that inspired me to make this tool. 86 | -------------------------------------------------------------------------------- /mathrandomcrack/xs128crack.py: -------------------------------------------------------------------------------- 1 | from .xs128 import * 2 | 3 | import logging 4 | from sage.all import Matrix, GF 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | # Should be a lot more than enough to get only one solution to any system (i think...) 9 | # Avoids the solver DOSing itself with too many equations 10 | MAX_EQUATIONS = 10000 11 | 12 | class StateBitDeps(): 13 | """ 14 | A class that represents a 64-bit state0 of xs128 relatively to an initial 128-bit state. 15 | Each bit of the current xs128 state0 can be expressed as a subset sum of the initial state bits. 16 | 17 | Attributes: 18 | data: A 64-long list of 128 bit integers that represents dependency to the initial 128-bit state. 19 | data[i] represents a bitmask of all the bits from the initial state to sum together to obtain the i-th bit of the current state. 20 | """ 21 | def __init__(self, data): 22 | assert len(data) == HALF_STATE_SIZE 23 | self.data = data 24 | 25 | def __and__(self, mask): 26 | if mask != (1 << HALF_STATE_SIZE) - 1: 27 | raise NotImplementedError("StateBitDeps & mask not implemented") 28 | return StateBitDeps(self.data) 29 | 30 | def __xor__(self, other): 31 | return StateBitDeps([self.data[i] ^ other.data[i] for i in range(HALF_STATE_SIZE)]) 32 | 33 | def __lshift__(self, shift): 34 | # Shifting left makes the less significant bits zero 35 | return StateBitDeps([0 for _ in range(shift)] + self.data[:-shift]) 36 | 37 | def __rshift__(self, shift): 38 | # Shifting right makes the most significant bits zero 39 | return StateBitDeps(self.data[shift:] + [0 for _ in range(shift)]) 40 | 41 | def to_coeff(self, index): 42 | """ 43 | Convert the bitmask of the index-th bit to a list of coefficients in GF(2). 44 | """ 45 | return [int((self.data[index] >> i) & 1) for i in range(STATE_SIZE)] 46 | 47 | class StateEquation(): 48 | """ 49 | A class that represents a linear equation with 128 unknowns with values in GF(2). 50 | 51 | Attributes: 52 | coefficients: 128 coefficients in GF(2). 53 | 54 | result: the result of the equation in GF(2). 55 | """ 56 | def __init__(self, coefficients, result): 57 | assert len(coefficients) == STATE_SIZE 58 | assert all(c in [0, 1] for c in coefficients) 59 | assert result in [0, 1] 60 | self.coefficients = coefficients 61 | self.result = result 62 | 63 | def solve_linear_system(equations): 64 | """ 65 | Solve a list of equations in GF(2). Yield all the solutions. 66 | 67 | Attributes: 68 | equations: a list of StateEquation that represents the linear system. 69 | """ 70 | M = [] 71 | b = [] 72 | # Create linear system 73 | for eq in equations: 74 | row = [coeff for coeff in eq.coefficients] 75 | M.append(row) 76 | b.append(eq.result) 77 | M = Matrix(GF(2), M) 78 | b = Matrix(GF(2), b).transpose() 79 | # Find a solution 80 | v0 = M.solve_right(b).transpose()[0] 81 | # Iterate over all solutions 82 | K = M.right_kernel() 83 | total_solutions = len(K) 84 | if total_solutions > 100: 85 | logger.warning(f'Found {len(K)} valid xs128 seed(s)') 86 | else: 87 | logger.debug(f'Found {len(K)} valid xs128 seed(s)') 88 | for v in K: 89 | yield sum(int(c) << i for i, c in enumerate(v0 + v)) 90 | 91 | def recover_seed_from_known_bits(known_states_bits): 92 | """ 93 | Recover all the possible initial xs128 128-bit states from a list of known bits of successive xs128 state0s. 94 | The position of known bits can vary between states. 95 | Intermediate states in the list can have no known bits. 96 | 97 | Arguments: 98 | known_states_bits: a list of 64-bit vectors where known_states_bits[i][j] is: 99 | - 0 or 1 if the j-th bit of the i-th state0 of xs128 is known. 100 | - None if the j-th bit of the i-th state0 of xs128 is unknown. 101 | 102 | Return a generator that yields all possible initial 128-bit states of xs128 as a (state0, state1) tuple. 103 | """ 104 | assert all(len(state) == 64 for state in known_states_bits) 105 | # Initial state before the xs128 call 106 | # Initial s0 is the low 64 bits of the initial state 107 | # Initial s1 is the high 64 bits of the initial state 108 | s0 = StateBitDeps([1 << i for i in range(HALF_STATE_SIZE)]) 109 | s1 = StateBitDeps([1 << i for i in range(HALF_STATE_SIZE, STATE_SIZE)]) 110 | equations = [] 111 | # Generate bit dependencies between all states 112 | total_equations = 0 113 | for state_bits in known_states_bits: 114 | s0, s1 = xs128(s0, s1) 115 | # For each known bit, we generate a new equation 116 | for i, bit in enumerate(state_bits): 117 | if bit is not None: 118 | total_equations += 1 119 | coefficients = s0.to_coeff(i) 120 | equations.append(StateEquation(coefficients, bit)) 121 | if total_equations > MAX_EQUATIONS: 122 | total_equations = MAX_EQUATIONS 123 | equations = equations[:MAX_EQUATIONS] 124 | logger.debug(f'Total number of equations in linear system reduced to {MAX_EQUATIONS}') 125 | break 126 | logger.debug(f'Total number of equations in linear system: {total_equations}') 127 | if total_equations < 110: 128 | logger.error(f'Number of equations is too small and will generate too many possible seeds') 129 | elif total_equations < 140: 130 | logger.warning(f'Number of equations is small and will generate a lot of possible seeds') 131 | # Solve the linear system of equations to find all possible seeds 132 | seeds = solve_linear_system(equations) 133 | for seed in seeds: 134 | seed0 = seed & ((1 << HALF_STATE_SIZE) - 1) 135 | seed1 = seed >> HALF_STATE_SIZE 136 | yield seed0, seed1 137 | 138 | -------------------------------------------------------------------------------- /mathrandomcrack/mathrandom.py: -------------------------------------------------------------------------------- 1 | from .xs128 import * 2 | 3 | import copy 4 | import struct 5 | from random import randint 6 | 7 | MATH_RANDOM_CACHE_SIZE = 64 8 | 9 | def v8_to_double(state0): 10 | """ 11 | Convert a 64-bit integer state0 from xs128 to a double output. 12 | The result is between 0.0 and 1.0. 13 | The 11 least significant bits of state0 are lost during conversion. 14 | 15 | See also: https://github.com/v8/v8/blob/14.3.21/src/base/utils/random-number-generator.h#L111 16 | """ 17 | random_0_to_2_53 = state0 >> 11 18 | return random_0_to_2_53 / (1 << 53) 19 | 20 | def v8_from_double(double): 21 | """ 22 | Convert a double back to a 64-bit integer. 23 | The 11 least significant bits of the result cannot be recovered. 24 | """ 25 | random_0_to_2_53 = int(double * (1 << 53)) 26 | return random_0_to_2_53 << 11 27 | 28 | def int64_to_bits(val): 29 | """ 30 | Convert a 64-bit integer to a list of bits. 31 | """ 32 | return [(val >> i) & 1 for i in range(64)] 33 | 34 | class MathRandom(): 35 | """ 36 | A class that simulates V8 Math.random behaviour. 37 | 38 | Attributes: 39 | state0, state1: the 128-bit state to use for next cache refill. 40 | 41 | cache_idx: the index in the internal cache. 42 | Decrements every time a random value is consumed by next(). 43 | 44 | cache: the 64-long list of random 64-bit values generated by xs128. 45 | """ 46 | def __init__(self, state0=None, state1=None): 47 | """ 48 | Initialize internal Math.random state. 49 | 50 | Arguments: 51 | (optional) state0 and state1 of xs128. If not specified, random values will be set. 52 | """ 53 | if not state0: 54 | state0 = randint(0, 1 << HALF_STATE_SIZE) 55 | if not state1: 56 | state1 = randint(0, 1 << HALF_STATE_SIZE) 57 | self.state0 = state0 58 | self.state1 = state1 59 | self.cache_idx = -1 60 | self.cache = [] 61 | self._refill() 62 | 63 | def next(self): 64 | """ 65 | Output the result of a call to Math.random() (a double between 0.0 and 1.0). 66 | Decrement cache_idx and refill the cache if needed. 67 | """ 68 | if self.cache_idx < 0: 69 | self._refill() 70 | val = v8_to_double(self.cache[self.cache_idx]) 71 | self.cache_idx -= 1 72 | return val 73 | 74 | def previous(self): 75 | """ 76 | Output the result of the previous call to Math.random() (a double between 0.0 and 1.0). 77 | Increment cache_idx and refill the cache backwards if needed. 78 | """ 79 | self.cache_idx += 1 80 | if self.cache_idx > 63: 81 | self._refill_backwards() 82 | val = v8_to_double(self.cache[self.cache_idx]) 83 | return val 84 | 85 | def recover_from_previous_state(self, prev_state0, prev_state1, cache_idx): 86 | """ 87 | Recover a MathRandom internal state using the values of state0 and state1 before the previous refill. 88 | Refill the entire cache and set cache_idx accordingly. 89 | 90 | Arguments: 91 | prev_state0, prev_state1: the values of state0 and state1 before the previous refill. 92 | 93 | cache_idx: the cache index that should be set after refill. 94 | """ 95 | self.cache = [] 96 | self.cache_idx = -1 97 | self.state0 = prev_state0 98 | self.state1 = prev_state1 99 | self._refill() 100 | self.cache_idx = cache_idx 101 | 102 | def _refill(self): 103 | """ 104 | Refill the Math.random cache using xs128. 105 | Can only be used when Math.random cache is empty. 106 | 107 | A new 64-long list of random values is stored in cache. 108 | The cache_idx is set to the last index of the cache (63). 109 | """ 110 | assert self.cache_idx == -1 111 | self.cache = [] 112 | for _ in range(MATH_RANDOM_CACHE_SIZE): 113 | self.state0, self.state1 = xs128(self.state0, self.state1) 114 | self.cache.append(self.state0) 115 | self.cache_idx = MATH_RANDOM_CACHE_SIZE - 1 116 | 117 | def _refill_backwards(self): 118 | """ 119 | Refill the Math.random cache backwards using xs128. 120 | Can only be used when Math.random cache is full. 121 | 122 | A new 64-long list of random values is stored in cache. 123 | The cache_idx is set to the first index of the cache (0). 124 | """ 125 | assert self.cache_idx == 64 126 | self.cache = [] 127 | # First loop generates values of the current cache 128 | for _ in range(MATH_RANDOM_CACHE_SIZE): 129 | self.state0, self.state1 = reverse_xs128(self.state0, self.state1) 130 | saved_state0, saved_state1 = self.state0, self.state1 131 | # Second loop fills the previous cache 132 | for _ in range(MATH_RANDOM_CACHE_SIZE): 133 | self.cache.append(self.state0) 134 | self.state0, self.state1 = reverse_xs128(self.state0, self.state1) 135 | self.state0, self.state1 = saved_state0, saved_state1 136 | # Cache was generated backwards 137 | self.cache = self.cache[::-1] 138 | self.cache_idx = 0 139 | 140 | def __copy__(self): 141 | """ 142 | copy.copy() helper function. 143 | """ 144 | new_math_random = MathRandom() 145 | new_math_random.state0 = self.state0 146 | new_math_random.state1 = self.state1 147 | new_math_random.cache_idx = self.cache_idx 148 | new_math_random.cache = [d for d in self.cache] 149 | return new_math_random 150 | 151 | def __eq__(self, other): 152 | return self.cache_idx == other.cache_idx \ 153 | and self.state0 == other.state0 \ 154 | and self.state1 == other.state1 \ 155 | and self.cache == other.cache 156 | 157 | -------------------------------------------------------------------------------- /mathrandomcrack/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import ast 3 | import logging 4 | import math 5 | import sys 6 | 7 | from .mathrandomcrack import * 8 | 9 | def parse_args(): 10 | parser = argparse.ArgumentParser( 11 | prog = 'python3 -m mathrandomcrack', 12 | description ='A tool to recover the internal state of the V8 implementation of Math.random() ' \ 13 | 'and predict previous and next values of Math.random() calls', 14 | epilog = 'Example usages:\n' \ 15 | ' python3 -m mathrandomcrack --method doubles --next 10 ./samples/doubles.txt\n' \ 16 | ' python3 -m mathrandomcrack --method scaled --next 5 --previous 5 --factor 36 --output-fmt scaled ./samples/scaled_values.txt\n'\ 17 | ' python3 -m mathrandomcrack --method bounds --next 10 ./samples/bounds.txt --debug\n', 18 | formatter_class=argparse.RawTextHelpFormatter) 19 | parser.add_argument('--method', required=True, choices=['doubles', 'scaled', 'bounds'], 20 | help='the kind of leaked values to use to recover possible Math.random() states\n'\ 21 | '"doubles": one output of Math.random() per line (between 0.0 and 1.0)\n'\ 22 | '"scaled": one output of Math.floor(Math.random() * factor + translation) per line\n'\ 23 | '"bounds": one pair of space-separated min / max bounds of Math.random() outputs per line') 24 | parser.add_argument('--factor', default=1, type=int, 25 | help='the factor to use for method / output_fmt "scaled"') 26 | parser.add_argument('--translation', default=0, type=int, 27 | help='the translation to use for method / output_fmt "scaled"') 28 | parser.add_argument('--next', default=10, type=int, 29 | help='how many next Math.random() outputs to predict') 30 | parser.add_argument('--previous', default=0, type=int, 31 | help='how many previous Math.random() outputs to predict') 32 | parser.add_argument('--show-leaks', action='store_true', 33 | help='show the recovered leaked values corresponding to the input file') 34 | parser.add_argument('--output-fmt', default='doubles', choices=['doubles', 'scaled'], 35 | help='the format of the predicted values\n'\ 36 | '"doubles" (default): a list of doubles\n'\ 37 | '"scaled": a list of integers generated with Math.floor(Math.random() * factor + translation') 38 | parser.add_argument('--debug', action='store_true', 39 | help='raise log level') 40 | parser.add_argument('file', 41 | help='the file containing the random leaked values') 42 | args = parser.parse_args() 43 | 44 | if args.method == 'scaled' and args.factor < 2: 45 | raise ValueError(f'--factor should be specified and larger than 1 when using method "scaled"') 46 | 47 | return args 48 | 49 | def parse_file(filename, method): 50 | leaks = [] 51 | indices = [] 52 | curr_index = 0 53 | with open(filename, 'r') as f: 54 | for line in f: 55 | if line.startswith("#"): 56 | # Skip commented lines 57 | continue 58 | if not line.strip(): 59 | # Empty line means unknown state 60 | pass 61 | elif method == 'doubles': 62 | d = ast.literal_eval(line) 63 | assert type(d) in [int, float] and d >= 0.0 and d <= 1.0 64 | leaks.append(d) 65 | indices.append(curr_index) 66 | elif method == 'scaled': 67 | i = ast.literal_eval(line) 68 | assert type(i) is int and i >= 0 and i <= pow(2, 64) - 1 69 | leaks.append(i) 70 | indices.append(curr_index) 71 | elif method == 'bounds': 72 | splitted = line.split(' ') 73 | bounds = [ast.literal_eval(s) for s in splitted] 74 | assert len(bounds) == 2 and all(type(b) in [int, float] and b >= 0 and b <= 1.0 for b in bounds) 75 | leaks.append(bounds) 76 | indices.append(curr_index) 77 | else: 78 | raise NotImplementedError(f'Unsupported method "{method}"') 79 | curr_index += 1 80 | return leaks, indices 81 | 82 | def recover_all_states(leaks, indices, args): 83 | if args.method == 'doubles': 84 | return recover_state_from_math_random_doubles(leaks, indices) 85 | elif args.method == 'scaled': 86 | return recover_state_from_math_random_scaled_values(leaks, args.factor, args.translation, indices) 87 | elif args.method == 'bounds': 88 | return recover_state_from_math_random_approximate_values(leaks, indices) 89 | else: 90 | raise NotImplementedError(f'Unsupported method "{method}"') 91 | 92 | def format_random(value, args): 93 | if args.output_fmt == 'doubles': 94 | return value 95 | elif args.output_fmt == 'scaled': 96 | return math.floor(value * args.factor + args.translation) 97 | else: 98 | raise NotImplementedError(f'Unsupported output_fmt "{method}"') 99 | 100 | if __name__ == '__main__': 101 | args = parse_args() 102 | logging.basicConfig(stream=sys.stderr, level=logging.DEBUG if args.debug else logging.INFO) 103 | leaks, indices = parse_file(args.file, args.method) 104 | 105 | found = False 106 | for state in recover_all_states(leaks, indices, args): 107 | found = True 108 | print('Found a possible Math.random internal state') 109 | # Show --previous values 110 | if args.previous > 0: 111 | print(f'Predicted previous {args.previous} values:', 112 | [format_random(state.previous(), args) for _ in range(args.previous)][::-1]) 113 | [state.next() for _ in range(args.previous)] # Return to initial state 114 | # Show leaked values if --show-leaks 115 | if args.show_leaks: 116 | print(f'Recovered leaked values:', 117 | [format_random(state.next(), args) for _ in range(max(indices) + 1)]) 118 | else: 119 | [state.next() for _ in range(max(indices) + 1)] # Skip leaks 120 | if args.next > 0: 121 | # Show --next values 122 | print(f'Predicted next {args.next} values:', 123 | [format_random(state.next(), args) for _ in range(args.next)]) 124 | print() 125 | 126 | if not found: 127 | print("Couldn't recover any possible Math.random internal state. Please check your values file.") 128 | 129 | -------------------------------------------------------------------------------- /mathrandomcrack/mathrandomcrack.py: -------------------------------------------------------------------------------- 1 | from .mathrandom import * 2 | from .xs128crack import recover_seed_from_known_bits 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | def recover_state_from_math_random_known_bits(known_bits, positions=None): 9 | """ 10 | Recover all the possible MathRandom states given a list of known bits of values generated by Math.random(). 11 | 12 | Arguments: 13 | known_bits: a list of 64-bit vectors where known_bits[i][j] is: 14 | - 0 or 1 if the j-th bit of the i-th value generated by Math.random() is known. 15 | - None if the j-th bit of the i-th value generated by Math.random() is unknown. 16 | 17 | positions: a list that defines the position of the call that generated each known_bits value with Math.random(). 18 | If not specified, it will be assumed that values represented by known_bits were generated by successive Math.random() calls. 19 | 20 | Yield possible MathRandom() objects that are initialized to a valid internal state before the 21 | generation of the given list of known_bits values at specified positions. 22 | """ 23 | assert all(len(state) == 64 for state in known_bits) 24 | # If positions are not specified, assume the doubles are successive values of Math.random() 25 | if not positions: 26 | positions = [i for i in range(len(known_bits))] 27 | assert len(known_bits) == len(positions) 28 | index_to_known_bits = {positions[i]:known_bits[i] for i in range(len(known_bits))} 29 | # Bruteforce the cache_idx value at the first Math.random call 30 | for cache_idx in range(MATH_RANDOM_CACHE_SIZE): 31 | logger.debug(f'Trying to find a good seed for cache index {cache_idx}') 32 | known_states_bits = [] 33 | # Account for Math.random cache reverting outputs order by blocks of size 64 34 | for i in range(MATH_RANDOM_CACHE_SIZE * ((max(positions) // MATH_RANDOM_CACHE_SIZE) + 2)): 35 | cache_n = i // MATH_RANDOM_CACHE_SIZE 36 | value_index = cache_n * MATH_RANDOM_CACHE_SIZE + cache_idx - (i % MATH_RANDOM_CACHE_SIZE) 37 | if value_index in index_to_known_bits: 38 | # If we know bits of the value at this index, add its bits to the list of known states bits 39 | known_states_bits.append(index_to_known_bits[value_index]) 40 | else: 41 | # Else, we don't know any bits for this state 42 | known_states_bits.append([None for _ in range(64)]) 43 | # Try to recover possible seeds for this starting cache_idx 44 | seeds = recover_seed_from_known_bits(known_states_bits) 45 | try: 46 | for seed in seeds: 47 | math_random = MathRandom() 48 | math_random.recover_from_previous_state(seed[0], seed[1], cache_idx) 49 | yield math_random 50 | except ValueError as e: 51 | # No solution, cache_idx is wrong 52 | pass 53 | 54 | def recover_state_from_math_random_doubles(doubles, positions=None): 55 | """ 56 | Recover all the possible MathRandom states given a list of doubles generated by Math.random(). 57 | 58 | Arguments: 59 | doubles: a list of doubles outputs generated by V8 Math.random(). 60 | The doubles don't have to be generated successively but their positions must be known. 61 | 62 | positions: a list that defines the position of the call that generated each double with Math.random(). 63 | If not specified, it will be assumed that doubles were generated by successive Math.random() calls. 64 | 65 | Yield possible MathRandom() objects that are initialized to a valid internal state before the 66 | generation of the given list of doubles at specified positions. 67 | """ 68 | assert all(0.0 <= d <= 1.0 for d in doubles) 69 | # Convert doubles to known bits 70 | known_bits = [] 71 | for double in doubles: 72 | # V8 double conversion loses 11 bits of information 73 | known_bits.append([None for _ in range(11)] + int64_to_bits(v8_from_double(double))[11:HALF_STATE_SIZE]) 74 | # Recover possible states from known bits 75 | for math_random in recover_state_from_math_random_known_bits(known_bits, positions): 76 | yield math_random 77 | 78 | def recover_state_from_math_random_scaled_values(scaled_vals, factor, translation=0, positions=None): 79 | """ 80 | Recover all the possible MathRandom states given a list of values generated by Math.floor(Math.random() * factor + translate). 81 | 82 | Arguments: 83 | scaled_vals: a list of scaled values outputs generated by V8 Math.floor(Math.random() * factor) 84 | The values don't have to be generated successively but their positions must be known. 85 | 86 | factor, translation: the integers used in the expression Math.floor(Math.random() * factor + translation) 87 | 88 | positions: a list that defines the position of the call that generated each values with Math.floor(Math.random() * factor). 89 | If not specified, it will be assumed that values were generated by successive Math.random() calls. 90 | 91 | Yield possible MathRandom() objects that are initialized to a valid internal state before the 92 | generation of the given list of scaled values at specified positions. 93 | """ 94 | assert type(factor) is int 95 | assert type(translation) is int 96 | # Convert scaled values to known bits 97 | known_bits = [] 98 | for scaled_val in scaled_vals: 99 | # Recover the lower and higher bound of the internal xs128 state 100 | low, high = v8_from_double((scaled_val - translation) / factor), v8_from_double((scaled_val - translation + 1) / factor) | 0xfff 101 | # Find the common bits in the state representation of all values between the bounds 102 | common_known_bits = common_bits_between(low, high) 103 | # Only keep the common bits 104 | known_bits.append([None for _ in range(64 - len(common_known_bits))] + common_known_bits) 105 | # Recover possible states from known bits 106 | for math_random in recover_state_from_math_random_known_bits(known_bits, positions): 107 | yield math_random 108 | 109 | def recover_state_from_math_random_approximate_values(bounds, positions=None): 110 | """ 111 | Recover all the possible MathRandom states given a list of bounds that bound values generated by Math.random(). 112 | 113 | Arguments: 114 | bounds: a list of tuples that represents the known bounds of values generated by Math.random(). 115 | 116 | positions: a list that defines the position of the call that generated each values with Math.random(). 117 | If not specified, it will be assumed that values were generated by successive Math.random() calls. 118 | 119 | Yield possible MathRandom() objects that are initialized to a valid internal state before the 120 | generation of the given list of approximated values at specified positions. 121 | """ 122 | assert all(len(b) == 2 for b in bounds) 123 | # Convert bounds to known bits 124 | known_bits = [] 125 | for b in bounds: 126 | # Recover the lower and higher bound of the internal xs128 state 127 | low, high = v8_from_double(float(b[0])), v8_from_double(float(b[1])) | 0xfff 128 | # Find the common bits in the state representation of all values between the bounds 129 | common_known_bits = common_bits_between(low, high) 130 | # Only keep the common bits 131 | known_bits.append([None for _ in range(64 - len(common_known_bits))] + common_known_bits) 132 | # Recover possible states from known bits 133 | for math_random in recover_state_from_math_random_known_bits(known_bits, positions): 134 | yield math_random 135 | 136 | def common_bits_between(low, high): 137 | """ 138 | Returns the list of most significant bits that are the same for all value between low and high. 139 | 140 | Arguments: 141 | low, high: 64-bit integers for the bounds. 142 | """ 143 | low = max(0, low) 144 | high = min(2**64-1, high) 145 | exp = 63 146 | common_bits = [] 147 | s = 0 148 | while True: 149 | v = pow(2, exp) 150 | if s + v <= low <= high: 151 | common_bits.append(1) 152 | s += v 153 | elif low <= high <= s + v: 154 | common_bits.append(0) 155 | else: 156 | break 157 | exp -= 1 158 | return common_bits[::-1] 159 | 160 | -------------------------------------------------------------------------------- /tests/test_mathrandomcrack.py: -------------------------------------------------------------------------------- 1 | import math 2 | import logging 3 | import unittest 4 | 5 | from mathrandomcrack.mathrandomcrack import * 6 | 7 | logging.basicConfig(level=logging.ERROR) 8 | 9 | class TestMathRandomCrack(unittest.TestCase): 10 | 11 | def test_recover_state_from_math_random_doubles(self): 12 | known_doubles = [0.3729983038966259, 0.17496511670650206, 0.49159038738927563, 0.9421448261165485] 13 | 14 | expected_next = [0.4169322132746547, 0.41772567549954465, 0.49987429368004344, 0.900351160894732, 0.11675650086754474, 0.9835730792649936, 0.4365143224561563, 0.24710079059446166, 0.6719745383456214, 0.41875075942525675, 0.3369734964621547, 0.5641798587145327, 0.5698155574264084, 0.17847408913260177, 0.2649428843215077, 0.380905874015373, 0.263420627102946, 0.32492249540981777, 0.9309357395692637, 0.5356031900205944, 0.858467905098631, 0.5925762380556829, 0.9099143317573467, 0.7238843171279742, 0.1416869712519344, 0.40059256348574523, 0.5622142316529446, 0.918747664580273, 0.633638951418691, 0.44294502976453964, 0.9771591618000606, 0.9692850650182503, 0.422754219845198, 0.9473761797962288, 0.2623898822732029, 0.8311024184508644, 0.043959690564190135, 0.7404585341229147, 0.9774674535534883, 0.17526631589064878, 0.21099720937227284, 0.976079331357949, 0.8177470111768766, 0.7017129189715279, 0.031555940805606975, 0.1930326870018687, 0.30491143066350623, 0.6194209912616457, 0.06324300201261768, 0.306828230704714, 0.9014431675257254, 0.41868001999319804, 0.23319336322434547, 0.5845150673976428, 0.5367912237911235, 0.9882154316624128, 0.9075833101666309, 0.7473239507459919, 0.5406684714524379, 0.3363948330011409, 0.901320127203257, 0.9732303961735218, 0.6455133831352127, 0.7576268453085985, 0.5647368375815475, 0.4659615932762906, 0.5354022303576509, 0.4970983867987443, 0.9278258214841121, 0.9992673904846235, 0.9749618674924401, 0.1584823349451927, 0.9622199718454659, 0.5518860013949756, 0.471320273216671, 0.4184169317544604, 0.8670524127119378, 0.46155628597384446, 0.9087864474745928, 0.7893797553155282, 0.44237307624597033, 0.852150843037217, 0.07301482262959291, 0.5925095524749455, 0.42446481143695325, 0.341532429872783, 0.0014043131869174719, 0.05474809985287277, 0.8244937496746325, 0.9139707334842332, 0.9286557246660236, 0.36527721294136073, 0.34896468064704866, 0.2046638953973562, 0.269845981219898, 0.2051722137082952, 0.6240641988408253, 0.801941203900811, 0.339711548982112, 0.8676256265747037] 15 | 16 | found_correct_state = False 17 | for recovered_math_random in recover_state_from_math_random_doubles(known_doubles): 18 | # Verify that the state generates the correct doubles 19 | for d in known_doubles: 20 | self.assertEqual(recovered_math_random.next(), d) 21 | # Check if it generates the expected next doubles 22 | found_correct_state = all(d == recovered_math_random.next() for d in expected_next) 23 | if found_correct_state: 24 | break 25 | self.assertTrue(found_correct_state) 26 | 27 | def test_recover_state_from_math_random_doubles_scattered(self): 28 | generated_doubles = [0.28312656309821627, 0.2126296311778575, 0.045291001697600364, 0.9069011015169577, 0.5988258696130254, 0.8028144523905971, 0.2993948573359255, 0.7836084709175235, 0.36330960376322163, 0.5966969790645456] 29 | 30 | # Assume we only know some of the generated values and their position 31 | positions = [0, 4, 5, 9] 32 | known_doubles = [generated_doubles[pos] for pos in positions] 33 | 34 | expected_next = [0.918783805780444, 0.5377492878426327, 0.9505629026182582, 0.41944301946452134, 0.06084535675857783, 0.15405402717717143, 0.1546677423098145, 0.042852644838147325, 0.9322033840087246, 0.5480142124404163, 0.6700505956070484, 0.8312332569792561, 0.8029383061489065, 0.25026063311081703, 0.46235894959165325, 0.548745003628291, 0.42810673627480333, 0.1561269490438607, 0.013655815770782787, 0.9518497992055682, 0.3651092861058074, 0.16883307969569128, 0.49229622272914797, 0.05156783389272701, 0.21131101491653004, 0.28408818858209817, 0.3214689702318726, 0.12225616245616666, 0.2598736358356091, 0.8070402831383899, 0.05280059650468172, 0.7253832196232776, 0.9846244194320897, 0.8754534301105803, 0.418982918984675, 0.5861760514383587, 0.5347172569864876, 0.5327400670161115, 0.03400993729608859, 0.1202737829285383, 0.3406961268218863, 0.8883879978982208, 0.032780973354808784, 0.28472216609731704, 0.271036866072481, 0.9905706569974092, 0.13309724391422573, 0.31259746081681783, 0.008206300461536586, 0.14454751793269416, 0.41075733967529315, 0.8464258408804377, 0.04984268419886628, 0.2708405932508332, 0.03584805164928184, 0.9192499700789711, 0.7812702853689516, 0.24376989771012858, 0.2742573843918321, 0.4408069888787156, 0.3579580100744746, 0.4806862252113273, 0.9488107945220575, 0.8215923736436783, 0.3988689513261501, 0.06528325610562502, 0.6498468895265944, 0.11432047732247153, 0.7383617665021109, 0.6107634321359064, 0.3395634254885117, 0.06751690618683925, 0.020233781330262213, 0.16096845144775407, 0.26125412386802893, 0.07147961556802096, 0.9499834345765892, 0.11393201180309231, 0.0265812765988499, 0.5103476761188042, 0.10961313192089339, 0.14364477732321845, 0.6575009304135266, 0.033323587885655814, 0.7847446688452391, 0.9416600696024611, 0.19665670938707347, 0.9113186793492043, 0.3410601077410782, 0.2635955798782108, 0.23524393516288966, 0.2923567544600475, 0.21431544432253602, 0.662795404740445, 0.9045701589788158, 0.7546370695688394, 0.7148389295938502, 0.49036908631505527, 0.993659282611439, 0.2330635451997748] 35 | 36 | found_correct_state = False 37 | for recovered_math_random in recover_state_from_math_random_doubles(known_doubles, positions): 38 | # Verify that the state generates the correct doubles 39 | for d in generated_doubles: 40 | self.assertEqual(recovered_math_random.next(), d) 41 | # Check if it generates the expected next doubles 42 | found_correct_state = all(d == recovered_math_random.next() for d in expected_next) 43 | if found_correct_state: 44 | break 45 | self.assertTrue(found_correct_state) 46 | 47 | def test_recover_state_from_math_random_scaled_values(self): 48 | factor = 36 49 | translation = 1 50 | known_values = [17, 17, 24, 30, 24, 6, 32, 29, 9, 36, 26, 12, 34, 5, 3, 28, 26, 8, 29, 32, 19, 21, 27, 10, 31, 28, 19, 6, 1, 6, 33, 36, 18, 23, 33, 21, 32, 33, 36] 51 | 52 | expected_next = [0.4218129454423729, 0.45160340069398586, 0.2808396068917298, 0.11752035209172385, 0.5407210381751475, 0.5996593302014471, 0.7971148341317318, 0.7493914264865006, 0.22021416366697522, 0.595437422353347, 0.5158823896816561, 0.8012747547437711, 0.1456076411816951, 0.7750987028893456, 0.8528656035182629, 0.7915830419453418, 0.9562241901791876, 0.13307286635832816, 0.9811023405348873, 0.8805682376821508, 0.5670978480051219, 0.06357006730036352, 0.7901076428521345, 0.9075551267446471, 0.24417973895373257, 0.030191638241039653, 0.9439447860321662, 0.5638874053373243, 0.2468666279713092, 0.00048764770310028016, 0.2392902385607587, 0.7608255146055712, 0.5300686919176696, 0.07907036751969942, 0.9792668117408088, 0.9935717661101162, 0.8720538447767797, 0.48916916380121944, 0.6935281844627795, 0.09409664043040245, 0.11846729731436878, 0.7914575100178245, 0.5643580279839011, 0.8148564745196389, 0.9374661068983167, 0.11457063360142794, 0.3764500110232335, 0.04760222935159042, 0.7244870462589171, 0.052657570753572136, 0.17701549519135484, 0.015289561706595789, 0.5731095951300582, 0.6842799477489889, 0.35628955377321725, 0.01786445851674956, 0.9529863206295178, 0.3872798179519642, 0.14590307583332707, 0.7203173628581008, 0.8364441744600604, 0.5561381429882156, 0.20527212924707472, 0.3711839792487389, 0.636410449345258, 0.6666483093416258, 0.6993914522643667, 0.0697141925245518, 0.7599196882689709, 0.20143662428724363, 0.5196496788939513, 0.2266112028554612, 0.1934832998316791, 0.7231498596583112, 0.7306447269575812, 0.8487982579409244, 0.3900265701116067, 0.4127085000309396, 0.6780834979898586, 0.7374232378606689, 0.014868744938873912, 0.5785470209416631, 0.6386693073933304, 0.4282420885525635, 0.22616552898226727, 0.22909694113069956, 0.912029733053979, 0.3713828980949484, 0.9573656634848178, 0.20452268345404157, 0.6718667050325263, 0.8476482962066186, 0.24328020833430808, 0.5187342625346325, 0.23193413108683258, 0.47151622507746227, 0.6124764527742227, 0.2537824681668992, 0.41268869750134884, 0.28086810498820325] 53 | 54 | found_correct_state = False 55 | for recovered_math_random in recover_state_from_math_random_scaled_values(known_values, factor, translation): 56 | # Verify that the state generates the correct integers 57 | for d in known_values: 58 | self.assertEqual(math.floor(factor * recovered_math_random.next() + translation), d) 59 | # Check if it generates the expected next doubles 60 | found_correct_state = all(d == recovered_math_random.next() for d in expected_next) 61 | if found_correct_state: 62 | break 63 | self.assertTrue(found_correct_state) 64 | 65 | def test_recover_state_from_math_random_approximate_values(self): 66 | generated_doubles = [0.7909911741689747, 0.8514657413496938, 0.4439369897047407, 0.4915718834590873, 0.13826002883337962, 0.8000790562426907, 0.1757029012338981, 0.708534280438173, 0.37443492108086396, 0.20115230687717556] 67 | 68 | # Assume we only know an approximation of each value 69 | approx_values = [round(d, 5) for d in generated_doubles] 70 | # And we can bound each value 71 | bounds = [(v-0.00001, v+0.00001) for v in approx_values] 72 | 73 | expected_next = [0.832869258510279, 0.8811329639752462, 0.8361813885437983, 0.0980735041763866, 0.5651374308947414, 0.851033367410471, 0.0016466719576158084, 0.4114342559570725, 0.7070033390886351, 0.8849320705765845, 0.09560183345039053, 0.29098304386949525, 0.09627202648312383, 0.07653847395109004, 0.15589441956758654, 0.812797652737907, 0.6466098215668196, 0.7687984338635581, 0.7755099124044598, 0.8821110593165641, 0.8755463098460196, 0.5567850542588358, 0.6973371338938418, 0.9712743482826413, 0.4142153412901831, 0.23078985965214704, 0.15529888443152717, 0.6453405292152657, 0.2014977565881514, 0.7337569210056293, 0.3408945645328212, 0.247649887558837, 0.29792393948750584, 0.09493177170963008, 0.8257148896444417, 0.8480777825256663, 0.40463775619885634, 0.3484256689817232, 0.5189281378340919, 0.8779626177426069, 0.47858656878592154, 0.14748951770727114, 0.37811747171707977, 0.9802528321677846, 0.1917359433869127, 0.6302420397763413, 0.5462325939874954, 0.9023432491768382, 0.15661481266908717, 0.18368719608116402, 0.8702267230671159, 0.9956011842567898, 0.25909035014634685, 0.39645801202662223, 0.16323042609395266, 0.1470684302355293, 0.8690796876675885, 0.10713134739273344, 0.5467303166625644, 0.6446643125136784, 0.9724278254710444, 0.7121294642412068, 0.3437440512946155, 0.37266001051344466, 0.9343849307240706, 0.30845689709920066, 0.0016914166814636644, 0.07050578137082142, 0.26429895395155556, 0.2167624714059292, 0.5882329802315368, 0.46527371880015134, 0.538418103184017, 0.8484569974205872, 0.6649316525315724, 0.34641089087168275, 0.9202104923388447, 0.558577327876703, 0.9454254617072873, 0.007762132687332168, 0.9088995583989011, 0.7295464666438887, 0.03317193411197106, 0.4917729866482916, 0.04021447888661822, 0.6258812954589608, 0.6281160166233543, 0.12949027206140085, 0.2448463138499607, 0.483657049176175, 0.8788222845261114, 0.5835504598386492, 0.3479085175956548, 0.8480525116511206, 0.8237154869533817, 0.6622618000529038, 0.8947612784574471, 0.6223522219019176, 0.09849707941303143, 0.7981554615376663] 74 | 75 | found_correct_state = False 76 | for recovered_math_random in recover_state_from_math_random_approximate_values(bounds): 77 | # Skip the approximate values 78 | for d in bounds: 79 | recovered_math_random.next() 80 | # Check if it generates the expected next doubles 81 | found_correct_state = all(d == recovered_math_random.next() for d in expected_next) 82 | if found_correct_state: 83 | break 84 | self.assertTrue(found_correct_state) 85 | --------------------------------------------------------------------------------