4、叉叉助手逆向分析续集--模拟实现游戏插件框架--再扩展到脱壳机

2014/04/08 android 共 15935 字,约 46 分钟

这里以HOOK程序启动后调用天天星联盟为例,下面是抠出来的天天星联盟插件APK代码:

package com.xxAssistant.UI;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.widget.RelativeLayout;
import android.widget.Toast;

public class UniversalUI extends RelativeLayout
{
  private static String mSoPath;
  private static UniversalUI me = null;
  private Activity mActivity;
  private Context mContext;
  private boolean mShow;

  private UniversalUI(Activity paramActivity)
  {
    super(paramActivity);
    this.mContext = paramActivity;
    this.mActivity = paramActivity;
    this.mShow = false;
  }

  public static void init(Activity paramActivity, String paramString)
  {
    if (me == null);
    for (UniversalUI localUniversalUI = new UniversalUI(paramActivity); ; localUniversalUI = me)
    {
      me = localUniversalUI;
      mSoPath = paramString;
      me.show();
      return;
    }
  }

  private native void setqudao();

  private void show()
  {
    if (!this.mShow)
    {
      this.mShow = true;
      Log.d("native", "叉叉辅助已成功装载");
      Toast.makeText(this.mContext, "叉叉辅助已成功装载", 1).show();
      System.load(mSoPath);
      xxdohook();
    }
  }

  private native void xxdohook();
}

测试只要把so加载和调用的部分去掉即可,反编译后去掉UniversalUI.smali的以下代码:


   .line 40
    sget-object v0, Lcom/xxAssistant /UI /UniversalUI; ->mSoPath:Ljava/ lang/String;

    invoke-static {v0 }, Ljava/ lang/System;->load (Ljava /lang /String; )V

    .line 41
    invoke-direct {p0}, Lcom/xxAssistant /UI /UniversalUI; ->xxdohook() V

以及:

.method private native xxdohook() V
.end method

回编译后再次确认下:

package com.xxAssistant.UI;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.widget.RelativeLayout;
import android.widget.Toast;

public class UniversalUI extends RelativeLayout
{
  private static String mSoPath;
  private static UniversalUI me = null;
  private Activity mActivity;
  private Context mContext;
  private boolean mShow;

  private UniversalUI(Activity paramActivity)
  {
    super(paramActivity);
    this.mContext = paramActivity;
    this.mActivity = paramActivity;
    this.mShow = false;
  }

  public static void init(Activity paramActivity, String paramString)
  {
    if (me == null);
    for (UniversalUI localUniversalUI = new UniversalUI(paramActivity); ; localUniversalUI = me)
    {
      me = localUniversalUI;
      mSoPath = paramString;
      me.show();
      return;
    }
  }

  private native void setqudao();

  private void show()
  {
    if (!this.mShow)
    {
      this.mShow = true;
      Log.d("native", "叉叉辅助已成功装载");
      Toast.makeText(this.mContext, "叉叉辅助已成功装载", 1).show();
    }
  }
}

最后记得签名APK,然后把修改后的APK复制到项目工程的assets目录下:

运行时动态将这两个资源文件复制到app_plugin目录下,叉叉助手是把不同的插件以id分别放到app_plugin下的子目录中,我们仅作测试不再创建子目录。

运行效果:

img

点击“浏览”按钮选择要HOOK的APP:

img

选择后程序会自动分析其包名和启动Acitivity,然后点击“HOOK”按钮开始注入:

img

注入成功后便可以点击“启动”按钮运行刚刚选择的APP了:

img

程序界面上显示了叉叉助手插件TOAST出的信息。

由于界面上设置了动态可配置的包名,随意选择另外一个APP并启动:

img

也就是说这种方法具有通用性。

LOG信息输出到界面中也是为了方便查看,不用每次都去用LOGCAT等工具查看,原理是底层通过反射调用java层函数:

void LOGMSG(const char *lpszText)
{
    LOGD("%s", lpszText);

    if ( g_context!=NULL ) {
        jclass jclazz = g_env->GetObjectClass(g_context);
        jmethodID logMsg = g_env->GetMethodID(jclazz, "logMsg", "(Ljava/lang/String;)V");
        if ( logMsg ) {
            //LOGD("found: logMsg");
            jstring jstr = str2jstring(g_env, lpszText, strlen(lpszText), NULL);
            //LOGD("CallObjectMethod 1");
            g_env->CallVoidMethod(g_context, logMsg, jstr);
            //LOGD("CallObjectMethod 2");
        }
    }
}

但是这个仅限于在当前进程中,一旦注入到其他进程中,这种方式就不行,可以考虑其他通信方式。

执行流程:injectso把middle注入到父进程,middle启动时调用该函数。

/*
由于libandroidloader.so使用动态方式加载libsubstratedvm.so并获取MSJavaHookMethod进行调用会崩溃,
一定需要隐式链接:
LOCAL_LDLIBS := -L$(LOCAL_PATH) -llog -ldl libsubstratedvm.so
所以libandroidloader.so在加载前进程模块中一定要存在libsubstratedvm.so和libsubstrate.so,
因此需要先注入一个跳板模块(这里就是middle)来完成对以上两个so的加载,然后再加载libandroidloader.so,
这样libandroidloader.so在启动时它所需要的两个so都已经被加载完成了,就不会出现崩溃问题了。
*/

HOOK android/app/ActivityThread::handleBindApplication,这样当APP启动时就能触发到回调函数。

OnCallback_handleBindApplicaton

//参数二的形式为:android.app.ActivityThread@41683ec8
//参数三的形式为:AppBindData{appInfo=ApplicationInfo{4178ead0 com.huawei.ChnUnicomAutoReg}}
//因此可以通过参数三的包名进行过滤
static void OnCallback_handleBindApplicaton(JNIEnv *jni, jobject jthis, jobject appBindData)
{
    LOGD("[%s] begin", __FUNCTION__);
    string strName;
    strName = getObjectClassName(jni, appBindData);
    if ( (int)strName.size() > 0 ) {
        LOGD("[%s] class: %s", __FUNCTION__, strName.c_str());
    }

    //读取hook配置信息,这里每次都要读,考虑到配置文件会发生变动
    bool bSuccess = LoadConfig(g_cfg);
    if ( bSuccess==false ) {
        LOGE("[%s] load config failed", __FUNCTION__);
    }else{
        //根据包名过滤,hook指定的apk
        //string::size_type position = strName.npos;
        if ( strName.find(g_cfg.strHostPackage)!=strName.npos ) {
            //是目标app
            LOGD("[%s] target apk running: %s", __FUNCTION__, g_cfg.strHostPackage.c_str());

            //////////////////////////////////////////////////////////////////////////
            //hook目标的activity的类加载以及onCreate
            //////////////////////////////////////////////////////////////////////////
            MSJavaHookClassLoad(jni, g_cfg.strHostActivity.c_str(), &OnCallback_JavaClassLoad, NULL);
            //////////////////////////////////////////////////////////////////////////

            //上下两块独立互不干扰,可以同时使用也可分开使用

            //////////////////////////////////////////////////////////////////////////
            //简易脱壳机:可脱我们自己的壳,梆梆加固,爱加密,360加固保等
            //////////////////////////////////////////////////////////////////////////
            void * handle=dlopen("libdvm.so", RTLD_NOW);
            LOGD("handle:%p", handle);
            JNINativeMethod *dvm_dalvik_system_DexFile =  (JNINativeMethod*)dlsym(handle, "dvm_dalvik_system_DexFile");
            dvmCreateCstrFromString = (dvmCreateCstrFromStringPtr)dlsym(handle, "_Z23dvmCreateCstrFromStringPK12StringObject");
            LOGD("dvmCreateCstrFromString:%p", dvmCreateCstrFromString);

            dvmRawDexFileOpen = (dvmRawDexFileOpenPtr)dlsym(handle, "_Z17dvmRawDexFileOpenPKcS0_PP10RawDexFileb");
            dvmJarFileOpen = (dvmRawDexFileOpenPtr)dlsym(handle, "_Z14dvmJarFileOpenPKcS0_PP7JarFileb");
            LOGD("dvmRawDexFileOpen:%p", dvmRawDexFileOpen);
            LOGD("dvmJarFileOpen:%p", dvmJarFileOpen);
            if ( dvmRawDexFileOpen ) {
                MSHookFunction(dvmRawDexFileOpen, newdvmRawDexFileOpen, &olddvmRawDexFileOpen);
            }
            if ( dvmJarFileOpen ) {
                MSHookFunction(dvmJarFileOpen, newdvmJarFileOpen, &olddvmJarFileOpen);
            }

            //dvmDexFileOpenPartial = (dvmDexFileOpenPartialPtr)dlsym(handle, "_Z21dvmDexFileOpenPartialPKviPP6DvmDex");
            //LOGD("dvmDexFileOpenPartial: %p", dvmDexFileOpenPartial);
            //if ( dvmDexFileOpenPartial ) {
            //    MSHookFunction(dvmDexFileOpenPartial, newdvmDexFileOpenPartial, &olddvmDexFileOpenPartial);
            //}

            //dvmDexFileOpenPartial会调用dexFileParse,因此只需要hookdexFileParse即可。
            dexFileParse = (dexFileParsePtr)dlsym(handle, "_Z12dexFileParsePKhji");
            LOGD("dexFileParse: %p", dexFileParse);
            if ( dexFileParse ) {
                MSHookFunction(dexFileParse, newdexFileParse, &olddexFileParse);
            }

            if(dvm_dalvik_system_DexFile){
                lookup(dvm_dalvik_system_DexFile, "openDexFile", "([B)I", &openDexFile);
                LOGD("openDexFile:%p",openDexFile);
                MSHookFunction(openDexFile, newDalvik_dalvik_system_DexFile_openDexFile_bytearray, &oldDalvik_dalvik_system_DexFile_openDexFile_bytearray);

                lookup(dvm_dalvik_system_DexFile, "openDexFile", "(Ljava/lang/String;Ljava/lang/String;I)I", &openDexFile);
                LOGD("openDexFile:%p",openDexFile);
                MSHookFunction(openDexFile, newDalvik_dalvik_system_DexFile_openDexFile, &oldDalvik_dalvik_system_DexFile_openDexFile);
            }
            //////////////////////////////////////////////////////////////////////////
        }

    }

    //继续原函数处理
    (*old_handleBindApplication)(jni, jthis, appBindData);
    LOGD("[%s] end", __FUNCTION__);
}

当目标的启动类被加载时触发回调OnCallback_JavaClassLoad

//当类被加载时触发的回调函数
static void OnCallback_JavaClassLoad(JNIEnv *jni, jclass _class, void *arg)
{
    LOGD("[%s] begin", __FUNCTION__);

    if (!g_cfg.IsValid()) {
        LOGE("[%s] hook config is not valid", __FUNCTION__);
        return;
    }
    jmethodID onCreate = jni->GetMethodID(_class, "onCreate", "(Landroid/os/Bundle;)V");
    if (onCreate == NULL) {
        LOGE("[%s] \"onCreate\" not found in class: %s", __FUNCTION__, g_cfg.strHostActivity.c_str());
        return;
    }

    old_onCreate = NULL;
    LOGD("[%s] hook \"onCreate\"", __FUNCTION__);
    MSJavaHookMethod(jni, _class, onCreate, (void *) (&OnCallback_ClassOnCreate), (void **) (&old_onCreate));
    if ( old_onCreate==NULL ) {
        LOGE("[%s] old_onCreate returned NULL", __FUNCTION__);
    }

    LOGD("[%s] end", __FUNCTION__);
}

主要目的是HOOK onCreate函数,这样当启动类的onCreate被调用时首先是进入到我们的hook函数:

//当类的onCreate调用时触发回调,动态加载插件apk并调用插件函数
/*
ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();  
DexClassLoader localDexClassLoader = new DexClassLoader(dexpath, dexoutputpath, null, localClassLoader);

动态加载时参数dexOutputDir为/data/data/目标APK包名/app_dex,获取方法:
getDir("dex",0).getAbsolutePath()
其他路径会失败,错误:optimizedDirectory not readable/writable:+路径;
参见:http://www.cnblogs.com/LittleRedPoint/p/3429709.html
由于app_dex目录默认是不存在的,需要动态创建以及修改文件夹权限,比较繁琐,这里直接使用cache目录。

old_onCreate在最开始调用了,所以后面的函数都可以不限制地调用return
*/
static void OnCallback_ClassOnCreate(JNIEnv *jni, jobject activity, jobject bundle)
{
    string strName;
    strName = getObjectClassName(jni, activity);
    LOGD("[%s] begin: %s::OnCreate", __FUNCTION__, strName.c_str());

    (*old_onCreate)(jni, activity, bundle);

    LOGD("[%s] dynamic load plug apk", __FUNCTION__);
    if (!g_cfg.IsValid()){
        LOGD("[%s] config invalid", __FUNCTION__);
        return;
    }

    jclass ClassLoader = jni->FindClass("java/lang/ClassLoader");
    if ( ClassLoader==NULL ) {
        LOGE("[%s] not found: java/lang/ClassLoader", __FUNCTION__);
        return;
    }
    
    jmethodID getSystemClassLoader = jni->GetStaticMethodID(ClassLoader, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
    if ( getSystemClassLoader==NULL ) {
        LOGE("[%s] not found: getSystemClassLoader", __FUNCTION__);
        return;
    }
    jobject systemLoader = jni->CallStaticObjectMethod(ClassLoader, getSystemClassLoader);
    if ( systemLoader==NULL ) {
        LOGE("[%s] getSystemClassLoader() return NULL", __FUNCTION__);
        return;
    }
    
    jclass dexLoaderClass = jni->FindClass("dalvik/system/DexClassLoader");
    if ( dexLoaderClass==NULL ) {
        LOGE("[%s] not found: dalvik/system/DexClassLoader", __FUNCTION__);
        return;
    }
    jmethodID initDexLoaderMethod = jni->GetMethodID(dexLoaderClass, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V");  
    if ( initDexLoaderMethod==NULL ) {
        LOGE("[%s] not found: dalvik/system/DexClassLoader::<init>", __FUNCTION__);
        return;
    }
    
    //注意这里必须是当前进程的目录,不能跨进程目录读写
    string app_dex = "/data/data/" + g_cfg.strHostPackage + "/cache";
    //LOGD("app_dex is %s", app_dex.c_str());
    jstring apkPath = jni->NewStringUTF(g_cfg.strPlugApkPath.c_str());  
    jstring dexOutputDir = jni->NewStringUTF(app_dex.c_str());  
    jobject dexClassLoader = jni->NewObject(dexLoaderClass, initDexLoaderMethod, apkPath, dexOutputDir, NULL, systemLoader);
    //jni->DeleteLocalRef(apkPath);
    //jni->DeleteLocalRef(dexOutputDir);
    if ( dexClassLoader==NULL ) {
        LOGE("[%s] new DexClassLoader(...) return NULL", __FUNCTION__);
        return;
    }
    
    jmethodID loadClass = jni->GetMethodID(dexLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");  
    if ( loadClass==NULL ) {
        LOGE("[%s] not found: dalvik/system/DexClassLoader::loadClass", __FUNCTION__);
        return;
    }
    
    jstring javaClassName = jni->NewStringUTF(g_cfg.strPlugActivity.c_str());  
    jclass plugClass = (jclass)jni->CallObjectMethod(dexClassLoader, loadClass, javaClassName);  
    //jni->DeleteLocalRef(apkPath);
    if ( plugClass==NULL ) {
        LOGE("[%s] DexClassLoader::loadClass not found class: %s", __FUNCTION__, g_cfg.strPlugActivity.c_str());
        return;
    }
    
    //调用插件的静态函数init,原型:public static void init(Activity activity, String soPath)
    jmethodID plugUIinit = jni->GetStaticMethodID(plugClass, "init", "(Landroid/app/Activity;Ljava/lang/String;)V");
    if ( plugUIinit==NULL ) {
        LOGE("[%s] not found \"init\" in plug activity: public static void init(Activity activity, String soPath)", __FUNCTION__);
    }else{
        LOGD("[%s] invoke %s::init", __FUNCTION__, g_cfg.strPlugActivity.c_str());
        jstring soPath = jni->NewStringUTF(g_cfg.strPlugSoPath.c_str());
        jni->CallStaticVoidMethod(plugClass, plugUIinit, activity, soPath);
        //jni->DeleteLocalRef(soPath);
    }

    LOGD("[%s] end", __FUNCTION__);
}

其实主要目的就是(jni->CallStaticVoidMethod(plugClass, plugUIinit, activity, soPath);)调用插件某类的静态函数,传给两个参数。

但是实现上确实比较繁琐。

运行效果就是开头的几张图。

引申到脱壳机:在外壳加载原始apk的过程中,会调用某些系统api,只要hook拦截下来并dump出相应的数据即可。

再回到OnCallback_handleBindApplicaton函数分析下半部分。

涉及到的:

ODEX转DEX的操作(参考http://blog.csdn.net/asmcvc/article/details/39078421):

1、ODEX反编译:

java -jar baksmali.jar -x a.odex

2、反编译结果再打包成DEX:

java -Xmx512M -jar smali.jar out -o classes.dex

3、DEX转JAR看源码。

如果出现诸如以下错误:

odex转dex遇到的异常 Cannot locate boot class path file /system/framework/core.odex

请按照上面链接中的解决办法解决。

爱加密:

static char chCount = 0x30;

//dvmDexFileOpenPartial会调用dexFileParse,因此只需要HOOK dexFileParse即可。
int newdvmDexFileOpenPartial(const void* addr, int len, void* ppDvmDex)
{
    LOGD("[%s] begin", __FUNCTION__);
    int nret = olddvmDexFileOpenPartial(addr, len, ppDvmDex);

    string dexFileName = "/data/data/" + g_cfg.strHostPackage + "/cache/o.odex";
    dexFileName.append(1, ++chCount);
    if ( saveFile(addr, len, dexFileName.c_str()) ) {
        int nret = chmod(dexFileName.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
        LOGD("[%s] dumped file: %s chmod return: %d error: %s", __FUNCTION__, dexFileName.c_str(), nret, dlerror());
    }

    LOGD("[%s] end", __FUNCTION__);

    return nret;
}

梆梆加固:

[newDalvik_dalvik_system_DexFile_openDexFile] sourceName: /data/data/com.mxy/.cache/classes.jar outputName: /data/data/com.mxy/.cache/classes.dex
[newdvmJarFileOpen] fileName: /data/data/com.mxy/.cache/classes.jar odexOutputName: /data/data/com.mxy/.cache/classes.dex
[newdvmRawDexFileOpen] fileName: /data/data/com.example.helloapplication/app_bangcleplugin/container.dex odexOutputName: /data/data/com.example.helloapplication/app_outdex.container.e8b06f9b6317e8fc0c1ddeafd7f71664/container.dex
[newdvmRawDexFileOpen] fileName: /data/data/com.example.helloapplication/app_bangcleplugin/collector.dex odexOutputName: /data/data/com.example.helloapplication/app_outdex.collector.f961d3dbc359c93741d5c63d553c0f57/collector.dex
void newDalvik_dalvik_system_DexFile_openDexFile(const u4* args, JValue* pResult)
{
    LOGD("[%s] begin", __FUNCTION__);
    oldDalvik_dalvik_system_DexFile_openDexFile(args, pResult);

    StringObject* sourceNameObj = (StringObject*) args[0];
    StringObject* outputNameObj = (StringObject*) args[1];
    char* sourceName = NULL;
    char* outputName = NULL;

    if ( dvmCreateCstrFromString!=NULL ) {
        sourceName = dvmCreateCstrFromString(sourceNameObj);
        outputName = dvmCreateCstrFromString(outputNameObj);
        LOGD("[%s] sourceName: %s outputName: %s", __FUNCTION__, sourceName, outputName);

        //复制备份
        if ( strstr(sourceName, "classes.jar")!=NULL ) {
            string dexFileName = "/data/data/" + g_cfg.strHostPackage + "/cache/classes.jar";
            dexFileName.append(1, ++chCount);
            LOGD("[%s] backup file to: %s", __FUNCTION__, dexFileName.c_str());
            copyFile(sourceName, dexFileName.c_str());    
            chmod(dexFileName.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
            LOGD("[%s] dumped file: %s", __FUNCTION__, dexFileName.c_str());
        }/*else if ( strstr(sourceName, "collector.dex")!=NULL || strstr(sourceName, "container.dex")!=NULL ) {
            string dexFileName = "/data/data/" + g_cfg.strHostPackage + "/cache/out.dex";
            dexFileName.append(1, ++chCount);
            LOGD("[%s] backup file to: %s", __FUNCTION__, dexFileName.c_str());
            copyFile(sourceName, dexFileName.c_str());        
        }*/

    }

    LOGD("[%s] end", __FUNCTION__);
}

梆梆加固的被Dalvik_dalvik_system_DexFile_openDexFile拦截到时判断输入参数,如果sourceName包含classes.jar,则复制备份出来,这个classes.jar其实是一个zip包,里面只有一个文件:classes.dex,classes.dex可以使用三方工具转换成jar文件,就是目标的DEX。collector.dex和collector.dex备份出来后发现是插件。

360加固保:

360的需要HOOK函数dexFileParse,可以DUMP出ODEX,但是直接使用baksmali.jar操作会出错:

Error occurred while loading boot class path files. Aborting.
org.jf.util.ExceptionWithContext: Cannot locate boot class path file /system/fra
mework/core.odex
        at org.jf.dexlib2.analysis.ClassPath.loadClassPathEntry(ClassPath.java:2
17)
        at org.jf.dexlib2.analysis.ClassPath.fromClassPath(ClassPath.java:161)
        at org.jf.baksmali.baksmali.disassembleDexFile(baksmali.java:67)
        at org.jf.baksmali.main.main(main.java:280)

这个应该需要再额外设置一些东西,此处不再研究。综合一下,由于函数dvmDexFileOpenPartial内部也要调用dexFileParse,因此爱加密和360加固的脱壳方法一致,统一HOOK函数dexFileParse:

//dvmDexFileOpenPartial会调用dexFileParse,因此只需要HOOK dexFileParse即可。
void* newdexFileParse(const u1* data, size_t length, int flags)
{
    LOGD("[%s] begin", __FUNCTION__);
    void* nret = olddexFileParse(data, length, flags);

    string dexFileName = "/data/data/" + g_cfg.strHostPackage + "/cache/o.odex";
    dexFileName.append(1, ++chCount);
    if ( saveFile(data, length, dexFileName.c_str()) ) {
        int nret = chmod(dexFileName.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
        LOGD("[%s] dumped file: %s chmod return: %d error: %s", __FUNCTION__, dexFileName.c_str(), nret, dlerror());
    }

    LOGD("[%s] end", __FUNCTION__);

    return nret;
}

易盾的加壳程序:

//hook内存加载odex的函数
void newDalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args, JValue* pResult)
{
    LOGD("[%s] begin", __FUNCTION__);
    ArrayObject* fileContentsObj = (ArrayObject*) args[0];

    string dexFileName = "/data/data/" + g_cfg.strHostPackage + "/cache/out.odex";
    dexFileName.append(1, ++chCount);
    if ( saveFile(fileContentsObj->contents, fileContentsObj->length, dexFileName.c_str()) ) {
        chmod(dexFileName.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
        LOGD("[%s] dumped file: %s", __FUNCTION__, dexFileName.c_str());
    }

    LOGD("[%s] end", __FUNCTION__);
    oldDalvik_dalvik_system_DexFile_openDexFile_bytearray(args, pResult);
}

其他应用场景:

API Monitor

APKScan可以基于此原理

大家可以根据需要完善或修改。

文档信息

Search

    Table of Contents