最近在研究Android绿色安全这一块,具体到上层的业务就是“去第三方APP的广告”。如果既想使用第三方APP,又不想看到一些无良的广告,那dex注入基本无法避免。本文针对网上一些大牛分享的文章,进行了一些简单的实现,总结和分享自是不能少的。Ps:感谢金山毒霸实现了该功能,感谢大牛们破解之后的无私分享。
参考文章
金山手机毒霸工作原理【引用1】
【原创】手机毒霸去广告功能分析三: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作用。
实现流程
必备工具
- root工具,想什么办法把测试机器root掉,或者直接用虚拟机;
- NDK工具,各路注入工具都需要ndk来编译,本人使用的版本是:android-ndk-r8b;
- 基础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
验证结果
- 测试机器上点击目标APP HelloWord;
Push各路工具和数据到测试机器:
adb push DexInject.apk /data/injects
adb push inject /data/
adb push libhelloTool.so /data/运行注入工具:
这是按home退出APP再进入,通过日志过滤器可以得到: