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.