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作用。
實現流程
必備工具
注意:為了快速調通,這里所有參數都是寫死的,這就限制了后續驗證流程必須要實現過程中的代碼編寫的一致。如果只是作為預研、測試是否可行的階段,這種做法無可厚非;相對的如果是正規的開發流程中,在迭代周期里面做好通用性的設計是必要的。
開始實現
建立目標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[]) {//獲取JNIEnvvoid* handle = dlopen("/system/lib/libandroid_runtime.so", RTLD_NOW);getJNIEnv = dlsym(handle, "_ZN7android14AndroidRuntime9getJNIEnvEv");JNIEnv* env = getJNIEnv();//調用ClassLoader中的getSystemClassLoader方法獲取當前進程的ClassLoaderjclass 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路徑為參數構造注入后的DexClassLoaderjstring 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,加入攔截操作代碼(打印);
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
驗證結果
Push各路工具和數據到測試機器:
adb push DexInject.apk /data/injects
adb push inject /data/
adb push libhelloTool.so /data/
運行注入工具:
這是按home退出APP再進入,通過日志過濾器可以得到:
原文地址: http://taoyuanxiaoqi.com/2015/03/16/dexinject/
總結
- 上一篇: Android apk动态加载机制的研究
- 下一篇: APK加壳【1】初步方案实现详解