Patching Steam executable files can prove useful for diverse purposes, one of those being vulnerability research in the Steam client. This short post will explain how you can circumvent a few obstacles to achieve that, before presenting an applied example.


If you’re trying to modify a binary from the Steam client, you will most likely end up facing one or both of the following problems:

  • Steam tends to pull updates when local files are not up-to-date;
  • there is an additional integrity check for certain executables in the Steam folder.

The first problem is easy to address: by creating a file named Steam.cfg at the root of your Steam folder that contains the following setting, you can prevent Steam from pulling updates automatically.

BootStrapperInhibitAll=Enable

Regardless, preventing automatic updates is always a crucial point when you’re reverse engineering a product — several times I have had to start working on new IDA bases all over again because binaries got carelessly replaced.

As for the integrity check, we need to find where it comes from. Let’s say we wanted to modify the steamerrorreporter.exe file: we can assume that at some point, there is a certain component in Steam that opened this file for reading in order to assess whether it was tampered with or not.

A quick use of Process Monitor to find which process opened the steamerrorreporter.exe file for reading points towards steam.exe, and more especially the following module: SteamUI.dll. This DLL is responsible for all kinds of logic in the Steam client (including some funny stuff that you wouldn’t necessarily think lies inside a DLL with “UI” in its name, stay tuned for more).

Climbing up the call stack and exploring around a bit, we end up in a function that starts like the following:

bool __cdecl sub_6F6B80(int a1, int a2, int a3, int a4, int a5) {

  if ( g_VProfProfilesRunningCount ) {
    v6 = VProfInternalEnterScopeCurrentThread("CCrypto::RSAVerifySignature");
  }

  // ...

We understand the purpose of this function is to verify whether a given RSA signature is valid from an input buffer and a public key. It is merely a wrapper around the OpenSSL library.

This is, by the way, a typical example of a function leaking its name because of profiling logic: very convenient when you’re having a hard time reversing a huge binary without any debug symbol. Coming up with a small IDA script to automatically rename these kinds of functions (there are a lot in Steam and other Valve related products) is an appropriate strategy.

There is only one cross-reference to this CCrypto::RSAVerifySignature method: a function that I will call VerifyPEIntegrity.

It starts off with a few sanity checks to ensure it’s dealing with a PE binary:

if ( Size < 0x200 )
  return ERR;
if ( *(_WORD *)FileContents != 'ZM' )
  return ERR;
v4 = *((_DWORD *)FileContents + 15);
if ( v4 < 0x40 || v4 >= Size - 248 || *(_DWORD *)&FileContents[v4] != 'EP' )
  return ERR;

Then, it looks for the string “VLV” at a specific place:

if ( *((_DWORD *)FileContents + 16) != 'VLV' )
  return ERR;
if ( *((_DWORD *)FileContents + 17) != 1 )
  return ERR;

Something related to Valve? What could it mean? Let’s check it out inside an actual binary from the Steam folder:

╭─face@0xff ~/Steam 
╰─$ hexdump -C steamerrorreporter.exe | head -n 20
00000000  4d 5a 90 00 03 00 00 00  04 00 00 00 ff ff 00 00  |MZ..............|
00000010  b8 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 10 01 00 00  |................|
00000040  56 4c 56 00 01 00 00 00  00 94 08 00 0b ab c0 63  |VLV............c|
00000050  9c ba 32 41 f5 6d 22 8c  89 b3 65 89 56 71 82 2b  |..2A.m"...e.Vq.+|
00000060  e3 9f 76 d2 8e c9 06 53  cb 07 ae 53 15 4c 57 7a  |..v....S...S.LWz|
00000070  58 c3 f7 84 69 16 43 6d  7a 1b b0 fb 30 48 75 d1  |X...i.Cmz...0Hu.|
00000080  67 fc 7c f8 87 30 5d 26  8e 78 58 a0 ed 70 3a d8  |g.|..0]&.xX..p:.|
00000090  c3 a5 b0 0f ca ae 11 61  9d 80 29 ff 13 eb e6 9a  |.......a..).....|
000000a0  f9 4a b8 fa d9 b3 cb b2  78 b0 ea da 09 a1 88 14  |.J......x.......|
000000b0  05 65 98 68 90 0b a3 f9  42 b1 a3 24 ca 37 24 f4  |.e.h....B..$.7$.|
000000c0  6f df 36 c5 a9 3d 14 19  47 d9 39 73 16 e8 9f e9  |o.6..=..G.9s....|
000000d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*                                                                             
00000110  50 45 00 00 4c 01 05 00  09 ab c0 63 00 00 00 00  |PE..L......c....|
00000120  00 00 00 00 e0 00 02 01  0b 01 0e 1d 00 d6 02 00  |................|
00000130  00 ba 05 00 00 00 00 00  e6 9c 00 00 00 10 00 00  |................|
00000140  00 f0 02 00 00 00 40 00  00 10 00 00 00 02 00 00  |......@.........|
00000150  06 00 00 00 00 00 00 00  06 00 00 00 00 00 00 00  |................|

There is a custom block of data sitting between the DOS header and the NT header, in place of the usual DOS stub. It is composed of a magic number (VLV), a version number (1), a signed data size, a timestamp, and a 128-byte signature.

Later on, this 128-byte signature (highlighted in yellow) is extracted from this header, and null bytes are put in place of the extracted data:

qmemcpy(sig, FileContents + 0x50, 0x80);
memset_(FileContents + 0x50, 0, 0x80);

Eventually, the function loops through a list of Valve public keys:

SignatureOK = 0;
k = 0;
do {
  if (k >= nPublicKeys)
    break;
  SignedDataSize = *(_DWORD *)(FileContents + 0x48);
  a5[1] = 1;
  a5[2] = 0;
  a5[3] = 0;
  a5[0] = (int)&off_949C90;
  if (sub_6F6730(a5, PublicKeys[k]) && sub_6F6480(a5))
    SignatureOK = CCrypto::RSAVerifySignature(FileContents, SignedDataSize, sig, 128, a5);
  else
    SignatureOK = 0;
  sub_6F6390(a5);
  ++k;
}
while (!SignatureOK);

We understand that the 128-byte signature is an embedded RSA signature for the whole file. If the signature is verified against at least one of Valve’s public keys, then the file is deemed authentic.

Of course, this whole verification can be easily circumvented by patching this function itself, so that it always returns 0 (valid). In order to achieve that, we can replace the setz al (0F 94 C0) in the epilog by a xor eax, eax ; nop (31 C0 90).

VerifyPEIntegrity+20F     setz    al          ; <-- patch
VerifyPEIntegrity+212     pop     esi
VerifyPEIntegrity+213     mov     esp, ebp
VerifyPEIntegrity+215     pop     ebp
VerifyPEIntegrity+216     retn

Voilà, nothing can stop us from tampering with Steam binaries now!

Application: patching the Steam Error Reporter

Here is an example use case where patching a Steam binary can come in handy during vulnerability research.

Whenever the Steam client crashes, it usually generates a crash dump for free, which is saved to a folder named dumps. These can be analyzed post-mortem, and used to report bugs to Valve.

However, these dumps are quite small by default (a few hundreds of kilobytes), and it is sometimes convenient to have the full crash dumps for deeper analysis. One can achieve that through patching the Steam Error Reporter component.

Indeed, crash events are sent over to the steamerrorreporter.exe process, which uses the function MiniDumpWriteDump from the Win32 API in order to generate crash dumps.

This function is not imported directly: it is rather retrieved through a GetProcAddress call. The code that eventually calls MiniDumpWriteDump can thus be identified by searching for references to the string "MiniDumpWriteDump".

FARPROC __thiscall GetMiniDumpWriteDump(CTX *this) {
  // ...

  if (!this->pMiniDumpWriteDump) {
    if (!this->DbghelpModule)
      this->DbghelpModule = LoadLibraryW(L"dbghelp.dll");
    if (this->DbghelpModule)
      this->pMiniDumpWriteDump = GetProcAddress(this->DbghelpModule, "MiniDumpWriteDump");
  }

  return this->pMiniDumpWriteDump;
}

Then later, in the parent function:

FARPROC pMiniDumpWriteDump = GetMiniDumpWriteDump((CTX *)this);

if (pMiniDumpWriteDump) {

  /* ... */

  pMiniDumpWriteDump(
    *(_DWORD *)(this + 16),
    *(_DWORD *)(this + 20),
    *(_DWORD *)(this + 131404),
    *(_DWORD *)(this + 40) | 4,
    v22,
    v27,
    0
  );

}

According to the Microsoft specification, the fourth argument is the DumpType information. We should set the 0x2 bit to enable the MiniDumpWithFullMemory flag; in other words, replace the or eax, 4 instruction (83 C8 04) with or eax, 2 (83 C8 02).

.text:00402E45     push    eax
.text:00402E46     mov     eax, [edi+28h]
.text:00402E49     or      eax, 4                   ;  <-- patch
.text:00402E4C     push    eax
.text:00402E4D     push    dword ptr [edi+2014Ch]
.text:00402E53     push    dword ptr [edi+14h]
.text:00402E56     push    dword ptr [edi+10h]
.text:00402E59     call    ebp

Steam is now able to generate full-fledged crash dumps with hundreds of megabytes!