From 2f860b0ad2439adfb82701e859b85a8f8719bf7d Mon Sep 17 00:00:00 2001 From: Jorian Woltjer <jorianwoltjer@hotmail.com> Date: Fri, 5 May 2023 10:37:09 +0200 Subject: [PATCH] Implement offsetting to previous internal states --- README.md | 32 +++++++++++++++++++++++- randcrack/randcrack.py | 48 ++++++++++++++++++++++++++++++++++-- randcrack/test.py | 18 ++++++++++++++ tests/test_randcrack.py | 54 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 randcrack/test.py diff --git a/README.md b/README.md index 0c7ae76..ad1d67d 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ This cracker works as the following way. It obtains first 624 32 bit numbers fro It is **important to feed cracker exactly 32-bit integers** generated by the generator due to the fact that they will be generated anyway, but dropped if you don't request for them. As well, you must feed the cracker exactly after new seed is presented, or after 624*32 bits are generated since every 624 32-bit numbers generator shifts it's state and cracker is designed to be fed from the begining of some state. -#### Implemented methods +### Implemented methods Cracker has one method for feeding: `submit(n)`. After submitting 624 integers it won't take any more and will be ready for predicting new numbers. @@ -54,3 +54,33 @@ print("Random result: {}\nCracker result: {}" Random result: 127160928 Cracker result: 127160928 ``` + +As well as predicting future values, it can recover the *previous* states to predict earlier values, ones that came before the numbers you submit. After having submitted enough random numbers to clone the internal state (624), you can use the `offset(n)` method to offset the state by some number. + +A positive number simply advances the RNG by `n`, as if you would ask for a number repeatedly `n` times. A **negative** number however will *untwist* the internal state (which can also be done manually with `untwist()`). Then after untwisting enough times it will set the internal state to exactly the point in the past where previous numbers were generated from. From then on, you can call the `predict_*()` methods again to get random numbers, now in the past. + +```python +import random, time +from randcrack import RandCrack + +random.seed(time.time()) + +unknown = [random.getrandbits(32) for _ in range(10)] + +cracker = RandCrack() + +for _ in range(624): + cracker.submit(random.getrandbits(32)) + +cracker.offset(-624) # Go back -624 states from submitted numbers +cracker.offset(-10) # Go back -10 states to the start of `unknown` + +print("Unknown:", unknown) +print("Guesses:", [cracker.predict_getrandbits(32) for _ in range(10)]) +``` + +> **Warning**: The `randint()`, `randrange()` and `choice()` methods all use `randbelow(n)`, which will internally may advance the state **multiple times** depending on the random number that comes from the generator. A number is generated with the number of bits `n` has, but it may still be above `n` the first time. In that case numbers keep being generated in this way until one is below `n`. +> +> This causes predicting **previous** values of these functions to become imprecise as it is not yet known how many numbers were generated with the single function call. You will still be able to generate all the numbers if you offset back further than expected to include all numbers, but there will be an amount of numbers before/after the target sequence (e.g. if the sequence is `[1, 2, 3]`, guesses may be `[123, 42, 1, 2, 3, 1337]`). +> +> This is not a problem with the `getrandbits()` method, as it always does exactly 1. And the `random()` method always does exactly 2 diff --git a/randcrack/randcrack.py b/randcrack/randcrack.py index c092238..eeb3b0c 100644 --- a/randcrack/randcrack.py +++ b/randcrack/randcrack.py @@ -211,6 +211,37 @@ def _regen(self): self.counter = 0 + def untwist(self): + w, n, m = 32, 624, 397 + a = 0x9908B0DF + + # I like bitshifting more than these custom functions... + MT = [self._to_int(x) for x in self.mt] + + for i in range(n-1, -1, -1): + result = 0 + tmp = MT[i] + tmp ^= MT[(i + m) % n] + if tmp & (1 << w-1): + tmp ^= a + result = (tmp << 1) & (1 << w-1) + tmp = MT[(i - 1 + n) % n] + tmp ^= MT[(i + m-1) % n] + if tmp & (1 << w-1): + tmp ^= a + result |= 1 + result |= (tmp << 1) & ((1 << w-1) - 1) + MT[i] = result + + self.mt = [self._to_bitarray(x) for x in MT] + + def offset(self, n): + if n >= 0: + [self._predict_32() for _ in range(n)] + else: + [self.untwist() for _ in range(-n // 624 + 1)] + [self._predict_32() for _ in range(624 - (-n % 624))] + if __name__ == "__main__": import random @@ -222,8 +253,21 @@ def _regen(self): random.seed(time.time()) + unknown = [random.getrandbits(32) for _ in range(1000)] + for i in range(624): cracker.submit(random.randint(0, 4294967294)) - print("Guessing next 32000 random bits success rate: {}%" - .format(sum([random.getrandbits(32) == cracker.predict_getrandbits(32) for x in range(1000)]) / 10)) + # Future values after syncing + percentage = sum([random.getrandbits(32) == cracker.predict_getrandbits(32) for x in range(1000)]) / 10 + print(f"Guessing next 32000 random bits success rate: {percentage}%") + assert percentage == 100 + + # Previous values + cracker.offset(-1000) # From guessing future + cracker.offset(-624) # From submitting + cracker.offset(-1000) # Back to start of unknown + + percentage = sum([unknown[i] == cracker.predict_getrandbits(32) for i in range(1000)]) / 10 + print(f"Guessing previous 32000 random bits success rate: {percentage}%") + assert percentage == 100 diff --git a/randcrack/test.py b/randcrack/test.py new file mode 100644 index 0000000..1ace8ff --- /dev/null +++ b/randcrack/test.py @@ -0,0 +1,18 @@ +import random +import time +from randcrack import RandCrack + +random.seed(time.time()) + +unknown = [random.getrandbits(32) for _ in range(10)] + +cracker = RandCrack() + +for _ in range(624): + cracker.submit(random.getrandbits(32)) + +cracker.offset(-624) # Go back -624 states from submitted numbers +cracker.offset(-10) # Go back -10 states to the start of `unknown` + +print("Unknown:", unknown) +print("Guesses:", [cracker.predict_getrandbits(32) for _ in range(10)]) diff --git a/tests/test_randcrack.py b/tests/test_randcrack.py index 364ab65..3ed7280 100644 --- a/tests/test_randcrack.py +++ b/tests/test_randcrack.py @@ -11,7 +11,7 @@ def test_submit_not_enough(): cracker = RandCrack() - for i in range(623): + for _ in range(623): cracker.submit(random.randint(0, 4294967294)) with pytest.raises(ValueError): @@ -23,11 +23,11 @@ def test_submit_too_much(): cracker = RandCrack() - for i in range(624): + for _ in range(624): cracker.submit(random.randint(0, 4294967294)) with pytest.raises(ValueError): - cracker.submit(random.randint(0, 4294967294)) + cracker.submit(random.getrandbits(32)) def test_predict_first_624(): @@ -35,7 +35,7 @@ def test_predict_first_624(): cracker = RandCrack() - for i in range(624): + for _ in range(624): cracker.submit(random.randint(0, 4294967294)) assert sum([random.getrandbits(32) == cracker.predict_getrandbits(32) for _ in range(1000)]) == 1000 @@ -46,16 +46,58 @@ def test_predict_first_1000_close(): cracker = RandCrack() - for i in range(624): + for _ in range(624): cracker.submit(random.randint(0, 4294967294)) assert sum([random.getrandbits(32) == cracker.predict_getrandbits(32) for _ in range(1000)]) == 1000 + def test_predict_random(): random.seed(time.time()) cracker = RandCrack() - for i in range(624): + for _ in range(624): cracker.submit(random.randint(0, 4294967294)) + assert sum([random.random() == cracker.predict_random() for _ in range(1000)]) == 1000 + + +def test_predict_previous(): + random.seed(time.time()) + + unknown = [random.getrandbits(32) for _ in range(1000)] + + cracker = RandCrack() + + for _ in range(624): + cracker.submit(random.getrandbits(32)) + + cracker.offset(-624) + cracker.offset(-1000) + + assert unknown == [cracker.predict_getrandbits(32) for _ in range(1000)] + + +def test_predict_previous_randbelow(): + random.seed(time.time()) + + # randint() uses randbelow() internally + unknown = [random.randint(1, 800) for _ in range(1000)] + + cracker = RandCrack() + + for _ in range(624): + cracker.submit(random.getrandbits(32)) + + cracker.offset(-624) + cracker.offset(-2000) # Go back too far to include everything + + guesses = [cracker.predict_randint(1, 800) for _ in range(2000)] + + def is_subseq(x, y): + return all(any(c == ch for c in y) for ch in x) + + # The sequence of unknown numbers should be in guesses, but there may be numbers before and after + # (e.g. if the sequence is [1, 2, 3], guesses may be [123, 42, 1, 2, 3, 1337]) + assert is_subseq(unknown, guesses)