Tinyhack.com

Patching .so files of an installed Android App

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.

native libraries are stored uncompressed and page aligned

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:

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.

Pristine install

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).

After I put in the patched libnativelib.so

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

Device used for testing

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.

Editing APK using HxD

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

Path is inside base.apk

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:

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:

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).

Exit mobile version