Bypassing app protection using proxy DLLs

Using LIEF and Visual Studio 2019 to build proxy DLLs

I've been modding some games on Steam recently, and some of them make use of the Steamworks product to add an extra layer of security, as well as adding other features such as the overlay and cloud saves. This isn't an article on how Steam DRM and Steamworks works so I'm not going to get into the details, but as part of my work I decided it would be good to build a proxy DLL that I can put in place of the real Steamworks DLL and then have the option to either forward calls, return a cached value, or breakpoint when certain functions were hit. Here's how I did it:

Creating the DLL skeleton

I've done this for x86, (x64 is an exercise for the reader), as the app I'm modding is an x86 app. Fortunately we're not using any inline asm (this doesn't work in x64 apps) so most of it will translate across the same.

Start a DLL project

In Visual Studio 2019 we click Create a new project and search for a DLL project, a normal one, not the MFC option:

We give it a name and then carry on. The next step is to enable ASM files to be included, which is a short process but poorly documented

Enable asm building

We'll start off by adding an .asm file. Right click on source files in the Solution Explorer, and under the Add menu click New Item

It should select C++ file by default, just ignore this and overwrite the extension in the name field to call it something else, we'll use detours.asm for this project:

Asm files are not built by default so we need to enable MASM in this project and then add our detours.asm file. Right-click on the name of the project (not the solution), and under Build Dependencies click on the Build Customizations... option

By default .masm is unchecked, so check this and click OK

Now we've added MASM to the project we need to make our .asm file build, so right click on our detours.asm and click Properties and in the dropdown by Item Type select Microsoft Macro Assembler

Note that we're doing this for all configurations and all platforms, you can easily do separate x86 and x64 files and add them to specific platforms, for example. Now we've done this, we can expect our .asm file will build.

Building from a .def file

Normally you'd use the __declspec(dllexport) keyword to annotate functions that you want to export, but that won't let you choose the ordinal. If we want to build a good proxy DLL we need to ensure that the app we're modding can import either by name or by ordinal, otherwise we might run into some unexpected results later. We add our def file (mine is called exports.def) in the same way as we added our .asm file above, and then we add it to the build in project properties:

We're all good to go, let's get some code in place!

Cloning the exports

LIEF is my go-to tool for any time I need to work with a binary. Install it via PIP, import, and then we can load the DLL export table in a couple of lines:

import lief
binary = lief.parse("/mnt/c/Program Files (x86)/Steam/steamapps/common/Reversed Dreamland/RD_Data/Plugins/steam_api.dll")
exports = [(e.name, e.ordinal) for e in binary.get_export().entries]

Note the /mnt/c because I do most of my coding from WSL but Steam is installed in the base Windows environment.

Once we've loaded the exports, we need to generate two things: export definitions, and stubs that we can populate. We can generate the def file like this:

with output = open("exports.txt", "w"):
    export_entries = ["\t%s @%d" % (x[0], x[1]) for x in exports]
    output.write("\n".join(export_entries))

And then if we want to generate some stubs we can do something similar:

with output = open("stubs.txt", "w"):
    funcs = ["%s PROC\n\tpush hOldDll\n\tpush %d\n\tcall [_imp__GetProcAddress@8]\n\tjmp eax\n%s ENDP" % (x[0], x[1], x[0]) for x in exports]
    output.write("\n".join(funcs))

We can then copy these into our files. Our exports.def file needs to start like this:

LIBRARY csteamworks_proxy
EXPORTS
    Function1 @1
    Function2 @2
    ...

We can then paste our defs underneath (the Function1 stuff is just an example by the way and should be removed). The stubs need a little bit more work, and I've done mine like this:

.386
.model flat, stdcall

.data
hOldDll DWORD 0;
pDllName BYTE "targetdll_old.dll",0
.code

EXTERN _imp__LoadLibraryA@4 : dword
EXTERN _imp__GetProcAddress@8 : dword

OPTION LANGUAGE: syscall
@init@0 PROC
    lea eax, [pDllName]
    ;push eax    
    ;call [_imp__LoadLibraryA@4]
    mov hOldDll, eax
    ret
@init@0 ENDP

Func1 PROC
    push hOldDll
    push 1
    call [_imp__GetProcAddress@8]
    jmp eax
Func1 ENDP

...

END

From here we can finally set up the dllmain.cpp that drives the whole operation:

// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"

extern "C" void __fastcall init(void);

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        init();
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

So when we load the DLL, it will call init() the first time which will call LoadLibrary() to put the DLL in memory, and then whenever we hit a function that we've exported it will jump straight through to the real thing, leaving us with an easy place to put our own breakpoints if we like. There's also nothing to stop you changing these proxy functions to do something else, add some logging, or just plain return a fixed value instead. Just copy this into the directory where the real DLL is, rename it to targetdll_old.dll or similar, and then you have your very own customisable proxy.

I hope you find this useful, this is just one more tool that we have at our disposal when it comes to analysing and modding.