Steam has a rather interesting login method – InformTFB

Steam has a rather interesting login method

Steam has a rather interesting login method

image

How do I transmit my password over the Internet? Typically, an SSL certificate is purchased, and TLS performs the task of securely moving the password from the client to the server. Of course, everything is not as dry as I try to imagine, but in General it is true and this approach has passed the test of time. However, this was not always the case, and one incredibly popular online store chose to add something of its own to the process. In this article, I will talk about a unique way to log in to Steam users and explore the deep rabbit hole of amazing details of its implementation.

Revealing the obvious

I found on StackOverflow a question dated 2013 about how to securely transmit a password over HTTP. The answers were quite unanimous: you need to get an SSL certificate. Conduct an experiment: set up your favorite traffic interception proxy, log in to a service that you often use, log in with your own account (or preferably a one-time one), and study the results. You will most likely see that the user name and password are passed in clear text in the HTTP request body. The only reason this works is because your connection to the server is encrypted using TLS.

It’s strange to think that this was once a problem

However, in the early 2010s, not to mention earlier, the Internet was different. Now we have services like Let’s Encrypt companies that issue free SSL certificates with a three-month validity period and the ability to automatically update them. At that time, there were almost no other options than purchasing an SSL certificate for money, but usually you could get longer validity periods and support. Of course, you can say that it is worth paying for the security and privacy of users, but this does not prevent questions like the one above from appearing.

So, we have come to understand that TLS is important, and now let’s replace it. Let’s imagine that we can’t transmit passwords over HTTPS and we somehow need to implement this on pure HTTP, while providing some level of security. There is a standardized and widely used title Authorization. However, when combined with the “Basic” HTTP authentication scheme, when used with pure HTTP, it does not provide any protection.

There are proven and tested request-response algorithms, the most notable of which is SRP designed for password authentication without transmitting a password, but they may have to be implemented independently, and even a small oversight can lead to serious damage. You can also assign authentication to an external service. The “login with service XYZ” scheme is widespread, but it has certain consequences. With all this in mind, we can say that passing secrets over a connection that is not intended for security is a non — trivial task.

So when a friend and I decided to explore Steam in search of traces of information that allows us to identify people, I was surprised that the Steam login page uses only TLS to ensure security.

Crypto is the cherry on the cake

Re-launch your favorite traffic interception proxy and go to the Steam login page. We will enter the user name and password, and then we will be asked (at least we should) to enter a one-time token generated by your chosen two-factor authentication method. We can stop here, because the magic I want to demonstrate has already happened. You’ll notice that clicking the login button triggers a request for a strange endpoint: /login/getrsakeyfollowed by /login/dologin.

Sequence of all relevant resources and requests

If we examine request K/login/getrsakey, we will find a response in JSON format containing fields with names that are well known to anyone who knows at least a little about public key cryptography. The RSA public key is passed to us, although the specific values may look a little strange. Obviously, publickey_modboth publickey_expdetermine the modulus and exponent used in encryption, but the first one is set in hexadecimal, and the second one is set in binary (I’ll come back to this later). There is also a timestamp, the starting point of which can not be determined immediately. With an appointment token_gid I haven’t figured it out yet.

{
    "success":true,
    "publickey_mod":"c85ba44d5a3608561cb289795ac93b34d4b9b4326f9c09d1d19a9923e2d136b8...",
    "publickey_exp":"010001",
    "timestamp":"1260462250000",
    "token_gid":"2701e0b0a4be3635"
}

The login page loads scripts when loading. A completely unobfuscated login.jsone contains the main login handler, so anyone can just analyze it and figure out what it’s doing. In addition, the site loads additional dependencies, in particular, jsbn.jsand rsa.js.

A search for the name given in the first line jsbn.js revealed that these two scripts were written by an MIT and Stanford graduate Tom Wu, who loves SOFTWARE engineering and computer cryptography. He released jsbn.jsand rsa.js as a pure JavaScript implementation of arbitrary-precision integers and RSA encryption/decryption. We can also find out that these libraries were last updated in 2005 and 2013, but I’ll come back to that later. Just remember that for now.

Going down the rabbit hole

So, we have all the right resources, and we can dig deeper into login.js. Its code is quite chaotic, with lots of callbacks and proxy function calls, but the most interesting parts can be found pretty quickly. In fact, the script can be reduced to a couple of steps. Each of the steps assumes that everything went correctly in the previous one.

  1. The user enters their username and password, and then clicks the login button.
  2. CalledDoLogin, which checks whether the username mask is filled in correctly and executes a request to /login/getrsakey.
  3. Called OnRSAKeyResponse. It checks whether the request is formed correctly.
  4. Called GetAuthCode. It executes some kind of platform-specific code if two-factor authorization methods are enabled for the user’s account.
  5. Called OnAuthCodeResponse. Here, the password is encrypted with RSA, and a request to /login/dologin.
  6. Called OnLoginResponse. The user logs in and is redirected to the Steam store.

Code b OnAuthCodeResponseshows why the requested public key is formatted in this way. Starting from line 387 of the source file, the modulus and exponent of the response /login/getrsakeyare passed unchanged to the RSA library. The user’s password is then encrypted with the transmitted public key and added to the K request /login/dologinat the next login stage.

var pubKey = RSA.getPublicKey(results.publickey_mod, results.publickey_exp);
var username = this.m_strUsernameCanonical;
var password = form.elements['password'].value;
password = password.replace(/[^\x00-\x7F]/g, ''); // remove non-standard-ASCII characters
var encryptedPassword = RSA.encrypt(password, pubKey);

I copied the source files to the local machine to learn more about the RSA library. The module and exponent are passed to a function RSAPublicKeythat behaves like a constructor in the” pre-class ” era of JavaScript. RSAPublicKey just wrap the values in the instances BigInteger provided by the script jsbn.js. To my surprise, the exponent is not represented in binary, but, like the modulus, in hexadecimal. (Also, it turned out that 0x010001— is a very popular encryption exponent in RSA implementations.) That is, it is now clear that password encryption is based on 2048-bit RSA with an encryption exponent of 65537.

let r = RSA.getPublicKey("c85ba44d5a360856..." /* insert your own long modulus here */, "010001");
console.log(r.encryptionExponent.toString()); // => "65537"
console.log(r.modulus.bitLength()); // => 2048

Let’s move on to the field timestamp. The response /login/getrsakeycontains a header Expires. It refers to a date in the past, meaning that the response should not be cached or stored in any way. If we keep track of it for /login/getrsakeylonger, we will notice that the public key is constantly changing frequently, as is the timestamp. This means that there is a limited window of time during which a specific RSA public key issued by Steam can be used for authentication.

This becomes even more obvious when examining a subsequent query to /login/dologin. Among other things, it contains the user name, encrypted password, and time stamp of the issued RSA public key. The login attempt fails when the timestamp is changed, as expected. But more importantly, it is impossible to reuse the old public key even if the password is properly encrypted.

I went one step further and wrote a simple Python script to collect public keys a one-time account for three days. I used cronjob to run it every five minutes. I wanted to check how often the Steam public key changes and, if possible, understand how the field behaves timestamp.

A whole bunch of public keys

I found out that the public key changes after every 12 input attempts, so we can logically assume that they are replaced every hour. The encryption exponent remains the same, nothing unexpected here. However, the aforementioned field turned out to be more intriguing timestamp. For every 12 public keys, the value timestampincreases by a certain amount, namely 3600000000. In addition, as you can see in the image above, this number loops after a certain amount of time. I warn you, further reasoning is full of unconfirmed guesses.

The timestamp field is looped

I found out that 3600000000 microseconds is equal to one hour, so I assumed that the field value timestampis actually set in microseconds. However, I have already mentioned that the timestamp value does not increase by exactly one hour with each new public key. Based on the data collected, I noticed that the difference between two consecutive timestamps is one hour plus 1-2. 6 seconds, and most of them are on the order of 1.05-1.25 seconds. But in this case, there is another interesting possibility.

Assume that a new public key is generated every hour plus one second. If I make a request to the endpoint exactly every five minutes (while completely ignoring network latency), then there is a chance that I will encounter the same public key not 12, but 13 times in a row. This should happen when the request coincides in time with the generation of a new public key. Fortunately, since this time is on the order of a second, the margin for error is not so small.

Unique public keys are shown in different colors (no scale required!)

After studying my own set of public keys, I found out that I had not encountered any such borderline case. Maybe I was just unlucky, or maybe I was thinking hypothetically, waiting for some revelation. In addition, with increasing fluctuations in timestamp values, it becomes more difficult to predict a specific moment when a borderline case can be observed — if, of course, such a situation occurs at all.

But remember, this difference of an hour and one or two seconds is between with two unique public keys. Let’s return to the assumption that a new public key is created every hour and second. Then, after 3600 public keys, all these extra seconds add up to a full hour, which leads to the borderline case described in the previous paragraph. If the time difference occurs between the full hour on the clock and the timestamp of the public key, then everything becomes clear and these additional seconds are associated with network latency. However, this is not the case for the data I have collected, so the situation is puzzling to say the least.

So to summarize: if all my assumptions were correct, then the field timestampand its time difference between public keys is incredibly mysterious. Whether it is necessary to account for leap years? Or to compensate for some other delay? Maybe it’s just an implementation bug that Valve left out? Perhaps it is not expressed in microseconds, but in something a little more random? Maybe it’s just there to get prying nerds like me off their heads. I’m leaning towards the latest version.

I realize I missed the weird looping of the field value timestampand didn’t even touch the field assignment token_gid. I think that the former is necessary due to some technical limitation, and the latter is presumably some way to protect against CSRF (cross — site request forgery) or a unique identifier. This is completely unsubstantiated speculation, because I’ve already learned more from this research than I expected. If you would like to explore the issue yourself and share your findings, I would be happy if you contact me, either by emailor on Twitter.

Another aspect worth mentioning is that when you request a public key endpoint with different user names, you get different responses. It is not clear whether public keys are taken from the pool and each user is assigned a different timestamp offset, or whether they are actually generated on the fly. In addition, /login/getrsakeyyou can use any custom user name in the request to. It doesn’t have to be registered with Steam. You can use this information as you see fit.

Okay, but what does that mean?

While researching this topic, I developed a strange love for the Steam login mechanism. Now I know that on top of using TLS (which is what you need to use) when logging in, Steam also uses 2048-bit RSA to encrypt user passwords using an alternating public key system that correctly invalidates old keys and acts differently for each user. All this work seems very redundant, because an SSL certificate is enough for a secure user login.

Therefore, the question arises: why bother creating such a strangely intricate system on top of a mechanism that is quite sufficient in itself? I have a theory, but remember — it’s just a hunch.

Remember the release dates of libraries BigIntegerand RSA? In addition, the login page still uses jQuery version 1.8.3, released in November 2012, as its source. All this points to a simple fact: the login mechanism has remained virtually unchanged for almost a decade. And as I said at the beginning of this post, the Internet was completely different back then.

Oh! I found a typo in changelog jQuery 1.8.3! Is there any reward program for finding grammatical errors?

The modern web is developing the concept of “HTTPS everywhere”, but the current situation is the result of a long and painful process. My theory is that in the old days, Steam provided a layer of security for users who accidentally or intentionally did not get to the SSL/TLS version of the login site. This means that even if a third party can analyze all the data transmitted between the user and the Steam servers, it will at least not be able to find out their password (without powerful computing resources).

I tried contacting a Valve employee who is definitely working on the Steam store. I gave him a brief summary of my analysis and theory. I asked him if he could confirm this, or if he knew anyone who worked for the company at the time of creating this login method. Of course, I know that Valve employees have more important things to do than respond to a non-urgent and non-weekly request from some nerd. At the time of writing, I’m still waiting for an answer, so I can only offer my own educated guess. Be that as it may, this study turned out to be very, very interesting. interesting. Not everything has been researched yet, and I’m not done here.

Anderson
Anderson
Web site editor and tester.

Leave a Reply

Your email address will not be published. Required fields are marked *