Advent of Cyber 2024: AoC game hacking v8

Musyoka Ian
12 min readDec 24, 2024

--

Hello guys and welcome again to another walk though possibly the last one this year. This time we’ll be handling an advent of cyber challenge called AoC game hacking v8 from Tryhackme. It’s a game hacking challenge that involves using frida a really amazing tool used for dynamic instrumentation and a bit of reverse engineering. We done a couple of walkthoughs a few weeks back using frida so this should be a walk in the park. For reversing we’ll use binary ninja. We have to bypass 3 authentication mechanisms to get all the three flags. Without much say let’s jump in

Starting the challenge we get in instance with a parrot VM which we can interact with

We get a directory called TryUnlockMe. On accessing the directory, we get a binary called TryunlockMe

On executing the binary a game starts

Interacting with the first penguin we are asked to provide an OTP

Since we don’t know the OTP and every answer we provide is wrong, We’ll have to reverse engineer the binary to get the OTP.

TryUnlockMe seems like the UI application but the login might be residing in a shared library to confirm this i used ldd command to check the binary’s dependencies. The command i used is

ldd ./TryUnlockMe

Based on the guidelines this is the binary we are supposed to analyze. I copied the shared library to my box and loaded it o binary ninja. Looking at the shared library we copied to our attacking box, the binaries appears to not be stripped. Meaning we can get all debugging symbols.

The command i used to check was

file libaocgame.so

The first challenge was requesting for an OTP. Looking at the shared library in frida we notice there is a function called set_otp()

The function seems to be having an integer value passed to it. Most likely this is our OTP. Using frida we can hook up the function and print the value being passed to the function. Time to start creating our script. I have a frida template that i usually use when creating any exploit using frida.

/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://github.com/Areizen/JNI-Frida-Hook
* https://github.com/evilpan/jni_helper
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/apkunpacker/FridaScripts
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/985 working
*/

const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
magenta: str => colors.colorize(str, '[35m'),
};


function func_name(){
try{

}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

From the template i created a main function that when called it will confirm if the shared library libaocgame.so was loaded and also prints out its base address and also the binary size.

/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://github.com/Areizen/JNI-Frida-Hook
* https://github.com/evilpan/jni_helper
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/apkunpacker/FridaScripts
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/985 working
*/

const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
magenta: str => colors.colorize(str, '[35m'),
};


function func_name(){
try{

}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

function Main(){
try{
var modules = Process.enumerateModules();
for(var m = 0; m < modules.length; m++){
if((modules[m].name).indexOf("libaocgame.so") >= 0){
console.log(colors.green("\t[+] libaocgame.so successfully loaded in memory"));
var libaocgame_properties = Process.findModuleByName("libaocgame.so");
console.log(colors.magenta("\t[+] Base address of libaocgame.so is: "+libaocgame_properties.base+" and has a size of: "+libaocgame_properties.size));
}
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}




Main();

Executing the script with frida we get to print the base address and size of the binary as seen below. To execute the binary with the script which we have just created we will use the following command

frida -l hook.js ./TryUnlockMe

We can see that the base address and binary size has been printed to the screen

  • Process.enumerateModules() lists all the modules (libraries) loaded by the current process.
  • The for loop iterates through each module, and if the module name contains "libaocgame.so", it proceeds with the logic to hook a function in this module.
  • Process.findModuleByName("libaocgame.so"): Finds the loaded module by name and retrieves information about it (base address and size).

Next we need to hook the set_otp function which has an offset of 0x1279

I used Interceptor.attach API from frida to hook that specific function and print the arguments. This can be found from the documentation below

Below is the code i wrote

/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://github.com/Areizen/JNI-Frida-Hook
* https://github.com/evilpan/jni_helper
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/apkunpacker/FridaScripts
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/985 working
*/

const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
magenta: str => colors.colorize(str, '[35m'),
};


function func_name(){
try{

}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

function Main(){
try{
var modules = Process.enumerateModules();
for(var m = 0; m < modules.length; m++){
if((modules[m].name).indexOf("libaocgame.so") >= 0){
console.log(colors.green("\t[+] libaocgame.so successfully loaded in memory"));
var libaocgame_properties = Process.findModuleByName("libaocgame.so");
console.log(colors.magenta("\t[+] Base address of libaocgame.so is: "+libaocgame_properties.base+" and has a size of: "+libaocgame_properties.size));
Hook_Native(libaocgame_properties.base, "0x1279", 1);
}
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

function Hook_Native(base_address, hook_address ,step){
try{
if(step === 1){
Interceptor.attach(base_address.add(hook_address), {
onEnter: function(args){
console.log(colors.green("\t[+] set otp called by the application with args: "+ args[0].toInt32()));
},
onLeave: function(retval){
//do nothing
}
})
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}



Main();

Hook_Native function in the code hooks into a native function at a specific address and logs its arguments when called.

  • base_address.add(hook_address): Adds the hook_address (an offset) to the base_address of libaocgame.so to get the exact address of the function to hook.
  • Interceptor.attach(): This Frida API function attaches a hook to the function at the specified address.
  • onEnter: This callback is executed when the hooked function is called. It logs the first argument (args[0]) passed to the function using args[0].toInt32() to convert the argument to an integer.
  • onLeave: This callback is executed when the hooked function returns. In this case, it does nothing (//do nothing).

Running the program with the additional code we wrote, we get that the OTP is leaked by the application

The OTP for our specific instance is 855616. Using the OTP we get the first flag

Advancing to the next character, we need to buy the flag to pass the second level

But looking at the coins that we have, we only have 1 coin. To earn coinsn we have to interact with the computer

Meaning we have to interact with the computer 999999 times to get $999999 and get the flag. I interacted the computer till i got 16 coins

Then the computer was broken!!!!!.

Meaning i can’t get 999999 coins. This means we will have to reverse engineer the purchase function and find a way to bypass with frida.

The offset to the purchase function is 0x12a8

Looking at the validate purchase function we see that 3 integer arguments are passed to it

Using frida I’ll hook the function and extract all the arguments passed to the function hopefully one of the parameters passed is the coins or the price of the item

Below is the code i wrote

/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://github.com/Areizen/JNI-Frida-Hook
* https://github.com/evilpan/jni_helper
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/apkunpacker/FridaScripts
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/985 working
*/

const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
magenta: str => colors.colorize(str, '[35m'),
};


function func_name(){
try{

}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

function Main(){
try{
var modules = Process.enumerateModules();
for(var m = 0; m < modules.length; m++){
if((modules[m].name).indexOf("libaocgame.so") >= 0){
console.log(colors.green("\t[+] libaocgame.so successfully loaded in memory"));
var libaocgame_properties = Process.findModuleByName("libaocgame.so");
console.log(colors.magenta("\t[+] Base address of libaocgame.so is: "+libaocgame_properties.base+" and has a size of: "+libaocgame_properties.size));
Hook_Native(libaocgame_properties.base, "0x1279", 1);
}
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

function Hook_Native(base_address, hook_address ,step){
try{
if(step === 1){
Interceptor.attach(base_address.add(hook_address), {
onEnter: function(args){
console.log(colors.green("\t[+] set otp called by the application with args: "+ args[0].toInt32()));
},
onLeave: function(retval){
//do nothing
}
})
} else if(step === 2){
Interceptor.attach(base_address.add(hook_address), {
onEnter: function(args){
console.log(colors.green("\t[+] validate_purchase called by the application with args: "+args[0].toInt32()+" "+args[1].toInt32()+" "+args[2].toInt32()))
},
onLeave: function(retval){

}
})
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}



Main();

  • The args object represents the parameters that are passed to the function. In this case, the function is likely validate_purchase, and we expect it to take at least three arguments.
  • args[0].toInt32(), args[1].toInt32(), and args[2].toInt32() convert the first, second, and third arguments to 32-bit integers (toInt32() is a Frida function to convert values to integers).

After loading the exploit script using frida i tried purchasing the advice which is 5 dollars

Looking at the screenshot below after spending 5 dollars we get that args[2] was 16 which was the exact amount of money we had before now we have 11 dollars

Purchasing another advice and seeing the output of validate purchase we see this time arg[2] is 11 which was the amount of coins we had left

Now having confirmed that the last argument is the amount of money we have, We can manipulate it so that we have enough money to buy the flag. To manipulate it added just one extra line of code

arg[2] = ptr(10000000)

My final hook implementation looked like below

Below is the code

/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://github.com/Areizen/JNI-Frida-Hook
* https://github.com/evilpan/jni_helper
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/apkunpacker/FridaScripts
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/985 working
*/

const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
magenta: str => colors.colorize(str, '[35m'),
};


function func_name(){
try{

}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

function Main(){
try{
var modules = Process.enumerateModules();
for(var m = 0; m < modules.length; m++){
if((modules[m].name).indexOf("libaocgame.so") >= 0){
console.log(colors.green("\t[+] libaocgame.so successfully loaded in memory"));
var libaocgame_properties = Process.findModuleByName("libaocgame.so");
console.log(colors.magenta("\t[+] Base address of libaocgame.so is: "+libaocgame_properties.base+" and has a size of: "+libaocgame_properties.size));
Hook_Native(libaocgame_properties.base, "0x1279", 1);
Hook_Native(libaocgame_properties.base, "0x12a8", 2);
}
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

function Hook_Native(base_address, hook_address ,step){
try{
if(step === 1){
Interceptor.attach(base_address.add(hook_address), {
onEnter: function(args){
console.log(colors.green("\t[+] set otp called by the application with args: "+ args[0].toInt32()));
},
onLeave: function(retval){
//do nothing
}
})
} else if(step === 2){
Interceptor.attach(base_address.add(hook_address), {
onEnter: function(args){
console.log(colors.green("\t[+] validate_purchase called by the application with args: "+args[0].toInt32()+" "+args[1].toInt32()+" "+args[2].toInt32()));
console.log(colors.yellow("\t[+] Manipulating the currency to be 10000000"));
args[2] = ptr(10000000);
},
onLeave: function(retval){

}
})
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}



Main();

I hook the application with the code above and tried but the flag and voila we have successfully bought the flag

We proceed to our final step which we have to bypass the bio-metrics. Looking at the bio-metrics function in binary ninja we notice that function always returns a false so long as it’s called

The function is at offset 0x12f6

We need to use frida to ensure while hook the function will always return a true

I wrote the following frida code that alters the return value to always return a true

Remember the base address and hook address have been implemented in the main function

The final script looks like below

/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://github.com/Areizen/JNI-Frida-Hook
* https://github.com/evilpan/jni_helper
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/apkunpacker/FridaScripts
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/985 working
*/

const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
magenta: str => colors.colorize(str, '[35m'),
};


function func_name(){
try{

}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

function Main(){
try{
var modules = Process.enumerateModules();
for(var m = 0; m < modules.length; m++){
if((modules[m].name).indexOf("libaocgame.so") >= 0){
console.log(colors.green("\t[+] libaocgame.so successfully loaded in memory"));
var libaocgame_properties = Process.findModuleByName("libaocgame.so");
console.log(colors.magenta("\t[+] Base address of libaocgame.so is: "+libaocgame_properties.base+" and has a size of: "+libaocgame_properties.size));
Hook_Native(libaocgame_properties.base, "0x1279", 1);
Hook_Native(libaocgame_properties.base, "0x12a8", 2);
Hook_Native(libaocgame_properties.base, "0x12f6", 3);
}
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}

function Hook_Native(base_address, hook_address ,step){
try{
if(step === 1){
Interceptor.attach(base_address.add(hook_address), {
onEnter: function(args){
console.log(colors.green("\t[+] set otp called by the application with args: "+ args[0].toInt32()));
},
onLeave: function(retval){
//do nothing
}
})
} else if(step === 2){
Interceptor.attach(base_address.add(hook_address), {
onEnter: function(args){
console.log(colors.green("\t[+] validate_purchase called by the application with args: "+args[0].toInt32()+" "+args[1].toInt32()+" "+args[2].toInt32()));
console.log(colors.yellow("\t[+] Manipulating the currency to be 10000000"));
/***
* The following articles helped when it came to writing int to memory
*
* https://github.com/frida/frida/issues/1793
*
*/
args[2] = ptr(10000000);
},
onLeave: function(retval){

}
})
}else if(step === 3){
Interceptor.attach(base_address.add(hook_address), {
onEnter: function(args){
console.log(colors.green("\t[+] check_biometrics called by the application"));
},
onLeave: function(retal){
retal.replace(ptr(0x1));
}
})
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}



Main();

I injected the last script into the game and tried bypassing bio-metrics again and looking at the screenshot below it worked

We have the final flag. This is just the basic usage of frida there are far much advanced thing that frida can do of which we can’t cover on this article but come next year am planning on releasing more advanced writeups for android and IOS covering frida. If you like the walkthough clap for me down below and follow me so that you won’t miss any upcoming walkthoughs

--

--

Musyoka Ian
Musyoka Ian

Written by Musyoka Ian

Penetration Tester/Analytical Chemist who Loves Cybersecurity. GitHub(https://github.com/musyoka101), ExploitDB(https://www.exploit-db.com/?author=10517)

No responses yet