Making a graphical interface in-game from scratch has never interested me. I'm always a little surprised when people say writing the interface code is their favorite part. I'd rather use console commands, config files or straight up re-compile with different settings. Well.. that was true until I started using ImGui.

ImGui test window in Counter-Strike: Source on Linux
ImGui test window in Team Fortress 2 on Linux

ImGui has been gaining popularity in some game hacking communities recently. It handles all the rendering, input, callbacks and all the other tedious user interface stuff that I would never want to do myself. It has a ton of useful widgets, multiple reference backends from OpenGL to Vulkan and it's easy enough to make a fully featured interface in a few minutes.

I was eager to get it working in my Linux projects. Once I had mastered the implementation I would probably include it in future tutorials. That was over a week ago, and that's where the story begins..

Starting off

To start off.. actually I had no idea where to start. Rendering on Windows is as simple as scanning for the IDirect3DDevice9 pointer, hooking the EndScene virtual function and drawing stuff before calling the original. Well, there's no DirectX outside of WINE on Linux so that doesn't seem to be an option.

But wait! If you've ever looked through the bin/linux64 directory you might spot a few libraries that doesn't quite look like they belong. After all, there's no DirectX on Linux, right? So what's this shaderapidx9_client.so doing here?

IDirect3DDevice9::EndScene in shaderapidx9_client.so This raises some questions.

Whaaaaat? After a little researching, it turns out that Valve use an abstraction layer to convert DirectX calls to OpenGL. There's even an EndScene function in there, although the one in this library is an import so I'll need to look elsewhere.

$ for i in *.*; do echo "-> $i"; nm -CD $i | grep " T " | grep "EndScene"; done
-> libtogl_client.so
2eb20 T IDirect3DDevice9::EndScene()

Can't say that's a surprise. These functions aren't virtual though so a new approach to hooking is needed.

dlsym, RTLD_NEXT and LD_PRELOAD

Turns out there's a really easy way to hook exported functions from shared libraries on Linux. Simply export the same symbol you want to hook and put your library in the LD_PRELOAD environment variable. When you need to get the original, just call dlsym with RTLD_NEXT and it'll return the real symbol address.

I'll be using this helper function for now since RTLD_NEXT will only work when linking with libtogl_client.so.

template <typename T> inline const T GetNextFunction(const char* filename, const char* symbol) {
  return reinterpret_cast<T>(dlsym(dlopen(filename, RTLD_NOW), symbol));
};
typedef long HRESULT;

#define libtogl "./bin/linux64/libtogl_client.so"
#define EndScene_ "_ZN16IDirect3DDevice98EndSceneEv"
 
class IDirect3DDevice9 {
  public:
    HRESULT EndScene();
};

typedef HRESULT (*EndScene_t) (IDirect3DDevice9*);

HRESULT IDirect3DDevice9::EndScene() {
  static EndScene_t oEndScene = GetNextFunction<EndScene_t>(libtogl, EndScene_);

  static int frame = 0;
  frame++;
 
  printf("IDirect3DDevice9::EndScene - thisptr: 0x%lX, frame: %i\n", uintptr_t(this), frame);

  return oEndScene(this);
};

Compile, confirm the exported symbol matches the one called in the replacement function and preload.

g++ -std=c++14 -fPIC -shared -o libdx9hook.so dx9hook.cc -ldl
$ nm -g libdx9hook.so | grep "_ZN16IDirect3DDevice98EndSceneEv"
0000000000000860 T _ZN16IDirect3DDevice98EndSceneEv
LD_PRELOAD=/home/aixxe/libdx9hook.so ./csgo.sh
Hook working as intended Easier than expected?

I was having a lot of fun at this point so I thought I'd try hooking some other stuff to see what else I could do.

#define SetRenderState_ "_ZN16IDirect3DDevice914SetRenderStateE19_D3DRENDERSTATETYPEj"
#define DrawIndexedPrimitive_ "_ZN16IDirect3DDevice920DrawIndexedPrimitiveE17_D3DPRIMITIVETYPEijjjj"

typedef enum _D3DPRIMITIVETYPE {} D3DPRIMITIVETYPE;
typedef enum _D3DRENDERSTATETYPE {
  D3DRS_ZENABLE = 7
} D3DRENDERSTATETYPE;

class IDirect3DDevice9 {
  public:
    HRESULT DrawIndexedPrimitive(_D3DPRIMITIVETYPE, int, unsigned int, unsigned int, unsigned int, unsigned int);
};
 
typedef HRESULT (*DrawIndexedPrimitive_t) (IDirect3DDevice9*, _D3DPRIMITIVETYPE, int, unsigned int, unsigned int, unsigned int, unsigned int);
typedef HRESULT (*SetRenderState_t) (IDirect3DDevice9*, _D3DRENDERSTATETYPE, unsigned int);
 
HRESULT IDirect3DDevice9::DrawIndexedPrimitive(_D3DPRIMITIVETYPE type, int base_vertex_index, uint32_t min_vertex_index, uint32_t vertices, uint32_t start_index, uint32_t count) {
  GetNextFunction<SetRenderState_t>(libtogl, SetRenderState_)(this, D3DRS_ZENABLE, false);
  GetNextFunction<DrawIndexedPrimitive_t>(libtogl, DrawIndexedPrimitive_)(this, type, base_vertex_index, min_vertex_index, vertices, start_index, count);
}
$ nm -g libdx9hook.so | grep "_ZN16IDirect3DDevice920DrawIndexedPrimitiveE17_D3DPRIMITIVETYPEijjjj"
00000000000009b0 T _ZN16IDirect3DDevice920DrawIndexedPrimitiveE17_D3DPRIMITIVETYPEijjjj
D3DRS_ZENABLE disabled on de_dust2
D3DRS_ZENABLE disabled on aim_ag_texture2
Unplayable, but it looks cool.

Okay, back to ImGui. As expected, the reference implementation for DirectX 9 extensively uses the WinAPI. I spent a while on MSDN writing equivalent type definitions and wrapper functions for all the IDirect3DDevice9 functions and then it hit me.

Limited subset of Direct3D 9.0c

ToGL is not a complete implementation. I looked for CreateStateBlock and various other enumerations but they were nowhere to be found. Stubbornly, I decided to continue without them, compiled, loaded and.. nothing.

Back to the drawing board.

The OpenGL approach

Okay, forget about DirectX. If the game is translating DirectX to OpenGL calls then I should just target OpenGL instead. I spent some time looking up common OpenGL calls and decided to try hooking glClearColor next. This time I was using RTLD_NEXT since linking with OpenGL libraries is as simple as adding -lGL to the linker options.

typedef void (*glClearColor_t) (GLclampf, GLclampf, GLclampf, GLclampf);
glClearColor_t original_clearcolor = nullptr;

extern "C" void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha) {
  static glClearColor_t original_fn = reinterpret_cast<glClearColor_t>(dlsym(RTLD_NEXT, "glClearColor"));
  return original_fn(0.f, 0.f, 1.f, 1.f);
}

Compiled, pre-loaded, started the game aaaaaand.. nothing. So it's around this time I start looking through the ToGL source code. First point of interest: this Lookup function in CDynamicFunctionOpenGLBase. Comments indicate this looks up a specified symbol in a library, this might explain why my hooked function wasn't being called.

Post note: After cleaning up all my old code for this write up I noticed this code actually works now. Isn't that just fantastic? At the time it didn't seem to do anything at all which prompted the following workaround. For what it's worth, hooking the glClearColor here and the one mentioned in a few moments produce different results so I decided to leave this part in.

bool Lookup(const char* fn, bool &okay, FunctionType fallback = NULL) {
  if (!okay)
    return false;
  else if (this->m_pFn == NULL) {
    this->m_pFn = (FunctionType)VoidFnPtrLookup_GlMgr(fn, okay, false, (void*)fallback);
    this->SetFuncName(fn);
  }

  okay = m_pFn != NULL;
  return okay;
}

I could probably backup and swap the m_pFn pointer here with one to my hooked glClearColor. My end goal was to get ImGui working but doing it during runtime would be a bonus. I kept this in mind and continued looking.

void* VoidFnPtrLookup_GlMgr(const char* fn, bool &okay, const bool bRequired, void* fallback) {
  void *retval = NULL;
  
  if ((!okay) && (!bRequired))
    return NULL;

  retval = (*gGL_GetProcAddressCallback)(fn, okay, bRequired, fallback);
  
  if ((retval == NULL) && (fallback != NULL)) {
    retval = fallback;
  }

  okay = (okay && (retval != NULL));
  
  if (bRequired && !okay)
    fprintf(stderr, "Could not find required OpenGL entry point '%s'!\n", fn);

  return retval;
}

Now at VoidFnPtrLookup_GlMgr and the gGL_GetProcAddressCallback instantly catches my attention.

COpenGLEntryPoints* GetOpenGLEntryPoints(GL_GetProcAddressCallbackFunc_t callback) {
  if (gGL == NULL) {
    gGL_GetProcAddressCallback = callback;
    gGL = new COpenGLEntryPoints();
 
    if (!gGL->m_bHave_OpenGL)
      Error("Missing basic required OpenGL functionality.");
  }
 
  return gGL;
}
$ nm -g libtogl_client.so | grep "GetOpenGLEntryPoints"
3b030 T GetOpenGLEntryPoints

There's nowhere else in the code that defines one of these callbacks and since this is an exported function it makes sense that another library used by the game is going to call it with their own callback function. Luckily, I won't have to look through each library manually, all I have to do is set a breakpoint on this function.

$ DEBUGGER=gdb ./csgo.sh
(gdb) b GetOpenGLEntryPoints
Function "GetOpenGLEntryPoints" not defined.
Make breakpoint pending on future shared library load? (y or [n]) Y
Breakpoint 2 (GetOpenGLEntryPoints) pending.
(gdb) c
Continuing.

A few moments later..

Thread 1 "csgo_linux64" hit Breakpoint 2, 0x00007ffff5b14034 in GetOpenGLEntryPoints ()
   from /home/aixxe/.steam/steam/steamapps/common/Counter-Strike Global Offensive/bin/linux64/libtogl_client.so
(gdb) info stack
#0  0x00007ffff5b14034 in GetOpenGLEntryPoints ()
   from /home/aixxe/.local/share/Steam/steamapps/common/Counter-Strike Global Offensive/bin/linux64/libtogl_client.so
#1  0x00007ffff68f6abf in ?? () from bin/linux64/launcher_client.so
#2  0x00007ffff68f7306 in ?? () from bin/linux64/launcher_client.so
#3  0x00007ffff68f797b in ?? () from bin/linux64/launcher_client.so
#4  0x00007ffff68f79f5 in ?? () from bin/linux64/launcher_client.so
#5  0x00007ffff68cfa4f in ?? () from bin/linux64/launcher_client.so
#6  0x00007ffff68f3a76 in ?? () from bin/linux64/launcher_client.so
#7  0x00007ffff68f3b3f in ?? () from bin/linux64/launcher_client.so
#8  0x00007ffff68f3b79 in ?? () from bin/linux64/launcher_client.so
#9  0x00007ffff68d0f69 in LauncherMain () from bin/linux64/launcher_client.so
#10 0x00007ffff760c291 in __libc_start_main () from /usr/lib/libc.so.6
#11 0x00000000004006f5 in _start ()
(gdb) info proc map
process 1002
Mapped address spaces:
 
          Start Addr           End Addr       Size     Offset objfile
      0x7ffff68c1000     0x7ffff6914000    0x53000        0x0 ./bin/linux64/launcher_client.so

Now to subtract the library start address from the #1 address on the call stack.

0x7FFFF68F6ABF - 0x7FFFF68C1000 = launcher_client.so+35ABF
launcher_client.so+35ABF

Looks like the callback function is in this library too, over at 0x34D20. Checking the RDI register confirms this.

(gdb) info registers rdi
rdi            0x7ffff68f5d20 140737329978656
0x7FFFF68F5D20 - 0x7FFFF68C1000 = launcher_client.so+34D20
launcher_client.so+34D20
extern:000000000026AF64                 extrn SDL_GL_GetProcAddress:near

This explains why my preloaded glClearColor function wasn't being called. Needless to say, I hooked this right away.

typedef void* (*SDL_GL_GetProcAddress_t) (const char*);

extern "C" void* SDL_GL_GetProcAddress(const char* proc) {
  void* proc_addr = reinterpret_cast<SDL_GL_GetProcAddress_t>(dlsym(RTLD_NEXT, "SDL_GL_GetProcAddress"))(proc);

  printf("** SDL_GL_GetProcAddress: %s -> 0x%lX **\n", proc, proc_addr);

  return proc_addr;
}
Various OpenGL procedure addresses

Now all I needed to do was to compare the proc string against glClearColor and return a pointer to my function instead.

typedef void (*glClearColor_t) (GLclampf, GLclampf, GLclampf, GLclampf);
glClearColor_t original_clearcolor = nullptr;

void hkClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha) {
  // Always call glClearColor with opaque blue.
  return original_clearcolor(0.f, 0.f, 1.f, 1.f);
}

extern "C" void* SDL_GL_GetProcAddress(const char* proc) {
  void* proc_addr = reinterpret_cast<SDL_GL_GetProcAddress_t>(dlsym(RTLD_NEXT, "SDL_GL_GetProcAddress"))(proc);

  if (!strcmp(proc, "glClearColor")) {
    original_clearcolor = reinterpret_cast<glClearColor_t>(proc_addr);
    return reinterpret_cast<void*>(&hkClearColor);
  }

  return proc_addr;
}
g++ -std=c++14 -fPIC -shared -o libglhook.so glhook.cc -ldl -lSDL2
glClearColor in the menu
glClearColor in de_dust2
It's something.

This is where things started to slow down. Making the game blue in some areas is great and all, but I still wasn't sure how to draw anything. I knew nothing else about OpenGL and I was getting pretty burnt out at this point so I decided to open source all my findings and code so far and take a break.

Introducing SDL_GL_SwapWindow

Less than a day later and someone had managed to get OpenGL drawing working with parts of my code. Turns out that SDL_GL_GetCurrentContext, SDL_GL_CreateContext, SDL_GL_MakeCurrent and SDL_GL_SwapWindow with some direct OpenGL calls inbetween were enough to get a single red square drawing on the screen. Excited by the appearance of this geometric shape, I immediately got to work and within an hour I had a proof-of-concept up on GitHub.

ImGui test window in Counter-Strike: Global Offensive on Linux

That's great, but..

I wasn't particuarly thrilled at the prospect of restarting the game every time I made changes. I considered continuing to preload the SDL hooking library but provide export functions that would allow other shared libraries to manipulate the GUI. I thought it might be worth a try to move the drawing code to various virtual function hooks. This didn't really work well. I tried drawing in IPanel::PaintTraverse, IBaseClientDLL::FrameStageNotify, IMaterialSystem::SwapBuffers and IMaterialSystem::EndFrame but none could match the consistency of SDL_GL_SwapWindow.

I had to do something else, it was driving me insane. I started collaborating with Emma on chameleon-ng, a new reworked version of my year old Chameleon project. We finished the project in five days, after that it was back to the grind.

Coming back with a fresh mindset I took another look at SDL_GL_SwapWindow. This time I disassembled it during runtime with gdb and suddenly it all made sense. Looking back now I honestly can't believe I didn't realize this sooner.

Dump of assembler code for function SDL_GL_SwapWindow:
=> 0x00007ffff581d830 :  mov    0x2b5fd9(%rip),%rax        # 0x7ffff5ad3810
   0x00007ffff581d837 : jmpq   *%rax
End of assembler dump.

If my calculations are correct.. the address in 0x7FFFF5AD3810 is the real SDL_GL_SwapWindow function!

(gdb) x/xg 0x7ffff5ad3810
0x7ffff5ad3810: 0x00007ffff58846a0
(gdb) b *0x7ffff58846a0
Breakpoint 3 at 0x7ffff58846a0
(gdb) c
Continuing.

Thread 1 "csgo_linux64" hit Breakpoint 3, 0x00007ffff58846a0 in ?? ()
   from /home/aixxe/.local/share/Steam/steamapps/common/Counter-Strike Global Offensive/bin/linux64/libSDL2-2.0.so.0
(gdb) bt
#0  0x00007ffff58846a0 in ?? ()
   from /home/aixxe/.local/share/Steam/steamapps/common/Counter-Strike Global Offensive/bin/linux64/libSDL2-2.0.so.0

The exported SDL_GL_SwapWindow jumps to 0x7FFFF5AD3810 which in turn, jumps to 0x7FFFF58846A0 so all this time I just had to put a pointer to my replacement SwapWindow in 0x7FFFF5AD3810 and that would probably be enough. So I did.

// Pointer to 'SDL_GL_SwapWindow' in the jump table.
uintptr_t* swapwindow_ptr;

// Address of the original 'SDL_GL_SwapWindow'.
uintptr_t swapwindow_original;

void hkSwapWindow(SDL_Window* window) {
  static int count = 0; count++;
  printf("[SDL_GL_SwapWindow - count: %i, window: 0x%lX]\n", count, uintptr_t(window));

  // Call the original function.
  reinterpret_cast<void(*)(SDL_Window*)>(swapwindow_original)(window);
}

void __attribute__((constructor)) attach() {
  // Get the start address of the 'libSDL2-2.0.so.0' library.
  const uintptr_t sdl2_address = GetLibraryAddress("libSDL2-2.0.so.0");

  // Get the address of 'SDL_GL_SwapWindow' in the jump table. (7FFFF5AD3810 - 7FFFF57D6000 = 2FD810)
  swapwindow_ptr = reinterpret_cast<uintptr_t*>(sdl2_address + 0x2FD810);

  // Backup the original address.
  swapwindow_original = *swapwindow_ptr;

  // Write the address to our replacement function.
  *swapwindow_ptr = reinterpret_cast<uintptr_t>(&hkSwapWindow);
}

void __attribute__((destructor)) detach() {
  // Restore the original address.
  *swapwindow_ptr = swapwindow_original;
}

And it worked! Well, partially. I could load this with gdb and dlopen but dlclose was not cooperating. After confirming the detach function was working correctly, pinpointing the cause wasn't too difficult.

$ readelf -Ws libsdl-imgui.so | grep "UNIQUE"
   303: 0000000000291758     8 OBJECT  UNIQUE DEFAULT   25 _ZGVZN17ExampleAppConsole4DrawEPKcPbE6filter
   591: 0000000000291640   280 OBJECT  UNIQUE DEFAULT   25 _ZZN17ExampleAppConsole4DrawEPKcPbE6filter
   684: 0000000000291640   280 OBJECT  UNIQUE DEFAULT   25 _ZZN17ExampleAppConsole4DrawEPKcPbE6filter
   784: 0000000000291758     8 OBJECT  UNIQUE DEFAULT   25 _ZGVZN17ExampleAppConsole4DrawEPKcPbE6filter

A quick search through the project shows these symbols belonging to the demo window. Quickly removed it, added some basic placeholder text in it's place and unloading worked perfectly.

Attaching and detaching the library
Game process showing the hook in action
Extra debugging text thrown in for good measure.

Only one more thing to do. Drop that hardcoded offset and get it directly from the function.

template <typename T> inline T* GetAbsoluteAddress(uintptr_t instruction_ptr, int offset, int size) {
  return reinterpret_cast<T*>(instruction_ptr + *reinterpret_cast<uint32_t*>(instruction_ptr + offset) + size);
};

You can easily change the function name to hook a similar dynamic API function such as SDL_PollEvent. Unfortunately, this part is 64-bit specific so the 32-bit variant code still uses a hardcoded offset at the moment.

uintptr_t swapwindow_fn = reinterpret_cast<uintptr_t>(dlsym(RTLD_NEXT, "SDL_GL_SwapWindow"));
swapwindow_ptr = GetAbsoluteAddress<uintptr_t>(swapwindow_fn, 3, 7);

And with that, I was finally finished. It was a long, stressful process but also a fascinating learning experience. I'm pretty happy with the end result and it'll certainly be making an appearance in future tutorials. Hope this post wasn't too boring, just wanted to document the various different methods I tried for anyone who might be doing something similar in the future. Two versions of the source code are available, one for pre-loading and one for injecting at runtime. Have fun!

Last updated Thursday, 24 February 2022 at 11:01 AM.