dex注入实现详解

最近在研究Android绿色安全这一块,具体到上层的业务就是“去第三方APP的广告”。如果既想使用第三方APP,又不想看到一些无良的广告,那dex注入基本无法避免。本文针对网上一些大牛分享的文章,进行了一些简单的实现,总结和分享自是不能少的。Ps:感谢金山毒霸实现了该功能,感谢大牛们破解之后的无私分享。

参考文章

金山手机毒霸工作原理【引用1】

【原创】手机毒霸去广告功能分析一:总体分析【引用2】

【原创】手机毒霸去广告功能分析三:java代码(dex)注入

Android中的so注入(inject)和挂钩(hook) - For both x86 and arm 【引用3】

源码相关

android.os.Handler 自行eclipse 关联即可;

android.app.ActivityThread 在线代码

实现目标

系统:Android 4.2.2 平板
功能:将一段dex代码注入到HelloWord APP中,dex对应的java代码要求能够拦截目标APP中的onPause与onResume 回调,输出打印。

基本原理

其实原理在各路大牛的文章里面已经解释的很清楚了,这里再不厌其烦的絮叨絮叨,主要是捋一捋思路,别整乱喽。

1.获得root权限后,通过ptrace()注入到指定pid的进程中;

Android下的注入都是从Linux下ptrace()函数继承下来的,具体原理不便深入。网上大牛已有相关的开源工具,这里采用【引用3】篇幅中博主开源出的代码,注意修改对应参数即可。【引用3】中是以注入系统进程/system/bin/surfaceflinger为例的,我们这里需要修改成目标APP的包名。这部分拿到开源代码之后使用ndk编译生成注入工具文件inject;

2.注入代码调用功能库.so中的接口,它的作用是利用反射注入dex文件、并调用相应的java代码;

这里对应的就是【引用3】中hello.c部分了,作为注入的功能代码关键部分,这部分不能打印两句草草了事。这里采用【引用1】中对金山毒霸分析结果得出的代码拿出来来实现dex注入与java层代码调用,具体实现与分析见后文。这里也是C代码通过ndk编译生成注入功能库,呃,libhelloTool.so;

3.生成dex的java源码通过反射置换 ActivityThread 中mH属性中的的mCallback回调,来实现拦截Activity生命周期回调的HOOK功能;

通过上一步程序会执行到java层中,既然要下钩子,就要弄清楚我们要钩在哪里才有效。很明显的,既然要拦截界面的onPause、onResume消息,那就必须要了解Activity的生命周期回调在底层是如何实现消息分发的,知其所以然之后才好下手。有了前面提到的几篇大牛博客的文章,我们可以很清晰的定位到android.app.ActivityThread 类中的mH变量:

public final class ActivityThread {
…
final H mH = new H();
…
private void queueOrSendMessage(int what, Object obj, int arg1, int arg2) {
    synchronized (this) {
        if (DEBUG_MESSAGES) Slog.v(
            TAG, "SCHEDULE " + what + " " + mH.codeToString(what)
            + ": " + arg1 + " / " + obj);
        Message msg = Message.obtain();
        msg.what = what;
        msg.obj = obj;
        msg.arg1 = arg1;
        msg.arg2 = arg2;
        mH.sendMessage(msg);
    }
}
…
private class H extends Handler {
    public static final int LAUNCH_ACTIVITY         = 100;
    public static final int PAUSE_ACTIVITY          = 101;
    public static final int PAUSE_ACTIVITY_FINISHING= 102;
    public static final int STOP_ACTIVITY_SHOW      = 103;
    public static final int STOP_ACTIVITY_HIDE      = 104;
    public static final int SHOW_WINDOW             = 105;
    public static final int HIDE_WINDOW             = 106;
    public static final int RESUME_ACTIVITY         = 107;
    public static final int SEND_RESULT             = 108;
               …
               public void handleMessage(Message msg) {
        if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
        switch (msg.what) {
            case LAUNCH_ACTIVITY: {
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                ActivityClientRecord r = (ActivityClientRecord)msg.obj;



                r.packageInfo = getPackageInfoNoCheck(
                        r.activityInfo.applicationInfo, r.compatInfo);
                handleLaunchActivity(r, null);
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            } break;
                                 …
        }
     }
    …
}

从源码上就能看出来,底层消息派发都在内部类H中实现,而H实际上是Handler的子类。对应的H本身是个final类型的内部私有类,做手脚不甚方便,考虑到要拦截的实际情况,伸手到其父类中的属性的mCallback回调就是个很好的选择了。从前面所引博客中对金山毒霸的反编译情况来看即是这个思路。引用【引用2】中的一句话即:

f) 替换当前ActivityThread中的mH(Handler类型)的mCallback,用金山自定义的一个callback对象来包裹过原callback并且替换原callback,从而起到hook作用。

实现流程

必备工具

  1. root工具,想什么办法把测试机器root掉,或者直接用虚拟机;
  2. NDK工具,各路注入工具都需要ndk来编译,本人使用的版本是:android-ndk-r8b;
  3. 基础Android 开发环境,具体就不用多说了~

注意:为了快速调通,这里所有参数都是写死的,这就限制了后续验证流程必须要实现过程中的代码编写的一致。如果只是作为预研、测试是否可行的阶段,这种做法无可厚非;相对的如果是正规的开发流程中,在迭代周期里面做好通用性的设计是必要的。

开始实现

建立目标APP
这一步最简单了,新建一个HelloWord Android工程,运行安装到测试机器中,我这里设置了包名为:com.inject.helloword 后面注入工具中需要用到;

注入工具

如【引用3】的方法,简历文件夹填好配置文件。编辑injec.c文件更换参数,主要在main函数中:

int main(int argc, char** argv) {
    pid_t target_pid;
     //更换为目标应用的包名
    target_pid = find_pid_of("com.inject.helloword");
    if (-1 == target_pid) {
        printf("Can't find the process\n");
        return -1;
    }
     //设置注入代码库位置、调用接口与参数
    inject_remote_process(target_pid, "/data/libhelloTool.so", "hook_entry",  "I'm parameter!hehe", strlen("I'm parameter! hehe "));
    return 0;
}

如果想深入研究注入的原理,可以仔细分析相关函数的实现即可,NDK编译后生成注入工具inject文件;

kf2lc@kf2lc-OptiPlex-3020:~/develop/inject/jni$ ndk-build

Compile x86 : inject <= jni inject.c

Executable : inject

Install : inject =libs/x86/inject

Compile thumb : inject <= jni inject.c

Executable : inject

Install : inject =libs/armeabi-v7a/inject

注入代码库

建立文件夹如【引用3】所述,修改注入接口方法hook_entry如【引用1】中的分析实现,代码并注释如下:

#include <unistd.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <android/log.h> 
#include <elf.h> 
#include <fcntl.h> 
#include <jni.h>
#include <dlfcn.h>

#define LOG_TAG "DEBUG" 
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args)   

int invoke_dex_method(const char* dexPath, const char* dexOptDir, const char* className, const char* methodName, int argc, char *argv[]);

int hook_entry(char * a){ 
    LOGD("Hook success, pid = %d\n", getpid()); 
    LOGD("Hello %s\n", a); 
    //参数直接写死是个取巧的方法
    int ret = invoke_dex_method("/data/injects/DexInject.apk","/data/data/com.inject.helloword/cache","com/inject/dexinject/HookTool","dexInject",0,NULL);
    LOGD("Hello %d\n",ret);
    return 0; 
}

JNIEnv* (*getJNIEnv)();
/**
* PARAM:
* dexPath要注入的apk/jar路径
* dexOptDir  缓存路径,注意需要目标应用进程中可写的目录
* className  执行方法所在类名
* methodName 执行的方法名
* argc   参数之流这里没有使用
* argv  参数之流这里没有使用
*/
int invoke_dex_method(const char* dexPath, const char* dexOptDir, const char* className, const char* methodName, int argc, char *argv[]) {
    //获取JNIEnv
    void* handle = dlopen("/system/lib/libandroid_runtime.so", RTLD_NOW);
    getJNIEnv = dlsym(handle, "_ZN7android14AndroidRuntime9getJNIEnvEv");
    JNIEnv* env = getJNIEnv();

    //调用ClassLoader中的getSystemClassLoader方法获取当前进程的ClassLoader
    jclass classloaderClass = (*env)->FindClass(env,"java/lang/ClassLoader");
    jmethodID getsysloaderMethod = (*env)->GetStaticMethodID(env,classloaderClass, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
    jobject loader = (*env)->CallStaticObjectMethod(env, classloaderClass, getsysloaderMethod);

    //以进程现有的ClassLoader、要注入的dex路径为参数构造注入后的DexClassLoader
    jstring dexpath = (*env)->NewStringUTF(env, dexPath);
    jstring dex_odex_path = (*env)->NewStringUTF(env,dexOptDir);
    jclass dexLoaderClass = (*env)->FindClass(env,"dalvik/system/DexClassLoader");
    jmethodID initDexLoaderMethod = (*env)->GetMethodID(env, dexLoaderClass, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V");
    jobject dexLoader = (*env)->NewObject(env, dexLoaderClass, initDexLoaderMethod,dexpath,dex_odex_path,NULL,loader);

    //获取新出炉的DexClassLoader中findClass方法加载dex中要执行代码所在类
    jmethodID findclassMethod = (*env)->GetMethodID(env,dexLoaderClass,"findClass","(Ljava/lang/String;)Ljava/lang/Class;");
    jstring javaClassName = (*env)->NewStringUTF(env,className);
    jclass javaClientClass = (*env)->CallObjectMethod(env,dexLoader,findclassMethod,javaClassName);

    //获取注入dex中要执行的方法
    jmethodID start_inject_method = (*env)->GetStaticMethodID(env, javaClientClass, methodName, "()V");
    //执行之注意目标方法必须是静态公有的
    (*env)->CallStaticVoidMethod(env,javaClientClass,start_inject_method);
}

原帖中倒数第二句GetStaticMethodID方法所给的参数有误,这里修正一下,如此就完成了dex注入并调用了java代码中的:com.inject.dexinject. HookTool. dexInject() 方法。同上采用NDK编译,生成注入库:libhelloTool.so。

生成dex

说是dex注入,实际上从上一段的代码中可以知道最终采用的是DexClassLoader类来实现注入。托之前研究过一段APK加壳的福,对这里还相对比较了解,DexClassLoader的主要参数路径实际上应该是一个apk/jar的路径,具体可见相关的SDK文档。这里直接编一个APK丢进去就好。
建立Android应用工程DexInject,干掉无关的界面配置。建立如下类:

1.自定义Callback,加入拦截操作代码(打印);

public class HookCallback implements Callback{
     public static final int RESUME_ACTIVITY         = 107;
     public static final int PAUSE_ACTIVITY          = 101;

     private Callback mParentCallback;
     public HookCallback(Callback parentCallback){
               mParentCallback = parentCallback;
     }


@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case RESUME_ACTIVITY:
Log.d(HookTool.TAG, “hook activity resume!!!”);
break;

               case PAUSE_ACTIVITY:
                        Log.d(HookTool.TAG, "hook activity pause!!!");
               default:
                        Log.d(HookTool.TAG, "hook a " + msg.what);
                        break;
               }

               if(mParentCallback != null){
                        return mParentCallback.handleMessage(msg);
               }else{
                        return false;
               }
     }

}

2.工具类,拦截实现代码;

public class HookTool {
     public static final String TAG = "Inject";

     public static void dexInject() {
               Log.d(TAG, "this is dex code,welcome to HookTool~");
               try {          
                        Object currentActivityThread = ReflectUtils.invokeStaticMethod(
                                           "android.app.ActivityThread",                                                     "currentActivityThread",
                                           new Class[] {}, new Object[] {});

                        Handler localHandler = (Handler) ReflectUtils.getFiled("android.app.ActivityThread","mH",currentActivityThread);
                        HookCallback oriCallback = (HookCallback) ReflectUtils.getFieldObject(Handler.class, localHandler, "mCallback");
                        HookCallback hookCallBack = new HookCallback(oriCallback);
                        ReflectUtils.setFieldObject(Handler.class, localHandler, "mCallback", hookCallBack);
               } catch (IllegalArgumentException e) {
                        e.printStackTrace();
               }
     }

}

3.反射工具类

这部分就不贴了,反射工具到处都是。
最后编译生成DexInject.apk

验证结果

  1. 测试机器上点击目标APP HelloWord;
  2. Push各路工具和数据到测试机器:

    adb push DexInject.apk /data/injects
    adb push inject /data/
    adb push libhelloTool.so /data/

  3. 运行注入工具:

这是按home退出APP再进入,通过日志过滤器可以得到:

感谢您赏个荷包蛋~