Advent of Cyber 2024: AoC game hacking v8
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 thehook_address
(an offset) to thebase_address
oflibaocgame.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 usingargs[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 likelyvalidate_purchase
, and we expect it to take at least three arguments. args[0].toInt32()
,args[1].toInt32()
, andargs[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