前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Radare2静态分析apk

Radare2静态分析apk

原创
作者头像
无情剑客
修改2020-12-01 16:54:57
9010
修改2020-12-01 16:54:57
举报
文章被收录于专栏:Android逆向

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

代码语言:javascript
复制
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

•如果用C++编译器,那么JNIEnv就是_JNIEnv•如果用C编译器,那么JNIEnv就是JNINativeInterface

那_JNIEnv又是什么? 通过下面的代码可知*最终还是还是JNINativeInterface**

代码语言:javascript
复制
struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); }

    jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
        jsize bufLen)
    { return functions->DefineClass(this, name, loader, buf, bufLen); }

    . . .
#endif /*__cplusplus*/
};

JNINativeInterface又是什么?通过下面的代码可知定义了一系列的函数指针

代码语言:javascript
复制
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;

    jint        (*GetVersion)(JNIEnv *);

    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
                        jsize);
    jclass      (*FindClass)(JNIEnv*, const char*);

    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启动流程。

代码语言:javascript
复制
int main(int argc, char* const argv[])
{
    if (!LOG_NDEBUG) {
      String8 argv_String;
      for (int i = 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.
    bool zygote = false;
    bool startSystemServer = false;
    bool application = false;
    String8 niceName;
    String8 className;

    ++i;  // Skip unused "parent dir" argument.
    while (i < argc) {
        const char* 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方法的实现如下:

代码语言:javascript
复制
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    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虚拟机 001
    if (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_t i = 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");//002
        if (startMeth == NULL) {
            ALOGE("JavaVM unable to find main() in '%s'\n", className);
            /* keep going */
        } else {
            env->CallStaticVoidMethod(startClass, startMeth, strArray);

#if 0
            if (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虚拟机。

代码语言:javascript
复制
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
    . . .
    /*
     * 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;
    }

    return 0;
}

参考链接: https://www.androidos.net.cn/android/10.0.0_r6/xref/libnativehelper/JniInvocation.cpp

代码语言:javascript
复制
jint JniInvocationImpl::JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
  //调用前面从libart.so中找到的创建虚拟机的函数
  return JNI_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:

代码语言:javascript
复制
    static {
        System.loadLibrary("native-lib");
    }
    . . .
    tv.setText(stringFromJNI());

native-lib.cpp

代码语言:javascript
复制
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

CMakeList.txt

代码语言:javascript
复制
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走一波,看看最终的返回值是什么。

代码语言:javascript
复制
function frida_Java() {
    Java.perform(function () {
          var targetClass = Java.use("com.example.myapplication.MainActivity");
          targetClass.stringFromJNI.implementation = function(){
            var result = this.stringFromJNI();
            console.log("burning result "+result);
            return result;
          }

          var targetFunc = 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方法。

代码语言:javascript
复制
frida -U  -f  com.example.myapplication -l answner.js  --no-pause

运行结果如下:

如果我想改返回值,

代码语言:javascript
复制
          targetClass.stringFromJNI.implementation = function(){
            var result = this.stringFromJNI();
            console.log("burning result "+result);
            result = "欢迎关注我的微信公众号:无情剑客";
            return result;
          }

写在最后

通过Radare2静态分析so文件,不能看出直观的逻辑,即使使用decompiler也不是非常友好,使用ESIL模拟定位JNIEnv没能成功定位。希望Radare2在类型处理方面加强一些。动态调式在后续文章中会更新。

公众号

更多内容,欢迎关注我的微信公众号:无情剑客。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Android的so文件
  • JNIEnv在什么时候创建
  • JNIEnv是什么
  • 创建JNIEnv
  • JNIEnv基础
  • JNIEnv编程
  • 使用Radare2静态分析
  • 写在最后
  • 公众号
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档