6 ms·
Inverting the Xorshift128 random number generator
- viraptor 1y ago> I couldn’t believe that z3 was the best one can do It probably never is, but if you don't care about a nice solution or don't care about anything past "is it possible?", then z3 and smt2 are amazing. You often don't even need to understand the whole problem. "take constraints, give solution" works great in those situations.
- Sesse__ 1y agoI've tried to use Z3 to find hash collisions in a simple hash, and it was amazingly bad at it. It took forever, whereas just trying random values until some were (multi)colliding was sub-second :-)
- NooneAtAll3 1y agolooking at the CVE report itself, Math.random() not being crypto-level seems to be known? - and vulnerability comes from Node.js using it for some crypto purpose so OP simply did a good exercise for himself recreating exact weakness of it
- tptacek 1y agoNo, the post takes the attack from 5 observations down to 3.
- NooneAtAll3 1y agono, the post takes a shoddy quickly-made implementation of an attack and improves it to its own better implementation of an attack neither are professional frontline research, because said frontline work has already been done long loong ago when Xorshift was popularized and definitely when it became javascript's *default* rng this is recreational cryptography, don't over-present it
- tptacek 1y agoPlease tell me more about what does and doesn't qualify as recreational cryptography.
- ScottContini 1y agoYou can call it recreational cryptography. I am no longer a professional cryptographer: I used to be. Now I have a full time job in the software industry and a family with kids. I don't have a lot of free time to work on cryptography like professional cryptographers do. The person you replied to is correct. To my knowledge, the best inversion of Math.random( ) is this one: https://github.com/PwnFunction/v8-randomness-predictor https://github.com/PwnFunction/v8-randomness-predictor . It takes 5 outputs from Math.random( ) to determine the seed. My research included a 2^50 algorithm to get it with 3 outputs. If there is a better implementation out there that does it in less than 2^50 work for 3 outputs, could you please provide a link to the implementation? Also, as I said in the blog, this is a first step. I think I can bring it down by a factor of 2^6 with another trick I am working on, but details are still being tested. As the saying goes, attacks always get better, never worse. The blog is also to encourage the aspiring or amateur cryptographer to have a look themselves. Nothing in my research is particularly deep, so I hope it shows a wider audience that what cryptographers do doesn't always require complex mathematics. This is a simple attack and I thought it was worth blogging about. I have another blog about why I left cryptography. Part of it is about being stuck in doing research that has no practical implications. To a real cryptographer, attacking XorShift128+ and Math.random( ) may seem uninteresting. I have a different view. Engineers make mistakes and use the wrong tools for the job all the time. We tell them it is wrong, but it is so much more powerful to prove it. When I looked at CVE-2025-7783, I just shook my head: the web security community is stuck using less than ideal tools (requiring 5 outputs to invert the algorithm) because the cryptographic community does not value producing tools to invert things that are not designed for cryptographic purposes. I think this attitude is doing a disfavour to the web security community.
- ScottContini 1y agoAlso: I just realised who the person is that you were replying to, tptacek. Maybe you should Google his name. He's well known in cryptography, glad he is on my side! :-)
- Retr0id 1y agoIf you represent the state as a 128-long vector of GF(2) elements, you can model the state transition function as a matrix multiplication. This allows you to describe any output bit (at any offset in the output stream) as a function of the 128 initial state elements. Treating the initial state vector as 128 unknown variables, you can solve for them (via gaussian elimination) with any 128 bits from the output stream, so long as you know their offsets in the output sequence. Although, depending on what those offsets are there's a ~50% chance there's no unique solution, in which case you may need a few more bits. For the case of two consecutive 64-bit outputs, you get a unique solution. For 128 samples, the gaussian elimination can be done in 2^21 steps, or only 2^14 steps if you can put a whole bit-vector in one register (which you can!) If the offsets are known in advance, you can pre-compute the gaussian elimination work and end up with a constant matrix that, if multiplied by the leaked bits, obtains the initial state vector.
- tptacek 1y agoIf you follow the ChatGPT log, that's the angle he takes as well.
- Retr0id 1y agoOh that's fascinating, I didn't get that impression at all from the article itself.
- ScottContini 1y agoIf you look at the chatgpt conversation which is linked at the bottom of the article (it will take a minute to load because it is very long), you will see that my initial approach was using matrices over GF(2). I am in the process of writing a blog about how I thought I could solve this quickly using ChatGPT, but unfortunately it took me down a bunch of rabbit holes that didn't work. Ultimately the ideas that did work were my own, but I still think using ChatGPT is useful if you know how to use it right (keep it on a tight leash, more details coming soon). Back to the matrices... I thought I have an idea using matrices that would solve it really fast. It didn't work. There were dependencies in the matrix that I did not expect that made the idea fail. You can see all the details in the chat log. With your recommendation, it is not clear to me that you are dealing with the problem that there is an integer addition of the state values to get the outputs. This integer addition ruins the direct usage of linear algebra to solve the problem. The initial approach that ChatGPT suggested, which is its own idea (not mine), was that we could bring in the integer carry bits as unknown variables and derive them via induction. I could come back to that idea, but I am sceptical that will work. Eventually I got fed up with ChatGPT's implementation and it trying to help debug, which was only slowing me down. It kept trying to change the underlying data structures during the debugging. I had to scold it about that: you cannot change things when you are debugging it. Ultimately, I did away with the linear algebra and just approached it from some simple observations about how bits affect other bits. It is linear algebra under the hood, but I did not use matrices at this point. One of the reasons why I wrote the blog is to encourage more people to look at this. We should be able to invert this in 2 or 3 outputs, whereas Z3 requires 5. Nothing I am doing is complex. I would love to see someone produce a tool that breaks it really quickly. And I really mean a tool, not just a statement on why it is weak. We all know it is weak, but web security researchers need tools to use that demonstrate exploits based upon primitives that are wrongly used. So I would strongly encourage people with ideas to implement them and blog about the ones that work. I have another idea on the plate that I think might bring the search down to 2^20 effort, but need time to work out details. Free time is the hardest thing in life for me to find: I just have a busy life. But I also have a passion for building fast tools and puzzle solving.
- mococa 1y agoI have recently replaced Lua's random for this implemetation https://nullonerror.org/2025/08/02/replacing-lua-s-math-random-module-with-the-xorshift-algorithm/ https://nullonerror.org/2025/08/02/replacing-lua-s-math-rand...
- bazzargh 1y agoA word of caution. A few years ago we had a production impact event where customers were getting identical cookies (and so started seeing each others sessions). When I took a look at the code, what I found was that they were doing something very like your code - using a time() based seed and an PRNG. Whenever we deployed new nginx configs, those servers would roll out and restart, getting _similar_ time() results in the seed. But the individual nginx workers? Their seeds were nearly identical. Not every call to the PRNG was meant for UUIDs, but enough were that disaster was inevitable. The solution is to use a library that leverages libuuid (via ffi or otherwise). A "native lua" implementation is always going to miss the entropy sources available in your server and generate clashes if it's seeded with time(). (eg https://github.com/Kong/lua-uuid https://github.com/Kong/lua-uuid, https://github.com/bungle/lua-resty-uuid https://github.com/bungle/lua-resty-uuid)
- tptacek 1y agoWhy would you ever use an insecure RNG to generate a cookie?
- bazzargh 1y agoIn the code I saw, at least twice in its history people had introduced a "pure lua" solution for speed, and were clearly unaware of the shotgun they'd just pointed at their feet. (as in, somebody saw the issue and fixed it, and then someone else _fixed it back_ before I came along). But in case _I'm_ messing up here, I'll bow to your expertise: libuuid uses /dev/random, which uses a CSPRNG (ChaCha20) with entropy ingested via Blake2 from whatever sources the system can get, right? We did actually do a bunch of before/after testing showing the collision rates (zero after), and I believe the cookie in question has been replaced with a third party identity system in the intervening years - but if we did it wrong, I'd like to know.
- Aardwolf 1y agoXorshift128+ is not a cryptographic rng though, so at least this isn't a cryptographic attack... Should programming languages use cryptographic rngs like a ChaCha20 based one in their standard libraries to stop accidental use of non cryptographic rngs for cryptographic purposes? But that comes at the cost of speed
- kstrauser 1y agoI think some naming conventions could go a long way. If you want to import `fast_unsafe_random`, you might think twice.
- cozzyd 1y agofunsafe_random
- thomasmg 1y agoI agree, why would you slow down things for everybody if it's only a problem for cryptographic purposes. Xorshift128+ etc are around 10 to 30 times faster than ChaCha20. The challenge is things that don't _obviously_ need cryptographically secure generators. For example, do you need a secure generator for the seed of a hash table, or a sorting algorithm? (For those that do use a seed). Some will argue that yes, this is important. Until a few years ago, the hash tables used static hash algorithms without any randomization, but "hash flooding" changed that. I think that nowadays, still many hash table implementations don't use secure generators. Then, there's secure and insecure hash functions. Secure hash functions like SHA-256 are (compared to non-secure functions) specially slow for short keys. There are "somewhat" secure hash function algorithms like SipHash that can be used for this purpose.
- tptacek 1y agoI think people overthink this, and we should just have a library standard full-strength CSPRNG, and then people with fussy fast-randomness needs (Monte Carlo or whatever) can just pull in libraries. "Don't need secure random and can't use it" is a very niche problem space. It'll never happen in Go though! They're over a decade committed to this library shape.
- cratermoon 1y ago> I was truly amazed to see ChatGPT understand my reasoning and even come up with its own ideas to help improve the research. ChatGPT did nothing of the sort. The creators of ChatGPT happened to have in their corpus a sufficient amount of text from related research, forums and blogs discussing RNGs, and related math and programming topics to create a model that can generate plausible-sounding synthetic text.