Facepunch / garrysmod-requests

Feature requests for Garry's Mod
84 stars 24 forks source link

Tamper-proof clientside filesystem path #1663

Open xaviergmail opened 4 years ago

xaviergmail commented 4 years ago

The issue

Currently, every addon share the same data folder to read and write files. This obviously has its advantages, but is also problematic for the following reasons:

Examples of malicious usage:

My proposed solution

Add a new filesystem path restricted to server-specific usage bound to a new function for writing e.g file.SWrite (name is up for debate, I couldn't think of anything good) and add the "SDATA" path for other file.* operations.

The reason why I believe it should be a new function is to avoid potentially breaking changes if we were to add a second argument to file.Write in the event that existing scripts currently unknowingly pass more than two arguments (e.g by passing in the result of a function call with multiple return values- file.Write("test.txt", string.gsub(...))

The path would only be accessible by the client (both read and write) when connected to the corresponding server.

This new path could be exposed on the client's filesystem as MOD/server_data/<uuid> where UUID identifies a group of servers. This could be done by retrieving the Steam User ID for which the current connected server's Steam ID belongs to.

For example, my User Steam ID is 123123. I run 3 servers: 10.0.0.1 - GS Steam ID 111 10.0.0.2 - GS Steam ID 222 10.0.0.3 - GS Steam ID 333

When my game client is connected to either one of those three servers, it would have read+write access to MOD/server_data/123123.

There is one caveat; I'm not sure if it's possible to get the user associated with the game server's SteamID?

Disclaimer: I completely understand that this feature is not to be considered as complete protection and that addon creators should still code defensively. I also understand that this does not prevent players from editing their own data! The goal of this is to allow for things such as dynamic content and cross-server configuration to be stored directly on clients filesystems (rather than storing in say a database on the servers) without fear of the files being tampered with by malicious competitors.

robotboy655 commented 4 years ago

There is one caveat; I'm not sure if it's possible to get the user associated with the game server's SteamID?

I am fairly certain there isn't a way.

aStonedPenguin commented 4 years ago

Alternatively a random key could be created on the server and an accompanying function to change it for multi serer use. Then the checksum of that key and the players steamid could be sent to the client for use as the server identifier.

xaviergmail commented 4 years ago

The client should not trust what the server sends it directly, as anyone could easily modify the response payload with a binary module and a bit of work therefore rendering this unnecessary.

The only (slightly overkill) method I can think of right now would be to resort to cryptography (cough) would be nice

Server operators would generate an (RSA, deterministic) keypair and point convars to the public and private keys somewhere on the filesystem. Convar wouldn't even be necessary either as the files could reside in the garrysmod directory with a static name.

Handshake process:

English is hard. Have a Bash script instead:

STEAMID=76561198040907330

# Server:
# -- Server Operator Prerequisite
openssl genrsa -out private.rsa 2048
openssl rsa -in private.rsa -pubout > public.rsa

for x in {1..5}; do
    # -- On client connect:
    echo $STEAMID | openssl dgst -sha1 -sign private.rsa -out signed.bin
    # -- Send signed.bin and public.rsa to client

    # Client:
    valid=$(echo $STEAMID | openssl dgst -sha1 -verify public.rsa -signature signed.bin)
    if [[ $valid = 'Verified OK' ]]; then
        hash=$(cat signed.bin | sha1sum)
        echo  "Data dir: server_data/$hash"
    fi
done

I understand that hashing the signature might be a little unorthodox but I don't see it posing any immediate issue, especially when evaluating the risks at stake.

You could further expand upon this by generating the keypair at runtime using a sv_datafolder_secret convar as a seed to the PRNG, but this approach would significantly increase implementation complexity. I doubt generating a keypair would be too much of a big deal for people who see the benefit in this request anyway

Edit: Yes, this approach still relies on trusting the server at the end of the day, but it makes real-world spoofing attempts practically impossible

viral32111 commented 4 years ago

I'm not a cryptography expert but wouldn't it just be easier for the server to clearsign all data it stores on the client (such as a txt config file or something), then when reading it back just verify the signature authenticity?

It wouldn't prevent the data from being modified (for this you'd need to have permissions on the files/directories), but the server would immeditely know if it has been modified. This way no other servers, not even the client themselves can modify the data with malicious intentions as they can't resign the data (only the server would have the private key). The client nor the server has to rely on any trust whatsoever, unless you need to read back the data on the client realm at which point you would need the server's public key to verify it, but that has many issues too (see my comment below).

viral32111 commented 4 years ago
  • Server signs the client's SteamID64(string)using its private key using deterministic signing
  • Server sends the signature and its public key to the client
  • Client uses that public key to verify signature integrity

Also, this handshake is more or less pointless w/o the use of a third party to sign the public key. Anyone intercepting the connection (binary modules, malicious software, network/packet listeners, any sort of MiTM) could just read the signed data, resign it with their own private key and then forward their private key along with their created signature, as far as the client is concerned it would be valid. (unless of course you also encrypt the originally signed data from the server). From what I can tell this handshake would only really verify that the data has not been lost from network transfer, not authenticity, so it has basically the same level of security as a simple checksum.

xaviergmail commented 4 years ago

From what I can tell this handshake would only really verify that the data has not been lost from network transfer, not authenticity, so it has basically the same level of security as a simple checksum.

No, because signing the player's Steam ID with a different key will ultimately end up in the signature itself being different. Sure, someone could attempt to forge the signature and public key. However, as the signature itself is what's being used to generate the path, and the fact that the player's Steam ID is what's being signed, the level of entropy becomes significant to the point where it's practically impossible for a server to forge this data and the client can trust it with a high level of confidence.

In order for this to be forged by another server, someone would need to

  1. Know the original signature of the victim's Steam ID to begin with for the server they're trying to target (Not possible to obtain because the signature is converted with a one-way hash and that digest is what's used on the filesystem)
  2. Generate a keypair capable of producing the original signature

Not to mention that the attacker would also have to account for potential collisions without certainty of it working for every Steam ID

xaviergmail commented 4 years ago

I have to add that I'm not an expert in cryptography whatsoever. I only know enough to get by, but I have written a proof-of-concept, and it seems to be reliable! I will attempt to write the proof of concept using CryptoPP some time this week and put it up on my profile so that I can gather feedback from experts in the domain.

GGG-KILLER commented 4 years ago

tl;dr

Since the original server doesn't send its private key, there's no way to generate the same signature as it through MiTM. Protecting against any kind of tampering with the original server should be a non-goal of this proposal since if an attacker has access to the original server it's already game over. As should be validating if the server the client is connecting to is original or fake.

Long version

Terminology I'll be using on this comment:

Also, this handshake is more or less pointless w/o the use of a third party to sign the public key.

Yes, but as xavier pointed out, that will generate a different signature than the authentic server's. And since the point is preventing a malicious server from tampering with authentic server's data stored on the client, a malicious server being unable to generate the same signature as the authentic server should suffice. Validating whether the server the client is connecting to is the authentic server or a malicious server doesn't seem to be a goal of this proposal.

Anyone intercepting the connection (binary modules, malicious software, network/packet listeners, any sort of MiTM) could just read the signed data, resign it with their own private key and then forward their private key along with their created signature, as far as the client is concerned it would be valid. From what I can tell this handshake would only really verify that the data has not been lost from network transfer, not authenticity, so it has basically the same level of security as a simple checksum.

Not really. Since the authentic server signs it with its private key (which only the authentic server has), a malicious server wouldn't be able to produce a signature that matches the authentic server's since they'd need the authentic server's private key, and the only data the authentic server sends is the signed data and the public key.

Process summary:

  1. Authentic server signs an unique identifier with its private key (which only the authentic server has and is required to forge signed data);
  2. Authentic server sends the signed unique identifier along with its public key (which can only be used to verify data signed by the server);
  3. Client receives signed unique identifier along with the public key and validates it;
  4. Client uses the unique identifier as the protected clientside filesystem root directory name.

The unique identifier should be configurable ideally (or the SteamID64 as xavier suggested) and certificates should be stored in a known path with a known name so that server networks can have all their servers use the same path.

An additional security measure of not allowing these files to be read from lua would be nice as a protection against backdoors (I know I said this isn't meant to prevent tampering of the authentic server but this should be simple to do, I think).

Again, protecting against any kind of tampering of the authentic server should be a non-goal since if an attacker has access to your server it's already game over. And preventing malicious servers from impersonating the authentic server is also a non-goal.