├── LICENSE ├── README.md ├── functions.py ├── poc_IFrom2S.py ├── poc_KFrom3I.py ├── poc_predict.py ├── poc_stateRewind.py ├── recover_32bitSeed.py ├── recover_64bitSeed.py ├── recover_BytesV1Seed.py ├── recover_BytesV2Seed.py ├── recover_DefaultSeed.py └── recover_FloatSeed.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stackered 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python random playground 2 | 3 | This repository contains code snippets and POCs associated to our article on [breaking Python's PRNG with a few values and no bruteforce](https://stackered.com/blog/python-random-prediction/). 4 | 5 | The functions shared accross all POCs are located in [functions.py](./functions.py). 6 | 7 | # POCs 8 | 9 | - [poc_predict](./poc_predict.py) : This POC shows how to predict the futur PRNG outputs given 624 consecutive outputs. 10 | - [poc_IFrom2S](./poc_IFrom2S.py) : This POC shows how to recover an **initial state** `I` value from a pair of **current state** `S` values. 11 | - [poc_KFrom3I](./poc_KFrom3I.py) : This POC shows how to recover a value of `K` (the seed array) from three consecutive **initial state** `I` values. 12 | - [poc_stateRewind](./poc_stateRewind.py) : This POC shows how to rewind a full state `S` up to the **initial state** `I`. 13 | 14 | # Example seed recovery 15 | 16 | - [recover_32bitSeed](./recover_32bitSeed.py) : Example recovery of a 32-bit seed using 6 outputs. 17 | - [recover_64bitSeed](./recover_64bitSeed.py) : Example recovery of a 64-bit seed using 8 outputs. 18 | - [recover_FloatSeed](./recover_64bitSeed.py) : Example recovery of a 64-bit seed using 8 outputs. This time the PRNG is seeded with a float. 19 | - [recover_BytesV1Seed](./recover_BytesV1Seed.py) : Example recovery of a 64-bit seed using 8 outputs. This time the PRNG is seeded with bytes, using the version 1 algorithm. 20 | - [recover_BytesV2Seed](./recover_BytesV2Seed.py) : Example recovery of a 7 characters long seed using 8 outputs. This time the PRNG is seeded with bytes, using the version 2 algorithm (the default). 21 | - [recover_DefaultSeed](./recover_DefaultSeed.py) : Example recovery of the operating system's CSPRNG generated seed using 624 outputs. The PRNG is not seeded explicitely (the default case). -------------------------------------------------------------------------------- /functions.py: -------------------------------------------------------------------------------- 1 | def unshiftRight(x, shift): 2 | res = x 3 | for i in range(32): 4 | res = x ^ res >> shift 5 | return res 6 | 7 | 8 | def unshiftLeft(x, shift, mask): 9 | res = x 10 | for i in range(32): 11 | res = x ^ (res << shift & mask) 12 | return res 13 | 14 | 15 | def untemper(v): 16 | v = unshiftRight(v, 18) 17 | v = unshiftLeft(v, 15, 0xefc60000) 18 | v = unshiftLeft(v, 7, 0x9d2c5680) 19 | v = unshiftRight(v, 11) 20 | return v 21 | 22 | 23 | def invertStep(si, si227): 24 | # S[i] ^ S[i-227] == (((I[i] & 0x80000000) | (I[i+1] & 0x7FFFFFFF)) >> 1) ^ (0x9908b0df if I[i+1] & 1 else 0) 25 | X = si ^ si227 26 | # we know the LSB of I[i+1] because MSB of 0x9908b0df is set, we can see if the XOR has been applied 27 | mti1 = (X & 0x80000000) >> 31 28 | if mti1: 29 | X ^= 0x9908b0df 30 | # undo shift right 31 | X <<= 1 32 | # now recover MSB of state I[i] 33 | mti = X & 0x80000000 34 | # recover the rest of state I[i+1] 35 | mti1 += X & 0x7FFFFFFF 36 | return mti, mti1 37 | 38 | 39 | def init_genrand(seed): 40 | MT = [0] * 624 41 | MT[0] = seed & 0xffffffff 42 | for i in range(1, 623+1): # loop over each element 43 | MT[i] = ((0x6c078965 * (MT[i-1] ^ (MT[i-1] >> 30))) + i) & 0xffffffff 44 | return MT 45 | 46 | 47 | def recover_kj_from_Ji(ji, ji1, i): 48 | # ji => J[i] 49 | # ji1 => J[i-1] 50 | const = init_genrand(19650218) 51 | key = ji - (const[i] ^ ((ji1 ^ (ji1 >> 30))*1664525)) 52 | key &= 0xffffffff 53 | # return K[j] + j 54 | return key 55 | 56 | 57 | def recover_Ji_from_Ii(Ii, Ii1, i): 58 | # Ii => I[i] 59 | # Ii1 => I[i-1] 60 | ji = (Ii + i) ^ ((Ii1 ^ (Ii1 >> 30)) * 1566083941) 61 | ji &= 0xffffffff 62 | # return J[i] 63 | return ji 64 | 65 | 66 | def recover_Kj_from_Ii(Ii, Ii1, Ii2, i): 67 | # Ii => I[i] 68 | # Ii1 => I[i-1] 69 | # Ii2 => I[i-2] 70 | # Ji => J[i] 71 | # Ji1 => J[i-1] 72 | Ji = recover_Ji_from_Ii(Ii, Ii1, i) 73 | Ji1 = recover_Ji_from_Ii(Ii1, Ii2, i-1) 74 | return recover_kj_from_Ji(Ji, Ji1, i) 75 | 76 | 77 | def rewindState(state): 78 | prev = [0]*624 79 | # copy to not modify input array 80 | s = state[:] 81 | I, I0 = invertStep(s[623], s[396]) 82 | prev[623] += I 83 | # update state 0 84 | # this does nothing when working with a known full state, but is important we rewinding more than 1 time 85 | s[0] = (s[0]&0x80000000) + I0 86 | for i in range(227, 623): 87 | I, I1 = invertStep(s[i], s[i-227]) 88 | prev[i] += I 89 | prev[i+1] += I1 90 | for i in range(227): 91 | I, I1 = invertStep(s[i], prev[i+397]) 92 | prev[i] += I 93 | prev[i+1] += I1 94 | # The LSBs of prev[0] do not matter, they are 0 here 95 | return prev 96 | 97 | 98 | def seedArrayFromState(s, subtractIndices=True): 99 | s_ = [0]*624 100 | for i in range(623, 2, -1): 101 | s_[i] = recover_Ji_from_Ii(s[i], s[i-1], i) 102 | s_[0]=s_[623] 103 | s_[1]=recover_Ji_from_Ii(s[1], s[623], 1) 104 | s_[2]=recover_Ji_from_Ii(s[2], s_[1], 2) 105 | seed = [0]*624 106 | for i in range(623, 2, -1): 107 | seed[i-1] = recover_kj_from_Ji(s_[i], s_[i-1], i) 108 | # system overdefined for seed[0,1,623] 109 | seed[0] = 0 110 | # thus s1 = (const[1] ^ ((const[0] ^ (const[0] >> 30))*1664525)) 111 | s1_old = ((2194844435 ^ ((19650218 ^ (19650218 >> 30))*1664525))) & 0xffffffff 112 | seed[1] = recover_kj_from_Ji(s_[2], s1_old, 2) 113 | seed[623] = (s_[1] - (s1_old ^ ((s_[0] ^ (s_[0] >> 30))*1664525))) & 0xffffffff 114 | # subtract the j indices 115 | if subtractIndices: 116 | seed = [(2**32+e-i)%2**32 for i,e in enumerate(seed)] 117 | return seed 118 | 119 | 120 | def seedArrayToInt(s): 121 | seed = 0 122 | for e in s[::-1]: 123 | seed += e 124 | seed <<= 32 125 | return seed >> 32 -------------------------------------------------------------------------------- /poc_IFrom2S.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | I = random.getstate()[1] 6 | # this will force a state update 7 | random.random() 8 | S = random.getstate()[1] 9 | 10 | for i in range(227, 240): 11 | Ii, Ii1 = invertStep(S[i], S[i-227]) 12 | print(f"{Ii} == {I[i]&0x80000000}") 13 | print(f"{Ii1} == {I[i+1]&0x7FFFFFFF}") -------------------------------------------------------------------------------- /poc_KFrom3I.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | random.seed(12345) 6 | # K = [12345] 7 | # k = 1 8 | 9 | I = random.getstate()[1] 10 | 11 | for i in range(4, 624): 12 | print(i, recover_Kj_from_Ii(I[i], I[i-1], I[i-2], i)) -------------------------------------------------------------------------------- /poc_predict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | random.seed(1234) 6 | 7 | # You don't see some of the outputs 8 | for _ in range(1234): 9 | random.getrandbits(32) 10 | 11 | # you capture 624 consecutive outputs 12 | state = [untemper(random.getrandbits(32)) for _ in range(624)] 13 | 14 | print("Normal run :") 15 | 16 | print(random.getrandbits(32)) 17 | print(random.random()) 18 | print(random.randbytes(4).hex()) 19 | print(random.randrange(1, 100000)) 20 | 21 | print("\nPredicted run :") 22 | 23 | # set RNG state from observed ouputs 24 | random.setstate((3, tuple(state + [624]), None)) 25 | 26 | print(random.getrandbits(32)) 27 | print(random.random()) 28 | print(random.randbytes(4).hex()) 29 | print(random.randrange(1, 100000)) -------------------------------------------------------------------------------- /poc_stateRewind.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | I = list(random.getstate()[1][:-1]) 6 | 7 | S1 = [untemper(random.getrandbits(32)) for _ in range(624)] 8 | S2 = [untemper(random.getrandbits(32)) for _ in range(624)] 9 | S3 = [untemper(random.getrandbits(32)) for _ in range(624)] 10 | 11 | # rewind once 12 | I_ = rewindState(S1) 13 | S2_ = rewindState(S3) 14 | S1_ = rewindState(S2) 15 | 16 | print(I_ == I) 17 | print(S1_[1:] == S1[1:]) 18 | print(S2_[1:] == S2[1:]) 19 | 20 | # rewind multiple times 21 | I_ = rewindState(rewindState(rewindState(S3))) 22 | print(I_ == I) 23 | print(I_[:5]) 24 | print(I[:5]) 25 | -------------------------------------------------------------------------------- /recover_32bitSeed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | for i in range(16): 6 | random.seed(0x533D0 + i) 7 | # k = 1 8 | # K = [0x533D0+i] 9 | 10 | S = [untemper(random.getrandbits(32)) for _ in range(624)] 11 | 12 | I_227_, I_228 = invertStep(S[0], S[227]) 13 | I_228_, I_229 = invertStep(S[1], S[228]) 14 | I_229_, I_230 = invertStep(S[2], S[229]) 15 | 16 | I_228 += I_228_ 17 | I_229 += I_229_ 18 | 19 | # two possibilities for I_230 20 | seed1 = recover_Kj_from_Ii(I_230, I_229, I_228, 230) 21 | seed2 = recover_Kj_from_Ii(I_230+0x80000000, I_229, I_228, 230) 22 | # only the MSB differs 23 | print(hex(seed1), hex(seed2)) 24 | 25 | 26 | -------------------------------------------------------------------------------- /recover_64bitSeed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | for i in range(16): 6 | random.seed(0xDEADBEEF000533D0 + i) 7 | # k = 2 8 | # K = [0x533D0+i, 0xDEADBEEF] 9 | 10 | S = [untemper(random.getrandbits(32)) for _ in range(624)] 11 | 12 | I_227_, I_228 = invertStep(S[0], S[227]) 13 | I_228_, I_229 = invertStep(S[1], S[228]) 14 | I_229_, I_230 = invertStep(S[2], S[229]) 15 | I_230_, I_231 = invertStep(S[3], S[230]) 16 | 17 | I_228 += I_228_ 18 | I_229 += I_229_ 19 | I_230 += I_230_ 20 | 21 | # K[1] + 1 22 | seed_h = recover_Kj_from_Ii(I_230, I_229, I_228, 230) - 1 23 | # K[0] + 0 24 | # two possibilities for I_231 25 | seed_l1 = recover_Kj_from_Ii(I_231, I_230, I_229, 231) 26 | seed_l2 = recover_Kj_from_Ii(I_231+0x80000000, I_230, I_229, 231) 27 | 28 | seed1 = (seed_h << 32) + seed_l1 29 | seed2 = (seed_h << 32) + seed_l2 30 | 31 | # only the MSB of K[0] differs 32 | print(hex(seed1), hex(seed2)) 33 | 34 | 35 | -------------------------------------------------------------------------------- /recover_BytesV1Seed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | def V1(a): 6 | """ 7 | Copy of Random.seed in the case of bytes and version 1. 8 | """ 9 | a = a.decode('latin-1') if isinstance(a, bytes) else a 10 | x = ord(a[0]) << 7 if a else 0 11 | for c in map(ord, a): 12 | x = ((1000003 * x) ^ c) & 0xFFFFFFFFFFFFFFFF 13 | x ^= len(a) 14 | a = -2 if x == -1 else x 15 | return a 16 | 17 | SEED = b"my seed" 18 | # We can't recover the original seed in this case, just the equivalent 64-bit integer 19 | print(f"{hex(V1(SEED)) = }") 20 | random.seed(SEED, version=1) 21 | # k = 2 22 | # K = [0x4527321a, 0xe833f9ce] 23 | 24 | S = [untemper(random.getrandbits(32)) for _ in range(624)] 25 | 26 | I_227_, I_228 = invertStep(S[0], S[227]) 27 | I_228_, I_229 = invertStep(S[1], S[228]) 28 | I_229_, I_230 = invertStep(S[2], S[229]) 29 | I_230_, I_231 = invertStep(S[3], S[230]) 30 | 31 | I_228 += I_228_ 32 | I_229 += I_229_ 33 | I_230 += I_230_ 34 | 35 | # K[1] + 1 36 | seed_h = recover_Kj_from_Ii(I_230, I_229, I_228, 230) - 1 37 | # K[0] + 0 38 | # two possibilities for I_231 39 | seed_l1 = recover_Kj_from_Ii(I_231, I_230, I_229, 231) 40 | seed_l2 = recover_Kj_from_Ii(I_231+0x80000000, I_230, I_229, 231) 41 | 42 | seed1 = (seed_h << 32) + seed_l1 43 | seed2 = (seed_h << 32) + seed_l2 44 | 45 | # only the MSB of K[0] differs 46 | print(hex(seed1), hex(seed2)) 47 | 48 | 49 | -------------------------------------------------------------------------------- /recover_BytesV2Seed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | SEED = b"my seed" 6 | random.seed(SEED) 7 | # k = 18 8 | # K = [0xc83476be, 0x9f313ec1, 0xfdb09e63, 0xf3827c68, 0x7814c985, 0xb1e9e888, 0xa924e2f8, 0x1fe1760b, 0xca754857, 0x67433568, 0x287bc567, 0xaba62218, 0xf1a54538, 0xba893a44, 0x41256723, 0xc046d021, 0x73656564, 0x6d7920] 9 | 10 | S = [untemper(random.getrandbits(32)) for _ in range(624)] 11 | 12 | # because the seed has only 7 chars it can be recovered using 8 carefully chosen outputs 13 | I_230_, I_231 = invertStep(S[3], S[230]) 14 | I_231_, I_232 = invertStep(S[4], S[231]) 15 | I_232_, I_233 = invertStep(S[5], S[232]) 16 | I_233_, I_234 = invertStep(S[6], S[233]) 17 | 18 | I_231 += I_231_ 19 | I_232 += I_232_ 20 | I_233 += I_233_ 21 | 22 | 23 | # K[16] + 16 24 | seed_l = recover_Kj_from_Ii(I_233, I_232, I_231, 233) - 16 25 | # K[17] + 17 26 | # two possibilities for I_234 27 | seed_h1 = recover_Kj_from_Ii(I_234, I_233, I_232, 234) - 17 28 | seed_h2 = recover_Kj_from_Ii(I_234+0x80000000, I_233, I_232, 234) - 17 29 | 30 | seed1 = (seed_h1 << 32) + seed_l 31 | seed2 = (seed_h2 << 32) + seed_l 32 | 33 | # only the MSB of K[17] differs 34 | print(bytes.fromhex(hex(seed1)[2:])) 35 | print(bytes.fromhex(hex(seed2)[2:])) 36 | 37 | 38 | -------------------------------------------------------------------------------- /recover_DefaultSeed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | # random is not manually seeded so it uses 624 random values 6 | 7 | S = [untemper(random.getrandbits(32)) for _ in range(624)] 8 | I = rewindState(S) 9 | 10 | print("Normal run :") 11 | 12 | print(random.getrandbits(32)) 13 | print(random.random()) 14 | print(random.randbytes(4).hex()) 15 | print(random.randrange(1, 100000)) 16 | 17 | print("\nReseeded run :") 18 | 19 | seed_array = seedArrayFromState(I) 20 | seed = seedArrayToInt(seed_array) 21 | 22 | # the recovered seed is very big. Too big to be printed in decimal 23 | # print(hex(seed)) 24 | # The recovered seed is not exactly the same, but is equivalent. 25 | random.seed(seed) 26 | 27 | S_ = [untemper(random.getrandbits(32)) for _ in range(624)] 28 | 29 | assert(S_ == S) 30 | 31 | print(random.getrandbits(32)) 32 | print(random.random()) 33 | print(random.randbytes(4).hex()) 34 | print(random.randrange(1, 100000)) 35 | -------------------------------------------------------------------------------- /recover_FloatSeed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from functions import * 3 | import random 4 | 5 | SEED = 18726.12612 6 | # We can't recover the original seed in this case, just the equivalent 64-bit integer 7 | print(f"{hex(hash(SEED)) = }") 8 | random.seed(SEED) 9 | # k = 2 10 | # K = [0x6c004926, 0x4092ccf] 11 | 12 | S = [untemper(random.getrandbits(32)) for _ in range(624)] 13 | 14 | I_227_, I_228 = invertStep(S[0], S[227]) 15 | I_228_, I_229 = invertStep(S[1], S[228]) 16 | I_229_, I_230 = invertStep(S[2], S[229]) 17 | I_230_, I_231 = invertStep(S[3], S[230]) 18 | 19 | I_228 += I_228_ 20 | I_229 += I_229_ 21 | I_230 += I_230_ 22 | 23 | # K[1] + 1 24 | seed_h = recover_Kj_from_Ii(I_230, I_229, I_228, 230) - 1 25 | # K[0] + 0 26 | # two possibilities for I_231 27 | seed_l1 = recover_Kj_from_Ii(I_231, I_230, I_229, 231) 28 | seed_l2 = recover_Kj_from_Ii(I_231+0x80000000, I_230, I_229, 231) 29 | 30 | seed1 = (seed_h << 32) + seed_l1 31 | seed2 = (seed_h << 32) + seed_l2 32 | 33 | # only the MSB of K[0] differs 34 | print(hex(seed1), hex(seed2)) 35 | 36 | 37 | --------------------------------------------------------------------------------