From 18c42b938ba5a91ce3c91839cfd218e096bc0e51 Mon Sep 17 00:00:00 2001 From: Hayden K Date: Wed, 11 Nov 2026 23:01:10 +0000 Subject: Prevent random "No Steam Logon" kicks in L4D1/2 Committer's note: tweaked a little bit from Hayden's original code, but after some back-and-forth testing it seems to work as well as his did. Might benefit from some further tweaking/micro-optimisation later, but for now it's good enough to get a release out ahead of the L4D2 community race happening this weekend. --- src/nosteamlogon.c | 272 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 src/nosteamlogon.c (limited to 'src') diff --git a/src/nosteamlogon.c b/src/nosteamlogon.c new file mode 100644 index 0000000..a97a4ce --- /dev/null +++ b/src/nosteamlogon.c @@ -0,0 +1,272 @@ +/* + * Copyright © Hayden K + * Copyright © Willian Henrique + * Copyright © Michael Smith + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include "chatmsg.h" +#include "chunklets/x86.h" +#include "con_.h" +#include "errmsg.h" +#include "feature.h" +#include "gamedata.h" +#include "gametype.h" +#include "hook.h" +#include "intdefs.h" +#include "langext.h" +#include "mem.h" +#include "os.h" +#include "sst.h" +#include "vcall.h" +#include "x86util.h" + +FEATURE("No Steam Logon bugfix") +// TOOD(compat): theoretically we can support Portal 2 later, since this issue +// affects runners there too. L4D:S would also probably work but is untested +// (and who cares about L4D:S anyway?) +GAMESPECIFIC(L4D) +REQUIRE(chatmsg) +REQUIRE_GAMEDATA(vtidx_GetClientName) + +// FIXME: introduce a convar to control the chat reporting behaviour + +static void *Steam3Server = 0; + +// find CSteam3Server instance in hostname callback +static bool find_Steam3Server(const uchar *insns) { + for (const uchar *p = insns; p - insns < 24;) { + if (p[0] == X86_CALL) { + p = p + 5 + mem_loads32(p + 1); + if (p[0] == X86_MOVEAXI) { + Steam3Server = mem_loadptr(p + 1); + return true; + } + else { + errmsg_errorx("couldn't find Steam3Server object"); + return false; + } + } + NEXT_INSN(p, "Steam3Server"); + } + return false; +} + +struct CCallback { + void *vtable; + u8 flags; + int id; + void *thisptr; + struct { void *ptr; int adj; } memberfunc; +}; + +struct Steam3; +struct IClient; +struct CBaseClient; + +// TODO(opt): these two hook trampolines could be effectively unioned but we +// need a non-horrible way to do that in hook.h. currently not wasting real +// space anyway since all the hooks fit in one page but could be relevant later? + +// CSteam3Server::OnGSClientDenyHelper (L4D1 + L4D2 2000-2105) +typedef void (*VCALLCONV ClientDenyHelper_func)(struct Steam3 *, + struct CBaseClient *, int, char const *); +DEF_INLINE_HOOK(ClientDenyHelper_func, ClientDenyHelper) + +// CSteam3Server::OnValidateAuthResponseTicketHelper (L4D2 2106+) +typedef void (*VCALLCONV OVARTH_func)(struct Steam3 *, + struct CBaseClient *, int); +DEF_INLINE_HOOK(OVARTH_func, OVARTH) + +DECL_VFUNC_DYN(struct IClient, char *, GetClientName) + +static opt_small void dochat(struct CBaseClient *cl, const char *s, int len) { + // max length: see strings in hooks below + char buf[35 + 64 + 11 + 50 + 2], *p = buf; + struct IClient *icl = mem_offset(cl, 4); // second base interface/vtable + char *name = GetClientName(icl); int namelen = strlen(name); + bool abbrev = namelen > 61; + if (abbrev) namelen = 61; + mem_copy_fixed(p, "SST: Preventing kicking of player \"", 35); p += 35; + mem_copy(p, name, namelen); p += namelen; + if (abbrev) { + mem_storeu32(p + 61, '....'); // overwrite by one so it's a single mov + p += 3; + } + mem_copy_fixed(p, "\" (reason: ", 11); p += 11; + mem_copy(p, s, len); p += len; + mem_copy_fixed(p, ")", 2); // include \0 + chatmsg_broadcast(buf); +} + +static opt_small void VCALLCONV hookdest_ClientDenyHelper(struct Steam3 *this, + struct CBaseClient *cl, int reason, char const *optionaltext) { + switch (reason) { + case 3: dochat(cl, "k_EDenyNotLoggedOn", 18); break; + case 11: dochat(cl, "k_EDenySteamConnectionLost", 26); break; + case 12: dochat(cl, "k_EDenySteamConnectionError", 27); break; + default: orig_ClientDenyHelper(this, cl, reason, optionaltext); + } +} + +static opt_small void VCALLCONV hookdest_OVARTH(struct Steam3 *this, + struct CBaseClient *cl, int reason) { + // calling this unconditionally because it does some other stuff. we patch + // a switch jump table (see below) to skip the unwanted cases. the only + // reason this hook is still here is to display the chat message as well. + orig_OVARTH(this, cl, reason); + switch (reason) { + case 1: + dochat(cl, "k_EAuthSessionResponseUserNotConnectedToSteam", 45); + break; + case 6: + dochat(cl, "k_EAuthSessionResponseAuthTicketCanceled", 40); + break; + case 7: + dochat(cl, "k_EAuthSessionResponseAuthTicketInvalidAlreadyUsed", 50); + break; + case 8: + dochat(cl, "k_EAuthSessionResponseAuthTicketInvalid", 39); + } +} + +static struct CCallback *find_Steam3Callback(void) { + if (has_off_NoSteamLogonCallback) { + struct CCallback *cb = mem_offset(Steam3Server, off_NoSteamLogonCallback); + if_hot (cb->id == 203) return cb; // double-check in case it ever breaks + } + // in L4D2 this offset got changed all the time, so just search through a + // few different offsets for what looks like the right pointer. + for (int off = 100; off != 124; off += 4) { + struct CCallback *cb = mem_offset(Steam3Server, off); + // in L4D2 2.1.0.6 this got changed from 203 to 143. just accept both + if (cb->id == 143 || cb->id == 203) return cb; + } + return 0; +} + +static void *find_NoSteamLogon(void *memberfunc_ptr) { + const uchar *insns = memberfunc_ptr; + int callcnt = 0; + for (const uchar *p = insns; p - insns < 80;) { + if (p[0] == X86_CALL && ++callcnt == 2) { + return (void *)(p + 5 + mem_loads32(p + 1)); + } + NEXT_INSN(p, "NoSteamLogon function"); + } + errmsg_errorx("couldn't find NoSteamLogon function"); + return 0; +} + +static void **jumptable; +static void *orig_jumptable_addrs[4]; +static void *retaddr; + +// note: these are the same as the switch cases in the OVARTH hook, but off by 1 +// in the actual jump table for whatever reason. +static const s8 patchcases[] = {0, 5, 6, 7}; + +// note: this can find the switch jump table in all versions, but currently we +// only actually need it in 2206+. +static bool find_jumptable(void *NoSteamLogon, int id) { + uchar *insns = NoSteamLogon; + for (uchar *p = insns; p - insns < 128;) { + if (p[0] == X86_MISCMW && p[1] == 0x24 && p[2] == 0x85) { + jumptable = mem_loadptr(p + 3); + goto _1; + } + NEXT_INSN(p, "jump table"); + } + return false; +_1: // find the end of the no steam logon func + if (id == 203) insns = jumptable[2]; else insns = jumptable[0]; + for (uchar *p = insns; p - insns < 32;) { + if (p[0] == X86_2BYTE && p[1] == X86_2B_JZII) { + retaddr = p + 6 + mem_loads32(p + 2); + return true; + } + NEXT_INSN(p, "return address"); + } + return false; +} + +INIT { + struct con_var *cvar_hostname = con_findvar("hostname"); + if_cold (!cvar_hostname) return FEAT_FAIL; + // XXX: cb is usually SST-specific, but happens to have the same offset as + // the engine's cvar callbacks *usually*; however this won't work in Portal + // 2 where there's actually a *list* of callbacks. revist later... + if_cold (!find_Steam3Server((const uchar *)cvar_hostname->cb)) { + return FEAT_INCOMPAT; + } + struct CCallback *cb = find_Steam3Callback(); + if_cold (!cb) { + errmsg_errorx("couldn't find Steam3 callback"); + return FEAT_INCOMPAT; + } + void *NoSteamLogon = find_NoSteamLogon(cb->memberfunc.ptr); + if_cold (!NoSteamLogon) { + errmsg_errorx("couldn't find No Steam Logon function"); + return FEAT_INCOMPAT; + } + if (cb->id == 203) { // L4D1, or L4D2 before 2106 - old func + // simple case: we can just wrap the entire function, and skip the call + // when one of the offending switch cases is hit. + struct hook_featsetup_ret_ClientDenyHelper h = + hook_featsetup_ClientDenyHelper( + (ClientDenyHelper_func)NoSteamLogon); + if_cold (h.err) return h.err; + hook_commit_ClientDenyHelper(h.hookpos, &hookdest_ClientDenyHelper); + } + else { // after 2106 - new func + // the function apparently does some other stuff besides the switch + // case, so we hook the jump table itself. the function hook is only + // used for sending the chat messages. + if_cold (!find_jumptable(NoSteamLogon, cb->id)) { + errmsg_errorx("couldn't find jump table in No Steam Logon function"); + return FEAT_INCOMPAT; + } + if (!os_mprot(jumptable, 12 * sizeof(void *), PAGE_EXECUTE_READWRITE)) { + errmsg_errorsys("couldn't make jump table writable"); + return FEAT_FAIL; + } + struct hook_featsetup_ret_OVARTH h = hook_featsetup_OVARTH( + (OVARTH_func)NoSteamLogon); + if_cold (h.err) return h.err; + hook_commit_OVARTH(h.hookpos, &hookdest_OVARTH); + // replace the offending switch cases with the function return point + for (int i = 0; i < ssizeof(patchcases); ++i) { + orig_jumptable_addrs[i] = jumptable[patchcases[i]]; + jumptable[patchcases[i]] = retaddr; + } + } + return FEAT_OK; +} + +END { + if_cold (sst_userunloaded) { + // XXX: could avoid this silliness here if these were unioned. + if (retaddr) { + for (int i = 0; i < ssizeof(patchcases); ++i) { + jumptable[patchcases[i]] = orig_jumptable_addrs[i]; + } + unhook_OVARTH(); + } + else { + unhook_ClientDenyHelper(); + } + } +} + +// vi: sw=4 ts=4 noet tw=80 cc=80 -- cgit v1.2.3-54-g00ecf