Attacking Android Malware with Frida
In this blog post, I will showcase how Frida, a popular open source dynamic instrumentation framework, can be utilized for Android malware analysis across three distinct real world malware samples.
Isn’t Frida a pentesting tool?
Despite the fact that Frida is primarily known as a mobile application pentesting utility, it proves equally effective in mobile malware analysis and is particularly useful for validating findings from the static analysis phase of the applications examination process.
How does Frida work and how to use it on malware samples?
Frida works by injecting arbitrary JavaScript (or Python) code into the application’s process, effectively manipulating loaded methods or even native functions and consequently altering application’s dynamic behavior. This functionality includes inspecting or changing passed arguments and function’s return values, invoking methods on existing or newly created objects, as well as altering static variables and much more. The purpose of this article is to show a selection of such features for malware analysis. This article explores the following use cases:
1. Hooking Native Functions to Intercept Arguments for One-Time Executed Functions
2. Manually Invoking Native Methods for String Decryption
3. Utilizing Wrapper Scripts
Hooking Native Functions to Intercept Arguments for One-Time Executed Functions
This section explains the dynamic analysis process of an Android malware sample belonging to the Kangapack family which was discovered, analyzed and named by the security researcher Cryptax. While the comprehensive analysis of this native malware packer which stores its encrypted malicious payload in the file classes.dex is beyond the scope of this blog post, I will demonstrate how Frida framework with provided scripts can be used to intercept AES decryption arguments used for decrypting its malicious classes, also known as the payload. All scripts are available in my github repo and malware hashes used in analysis are provided at the IoC section.
The main goal of this subsection is to determine at what stage of the execution of the application’s the AES decryption arguments – its initialization vector and the decryption key required for manual decryption of the malicious payload – should be intercepted. Frida offers powerful capabilities for hooking into both Java and native layer methods and functions. For intercepting the decryption parameters in question, of particular interest is the AES decryption function EVP_DecryptInit_ex, an exported function from the OpenSSL library, operating at a native layer. Frida’s Interceptor.attach module allows us to dynamically hook under its hood during its execution and retrieve the desired arguments needed in malware analysis. Simply running the script and hooking into this function won’t yield desired results, as the corresponding native library containing the targeted exported native function must be loaded into memory first. The Android’s APIs responsible for dynamic library loading are System.loadLibrary and System.load, both of which call the operating system’s loader to load the provided native library into the memory. The simplest solution to avoid hooking undefined functions is introducing a hardcoded timing delay before the function hooking. In this case, however, implementing a delay isn’t a feasible solution since the AES decryption parameters are used only once and delaying would mean missing the right moment of functions execution. Since the decryption function is only executed once during the payload decryption process, it is crucial to hook the function before its execution to intercept decryption arguments with the decryption key and initialization vector.
To achieve this, some additional context and further analysis is needed. Dynamic analysis reveals that the native method abcdesCrypt is responsible for the payload decryption process. This method handles the AES decryption process, thereby decrypting the malicious classes. As seen from the first screenshot, in the same class API for loading libraries is called. We must ensure this class is instantiated by the time we execute our hook.
Method abcdesCrypt is the last decryption method in the Java layer. Its further implementation is located in compiled library and takes care of AES decryption process that decrypts malicious classes we call the payload. The purpose of this article is to present how to identify the appropriate point in execution flow of app for desired purposes – in this case to intercept AES IV and the decryption key.
Hooking the Java method using the script logCallStack.js, which prints the call stack to identify the external function that called it, ensuring the library is loaded in memory before the method is called.
Static analysis also confirms we found the first wrapping method.
Additionally, callgraph created by Androguard confirms the same execution chain as analyzed with Frida. Because the native library is loaded in the same class as native method is called from, we conduct that method abcdesCrypt is appropriate method to inject into. To confirm that library is loaded, we use the monitorLibraryLoad.js script, which checks loading status of a tageted native library before and after method’s execution, effectively checking whether selected method acts as a boundary point for successful hooking into the library’s native function.
Note that hooking a wrapping method is not the only way to hook native functions, as native function dl_open can also be intercepted. More on this in the future post about unpacking native Android packers and protectors.
All of the previous scripts helped us find the right point in application execution flow to hook AES decryption function implemented in native layer. The final script first hooks into the wrapping Java method, checks whether corresponding native library is loaded into memory, hooks into AES decryption function EVP_DecryptInit_ex and outputs its decryption arguments – the decryption key and initialization vector.
Finally hooking native function
Once we identify the appropriate point to hook the native function, we can intercept the initialization function responsible for AES decryption of the payload and output the decryption key and initialization vector. Here is the final script that hooks the wrapping Java method to ensure the library is loaded, then hooks the native function, and intercepts the decryption parameters: Here is the final script that hook the wrapping Java method to ensure lbrary is loaded and then hooks native function and intercepts the decryprion parameters. Script is also accessible on Github.
Java.perform(function () {
// Configuration
var targetClass = 'com.hwgapkspv.gouhwkh.Molfwernpozxswfsg';
var targetMethod = 'abcdesCrypt';
var libraryName = 'libapksadfsalkwes.so';
var nativeMethodSignature = '[B';
// Helper function to check if the library is loaded
function isLibraryLoaded(libName) {
var modules = Process.enumerateModules();
for (var i = 0; i < modules.length; i++) {
if (modules[i].name === libName) {
console.log(`[INFO] Library ${libName} is loaded.`);
return true;
}
}
console.log(`[INFO] Library ${libName} is not loaded.`);
return false;
}
// Hook the Java method
var targetClassInstance = Java.use(targetClass);
targetClassInstance[targetMethod].overload(nativeMethodSignature).implementation = function (arg0) {
console.log(`[INFO] Hooked method ${targetClass}.${targetMethod}`);
// Check if the library is loaded before the method call
console.log('[INFO] Checking library before method call:');
isLibraryLoaded(libraryName);
// Function to enumerate imports and attach to EVP_DecryptInit_ex
function enumerateAndAttach() {
var decryptAddress = null;
var imports = Module.enumerateImportsSync(libraryName);
for (var i = 0; i < imports.length; i++) {
if (imports[i].name === "EVP_DecryptInit_ex") {
decryptAddress = imports[i].address;
console.log("Found import EVP_DecryptInit_ex at address: " + decryptAddress);
break;
}
}
if (decryptAddress) {
console.log("Attaching to EVP_DecryptInit_ex at address: " + decryptAddress);
Interceptor.attach(decryptAddress, {
onEnter: function (args) {
var keyArgument = Memory.readByteArray(ptr(args[3]), 32);
var keyHexDump = hexdump(keyArgument, { offset: 0, length: 32, header: true, ansi: true });
var keyMessage = "[*] AES Key " + 3 + " " + args[3];
console.log(keyMessage);
console.log(keyHexDump);
var ivArgument = Memory.readByteArray(ptr(args[4]), 16);
var ivHexDump = hexdump(ivArgument, { offset: 0, length: 16, header: true, ansi: true });
var ivMessage = "[*] AES IV " + 4 + " " + args[4];
console.log(ivMessage);
console.log(ivHexDump);
}
});
} else {
console.log("EVP_DecryptInit_ex not found in imports.");
}
}
// Call the enumeration and attachment function before the method execution
enumerateAndAttach();
// Call the original method and capture the return value
var result = this[targetMethod](arg0);
// Check if the library is loaded after the method call
console.log('[INFO] Checking library after method call:');
isLibraryLoaded(libraryName);
return result;
};
});
Manually invoking decryption methods or native functions
When analyzing Android malware, we often inspect methods that decrypt strings by accepting another value like an encrypted string or integer as a parameter. In those cases, it is not particularly important whether we intercept the decrypted string or retrieve it ourselves. Frida framework can be used not only to intercept function arguments, but also to manually call methods and functions at any given time with the desired arguments. Packer Jiagu, one of the most commonly used protectors for malware applications, is known for employing decryption routines decrypting strings from passed integers in the native layer. Static string decryption is particularly complex and will be addressed in a future blog post focusing on native unpacking techniques In this analysis, we opted for dynamic string decryption by identifying the decryption method implemented in the native layer and manually invoking it.
The revealed strings in the shown example demonstrate that the application payload functions as a banking trojan, targeting and stealing cryptocurrency wallet information.
Using Frida scripts with Python wrapper
Analyzing some samples from a widely distributed Anubis family can sometimes triggers an error, causing the application to exit with the message: “Failed to spawn: unable to find front-door activity.”, effectively disabling Frida from the malware analysis process. Although the Launcher activity is declared in the application’s AndroidManifest.xml file and the corresponding code is present in the APK file, the app fails to start with Frida alone. To successfully start the application and hook into its methods, a Python wrapping script is required.
This wrapper script facilitates the attachment of a Frida script to applications process that encounter “Failed to spawn: unable to find front-door activity” error. It continuously monitors for the application to be manually opened by the user, either via the app drawer or ADB, and once the app is detected as running, the Frida script is automatically attached to the application process.