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
applicationRun
Process.enumerateModules()
in the Frida Console to get all loaded ModulesGet the module object of
libsnapped_native.so
Run
Memory.queryProtection
in the Frida ConsoleRepeat 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 protectionUse the
Memory.Protect
to query for read, write, and execute protectionPrint 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 patternPrint 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 torwx
Use
Memory.patchCode
to patch the Frida Detected branchClean 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.