During both mobile security and mobile resiliency assessments, you often end up instrumenting the application to analyze its internals. By using either Frida or a classical debugger, we can get valuable insight into the data flows and also modify some data on the fly to make the application behave the way we want it to.
My favorite type of assessment is an advanced Mobile Resiliency Assessment, where I get to figure out how effective the implemented RASP (Runtime Application Self Protection) solution is and try to bypass it using any technique possible. Having a broad toolset is quite important here, because you will quickly run into tool-specific detections, or you will bump into tool-specific limitations. Being able to tackle a problem in many different ways is essential to success!
In a recent assessment, I tried out a new way to examine a shared library contained within an Android application. The library had an extensive .init_array and also some interaction with the Android runtime. All the functions referenced in the init_array get executed even before main() or JNI_OnLoad and they are typically responsible for initializing the library. When dealing with RASP, they are often used to already perform detection, as this is the earliest that code inside the library can be executed.
Our goal is to convert an Android .so library with an .init_array to a normal binary which we can run without an Android application and manually execute the .init_array functions to we can properly instrument them.
More specifically, we’re going to:
.init_array and JNI.init_array function is encoded in the .so libraryinit_array function to a normal function using LIEFDisclaimer: The techniques described below are just one way of doing things, and depending on your specific scenario, there will most likely be easier solutions. This technique was used in a very specific client engagement with some weird requirements and then generalized for this blogpost. If you don’t need this solution, great! If you do, read on ;).
Let’s start with creating a small test app. We can easily do this by creating a new NDK project in Android studio, which will set everything up. The only thing that we really need to change is native-lib.cpp. The code below has:
__attribute__((constructor)) static void my_module_initialize(void)) which will check if the su binary can be found. This is an extremely basic root-detection example.stringFromJNI function which is called from MainActivity.onCreate. Thius function will return No Win :( by defaultJNI_OnLoad function which dynamically registers the stringFromJNI functionwin() function which returns false (unless you’re really lucky)I also added a few __attribute__((noinline)) properties to make it easier to solve the crackme later on.
#include <jni.h>
#include <string>
#include <android/log.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <unistd.h>
int checkSUBinary();
bool win();
__attribute__((constructor))
static void my_module_initialize(void)
{
srand(time(NULL));
if(checkSUBinary()){
__android_log_print(ANDROID_LOG_INFO, "StaticNative", "su binary found.");
}else{
__android_log_print(ANDROID_LOG_INFO, "StaticNative", "no su binary");
}
}
int __attribute__((noinline)) checkSUBinary() {
// Common paths for 'su' binary
const char* suPaths[] = {
"/system/bin/su",
"/system/xbin/su",
"/sbin/su",
"/su/bin/su"
};
for (int i = 0; i < sizeof(suPaths)/sizeof(suPaths[0]); i++) {
if (access(suPaths[i], F_OK) != -1) {
return 1; // 'su' binary exists
}
}
return 0; // 'su' binary not found
}
// Implementation of the native method
extern "C"
JNIEXPORT jstring JNICALL stringFromJNI(JNIEnv *env, jclass clazz) {
if (win()) {
return env->NewStringUTF("WIN! :)");
} else {
return env->NewStringUTF("No Win :(");
}
}
static JNINativeMethod method_table[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI}
};
static int registerNatives(JNIEnv *env) {
jclass clazz = env->FindClass("eu/nviso/nativestaticinit/MainActivity");
if (clazz == nullptr) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, method_table, sizeof(method_table) / sizeof(method_table[0])) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
__android_log_print(ANDROID_LOG_INFO, "StaticNative", "JNI_OnLoad called");
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
if (!registerNatives(env)) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
bool __attribute__((noinline)) win() {
return rand() == 0x42;
}Build the app (or download it from here) and extract it using apktool to get the native libraries.
Let’s take a look at the library using readelf using a version of readelf and objdump that understands Android ARM64 ELF files:
$ alias readelf=/home/jeroen/Android/Sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-readelf
$ alias objdump=/home/jeroen/Android/Sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-objdump
$ readelf -all ./base/lib/arm64-v8a/libnativestaticinit.so | grep INIT_ARRAY -C 10
[12] .eh_frame PROGBITS 0000000000000880 00000880
0000000000000154 0000000000000000 A 0 0 8
[13] .text PROGBITS 00000000000009d4 000009d4
0000000000000260 0000000000000000 AX 0 0 4
[14] .plt PROGBITS 0000000000000c40 00000c40
00000000000000d0 0000000000000000 AX 0 0 16
[15] .data.rel.ro PROGBITS 0000000000001d10 00000d10
0000000000000008 0000000000000000 WA 0 0 8
[16] .fini_array FINI_ARRAY 0000000000001d18 00000d18
0000000000000010 0000000000000000 WA 0 0 8
[17] .init_array INIT_ARRAY 0000000000001d28 00000d28
0000000000000008 0000000000000000 WA 0 0 8
[18] .dynamic DYNAMIC 0000000000001d30 00000d30
00000000000001d0 0000000000000010 WA 7 0 8
[19] .got.plt PROGBITS 0000000000001f00 00000f00
0000000000000070 0000000000000000 WA 0 0 8
[20] .data PROGBITS 0000000000002f70 00000f70
0000000000000018 0000000000000000 WA 0 0 8
[21] .comment PROGBITS 0000000000000000 00000f88
00000000000000b1 0000000000000001 MS 0 0 1
[22] .shstrtab STRTAB 0000000000000000 00001039
--
0x000000006ffffff9 (RELACOUNT) 6
0x0000000000000017 (JMPREL) 0x648
0x0000000000000002 (PLTRELSZ) 264 (bytes)
0x0000000000000003 (PLTGOT) 0x1f00
0x0000000000000014 (PLTREL) RELA
0x0000000000000006 (SYMTAB) 0x2f8
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000005 (STRTAB) 0x4b4
0x000000000000000a (STRSZ) 236 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x488
0x0000000000000019 (INIT_ARRAY) 0x1d28
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x1d18
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
0x000000006ffffff0 (VERSYM) 0x448
0x000000006ffffffe (VERNEED) 0x464
0x000000006fffffff (VERNEEDNUM) 1
0x0000000000000000 (NULL) 0x0
Relocation section '.rela.dyn' at offset 0x5a0 contains 7 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000001d10 000000000403 R_AARCH64_RELATIV 1d10
The library has an . of size 8, which can be found both in the section headers (init_array.), and in the dynamic section (init_arrayINIT_ARRAY, INIT_ARRAYSZ). The goal is to remove the initialization function from .init_array and convert it into a normal function by adding a symbol for it. This might not always work, but I haven’t encountered a situation where it doesn’t.
Both the INIT_ARRAY in the dynamic section and the . section point to init_array0x1d28. At that location, we can find the array containing all the pointers to the constructor functions:
$ objdump -j .init_array -s ./base/lib/arm64-v8a/libnativestaticinit.so ./base/lib/arm64-v8a/libnativestaticinit.so: file format elf64-littleaarch64 Contents of section .init_array: 1d28 00000000 00000000 ........
Unfortunately, the .init_array section is empty due to the usage of dynamic relocations (.rela.dyn):
$ readelf --relocs ./base/lib/arm64-v8a/libnativestaticinit.so Relocation section '.rela.dyn' at offset 0x5a0 contains 7 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000001d10 000000000403 R_AARCH64_RELATIV 1d10 000000001d18 000000000403 R_AARCH64_RELATIV 9ec 000000001d20 000000000403 R_AARCH64_RELATIV 9d4 000000001d28 000000000403 R_AARCH64_RELATIV a34 000000002f70 000000000403 R_AARCH64_RELATIV 80e 000000002f78 000000000403 R_AARCH64_RELATIV 793 000000002f80 000b00000101 R_AARCH64_ABS64 0000000000000af4 stringFromJNI + 0 Relocation section '.rela.plt' at offset 0x648 contains 11 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000001f18 000100000402 R_AARCH64_JUMP_SL 0000000000000000 __cxa_finalize@LIBC + 0 000000001f20 000200000402 R_AARCH64_JUMP_SL 0000000000000000 __cxa_atexit@LIBC + 0 000000001f28 000300000402 R_AARCH64_JUMP_SL 0000000000000000 __register_atfork@LIBC + 0 000000001f30 000400000402 R_AARCH64_JUMP_SL 0000000000000000 time@LIBC + 0 000000001f38 000500000402 R_AARCH64_JUMP_SL 0000000000000000 srand@LIBC + 0 000000001f40 000a00000402 R_AARCH64_JUMP_SL 0000000000000a78 _Z13checkSUBinaryv + 0 000000001f48 000600000402 R_AARCH64_JUMP_SL 0000000000000000 __android_log_print + 0 000000001f50 000700000402 R_AARCH64_JUMP_SL 0000000000000000 access@LIBC + 0 000000001f58 000c00000402 R_AARCH64_JUMP_SL 0000000000000b3c _Z3winv + 0 000000001f60 000800000402 R_AARCH64_JUMP_SL 0000000000000000 rand@LIBC + 0 000000001f68 000900000402 R_AARCH64_JUMP_SL 0000000000000000 __stack_chk_fail@LIBC + 0
The fourth entry of .rela.dyn says that 0xa34 should be stored at 0x1d28 which is the start of our .. This means that our target function is located at offset init_array0xa34:
$ objdump -D ./base/lib/arm64-v8a/libnativestaticinit.so --start-address=0xa34 | head -n 30 ./base/lib/arm64-v8a/libnativestaticinit.so: file format elf64-littleaarch64 Disassembly of section .text: 0000000000000a34 <_Z13checkSUBinaryv@@Base-0x44>: a34: a9bf7bfd stp x29, x30, [sp,#-16]! a38: 910003fd mov x29, sp a3c: aa1f03e0 mov x0, xzr a40: 94000094 bl c90 <time@plt> a44: 94000097 bl ca0 <srand@plt> a48: 9400009a bl cb0 <_Z13checkSUBinaryv@plt> a4c: 90000008 adrp x8, 0 <_Z13checkSUBinaryv@@Base-0xa78> a50: 90000009 adrp x9, 0 <_Z13checkSUBinaryv@@Base-0xa78> a54: 911e0908 add x8, x8, #0x782 a58: 911ec529 add x9, x9, #0x7b1 a5c: 7100001f cmp w0, #0x0 a60: 90000001 adrp x1, 0 <_Z13checkSUBinaryv@@Base-0xa78> a64: 9a880122 csel x2, x9, x8, eq a68: 911f5c21 add x1, x1, #0x7d7 a6c: 52800080 mov w0, #0x4 // #4 a70: a8c17bfd ldp x29, x30, [sp],#16 a74: 14000093 b cc0 <__android_log_print@plt> 0000000000000a78 <_Z13checkSUBinaryv@@Base>: a78: a9bf7bfd stp x29, x30, [sp,#-16]! a7c: 910003fd mov x29, sp a80: 90000000 adrp x0, 0 <_Z13checkSUBinaryv@@Base-0xa78> a84: 2a1f03e1 mov w1, wzr
Objdump assigns the label _Z13checkSUBinaryv@@Base-0x44 to this function, which is a bit weird. The likely reason is that there’s no symbol for this function and objdump simply takes the next symbol (_Z13checkSUBinaryv@@Base at 0xa78) and gives this one a negative offset from that address. For clarity sake, let’s call this function INIT0.
To convert INIT0 to a normal function, we need to do two things:
INIT0 from .init_arrayINIT0. This is optional, but convenient.To perform these steps, we’ll use the awesome LIEF framework from Romain Thomas.
INIT0 function from .init_arrayI tried many different things to remove INIT0 from .init_array, but most of them don’t work:
.init_array section to 0. This is apparently overwritten by the dynamic relocation during linking..init_array to be empty. This could work on other binaries, but our .init_array section already is empty in this caseINIT_ARRAYSZ to 0. This doesn’t seem to work in LIEF.INIT_ARRAY to 0. This caused the linker to fail. Probably due to the fact that INIT_ARRAYSZ was still 0x8..init_array section. This caused the linker to fail (understandably so).INIT_ARRAYSZ and INIT_ARRAY from the dynamic section. Bingo.Even though the . section still exists, and the dynamic relocation still exists, this last approach seems to work. We patch the library with the following python script:init_array
import lief
target = "libnativestaticinit.so"
# Load the binary
binary = lief.parse(target)
binary.remove(binary[lief.ELF.DYNAMIC_TAGS.INIT_ARRAYSZ])
binary.remove(binary[lief.ELF.DYNAMIC_TAGS.INIT_ARRAY])
binary.write("libnativestaticinit.so.patched")And confirm with readelf:
$ readelf -d libnativestaticinit.so.patched Dynamic section at offset 0xb90 contains 27 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [libandroid.so] 0x0000000000000001 (NEEDED) Shared library: [liblog.so] 0x0000000000000001 (NEEDED) Shared library: [libm.so] 0x0000000000000001 (NEEDED) Shared library: [libdl.so] 0x0000000000000001 (NEEDED) Shared library: [libc.so] 0x000000000000000e (SONAME) Library soname: [libnativestaticinit.so] 0x000000000000001e (FLAGS) BIND_NOW 0x000000006ffffffb (FLAGS_1) Flags: NOW 0x0000000000000007 (RELA) 0x570 0x0000000000000008 (RELASZ) 96 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000006ffffff9 (RELACOUNT) 4 0x0000000000000017 (JMPREL) 0x5d0 0x0000000000000002 (PLTRELSZ) 240 (bytes) 0x0000000000000003 (PLTGOT) 0x1d60 0x0000000000000014 (PLTREL) RELA 0x0000000000000006 (SYMTAB) 0x2c0 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000005 (STRTAB) 0x464 0x000000000000000a (STRSZ) 263 (bytes) 0x000000006ffffef5 (GNU_HASH) 0x438 0x000000000000001a (FINI_ARRAY) 0x1b78 0x000000000000001c (FINI_ARRAYSZ) 16 (bytes) 0x000000006ffffff0 (VERSYM) 0x3f8 0x000000006ffffffe (VERNEED) 0x414 0x000000006fffffff (VERNEEDNUM) 1 0x0000000000000000 (NULL) 0x0
Note: Other solutions may work on other platforms, or even other versions of Android. This approach was taken after some trial-and-error.
INIT0We could hardcode the location of INIT0 (0x8e4), but let’s make it a bit more dynamic and calculate the location of INIT0 based on the dynamic relocation. After we find the address, we can create a new symbol and set it to the identified address:
import lief
target = "libnativestaticinit.so"
# Load the binary
binary = lief.parse(target)
# Locate the relocation entry for the .init_array section
init_array = binary.get_section('.init_array')
init_array_address = init_array.virtual_address
init_array_size = init_array.size
dynamic_relocations = binary.dynamic_relocations
relocation_target = 0
for relocation in dynamic_relocations:
if relocation.address >= init_array_address and relocation.address < (init_array_address + init_array_size):
print(f"Found relocation for .init_array: {relocation}")
print(f"Original relocation address: {hex(relocation.address)}")
print(f"Function at {hex(relocation.addend)}")
relocation_target = relocation.addend
binary.remove(binary[lief.ELF.DYNAMIC_TAGS.INIT_ARRAYSZ])
binary.remove(binary[lief.ELF.DYNAMIC_TAGS.INIT_ARRAY])
# Create a new symbol
new_symbol = lief.ELF.Symbol()
new_symbol.name = "INIT0"
new_symbol.value = relocation_target
new_symbol.size = 0 # 0 for function
new_symbol.binding = lief.ELF.SYMBOL_BINDINGS.GLOBAL
new_symbol.type = lief.ELF.SYMBOL_TYPES.FUNC
new_symbol.visibility = lief.ELF.SYMBOL_VISIBILITY.DEFAULT
# Set the correct shndx value
text_section = binary.get_section(".text")
if text_section:
# Find the index of the .text section
text_section_index = -1
for idx, section in enumerate(binary.sections):
if section == text_section:
text_section_index = idx
break
if text_section_index != -1:
new_symbol.shndx = text_section_index
else:
print("Could not determine the index of the .text section")
else:
print("Could not find .text section")
# Add the new symbol to the symbol table
binary.add_dynamic_symbol(new_symbol)
# Save the modified ELF file
binary.write("libnativestaticinit.so.patched")$ python3 removeinit.py Found relocation for .init_array: 1b88 RELATIVE 64 954 0 DYNAMIC Original relocation address: 0x1b88 Function at 0x954
Verify that our new symbol has been created:
$ readelf -s libnativestaticinit.so.patched
Symbol table '.dynsym' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __register_atfork@LIBC (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND time@LIBC (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND srand@LIBC (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __android_log_print
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND access@LIBC (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND rand@LIBC (2)
9: 0000000000001998 124 FUNC GLOBAL DEFAULT 13 _Z13checkSUBinaryv
10: 0000000000001a14 72 FUNC GLOBAL DEFAULT 13 Java_eu_nviso_nativestati
11: 0000000000001a5c 28 FUNC GLOBAL DEFAULT 13 _Z3winv
12: 0000000000001a78 48 FUNC GLOBAL DEFAULT 13 JNI_OnLoad
13: 0000000000001954 0 FUNC GLOBAL DEFAULT 13 INIT0
Great! But there’s a problem… If we now use this library, it might crash since the JNI_OnLoad function will automatically be called by the JNI runtime. If the original INIT function initializes something that is used by JNI_OnLoad, the JNI_OnLoad function will (most likely) crash.
There’s an easy solution though; let’s just rename JNI_OnLoad to JNI_OnLoad0 so that it doesn’t automatically get called. Later, we can call JNI_OnLoad0 ourselves to make sure everything is properly initialized. This is very easy to do with LIEF with the following small addition:
JNI_OnLoad = binary.get_symbol("JNI_OnLoad")
JNI_OnLoad.name = "JNI_OnLoad0"A quick validation:
$ readelf -s libnativestaticinit.so.patched
Symbol table '.dynsym' contains 15 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __register_atfork@LIBC (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND time@LIBC (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND srand@LIBC (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __android_log_print
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND access@LIBC (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND rand@LIBC (2)
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@LIBC (2)
10: 0000000000001a78 124 FUNC GLOBAL DEFAULT 13 _Z13checkSUBinaryv
11: 0000000000001af4 72 FUNC GLOBAL DEFAULT 13 stringFromJNI
12: 0000000000001b3c 28 FUNC GLOBAL DEFAULT 13 _Z3winv
13: 0000000000001b58 220 FUNC GLOBAL DEFAULT 13 JNI_OnLoad0
14: 0000000000001a34 0 FUNC GLOBAL DEFAULT 13 INIT0
Perfect! Next, we need to create a wrapper/harness to load and use our library.
In order to fully interact with the binary, we want to make sure it has access to an Android ART environment. Luckily a possible approach was documented by Ch0pin and he even published the very useful JNIInvocation project. Be sure to read the original article, as I’m just going to quickly summarize the steps.
We need to:
Caller/CMakeLists.txt to link to libnativestaticinit.soCaller/build.sh to reference the correct toolchain locationlibnativestaticinit.so.patched to Caller/lib/libnativestaticinit.soCaller/caller.c for our use case# Caller/CMakeLists.txt
project(caller)
cmake_minimum_required(VERSION 3.8)
include_directories(AFTER ${CMAKE_SOURCE_DIR}/include)
link_directories(${CMAKE_SOURCE_DIR}/lib)
find_library(log-lib log REQUIRED)
# Native Library
# ==============
add_executable(caller "caller.c")
add_library(jenv SHARED "jnihelper.c")
target_link_libraries(caller jenv nativestaticinit)
# Caller/build.sh
rm -r build/
mkdir build && cd build
cmake -DANDROID_PLATFORM=31
-DCMAKE_TOOLCHAIN_FILE=$HOME/Android/Sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake
-DANDROID_ABI=arm64-v8a ..
makeIn caller.c, we want to do three things:
INIT0 to initialize the libraryJNI_OnLoad0 to initialize JNIMainActivity.stringFromJNIThe codes uses typical JNI features to interact with Android classes and methods and is overall pretty self-explanatory:
#include <stdlib.h>
#include <android/bitmap.h>
#include <jni.h>
#include "jenv.h"
JavaCTX ctx;
void INIT0();
void JNI_OnLoad0(JavaVM* vm);
int callJNI(){
jclass MainActivity = (*ctx.env)->FindClass(ctx.env, "eu/nviso/nativestaticinit/MainActivity");
if(MainActivity == NULL){
printf("Can't find MainActivity");
return -1;
}
jmethodID stringFromJNI = (*ctx.env)->GetStaticMethodID(ctx.env, MainActivity, "stringFromJNI", "()Ljava/lang/String;");
if(stringFromJNI == NULL){
printf("Can't find stringFromJNI");
return -1;
}
jstring nativeString = (*ctx.env)->CallStaticObjectMethod(ctx.env,MainActivity,stringFromJNI);
if(nativeString==NULL) {
printf("Can't create nativeString objectn");
return -1;
}
const char *descr = (*ctx.env)->GetStringUTFChars(ctx.env, nativeString, NULL);
if(descr!=NULL)
printf("Native string: %sn",descr);
return 0;
return -1;
}
int main(int argc, char **argv)
{
int status;
char *jvmoptions = "-Djava.class.path=/data/local/tmp/base.apk";
if((status = initialize_java_environment(&ctx,&jvmoptions,1)) != 0)
return status;
printf("[+] Library loadedn");
INIT0();
JNI_OnLoad0(ctx.vm);
callJNI();
printf("[+] Cleaning upn");
if(cleanup_java_env(&ctx)!=0)
return -1;
return 0;
}Run build.sh and push all the required files to the device:
./build.sh
adb push build/caller /data/local/tmp/
adb push build/lib/libnativestaticinit.so /data/local/tmp/
adb push base.apk /data/local/tmp/base.apk
adb push build/libjenv.so /data/local/tmp/Finally, we can run our binary. We do need to specify LD_LIBRARY_PATH to make sure the linker can find libjenv.so and libnativestaticinit.so:
lynx:/data/local/tmp $ LD_LIBRARY_PATH=./ ./caller
[+] Starting initialization
[+] Initialization completed successfully.
[+]Java VM pointer: 0xb4000074f940b310
[+]Java env pointer: 0xb4000074d940f9b0
[+] Library loaded
Native string: No Win :(
[+] Cleaning up
[+] Cleanup Java environment
Additionally, a message is printed to logcat:
$ adb logcat | grep StaticNative 05-17 12:07:46.366 14515 14515 I StaticNative: su binary found.
Great! Now let’s solve this little crackme to get the WIN message. We’ll do this with Frida as a quick solution.
The solution to the crackme is to hook the win() function to return true and the checkSUBinary() function to return false.
Since both functions are exported, we can easily hook them using DebugSymbol.getFunctionByName and Interceptor.attach:
let win = DebugSymbol.getFunctionByName("_Z3winv")
Interceptor.attach(win, {
onLeave(retval){
retval.replace(ptr(0x1));
}
})
let checkSUBinary = DebugSymbol.getFunctionByName("_Z13checkSUBinaryv")
Interceptor.attach(checkSUBinary, {
onLeave(retval){
retval.replace(ptr(0));
}
})Since the code is no longer running in an Android app, we can’t use frida on the host to launch the application. However, it is possible to run frida-inject directly on the device to inject Frida into a normal process. Frida-inject can be downloaded from the releases page of Frida.
LD_LIBRARY_PATH=/data/local/tmp/ ./frida-inject -f /data/local/tmp/caller -s hook.js
[+] Starting initialization
[+] Initialization completed successfully.
[+]Java VM pointer: 0xb400007596bfb4d0
[+]Java env pointer: 0xb400007576bf9eb0
[+] Library loaded
Native string: WIN! :)
[+] Cleaning up
[+] Cleanup Java environment
Process terminated
And the logcat output:
$ adb logcat | grep StaticNative 05-17 13:15:12.358 21069 21069 I StaticNative: no su binary

While the demo app in this post is very basic, this technique can really be beneficial when dealing with applications that implement some form of Runtime Application Self protection (RASP).
There’s no guarantee that it will work for every library, but you should never be afraid to try different approaches to find something that gets the job done :).

Jeroen Beckers is a mobile security expert working in the NVISO Software Security Assessment team. He travels around the world teaching SANS SEC575: iOS and Android Application Security Analysis and Penetration Testing and is a one of the authors of the OWASP Mobile Application Security (MAS) project, which includes: