eXPerience Posted June 2, 2023 Report Posted June 2, 2023 (edited) A game I was playing was updated recently, and the backend server started to reject the ticket returned from createAuthSessionTicket. There is no change in the payload format, so I'm confused what could possibly cause this. The backend accepts the session ticket only if it was generated via the game's launcher, and the ticket stops working once the game is closed. If I login and request an new app ticket via node-steam-user after the game launcher generated its own ticket and BEFORE submitting to the game backend, then the backend server rejects both tickets. Simplified code example: // Flow 1: User login user.on("loggedOn", (details, parental) => { // user.setPersona(SteamUser.EPersonaState.Invisible); user.gamesPlayed([APP_ID]); }); // Prints "Ticket xxx validated by [I:1:1]: OK" when POST request to backend is made user.on('authTicketStatus', details => { logger.info(`Ticket ${details.ticketGcToken} validated by ${details.steamID.steam3()}: ${SteamUser.EAuthSessionResponse[details.authSessionResponse]}`); }); user.on('appLaunched', appID => { if (appID != APP_ID) { return; } resolve(user); // resolves the promise for Flow 1 }) user.logOn(creds); // ======== Flow 2 ========= // // Create app ticket and auth with backend const user = await loginSteamUserFlow1(); const {sessionTicket} = await user.createAuthSessionTicket(APP_ID); payload.token = sessionTicket.toString('hex').toUpperCase(); // Backend rejects this call with some vague error message "OAuth exception" const resp = await client.post(`${GAME_HOST}/session`, {json: payload}) Parsed tickets: // valid ticket, generated by game // sessionExternalIP is some random IP everytime // ownershipTicketExternalIP is not exactly my external IP, but it probably is a previously held IP // tokenGenerated doesnt seem to be anywhere near the current time { authTicket: <Buffer 14 00 00 00 b4 truncated... 2 more bytes>, gcToken: 'xxxx', tokenGenerated: 2023-06-02T02:22:25.000Z, sessionExternalIP: '101.218.247.224', clientConnectionTime: 2734877, clientConnectionCount: 4, version: 4, steamID: SteamID { universe: 1, type: 1, instance: 1, accountid: xxxx }, appID: 958260, ownershipTicketExternalIP: 'xxx.xxx.xxx.xx', ownershipTicketInternalIP: '192.168.1.101', ownershipFlags: 0, ownershipTicketGenerated: 2023-05-24T02:05:48.000Z, ownershipTicketExpires: 2023-06-14T02:05:48.000Z, licenses: [ 313233 ], dlc: [], signature: <Buffer 31 74 f0 b7 truncated... 78 more bytes>, isExpired: false, hasValidSignature: true, isValid: true } // generated by node-steam-user, rejected by backend { authTicket: <Buffer 14 00 00 00 b1 truncated ... 2 more bytes>, gcToken: 'xxxx', tokenGenerated: 2023-06-02T03:28:41.000Z, sessionExternalIP: 'xxx.xxx.xxx.xxx', clientConnectionTime: 101, clientConnectionCount: 1, version: 4, steamID: SteamID { universe: 1, type: 1, instance: 1, accountid: xxxxx }, appID: 958260, ownershipTicketExternalIP: 'xxx.xxx.xxx.xxx', ownershipTicketInternalIP: '186.129.57.126', ownershipFlags: 0, ownershipTicketGenerated: 2023-06-01T07:30:15.000Z, ownershipTicketExpires: 2023-06-22T07:30:15.000Z, licenses: [ 313233 ], dlc: [], signature: <Buffer 5c 7d 7c 8f truncated ... 78 more bytes>, isExpired: false, hasValidSignature: true, isValid: true } Reading the README for node-steam-appticket, it seems that some fields can be spoofed. Can we specify our own `sessionExternalIP` when requesting & activating app tickets? Can we specify our own `ownershipTicketInternalIP` as well? Why is the `ownershipTicketGenerated` by the game launcher so old compared to node-steam-user? Is it cached locally by the Steam client? Does `user.gamesPlayed([APP_ID])` do anything to affect the validity of the app ticket? How should I go about reversing the communication of the game <-> Steam ? Edited June 2, 2023 by eXPerience Quote
Dr. McKay Posted June 2, 2023 Report Posted June 2, 2023 9 hours ago, eXPerience said: Can we specify our own `sessionExternalIP` when requesting & activating app tickets? Can we specify our own `ownershipTicketInternalIP` as well? Why is the `ownershipTicketGenerated` by the game launcher so old compared to node-steam-user? Is it cached locally by the Steam client? Does `user.gamesPlayed([APP_ID])` do anything to affect the validity of the app ticket? How should I go about reversing the communication of the game <-> Steam ? steam-user doesn't provide any way to specify your own sessionExternalIP value, but there's nothing stopping you from spoofing that field to be whatever you want, either. It's client-controlled and isn't authenticated by Steam as far as I'm aware. You could try changing the value directly in the steam-user code here and see what happens. Yes, the internal IP is determined from the private IP specified when you connected to Steam. By default, steam-user sends 0 unless you change the logonID value in the logOn method. You need to encode your desired internal IP as a 32-bit int, then xor it with 0xBAADF00D. For example, 192.168.1.2 encodes to 3232235778, then xor that by doing 3232235778 ^ 0xBAADF00D and you get 2158493696, which is what you should use for your logonID. Yes, ownership tickets are cached in userdata/your_account_id/config/localconfig.vdf under apptickets. Not as far as I'm aware You can use NetHook for that. When you inject it, you'll need to provide the filename of the game process you want to inject into. eXPerience 1 Quote
eXPerience Posted June 3, 2023 Author Report Posted June 3, 2023 (edited) Thank you @Dr. McKay for the pointers. The solution was the `server_secret` field for the ticket in the CMsgClientAuthList message. I couldnt find out how this value is obtained/generated so I'm settling on patching steam-user locally. The server secret was just the shortened name of the game. Diff: diff --git a/components/appauth.js b/components/appauth.js index 3f6b5b6..1f29a0f 100644 --- a/components/appauth.js +++ b/components/appauth.js @@ -84,7 +84,7 @@ class SteamUserAppAuth extends SteamUserAccount { return AppTicket.parseAppTicket(ticket, allowInvalidSignature); } - createAuthSessionTicket(appid, callback) { + createAuthSessionTicket(appid, server_secret_hex_string, callback) { return StdLib.Promises.callbackPromise(['sessionTicket'], callback, (resolve, reject) => { // For an auth session ticket we need the following: // 1. Length-prefixed GCTOKEN @@ -120,7 +120,7 @@ class SteamUserAppAuth extends SteamUserAccount { try { // We need to activate our ticket - await this.activateAuthSessionTickets(appid, buffer); + await this.activateAuthSessionTickets(appid, buffer, server_secret); resolve({sessionTicket: buffer}); } catch (err) { reject(err); @@ -171,7 +173,7 @@ class SteamUserAppAuth extends SteamUserAccount { }); } - activateAuthSessionTickets(appid, tickets, callback) { + activateAuthSessionTickets(appid, tickets, server_secret, callback) { if (!Array.isArray(tickets)) { tickets = [tickets]; } @@ -210,6 +212,7 @@ class SteamUserAppAuth extends SteamUserAccount { ticket_crc: StdLib.Hashing.crc32(ticket.authTicket), ticket: ticket.authTicket }; + if (server_secret) thisTicket.server_secret = Buffer.from(server_secret, 'hex'); // Check if this ticket is already active if (this._activeAuthTickets.find(tkt => tkt.steamid == thisTicket.steamid && tkt.ticket_crc == thisTicket.ticket_crc)) { Edited June 3, 2023 by eXPerience Quote
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.