Jump to content
McKay Development

Recommended Posts

Posted (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.

  1. Can we specify our own `sessionExternalIP` when requesting & activating app tickets?
  2. Can we specify our own `ownershipTicketInternalIP` as well?
  3. Why is the `ownershipTicketGenerated` by the game launcher so old compared to node-steam-user? Is it cached locally by the Steam client?
  4. Does `user.gamesPlayed([APP_ID])` do anything to affect the validity of the app ticket?
  5. How should I go about reversing the communication of the game <-> Steam ?
Edited by eXPerience
Posted
9 hours ago, eXPerience said:
  1. Can we specify our own `sessionExternalIP` when requesting & activating app tickets?
  2. Can we specify our own `ownershipTicketInternalIP` as well?
  3. Why is the `ownershipTicketGenerated` by the game launcher so old compared to node-steam-user? Is it cached locally by the Steam client?
  4. Does `user.gamesPlayed([APP_ID])` do anything to affect the validity of the app ticket?
  5. How should I go about reversing the communication of the game <-> Steam ?
  1. 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.
  2. 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.
  3. Yes, ownership tickets are cached in userdata/your_account_id/config/localconfig.vdf under apptickets.
  4. Not as far as I'm aware
  5. 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.
Posted (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 by eXPerience

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.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
×
×
  • Create New...