geni.site

Fixing CS:GO startup times on Windows 11

Despite having downgraded to Windows 11 a few months ago, I never really noticed how long CS:GO took to launch, likely because I had it on an HDD prior. But one day I saw a post on r/GlobalOffensive complaining about how long the game takes to boot on Windows 11, and I quickly realized it wasn’t meant to take as long as it does.

Inspired by the excellent write-up on reducing GTA Online load times, regardless of my limited reverse engineering experience, I decided to dig deeper.

Introduction

Going through various other Reddit posts complaining about the same issue (some from an entire year ago), I noted a few things that were common among all posts:

To understand what -insecure does, you’ll need to know about CS:GO’s 2 anti-cheat measures:

  1. Trusted Mode, which attempts to prevent untrusted programs from being injected into CS, and can be disabled through -allow_third_party_software.
  2. Valve Anti-Cheat (VAC), which aims to detect and ban cheaters. It can be disabled through -insecure, and disabling it also disables Trusted Mode.

Profiling

First, let’s measure how long it takes to launch on my PC.

CPU: i3-12100
SSD: Samsung 970 EVO Plus
RAM: 2x8GB (2400MHz) of random ADATA sticks I bought 5 years ago

Time from the "Play" button in Steam becoming "Stop" to the game window opening:
No launch options:                ~20 seconds
With -allow_third_party_software: ~20 seconds
With -insecure:                   ~5 seconds

Indeed, the game took about 20 seconds to launch, despite my machine having a pretty fast NVMe SSD. Additionally, -allow_third_party_software didn’t affect startup times, which should leave VAC as the lead suspect, right?

Looking at the Task Manager, the game seemed to be pegging a single core for most of those 20 seconds and not really doing much else. (beautiful diagram, I know)

Alright, let’s try Luke Stackwalker then.

…oh. We can’t run the game straight from the executable.

Minor setback

Using my legally acquired copy of IDA, I opened up csgo.exe in search of a fix.
Seems to just require a few environment variables.

Something odd to note is that a Trusted Mode setup function gets called regardless of what’s passed to the game. This is referenced again at the end of this post.

Back to profiling

Now that that’s out of the way, let’s actually get to profiling.
The obvious offender isn’t really called RuntimeCheck; that’s just the closest function Luke Stackwalker knows about. I wasted some 30 minutes thanks to this, though I figured it out once I put a breakpoint in RuntimeCheck and it never got hit.

It looks like the Steam overlay is calling GetModuleHandleW, which is eventually calling LdrpFindLoadedDllByMappingFile, which calls a function in csgo.exe. Sounds like something’s getting hooked. Removing the overlay DLL from the Steam directory restores fast startup times, meaning the problem does appear to be triggered thanks to the GetModuleHandleW calls. But that’s only a symptom, not the cause.

To find out which function is at fault, I’ll have to employ an advanced profiling technique: manually sampling by suspending the program in the IDA debugger.

The real culprit

Some short suspending later, I’ve found that this function is mostly what’s taking so long:

After investigating some of the functions in the call stack, it looks as though it’s NtOpenFile that’s getting hooked.


The hook makes it so that if NtOpenFile determines that it’s opening a module (a DLL), it checks if it’s trusted. If the module fails the check, it’s not opened.
It’s installed by Trusted Mode, explaining why -insecure isn’t affected by this bug.
(P.S.: I’ve written a section explaining why -allow_third_party_software is affected at the end of this post)

So I’ll just set a breakpoint at the function that checks if a module is trusted, and… it’s checking the same modules over and over. To see just how many modules were going through this function, I set a breakpoint that would print out every module that went through a check.

Every call to GetModuleHandleW by the Steam overlay is causing a trusted check. And, as it turns out, it calls GetModuleHandleW until it finds what graphics API the game is using. Which, until it’s actually ready to draw something, is none.

But what actually changed in Windows 11 that made this so slow? Was it always going through 3700 modules?

Windows changes

To investigate further, I spun up a Windows 10 VM, set up the IDA remote server, and ran the game.
At least it definitely tries to draw something a lot sooner than it does on Windows 11.
I didn’t bother fixing this since we don’t actually need the game to draw something.

I set my print breakpoint again, and…
Yeah, that’s a lot less. But why?

A faulty check

To find out why, let’s look at the hook itself again:
It checks if the file has a valid filename and if DesiredAccess has the FILE_EXECUTE (0x20) bit set before doing a check.

Remember the LdrpFindLoadedDllByMappingFile function that was in the Luke Stackwalker call graph? Well, it’s different in Windows 11:
Now here’s the Windows 10 version:
Notice something? Windows 11’s DesiredAccess has the FILE_EXECUTE (0x20) bit set.

How can we fix it?

I wrote a very basic PoC DLL that patches ntdll and changes DesiredAccess to where the FILE_EXECUTE bit is unset. (Disclaimer: This is certainly not the best way to fix this)

BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved) {
	(void) inst;
	(void) reserved;

	if (reason == DLL_PROCESS_ATTACH) {
		MODULEINFO module_info;
		GetModuleInformation(GetCurrentProcess(), GetModuleHandleA("ntdll.dll"), &module_info, sizeof module_info);

		u8* patch_addr = (u8*) module_info.lpBaseOfDll + 0x2F083;
		DWORD old_protect;
		VirtualProtect(patch_addr, 1, PAGE_EXECUTE_READWRITE, &old_protect);
		*patch_addr = 0x01;
		VirtualProtect(patch_addr, 1, old_protect, &old_protect);
	}

	return 1;
}

To test it, I had to disable the trusted checks, inject my DLL, then re-enable them. I won’t go into detail on how this is done since it facilitates cheating, but it’s pretty easy.

Results

And finally, let us reap the fruits of our labor: Right back to ~5s.

For comparison, here’s how long it usually takes:

Valve, pls fix.

Why does -allow_third_party_software still suffer from this bug?

The flag that tells TrustedLaunchSetup whether to install hooks or not is only unset if the game is launched with -insecure. Since TrustedLaunchSetup is always called, this means that -allow_third_party_software doesn’t do anything until we get further into the launch process.



It seems like client.dll disarms Trusted Mode somewhere, but I didn’t look any further into it.

Counter-Strike 2

This has been fixed in Counter-Strike 2; Valve has simply changed the faulty check to look like (DesiredAccess & 0x21) == 0x21 instead.