459 words
2 minutes
100% Robust PRNG

100% Robust PRNG#

Let’s try to create an unhackable of pseudo-random number (PRN) of the Python interpreter. We assume that the PRNs are generated by the getrandbits(31) function of the standard random library. You can ask for several consecutive PRNs, up to 1000 pieces. And then you have to guess the next PRN.

Server code#

import time
import random
import math
flag = "FAKE_FLAG"
m = random.getrandbits(31)
a = 2**10
b = 2**30
rand_numbers = []
d = """
Попробуем взломать генератор псевдослучайных чисел (ПСЧ) Python.
Считаем, что ПСЧ создаются функцией getrandbits(31) стандартной библиотеки random.
Вы можете попросить несколько последовательных ПСЧ, до 1000 штук.
А затем вы должны угадать следующее ПСЧ."""
print (f"{d}\n\n")
print ('1. Получить следующее число')
print ('2. Угадать следующее число')
ind = 0
while True:
if ind == 1000:
print ('Слишком долго думаешь. Отдохни ...')
break
inp = input('> ')
if inp == '1':
print (f"Следующее число: {random.getrandbits(31)}")
elif inp == '2':
ans = int(input('Ваше число: '))
my_ans = random.getrandbits(31)
if ans == my_ans:
print (f"\nФлаг: {flag}")
else:
print (f"\nОшибка. Мое число: {my_ans}")
break
ind += 1

Code is pretty similar to the last chall but this time its not choosing a specific seed. This means it is prob getting it from /dev/urandom which is impossible to guess

So this time we need to get a bunch of inputs and crack the Mersenne Twister (the PRNG python uses). Usually all we need are 624 inputs of 32 bit numbers and we are able to guess the next number. However this time we only get 31 bits which makes it challenging since we are missing a bit everytime

Now a lot of crackers found online dont support less than 32 bits but I was able to find one that does any number of bits by using z3 to crack the Mersenne Twister here

Mistsuu
/
randcracks
Waiting for api.github.com...
00K
0K
0K
Waiting...

Solution#

import math
import time
import random
from pwn import *
from mt19937_crack import RandomSolver
def getnextguess(l):
randomSolver = RandomSolver()
for i in l:
randomSolver.submit_getrandbits(i, 31)
randomSolver.solve()
return randomSolver.getrandbits(31)
io = remote('ctf.mf.grsu.by', 9046)
output = io.readuntil(b'>')
l = []
for i in range(999):
io.sendline("1")
o = io.recvline()
num = int(o.decode().split(': ')[1].strip(" \n'"))
print(i, num)
l.append(num)
print("NEXT NUM: ", getnextguess(l))
io.interactive()

Code is pretty much the same, we just grab 1000 inputs, submit it to the randomsolver and make it to solve. Then we get our next generated number

Since we only get 1000 inputs it can get pretty inaccurate (we should need about 624*2 = 1248 inputs) but if we try a few times we can get lucky and get the flag

...
995 712169511
996 24678483
997 945543443
998 1180336752
NEXT NUM: 818870482
[*] Switching to interactive mode
> $ 2
Ваше число: $ 818870482
Флаг: grodno{0ebcd057543753ee54f3a077c6644953923237233143562f0e60}

Conclusion#

Again pythons PRNG can be easily hacked so don’t use it for things that need to be secure