在Radare2静态分析apk(1)对Radare静态分析apk进行了简单的介绍。 补充一下: 通过r2 apk://URI可以直接对apk中的dex进行分析。
Android的so文件
在前面的文章中,对so文件进行了基本的介绍。Android的so有点不一样。
Android平台pic(位置无关代码)编译的原因,所有全局变量的引用都是通过got(全局偏移表)完成的,加载器会根据加载基址来修正,并向got填入正确的全局变量的地址。如某重定位数据a=S,app运行时的基址是A,pBuf的地址是B,则重定位a的值为S-A+B,这样便相当于从pBuf处加载so。
通过readelf -d来获取数据重定位的信息。后面会对android的so文件进行专门的分析。
JNIEnv在什么时候创建
开机的时候。
JNIEnv是什么
参考链接: http://androidxref.com/9.0.0_r3/xref/libnativehelper/include_jni/jni.h和https://www.androidos.net.cn/android/10.0.0_r6/xref/libnativehelper/include_jni/jni.h
#if defined(__cplusplus)typedef_JNIEnv JNIEnv; typedef_JavaVM JavaVM; #elsetypedefconststructJNINativeInterface* JNIEnv; typedefconststructJNIInvokeInterface* JavaVM; #endif
•如果用C++编译器,那么JNIEnv就是_JNIEnv•如果用C编译器,那么JNIEnv就是JNINativeInterface
那_JNIEnv又是什么? 通过下面的代码可知*最终还是还是JNINativeInterface**。
struct_JNIEnv { /* do not rename this; it does not seem to be entirely opaque */conststructJNINativeInterface* functions; #if defined(__cplusplus)jint GetVersion() { returnfunctions->GetVersion(this); } jclass DefineClass(const char *name, jobject loader, const jbyte* buf, jsize bufLen) { returnfunctions->DefineClass(this, name, loader, buf, bufLen); } . . . #endif /*__cplusplus*/};
JNINativeInterface又是什么?通过下面的代码可知定义了一系列的函数指针。
structJNINativeInterface { void* reserved0; void* reserved1; void* reserved2; void* reserved3; jint (*GetVersion)(JNIEnv *); jclass (*DefineClass)(JNIEnv*, constchar*, jobject, constjbyte*, jsize); jclass (*FindClass)(JNIEnv*, constchar*); jmethodID (*FromReflectedMethod)(JNIEnv*, jobject); jfieldID (*FromReflectedField)(JNIEnv*, jobject); /* spec doesn't show jboolean parameter */jobject (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean); jclass (*GetSuperclass)(JNIEnv*, jclass); . . . };
创建JNIEnv
参考链接: https://www.androidos.net.cn/android/10.0.0_r6/xref/frameworks/base/cmds/app_process/app_main.cpp
Zygote进程是在Init进程启动的时候创建的。至于Init进程是怎么来的,可参考Android启动流程。
int main(int argc, char* const argv[]) { if(!LOG_NDEBUG) { String8 argv_String; for(inti = 0; i < argc; ++i) { argv_String.append("\""); argv_String.append(argv[i]); argv_String.append("\" "); } ALOGV("app_process main with argv: %s", argv_String.string()); } AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv)); . . . // Parse runtime arguments. Stop at first unrecognized option.boolzygote = false; boolstartSystemServer = false; boolapplication = false; String8 niceName; String8 className; ++i; // Skip unused "parent dir" argument.while(i < argc) { constchar* arg = argv[i++]; if(strcmp(arg, "--zygote") == 0) { zygote = true; niceName = ZYGOTE_NICE_NAME; . . . } . . . if(zygote) { runtime.start("com.android.internal.os.ZygoteInit", args, zygote); } else if (className) { runtime.start("com.android.internal.os.RuntimeInit", args, zygote); } else{ fprintf(stderr, "Error: no class name or --zygote supplied.\n"); app_usage(); LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied."); } }
如果zygote是true,则说明当前运行在Zygote进程。走runtime.start(“com.android.internal.os.ZygoteInit”, args, zygote),start方法的实现如下:
voidAndroidRuntime::start(constchar* className, constVector<String8>& options, boolzygote) { ALOGD(">>>>>> START %s uid %d <<<<<<\n", className != NULL? className : "(unknown)", getuid()); . . . /* start the virtual machine */JniInvocation jni_invocation; jni_invocation.Init(NULL); JNIEnv* env; //启动Java虚拟机 001if(startVm(&mJavaVM, &env, zygote) != 0) { return; } onVmCreated(env); /* * Register android functions. *///为Java虚拟机注册JNI方法if(startReg(env) < 0) { ALOGE("Unable to register all android natives\n"); return; } /* * We want to call main() with a String array with arguments in it. * At present we have two arguments, the class name and an option string. * Create an array to hold them. */jclass stringClass; jobjectArray strArray; jstring classNameStr; stringClass = env->FindClass("java/lang/String"); assert(stringClass != NULL); strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL); assert(strArray != NULL); classNameStr = env->NewStringUTF(className); assert(classNameStr != NULL); env->SetObjectArrayElement(strArray, 0, classNameStr); for(size_ti = 0; i < options.size(); ++i) { jstring optionsStr = env->NewStringUTF(options.itemAt(i).string()); assert(optionsStr != NULL); env->SetObjectArrayElement(strArray, i + 1, optionsStr); } /* * Start VM. This thread becomes the main thread of the VM, and will * not return until the VM exits. */char* slashClassName = toSlashClassName(className != NULL? className : ""); jclass startClass = env->FindClass(slashClassName); if(startClass == NULL) { ALOGE("JavaVM unable to locate class '%s'\n", slashClassName); /* keep going */} else{ //调ZygoteInit的main方法jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V");//002if(startMeth == NULL) { ALOGE("JavaVM unable to find main() in '%s'\n", className); /* keep going */} else{ env->CallStaticVoidMethod(startClass, startMeth, strArray); #if 0if(env->ExceptionCheck()) threadExitUncaughtException(env); #endif} } free(slashClassName); ALOGD("Shutting down VM\n"); if(mJavaVM->DetachCurrentThread() != JNI_OK) ALOGW("Warning: unable to detach main thread\n"); if(mJavaVM->DestroyJavaVM() != 0) ALOGW("Warning: VM did not shut down cleanly\n"); }
在上文的注释002处,进入ygoteInit的main方法。Zygote便进入了Java框架层,也就是说Zygote开创了Java框架层。
在注释001处,调用startVM启动Java虚拟机。
intAndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, boolzygote) { . . . /* * Initialize the VM. * * The JavaVM* is essentially per-process, and the JNIEnv* is per-thread. * If this call succeeds, the VM is ready, and we can start issuing * JNI calls. */if(JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) { ALOGE("JNI_CreateJavaVM failed\n"); return-1; } return0; }
参考链接: https://www.androidos.net.cn/android/10.0.0_r6/xref/libnativehelper/JniInvocation.cpp
jint JniInvocationImpl::JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) { //调用前面从libart.so中找到的创建虚拟机的函数returnJNI_CreateJavaVM_(p_vm, p_env, vm_args); }
NIEnv是在Zygote初始化的时候调用libart.so中的”JNI_CreateJavaVM”方法创建的。JNIEnv来的竟然如此复杂!
JNIEnv基础
1.线程相关 : JNIEnv 是线程相关的, 即 在 每个线程中 都有一个 JNIEnv 指针, 每个JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv。JNIEnv 不能跨线程 :
•当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在 线程之间进行传递, 在同一个线程中, 多次调用 JNI层方法, 传入的 JNIEnv 是相同的;•本地方法匹配多JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此 可以接受不同的 JNIEnv;
1.所有的JNI调用都使用了JNIEnv*类型的指针,习惯上在CPP文件中将这个变量定义为evn,它是任意一个本地方法的第一个参数。env指针指向一个函数指针表,在VC中可以直接用”->”操作符访问其中的函数。2.jobject 指向在此 Java 代码中实例化的 Java 对象 LocalFunction的一个句柄,相当于this指针。后续的参数就是本地调用中有Java程序传进的参数。以下是我们经常会用到的字符串类型处理的函数:
•const char* GetStringUTFChars (jstring string,jboolean* isCopy) 返回指向字符串UTF编码的指针,如果不能创建这个字符数组,返回null。这个指针在调用ReleaseStringUTFChar()函数之前一直有效。 参数: string Java字符串对象 isCopy 如果进行拷贝,指向以JNI_TRUE填充的jboolean,否则指向以JNI_FALSE填充的jboolean。•void ReleaseStringUTFChars(jstring str, const char* chars) 通知虚拟机本地代码不再需要通过chars访问Java字符串。 参数: string Java字符串对象 chars 由GetStringChars返回的指针
•jstring NewStringUTF(const char *utf) 返回一个新的Java字符串并将utf内容拷贝入新串,如果不能创建字符串对象,返回null。通常在反值类型为string型时用到。 参数: utf UTF编码的字符串指针,对于数值型参数,在C/C++中可直接使用
JNIEnv编程
通过ANdroid studio 新建c++项目,需要下载ndk和CMakelist。 核心代码:
MainActivity:
static{ System.loadLibrary("native-lib"); } . . . tv.setText(stringFromJNI());
native-lib.cpp
#include <jni.h>#include <string>extern"C"JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::stringhello = "Hello from C++"; returnenv->NewStringUTF(hello.c_str()); }
CMakeList.txt
add_library( # Sets the name of the library.native-lib # Sets the library as a shared library.SHARED # Provides a relative path to your source file(s).native-lib.cpp )
使用Radare2静态分析
使用pdf看一下,只能看出有字符串”Hello from c++”。至于怎么处理的看不出来。decompiler看一下效果:还是不能直接看出来怎么处理的“hello from c++”。 如果能把int64_t转换为JNIEnv *,同时相关的函数也可以带出来。我查询了一些资料,通过ESIL能够模拟出JNIEnv的地址,不过我没有成功,然后通过tl命令进行链接。类型处理是Radare2做得不太好的地方。不过问题总有解决办法,既然静态分析看不出来什么,那就使用动态分析。动态分析的内容会在后续文章中介绍。
这个使用Frida走一波,看看最终的返回值是什么。
function frida_Java() { Java.perform(function () { vartargetClass = Java.use("com.example.myapplication.MainActivity"); targetClass.stringFromJNI.implementation = function(){ varresult = this.stringFromJNI(); console.log("burning result "+result); returnresult; } vartargetFunc = Module.getExportByName("libnative-lib.so", 'Java_com_example_myapplication_MainActivity_stringFromJNI'); console.log("burning"+targetFunc); Interceptor.attach(targetFunc, { onEnter: function (args) { console.log(args[0]); console.log("burning Env"+JSON.stringify(Java.vm.getEnv())); }, onLeave: function (retval) { console.log("retval "+retval.toString()); } }); }); } setImmediate(frida_Java,0);
通过如下的命令运行,这样才能hook onCreate方法。
frida-U-fcom.example.myapplication-lanswner.js--no-pause
运行结果如下:如果我想改返回值,
targetClass.stringFromJNI.implementation = function(){ varresult = this.stringFromJNI(); console.log("burning result "+result); result = "欢迎关注我的微信公众号:无情剑客"; returnresult; }
写在最后
通过Radare2静态分析so文件,不能看出直观的逻辑,即使使用decompiler也不是非常友好,使用ESIL模拟定位JNIEnv没能成功定位。希望Radare2在类型处理方面加强一些。动态调式在后续文章中会更新。
公众号
更多内容,欢迎关注我的微信公众号:无情剑客。
来源:freebuf.com 2020-11-30 20:32:10 by: 无情剑客Burning
请登录后发表评论
注册