Frida Advanced Usage Part 8 – Frida Memory Operations Continued

Introduction

Welcome to another blog post in our Advanced Frida usage series. It is a continuation of our previous blog where we discussed Memory.scan, Memory.copy, and MemoryAccessMonitor JavaScript APIs in analyzing native Android libraries and how to utilize them in performing memory operations.

In this article, we will focus on the remaining Frida memory operations APIs, Memory.scanSync, Memory.protect, and Memory.patchCode.

You can find the APK files used in this Blog at: You can find the APK files at: You can find the APK files at: https://github.com/8kSec/Blog-resources/tree/main/Frida-Series

Analysis

In this tutorial, we will use different Android applications to show usage of each API in performing different the named memory operations.

Memory Protection

Memory.protect Frida API allows one to update protection permissions of a given memory page region to allow read and write access permissions. This is helpful in modifying runtime memory for patching or execution of protected memory pages.

Frida version 16.2.1 introduced a Memory protection query that enables one to enumerate memory ranges satisfying the protection string argument to the Memory.queryProtection query API.

In our example, we will use the snapseed application responsible for photo editing in Android applications to show the usage of the API.

The steps we need to take are:

  • Attach the Frida Process to the snapseed application

  • Run Process.enumerateModules() in the Frida Console to get all loaded Modules

  • Get the module object of libsnapped_native.so

  • Run Memory.queryProtection in the Frida Console

  • Repeat the query with different protection parameters

Running the steps above in the Frida console, we can determine the protection of various functions.

The API is useful in querying permissions of different memory page regions for exploit primitives and checking violations.

To change the protection level of a memory region, we will use Memory.Protect API. It returns true or false depending on the success of the API.

Writing Frida script

We will create a script to use the Memory.Protect API to change the permissions of the snapseed library.

Steps taken are:

  • Get the target module to hook

  • Use the Memory.Protect to query for at least read and write protection

  • Use the Memory.Protect to query for read, write, and execute protection

  • Print out the protection results

The implementation script looks like this:

				
					if (Java.available) {
    Java.perform(function () {
        let eightksec = Process.findModuleByName("libsnapseed_native.so");
        //Update the protection of the Module
        // Memory.protect(address, size, protection)
        let protection_results = Memory.protect(
            ptr(eightksec.base),
            eightksec.size,
            "rw-"
        );
        console.log("\nModule Path:", eightksec.path);
        console.log("Protection for Atleast 'rw':", protection_results);
        protection_results = Memory.protect(
            ptr(eightksec.base),
            eightksec.size,
            "rwx"
        );

        console.log("Protection for 'rwx':", protection_results);
    });
}
				
			

Running Frida script

Let’s now test our Frida script.

From the above results, permissions allowed are at least `read` and `write` only.

Memory synchronous Scanning

Memory.scanSync Frida API is a synchronous version of Memory.scan used for finding occurrences of user patterns in a given memory range specified by the size. The returned array of matched objects contains absolute address and size as the properties.

TIn our example, we will use a simple android application that compares two strings and prints out You win message if the strings match.

				
					extern "C" JNIEXPORT jstring

JNICALL
Java_com_ksec_eightksec_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    const char *string_one = "eightksec-string";
    const char *string_two = "eightksec string";

    int comparison_one = std::strcmp(string_one, string_two);
    if (comparison_one == 0 ) {
        return  env->NewStringUTF("You Win");

    } else {
        return env->NewStringUTF("You Lose, Strings should be equal");
    }
}
				
			

The application always prints You lose everytime it is run because string one and two are not equal. The goal of the challenge is to find the string reference addresses in the application’s memory.

Writing Frida script

We write a Frida script to show a synchronous version of the Memory.scan API to search for user patterns in the entire memory range of the target module.

The API supports wildcard search using question marks ?? in the pattern string. In our analysis, we will use the question mark to match both eightksec string and eightksec-string in the application memory.

Steps we need to take:

  • Get the target module

  • Write the pattern to search for

  • Scan memory using the Memory.scanSync API for the pattern

  • Print the matched objects address and size

Here is the full implementation script of the Memory.scanSync API

				
					if (Java.available) {
	Java.perform(function () {
		let eight8ksec = Process.findModuleByName("libeightksec.so");
		if (eight8ksec != null) {
			// patern to search for is "eightksec string"
			let pattern = "65 69 67 68 74 6b 73 65 63 ?? 73 74 72 69 6e 67";
			console.log("\nPattern:", pattern);

			// Memory.scanSync(address, size, pattern)
			let eight8ksec_results = Memory.scanSync(
				eight8ksec.base,
				eight8ksec.size,
				pattern
			);

			//print out matched pattern address and address size
			console.log("Memory.scanSync Result array: \n",JSON.stringify(eight8ksec_results));

			// print the first matched Address and size of the pattern
			console.log("Address of First Match:",eight8ksec_results[0].address);
			console.log("Size of First Match:", eight8ksec_results[0].size);

			// print the second matched Address and size of the pattern
			console.log("Address of second Match:",eight8ksec_results[1].address);
			console.log("Size of second Match:", eight8ksec_results[1].size);
		}
	});
}
				
			

Running Frida script

Running the script against the application, we get the memory addresses of the matched patterns objects.

Further, this technique can utilised to search application memory for target addresses and use frida Interceptor to patch them.

Memory Patching

Memory.patchCode JavaScript API allows one to safely modify the specified size of bytes at a given memory address referenced as a nativePointer. Memory Patchode can be used together with writers to generate machine code directly written to the memory. The currently supported writers by Frida for different architectures are X86Writer, Arm64Writer, and ThumbWriter.

To get the correct target architecture and size of the pointer run the following commands in the frida console Process.arch and Process.pointerSize.

In our analysis, we will use a simple Android application that checks if Frida debugger is hooked into it. It prints Frida detected if the process is hooked using Frida else it prints Frida not detected.

				
					extern "C" JNIEXPORT jstring JNICALL
Java_com_ksec_eightksec_MainActivity_FridaDetectFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    FILE *fridaFile = fopen("/proc/self/maps", "r");
    if (fridaFile != nullptr) {
        char line[256];
        while (fgets(line, sizeof(line), fridaFile)) {
             char *result_pointer = strstr(line, "frida-agent");
            if (result_pointer != nullptr) {
                fclose(fridaFile);
                return env->NewStringUTF("Frida detected");
            }
        }
        fclose(fridaFile);
    }
    return env->NewStringUTF("Frida not detected");

}
				
			

To get the memory addresses of branches to hook, we use ghidra to reverse engineer the libeightksec.so library and get offsets of the branches.

From the assembly code above, the application branches to 0x001098c if the results of strstr are not NULL else the code will branch to 0x001019bc if the returned pointer is NULL.

The strstr is used for locating a substring and returns a pointer if found or NULL if the substring is not found.

Writing Frida Script

Steps taken are:

  • Get the base address of the Module

  • Get the Frida Detected and Not detected branches

  • Use Memory.protect to change the permissions to rwx

  • Use Memory.patchCode to patch the Frida Detected branch

  • Clean up the memory after patching

The full code for our script is as follows:

				
					if (Java.available) {
    let baseAddress = Module.findBaseAddress("libeightksec.so");

    // Check if the base address is not null
    if (baseAddress != null) {
        // Get the address for the Frida detect branch
        let frida_detected_branch = baseAddress.add(0x01988);
        console.log("\nFrida Detected branch address: ", frida_detected_branch);
        // Get the address for Frida not detected branch
        let frida_notDetected_branch = baseAddress.add(0x019b8);
        console.log("Frida Not Detected branch: ", frida_notDetected_branch);

        // Change Memory protection to allow "rwx"
        Memory.protect(frida_detected_branch, 0x1000, "rwx");

        Memory.patchCode(frida_detected_branch, 8, (code) => {
            // Instantiate a new Arm64 Writer
            let writer = new Arm64Writer(code);

            try {
                // Branch to Frida not Detected Branch
                writer.putBImm(frida_notDetected_branch);
                writer.flush();
                console.log("Memory Patched Successfully");
            } finally {
                // clean up Memory
                writer.dispose();
            }
        });
    }
}
				
			

Running Frida script

Let’s now test the script against our application.

Clicking the Check Frida button on the Application Interface, shows we have successfully altered the control flow of the program by Toasting Frida not detected while the process is hooked.

In real-world applications, Knowledge learned can be used in defeating various anti-debug mechanisms implemented by developers that might be roadblocks to further security analysis or debugging.


Conclusion

This marks the end of the second part Frida Memory operations blogpost as part of our Advanced Frida series. We have now learned how to utilize various memory operations in inspecting the native memory of Android applications.

The same techniques and knowledge can be applied to iOS applications in inspecting runtime memory for debugging and security posture analysis.

On Trend

Most Popular Stories

Hacking Android Games

Greetings and welcome to our blog post on the topic of Android game hacking. Today, we aim to provide you with an overview of the process involved in hacking Android games. It’s crucial to distinguish between app hacking and game hacking within the Android ecosystem.

Subscribe & Get InFormation

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.