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:
- The only solution was to launch the game in
-insecure
- The game took 20-30s to launch
- It only happened on Windows 11
To understand what -insecure
does, you’ll need to know about CS:GO’s 2 anti-cheat measures:
- Trusted Mode, which attempts to prevent untrusted programs from being injected into CS, and can be disabled through
-allow_third_party_software
. - 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.