If we installed an Android APK and we have a root access, we can modify the .so (native) filesof that app without altering the signature. This is true even if extractNativeLibs
is set to false in AndroidManifest.xml
. We can also patch the AOT compiled file (ODEX/VDEX) without changing the package signature, but that’s another story, I am just going to focus on the native code.
As a note: this is not a vulnerability, it requires root access. This method was discussed in the Mystique exploit presentation (2022). I just want to show that this is useful for pentest purpose, with an extra tip of how to write binary patch in C.
Background
I was doing a pentest on an Android app with a complex RASP. There are many challenges:
- If I unpack the APK file and repack it, it can detect the signature change
- If I use Frida, it can detect Frida in memory, even when I change the name using fridare
- It can detect Zygisk, so all injection methods that use Zygisk are detected
- It can detect hooks on any function, not just PLT. It seems that it is done by scanning the prologue of functions to see if it jumps to a location outside the binary; the app developer needs to call this check manually (this is quite an expensive operation), which is usually done before it performs some critical scenario.
- The RASP uses a native library, which is obfuscated
Given enough time, I am sure it is possible to trace and patch everything, but we are time-limited, and I was only asked to check a specific functionality.
When looking at that particular functionality, I can see that it is implemented natively in a non-obfuscated library. In this specific case, If I can patch the native code without altering the signature, I don’t need to deal with all the anti-frida, anti-hook, etc.
Android Native Libs Installation
Before Android 6.0, all native libraries were extracted during installation. So, when an app is installed, the original APK file and the extracted libraries are stored on device, which takes quite a lot of extra space for the user.
Since Android 6, there has been a setting in AndroidManifest.xml
called extractNativeLibs
. If this is set to true, then the behavior is the same as the previous version. If this is set to false
, the libraries are not extracted, but the libraries must be stored uncompressed and aligned to the page boundary inside the APK (using zipalign
). With this setting set to false
, the APK will be larger, but when installed, it will not take extra space for the extracted libraries.
Because the libraries are not compressed and are in a page-aligned position, Android can just mmap
the libraries to memory. In Android Gradle Plugin since 3.6.0 extractNativeLibs defaults to false (February 2020), this is the setting if we are dealing with recent apps.
We can see where the APK files of an Android application are installed using: adb shell pm path com.example.package
. If we have a split APK, then the native libraries are stored in a separate APK, otherwise everything will be in one APK (base.apk).
The APK signature is checked during installation, but it is only checked again during boot. This makes sense: binary APKs can be really big (hundreds of megabytes), and reverifying this on every app startup will take time. Unlike iOS, which signs every executable binary (it also encrypts the binary if it is installed from the app store), Android doesn’t have something like that.
If extractNativeLibs
is set to true
, we can just overwrite the extracted .so files with our new files, and we are done. If extractNativeLibs
is set to false
, we can still put the library in directory which would have been used if extractNativeLibs
is set to true.
For example, if the APK path is:
/data/app/~~xa3ANgaSg-DH4SuFIlqKLg==/com.tinyhack.testnative-FFtQq51Ol3Dmg2qvpJAYRg==/base.apk
Assuming we are using 64 bit Android, if we put our patched library in (we remove base.apk
and replace it with lib/arm64
:
/data/app/~~xa3ANgaSg-DH4SuFIlqKLg==/com.tinyhack.testnative-FFtQq51Ol3Dmg2qvpJAYRg==/lib/arm64
Then, this library will be loaded instead of the library with the same name inside the APK.
To prove this, I made a small app. This is the output when it was installed the first time.
The JNI Code is very simple:
#include <jni.h>
#include <string>
#include <dlfcn.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_tinyhack_nativelib_NativeLib_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_tinyhack_nativelib_NativeLib_libLocation(
JNIEnv* env,
jobject /* this */) {
//use dladdr to find the current library location
Dl_info info;
if (dladdr((void *)Java_com_tinyhack_nativelib_NativeLib_libLocation, &info)) {
return env->NewStringUTF(info.dli_fname);
}
std::string hello = "Can't find library location";
return env->NewStringUTF(hello.c_str());
}
To add the library:
adb push libnativelib.so /data/local/tmp
Then we copy it to the real destination (found using pm path
):
adb shell su -c cp /data/local/tmp/libnativelib.so /data/app/~~h8ArfmhA33K6xLYS0-KLSQ==/com.tinyhack.testnative-FKHrlxPDhIqH_YyxoglHzw==/lib/arm64
And this is the output after I put in the patched native lib. Two things are different: I changed the message from “C++” to “CXX”, and the path is now different (it doesn’t list base.apk in the path).
This change will survive accross reboots, since it doesn’t touch the APK.
For anyone reading this in the future, this is valid as of Android 13
Patching files inside the APK
Another thing that we can do is to patch a library directly inside the APK. We can then overwrite the installed APK and the app will run fine, but after a reboot, the app will be uninstalled since the signature is invalid.
Overwriting the library must be done at the same offset. So we can’t just re-zip the files (the offsets will change), use a hex editor, or write a code that will patch the APK at a certain offset.
Firt we push the APK to a writeable directory:
adb push .\a.apk /data/local/tmp/
Then copy it to the target as shown by pm path com.example.packagename
adb shell su -c cp /data/local/tmp/a.apk /data/app/~~xa3ANgaSg-DH4SuFIlqKLg==/com.tinyhack.testnative-FFtQq51Ol3Dmg2qvpJAYRg==/base.apk
And we can see the output: the path is still inside the base.apk, but the text has changed
We can also patch any files that are not compressed (the compression method inside the APK is “Stored”), for example: editing resource name or value. In theory the compressed file can also be edited, but if the compressed file size differs, then we need to adjust the headers. I have not found an easy way to manipulate the APK for compressed files.
Writing the patch in C
Now after we know that we *can* alter the code, and it will just run fine, its time to write the patch. Most tutorials I saw use assembly language for patching, but we can code a binary patch using C. In my case, I want to replace a complicated function with static data that I read from a file.
What I do is: I create a C file, annotate the functions and strings with section annotation.
//open
int __attribute__((naked)) __attribute__((noinline)) __attribute__((section(".my_open_section"))) my_open(const char *filename, int flags) {
}
//read
int __attribute__((naked)) __attribute__((noinline)) __attribute__((section(".my_read_section"))) my_read(int fd, void *buf, int count) {
}
The naked
attribute is used to make a zero sized function, and the noinline
will make sure that this function is not inlined
, we want it to be called.
Then write the replacement function
void __attribute__((section(".my_section"))) mycode(char *param1, int param2) {
//code here
}
We need to know the address of all the functions that we need to call, and use a linker script to specify the exact addresses of the functions. We can get this using Ghidra or other disassember/decompiler.
In this example: I want to patch a function located at 0x241278, and I found that open
is located at 0xA37670
and read
is at 0xA37680
. The following is the linker script (linker_script.ld
):
SECTIONS {
.my_section 0x241278 : { *(.my_section) *(.my_section_string) }
.my_open_section 0xA37670 : { *(.my_open_section) }
.my_read_section 0xA37680 : { *(.my_read_section) }
If you do this often, you can automate creating the function stubs by parsing the standard C include files and the output of Ghidra.
For strings: we want it to be in text area, but if space is not enough, we can put it in a new section. If we want it in the text area, we can annotate the string like this (note that the string is read only since it is in the text area):
void __attribute__((section(".my_section"))) mycode(char *param1, int param2) {
static const char filename[]
__attribute__((section(".my_section_string"))) =
"/data/data/com.example.com/files/camera.bin";
int fd = my_open(filename, 0);
if (fd >0) {
my_read(fd, param1, param2);
}
}
Then we can compile with this:
clang -O1 -T linker_script.ld -o output.elf code.c
Then we can extract the code that we are going to patch to dump.bin
:
objcopy --dump-section .my_section=dump.bin output.elf
Some limitations:
- Code size is limited, but we can load another library if necessary using
dlopen
, or we can add new section using LIEF. - It is not easy to use functions that are not imported (we can use syscalls directly, parse the ELF directly in memory, or use hardcoded offsets)
If we want to use global variables, we need to find a memory area in data segment that are unused.
Conclusion
Could the RASP do better to detect this? yes, but it may require more resources:
- It can detect if every loaded library is from .apk (assuming
extractNativeLibs
is alwaysfalse
) - It can hash all the libraries and check it at runtime
In my case, this could have been more difficult if my target binary is obfuscated and the function names are not clearly visible.
Of course, any checks by RASP are possible to be bypassed since they can’t work on the OS level. If possbile, use Google Play Integrity API in your app (or other attestation framework provided by the phone vendor).