sp1d3r

GTA Vice City | Infinite coins | Memory editing

Introduction #

I didn’t get to play GTA VC/SA in my childhood, otherwise, I would’ve wasted a lot of time grinding for coins to finish missions. Recently, I’ve been reading about frida and was very happy pwning applications. I took my chance in these games too!

Script #

You will need to install python and get frida, psutil and frida-tools packages from pip.

# In a shell
pip install frida frida-tools psutil
# The main.py script
import frida
import os
import argparse
import logging as logger
logger.basicConfig(level=logger.INFO, format='[%(levelname)s] %(message)s')
import psutil

CODE = """ptr("0x94add0").writeInt({coins})
    ptr("0x94add4").writeInt({coins})"""

def get_pid_by_name(process):
    for proc in psutil.process_iter():
        if process in proc.name():
            return proc.pid

    raise Exception(f"process with name:{process} not found!")

def main():
    logger.info("This works if the `gta-vc` executable has ASLR disabled.")

    pid = get_pid_by_name("gta-vc")
    logger.info(f"Attaching to pid:{pid} ...")
    session = frida.attach(pid)

    logger.info(f"Injecting code to pid:{pid}")
    logger.info(CODE)

    script = session.create_script(CODE)
    script.load()
    logger.info(f"Script loaded!")
    script.unload()

if __name__ == '__main__':
    parser = argparse.ArgumentParser(usage=f'python {__file__} -coins 123456')
    parser.add_argument('-coins', type=int)
    args = parser.parse_args()

    if args.coins:
        if args.coins > 99999999:
            logger.error(f"{args.coins} larger than maximum coins 99999999")
        else:
            CODE = CODE.format(coins=args.coins)
            main()
    else:
        os.system(f'python {__file__} -h')

Approach #

Frida is a “Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers”. It helps us to inject custom code into processes easily or play with its memory.

    / _  |   Frida 16.0.19 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Local System (id=local)

My first guess was that the coins variable should be an integer stored somewhere in the program memory. So, I used the following script to search the program memory:

import os
import logging as logger
logger.basicConfig(level=logger.ERROR)
import psutil
import frida

CODE = """
    function search(base, size, target) {
        const results = Memory.scanSync(base, size, target)
        if (results != '') {
            for (let i=0; i<results.length; i++) {
                console.log(results[i].address)
            }
        }
    }

    console.log(`Attach success`)

    const exe_base = Process.enumerateModules()[0].base;
    console.log(`${exe_base} ==> ASLR Disabled`)

    const ranges = Process.enumerateRanges('rw-');
    for (let i=1; i<ranges.length; i++) {
        console.log(`#${i} base:${ranges[i].base} size:${ranges[i].size} file:${ranges[i].file} prot:${ranges[i].protection}`)
        search(ranges[i].base, ranges[i].size, '64 00 00 00')
    }
"""

_CODE = """
    function search(base, size, target) {
        const results = Memory.scanSync(base, size, target)
        if (results != '') {
            for (let i=0; i<results.length; i++) {
                console.log(results[i].address)
            }
        }
    }

    // search(ptr("0x6f7000"), 3252224, '4e 4e')
    // search(ptr("0x700000"), 3252224, '3A 00')
"""

_CODE = """
    ptr("0x94add0").writeInt(1337)
    ptr("0x94add4").writeInt(1337)
"""

def get_pid_by_name(process):
    for proc in psutil.process_iter():
        if process in proc.name():
            return proc.pid

pid = get_pid_by_name("gta-vc")
session = frida.attach(pid)

logger.info(f"Injecting to process {pid} with code:")
logger.info(CODE)

script = session.create_script(CODE)
script.load()
script.unload()

Not so functional, but it just coveys the idea.

After using the above script, 0x94add0, 0x94add4 were the two locations where the coins variable / integer was found.

By inspecting the address with the memory map, the addresses belong to the .bss section of the image. This means that, the coins variable is a stack variable 😂

From my observations, the game tries to maintain a player structure similar to this:

struct player {
    // ...
    int coins;  // Actual number of coins at that instant
    int displayCoins;  // The coins which are displayed in the UI
                       // This second coins variable is maintained to update coins steadily
    // ...
};

If the first coins variable was overwritten, the UI would take a lot of time to display the correct number of coins. Hence, we overwrite both.

comments powered by Disqus