Game Hacking: Exploiting Executables and Libraries
Executables and Libraries
The executable file of an application is generally understood as a standalone binary file containing the compiled code we want to run. While some applications contain all the code they need to run in their executables, many applications usually rely on external code in library files with the shared object .so
extension.
Library files are collections of functions that many applications can reuse. Unlike applications, they can’t be directly executed as they serve no purpose by themselves. For a library function to be run, an executable will need to call it. The main idea behind libraries is to pack commonly used functions so developers don’t need to reimplement them for every new application they develop.
For example, imagine you are developing a game that requires adding two numbers together. Since mathematical functions are so commonly used, you could implement a library called libmaths
to handle all your math functions, one of which could be called add()
. The function would take two arguments (x
and y
) and return the sum
of both numbers.
Note: that the application trusts the library to perform the requested operation correctly. From an attacker’s standpoint, if we could somehow intercept the function calls from the executable to the library, we could alter the arguments sent or the return value. This would allow us to force the application to behave in strange ways.
Hacking with Frida
Frida is an instrumentation tool for analyzing, modifying, and interacting with running applications. It injects a thread into the target process, allowing real-time interaction using JavaScript. Frida’s Interceptor functionality enables observing and altering input/output of internal functions.
Using Frida, you can:
- Intercept function calls.
- Modify function arguments and return values on the fly.
- Observe the internal behavior of applications.
Walkthrough: Hacking the Game
Exploring the game a bit around, you will find a penguin asking for a PIN.
Terminate the previous game instance and execute the following Frida command to intercept all the functions in the libaocgame.so
library where some of the game logic is present:
1
frida-trace ./TryUnlockMe -i 'libaocgame.so!*'
If you revisit the NPC, you can trigger the OTP function on the console displayed as set_otpi
1
2
3
4
5
kali@kali:~/Desktop/TryUnlockMe/$ frida-trace ./TryUnlockMe -i 'libaocgame.so!*'
Instrumenting...
Started tracing 3 functions. Web UI available at http://localhost:1337/
/* TID 0x2240 */
7975 ms _Z7set_otpi()
Notice the output _Z7set_otpi
indicates that the set_otp
function is called during the NPC interaction; you can try intercepting it!
Open a new terminal, go to the
1
code /home/ubuntu/Desktop/TryUnlockMe/__handlers__/libaocgame.so
At this point, you should be able to select the _Z7set_otpi
JavaScript file with the hook defined. The i at the end of the set_otp
function indicates that an integer will be passed as a parameter. It will likely set the OTP by passing it as the first argument. To get the parameter value, you can use the log
function, specifying the first elements of the array args
on the onEnter
function: log("Parameter:" + args[0].toInt32());
Your JavaScript file should look like the following:
1
2
3
4
5
6
7
8
defineHandler({
onEnter(log, args, state) {
log('_Z7set_otpi()');
log("Parameter:" + args[0].toInt32());
},
onLeave(log, retval, state) {
}
});
Now you should be able to log something similar:
1
2
3
4
5
6
kali@kali:~/Desktop/TryUnlockMe/$ frida-trace ./TryUnlockMe -i 'libaocgame.so!*'
Instrumenting...
Started tracing 3 functions. Web UI available at http://localhost:1337/
/* TID 0x2240 */
39618 ms _Z7set_otpi()
39618 ms Parameter:641602
Then, you need to use that parameter as OTP; this value changes over time, so your will be different:
Enter the OTP, then you get Flag1:
Stage 2: Bypassing the In-Game Economy
Exploring the new stage, you will find another penguin with a costly item named Right of Pass. The game lets you earn coins by using the old PC on the field, but getting 1.000.000 coins that way sounds tedious. You can again use Frida to intercept the function in charge of purchasing the item. This time is a bit more tricky than the previous one because the function
buy_item
displayed as : _Z17validate_purchaseiii
has three i letters after its name to indicate that it has three integer parameters.
You can log those values using the log function for each parameter trying to buy something:
log("Parameter1:" + args[0].toInt32())
log("Parameter2:" + args[1].toInt32())
log("Parameter3:" + args[2].toInt32())
Your JavaScript buy_item
file should look like the following:
1
2
3
4
5
6
7
8
9
10
11
defineHandler({
onEnter(log, args, state) {
log('_Z17validate_purchaseiii()');
log('PARAMETER 1: '+ args[0]);
log('PARAMETER 2: '+ args[1]);
log('PARAMETER 3: '+ args[2]);
},
onLeave(log, retval, state) {
}
});
You should be able to log something similar:
1
2
3
4
07685 ms _Z17validate_purchaseiii()
365810 ms PARAMETER 1: 0x1 // Item ID
365810 ms PARAMETER 2: 0x5 // Price
365810 ms PARAMETER 3: 0x1 // Player Coins
By simple inspection, we can determine that the first parameter is the item ID, the second is the price, and the third is the player’s coins. If you manipulate the price and set it as zero, you can buy any item that you want: args[1] = ptr(0)
Your JavaScript buy_item
file should look like the following:
1
2
3
4
5
6
7
8
9
defineHandler({
onEnter(log, args, state) {
log('_Z17validate_purchaseiii()');
args[1] = ptr(0)
},
onLeave(log, retval, state) {
}
});
You can buy any item now, and you will get Flag2
Stage 3: Biometrics Check
This last stage is a bit more tricky because the output displayed by Frida
is _Z16check_biometricsPKc()
, so it does not handle integers anymore but strings, making it a bit more complex to debug.
By selecting the JavaScript file named _Z16check_biometricsPKc
, you can add the following code to the onEnter()
function as you did previously to debug the content of the parameter:
1
2
3
4
5
6
7
8
defineHandler({
onEnter(log, args, state) {
log('_Z16check_biometricsPKc()');
log("PARAMETER:" + Memory.readCString(args[0]))
},
onLeave(log, retval, state) {
}
});
You should be able to log something similar:
1
2
1279884 ms _Z16check_biometricsPKc()
1279884 ms PARAMETER:1trYRV2vJImp9QiGEreHNmJ8LUNMyfF0W4YxXYsqrcdy1JEDArUYbmguE1GDgUDA
This output does not seem very helpful; you may have to consider another way. You can log the return value of the function by adding the following log instruction in the onLeave
function:
1
2
3
onLeave(log, retval, state) {
log("The return value is: " + retval);
};
You should be able to log something similar:
1
69399931 ms The return value is: 0x0
So, the value returned is 0, which may indicate that it is a boolean flag set to False. Which value will set it to True? Can you trick the game into thinking the biometrics check worked?
The following instruction will set the return value to True: retval.replace(ptr(1))
1
2
3
4
5
6
7
8
9
defineHandler({
onEnter(log, args, state) {
log('_Z16check_biometricsPKc()');
},
onLeave(log, retval, state) {
log("The return value is: " + retval);
retval.replace(ptr(1)); // Set to True
}
});
With this modification, the game believes the biometrics check passed, and you can now get Flag 3:
THANKS FOR READING ❤️