Skip to content

Orpheu: Finding Functions in Libraries

A library is a set of bytes. Millions of bytes. They can represent numbers, functions (in machine code), arrays and strings. This article will try to explain how to find functions in libraries and how to use them. Finding a function means determining its location in the library, which is usually…

Coding·4 min read·Dani MinchViews:16

A library is a set of bytes. Millions of bytes. They can represent numbers, functions (in machine code), arrays and strings. This article will try to explain how to find functions in libraries and how to use them.

Finding a function means determining its location in the library, which is usually called an offset. Essentially, it is the number of bytes you have to skip to reach the function, starting from the first byte of the library.

In libraries compiled with GCC (Linux), offsets are marked with symbolic names, which let you easily recognize what is at a given offset.

In libraries compiled with VC++, offsets generally have no labels at all, so you have to resort to methods such as searching for a string in the library. This works because you can easily tie strings to events; for example, if you see the text “Terrorists Win”, you know you are dealing with a function related to the end of a round.

When looking for offsets, we can use the following methods:

  1. Finding a function by searching for strings.
  2. After finding a function, use it to find other functions, for example if you know that your function calls another function or is called by one.
  3. Apply the steps above in reverse order (combined)

To make this process easier, you should do it with the Linux library.
So you need to have your mod's libraries for both Linux and Windows, and also get the “IDA Pro Disassembler”.

In IDA

IDA Start

Click New

For the Linux library:

IDA Linux

For the Windows library:

IDA Windows

Select the library and open it.

Press CTRL+F5. This opens a dialog where you can choose where to save the file. This file is the decompiled version of your library (an IDA feature: it converts machine code back into C code, which is not very readable and does not exactly reproduce the original, but is more readable than machine code).

In the decompiled version of the Linux library, you will get code something like this:

C++
//----- (000B3C10) --------------------------------------------------------
int __cdecl InstallGameRules()
{
  int result; // eax@2
  int v1; // eax@2
  int v2; // eax@3

  (*(void (__cdecl **)(_DWORD))&g_engfuncs[156])("exec game.cfg\n");
  (*(void (**)(void))&g_engfuncs[160])();
  if ( *(float *)(gpGlobals + 20) == 0.0 )
  {
    v2 = __builtin_new(708);
    result = __18CHalfLifeMultiplay(v2);
  }
  else
  {
    v1 = __builtin_new(728);
    result = __17CHalfLifeTraining(v1);
  }
  return result;
}

This is the InstallGameRules function, where you can easily see that the string “exec game.cfg\n” is used.

Now let's look at the decompiled version of the Windows library:

C++
//----- (10088530) --------------------------------------------------------
int __cdecl sub_10088530()
{
  long double v0; // fst7@1
  int v1; // eax@2
  int v3; // eax@4

  dword_101623DC("exec game.cfg\n");
  dword_101623E0();
  v0 = *(float *)(LODWORD(dword_101625B8) + 20);
  if ( v0 == 0.0 )
  {
    v1 = (int)operator new(0x2E8u);
    if ( v1 )
      return sub_100C5EF0(v1, v0);
  }
  else
  {
    v3 = (int)operator new(0x2D0u);
    if ( v3 )
      return sub_10093D80(v3, v0);
  }
  return 0;
}

After a quick look it is easy to see that we are dealing with the same function.

This means that offset 88530 in the Windows Counter Strike file will give us the InstallGameRules function. You can see it in the pseudo-label “sub_10088530” (ignore the sub_10). This number is now presented to us as hexadecimal. We will write it as 0x88530.

To demonstrate another method, let's turn to the decompiled Linux library and look for the function that calls InstallGameRules.

C++
//----- (0011163C) --------------------------------------------------------
int *__usercall CWorld__Precache<eax>(long double a1<st0>, int a2)
{
  unsigned int v2; // edi@8
  __int16 v3; // fps@15
  long double v4; // fst6@15
  char v5; // c0@15
  char v6; // c2@15
  char v7; // c3@15
  int v8; // eax@16
  int v9; // ecx@18
  int v10; // eax@19
  int *result; // eax@25
  int v12; // [sp-10h] [bp-58h]@16
  float v13; // [sp-Ch] [bp-54h]@16

  g_pLastSpawn = 0;
  g_pLastCTSpawn = 0;
  g_pLastTerroristSpawn = 0;
  (*(void (__cdecl **)(_DWORD, char[4]))&g_engfuncs[240])("sv_gravity", "800");
  (*(void (__cdecl **)(_DWORD, char[4]))&g_engfuncs[240])("sv_maxspeed", "900");
  (*(void (__cdecl **)(_DWORD, _DWORD))&g_engfuncs[240])("sv_stepsize", "18");
  (*(void (__cdecl **)(_DWORD, _DWORD))&g_engfuncs[240])("room_type", "0");
  if ( g_pGameRules )
    __builtin_delete((void *)g_pGameRules);
  g_pGameRules = InstallGameRules();

In the last line you can see that the InstallGameRules function is called from CWorld__Precache (the real name is CWorld::Precache). Now we go back to the decompiled Windows library.

We replace all sub_10088530 values with InstallGameRules. We search for InstallGameRules.

C++
int __usercall sub_100DD350<eax>(int a1<ecx>, int a2<esi>, long double a3<st0>)
{
  int v3; // ebp@1
  int v4; // eax@3
  int v5; // esi@4
  int v6; // eax@6
  int v7; // ecx@7
  int v8; // eax@8
  signed int v9; // esi@17
  int v10; // eax@28
  int v11; // ecx@29
  int result; // eax@33
  float v13; // [sp-4h] [bp-10h]@25
  signed int v14; // [sp-4h] [bp-10h]@31

  v3 = a1;
  dword_10162EFC = 0;
  dword_10163D00 = 0;
  dword_10163D04 = 0;
  dword_10162430("sv_gravity", "800");
  dword_10162430("sv_maxspeed", "900");
  dword_10162430("sv_stepsize", "18");
  dword_10162430("room_type", L"0");
  if ( dword_10162304 )
    operator delete(dword_10162304);
  dword_10162304 = (void *)InstallGameRules();

And we find CWorld__Precache (offset 0xDD350).

That is basically it. By applying this knowledge and a little bit of your brain, you can find, in principle, any function.

Now about the types a function can use. You can check them by looking through the list of functions in the Linux version of the library in the IDA window under Names. I do not know whether there is an easy way to get the return type, but you can always guess it or check the Half Life SDK, if that makes sense.

For this case:

C++
{
    "name" : "InstallGameRules",
    "library" : "mod",
    "return" : 
    {
        "type" : "CHalfLifeMultiplay *"
    },
    "identifiers":
    [
        {
            "os" : "windows",
            "mod" : "cstrike",
            "value" : 0x88530
        }
    ]
}

This should go in the InstallGameRules file in the configs/orpheu/functions folder. Remember that if the function belonged to a class, you would have to create a folder named after the class.

There is one more thing to know. The offset is guaranteed to be the same every time the library is loaded, but it can easily change if the library gets updated. This problem is solved by creating function signatures.

In general, signature scanning means the following: instead of specifying an offset, you specify a set of bytes that can be found at that offset (the ones that make up the function). This set of bytes can easily change its location, but as long as it exists as a block, you can easily find it.

Translation: Dani Minch
Source: http://forums.alliedmods.net/showthread.php?t=118934

Related articles:
Orpheu Module (v2.5.1)
Function Signatures.