A (Small) Peek Into SteamDRMP.dll

5 minute read

During my past progress with building my project Steamless, I wanted to better understand the DRM itself rather then just removing it and not looking into it more. I took some time to peer into the protection setup that the DRM offers once it is unpacked in memory and the protected target is running.

One of the things that the DRM does is unpacks and loads a module from memory called SteamDRMP.dll. This module is packed away inside of the .bind section of the protected file, which is encrypted.

During the unpacking of the file while it is starting up, this file is decrypted and loaded in memory without extraction to disk. Then an export is referenced and called to prevent debugging on the game target.

The export looks like this:

char __stdcall steam(int a1, void *a2, int a3)
{
  char result; // al@5
  HMODULE v4; // eax@6
  FARPROC v5; // esi@6
  HANDLE v6; // eax@6
  char v7; // al@10
  char v8; // bl@10
  CHAR ProcName[4]; // [sp+4h] [bp-24h]@6
  CHAR ModuleName[4]; // [sp+1Ch] [bp-Ch]@6
  int v11; // [sp+38h] [bp+10h]@7
 
  if ( (unsigned int)a3 < 0xC8 || *((_DWORD *)a2 + 1) != 0xC0DEC0DE )
  {
    result = 81;
  }
  else
  {
    if ( !(*((_BYTE *)a2 + 52) & 0x20) )
    {
      if ( IsDebuggerPresent() )
        return 84;
      strcpy(ModuleName, "ntdll.dll");
      strcpy(ProcName, "NtSetInformationThread");
      v4 = GetModuleHandleA(ModuleName);
      v5 = GetProcAddress(v4, ProcName);
      v6 = GetCurrentThread();
      ((void (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD))v5)(v6, 17, 0, 0);
    }
    v11 = GetTickCount();
    if ( *((_BYTE *)a2 + 52) & 2 || (unsigned __int8)sub_10006160((HMODULE)(a1 - *((_DWORD *)a2 + 4))) )
    {
      v7 = sub_10006270(a2);
      v8 = v7;
      if ( v7 == 48 )
      {
        if ( *((_BYTE *)a2 + 52) & 4 || (result = sub_10005AE0(a1, a2), result == 48) )
        {
          if ( *((_BYTE *)a2 + 52) & 0x20 || GetTickCount() - v11 <= 10000 )
            result = 48;
          else
            result = 83;
        }
      }
      else
      {
        if ( v7 == 53 )
          sub_10006070(*((_DWORD *)a2 + 12));
        result = v8;
      }
    }
    else
    {
      result = 51;
    }
  }
  return result;
}

To start we can dissect this function that is exported and better understand the protection it offers.

The first checks we have are to ensure the file has been unpacked properly:

if ( (unsigned int)a3 < 0xC8 || *((_DWORD *)a2 + 1) != 0xC0DEC0DE )
{
result = 81;
}

The first check is currently unknown. The second is to ensure the stub header block is decrypted properly. When the stub header is xor decrypted there is a 4 byte signature at the start showing 0xC0DEC0DE to validate that the xor was correctly undone.

Next, the DRM module checks if a thread protection flag is set:

if ( !(*((_BYTE *)a2 + 52) & 0x20) )
{
    if ( IsDebuggerPresent() )
    return 84;
    strcpy(ModuleName, "ntdll.dll");
    strcpy(ProcName, "NtSetInformationThread");
    v4 = GetModuleHandleA(ModuleName);
    v5 = GetProcAddress(v4, ProcName);
    v6 = GetCurrentThread();
    ((void (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD))v5)(v6, 17, 0, 0);
}

This checks for the 0x20 flag in the header. If it is present, it first checks if a debugger is attached. If not then it continues to call NtSetInformationThread. 17 states that it wants to hide the current thread from the debugger.

Next we have a GetTickCount compare if another flag is set.

v11 = GetTickCount();
if ( *((_BYTE *)a2 + 52) & 2 || (unsigned __int8)sub_10006160((HMODULE)(a1 - *((_DWORD *)a2 + 4))) )
{
    v7 = sub_10006270(a2);
    v8 = v7;
    if ( v7 == 48 )
    {
    if ( *((_BYTE *)a2 + 52) & 4 || (result = sub_10005AE0(a1, a2), result == 48) )
    {
        if ( *((_BYTE *)a2 + 52) & 0x20 || GetTickCount() - v11 <= 10000 )
        result = 48;
        else
        result = 83;
    }
    }
    else
    {
    if ( v7 == 53 )
        sub_10006070(*((_DWORD *)a2 + 12));
    result = v8;
    }
}

So this time a flag 0x02 is checked for (unsure of the second check at this time). If the flag is set it continues with the GetTickCount check. Just after, there is a steam ticket validation. I am assuming this ensures that Steam is running or that you own the game. This also has a possibility to be a check if the game was launched from Steam properly or trying to be directly ran. The else case here calls on Steam to launch the application otherwise which is what points me to that.

The last chunk is some memory dump protection (from the look of it at a glance) then the GetTickCount compare to ensure the game reached the given point within 10 seconds. Otherwise the function will return with an invalid return.

So from this function a guess of the flags so far would be:

  • 0x02 = Launch validation of some sort.
  • 0x04 = Memory Dump Protection (Guessing currently.)
  • 0x20 = Debugger Checks (IsDebuggerPresent, GetTickCount, and NtSetInformationThread)

Return code wise:

  • 48 = GetTickCount timeout.
  • 51 = Possible valid return.
  • 53 = Valid return, relaunch target through Steam.
  • 81 = Invalid header.
  • 83 = Possible valid return.
  • 84 = Debugger present.

So some things to keep in mind when attaching to or debugging a SteamStub protected file:

  • Fake the return of IsDebuggerPresent
  • Fake the return of GetTickCount
  • Block the NtSetInformationThread API call.

Another thing that can be done is decrypting the stub header and replace the flags to remove the added checks.

This information is based on a simple static analysis of the functions present in the DLL. No debugging was done to step into the functions and determine full purpose. Take this information lightly as it can be wrong. It is again, a simple analysis.

This information is for educational purposes.

Comments