2
local digits = {'1', '2', '3', '4', '5', '6', '8', '9'} 
math.randomseed(os.time()) 
local result = digits[math.random(8)] 
for i = 2, 50 do 
    result = result..digits[math.random(8)] 
end 
print(result)

--> 88854243421464255299891111895292628431988589634664

This is a Lua script designed to spit out fifty random digits, excluding 0 and 7. I can't seem to find the Lua randomness algorithm. Is it possible to find the value returned by os.time()?

To put it in layman's terms, here's how the script works:

  1. Define a list of eight single-character strings.
  2. Seed the pseudo-random algorithm with the number of seconds since midnight on January 1, 1970.
  3. Select a number between one and eight using the pseudo-random algorithm, then use that to select an item from the list. E.g. the number selected is seven, the algorithm picks the seventh item in the list.
  4. Add that item onto the end of the result. Repeat until the result is fifty digits long.

So because seven is left out, the random algorithm really spit this out:

77754243421464255288781111785282627431877578634664

But because seven is missing, seven and eight became eight and nine, yielding this:

88854243421464255299891111895292628431988589634664

To clarify, I'm trying to figure out how the random algorithm works to find the key that was entered as a random seed.

Also asked here: https://forum.roblox.com/Forum/ShowPost.aspx?PostID=215349554 and here: https://scriptinghelpers.org/questions/42360/can-this-script-be-reverse-engineered-to-find-the-random-seed and here: https://crypto.stackexchange.com/questions/47027/can-this-code-be-reverse-engineered-to-find-the-random-seed

Zenon
  • 131
  • 6

2 Answers2

4

Can this code be reverse-engineered to find the random seed?

Maybe, maybe not. It depends on the implementation of math.random.

Is it possible to find the value returned by os.time()?

Of course. os.time() is defined as (emphasis mine)

The time function, when called without arguments, returns the current date and time, coded as a number. (In most systems, that number is the number of seconds since some epoch.)

We're talking about a 32 bit integer. This can be easily brute-forced. You can even assume some bounds. This was probably generated in the last month or so and there is no need to search for the seed which is in the future. There are only 22 bit of different seconds in the last 30 days.

You can simply iterate through the seeds from now to the past and generate the 50 digit long random value each time. Then you only need to compare it with the one you have and stop when you found the match. This shouldn't take longer than a minute.

Here's the full code for that:

function GetDigits(t)
    local digits = {'1', '2', '3', '4', '5', '6', '8', '9'}
    math.randomseed(t)
    local result = digits[math.random(8)]
    for i = 2, 50 do
        result = result..digits[math.random(8)]
    end
    return result
end

ct = os.time()
while ct > 1 do
    if ct % 100000 == 0 then
        print (ct)
    end
    if GetDigits(ct) == "88854243421464255299891111895292628431988589634664" then
        print ("found: " .. ct)
        break
    end
    ct = ct - 1
end

Output

1493400000
1493300000
1493200000
1493100000
1493000000
1492900000
1492800000
1492700000
1492600000
1492500000
1492400000
1492300000
1492200000
1492100000
1492000000
1491900000
1491800000
1491700000
1491600000
1491500000
found: 1491404649

This only works up to Lua v5.1, because the implementation changed in v5.2 and v5.3. I've tried it only on windows.

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
  • 1
    Try appending the chars to a table, then using `table.concat` to generate the string. With the algorithm as it is, Lua has to store 50 different strings in memory for each random seed that you try (e.g. for the OP's example result, Lua would store the strings "8", "88", "888", "8885", ..., "88854243421464255299891111895292628431988589634664"). Appending should greatly reduce the number of memory reads and writes that Lua needs to do. – Jack Taylor Apr 29 '17 at 10:41
  • 1
    @JackTaylor Thanks! I just timed it. The shown version takes 57 seconds on my machine and your improved version takes 48 seconds. – Artjom B. Apr 29 '17 at 10:59
  • Thanks! That answer seems about accurate to when I ran this code for the first time. I ran it in Lua 5.1 on Windows. My intention wasn't to generate a secure pseudo-random key. I excluded sevens and zeroes because I used this for an experiment I conducted for my psychology class. I tested peoples' memory by seeing how many numbers they could remember. Because seven and zero each have two syllables in English, there was a possibility they could've skewed the results. – Zenon Apr 29 '17 at 14:13
  • 2
    You could probably save quite a lot of time by doing the comparison one digit at a time, and rejecting the seed as soon as you find a mismatch. 87.5% of all seeds will fail at the first digit. – Ilmari Karonen Apr 29 '17 at 14:24
  • 1
    @Iimari: I implemented your suggestion [here](https://pastebin.com/pGngrxNi). It finds the random seed in 3.8s on my old Linux laptop. (To make it work I had to generate a new 50-digit target for the time 1491404649, as Linux gives different random numbers to Windows, but in the linked code I've used the Windows target.) – Jack Taylor Apr 29 '17 at 15:53
  • @JackTaylor Wow! It takes roughly a second on my machine. – Artjom B. Apr 29 '17 at 16:30
3

This isn't a full answer to your question, but it might help you to get started. You can find the C source for Lua's math.random function here (that's for Lua 5.1, but other versions are similar). Lua doesn't attempt to generate pseudo-random numbers itself, but instead outsources that task to the C standard library.

Specifically, math.random generates numbers using C's rand function, and math.randomseed sets the seed for the pseudo-random-number generator using C's srand function (covered in the same man page as rand). These functions are implemented differently on every system (e.g. the x86 Windows version is different from the x86 Linux version, there will be different versions for different chipsets, etc.), so you would have to analyse the algorithm for each of the systems you are interested in.

For information about how rand is implemented, this question should give you a start.

One thing you should definitely be careful of is that if someone can guess on what second you called os.time (or if they just called it at the same time as you), then they would be able to generate the exact same psuedo-random numbers as you by passing that value to math.randomseed. It would be better to use a seed with a higher entropy if you have one available.

Community
  • 1
  • 1
Jack Taylor
  • 5,588
  • 19
  • 35