360APK包与类更改分析

来源:互联网 发布:linux 查看磁盘信息 编辑:程序博客网 时间:2024/06/03 04:44

本文出处:http://www.cnblogs.com/wanyuanchun/p/3829918.html(感谢博主)

1 题目要求

这是360的全球招募无线攻防中的第二题,题目要求如下:

1)请以重打包的形式将qihootest2.apk的程序包名改为 "com.qihoo.crack.StubApplication",使得在同一手机上面可以重复安装并正确运行;

2)请写个Application类,并在Manifest里面注册你的Application。同时要求使用该Application加载原包的Application;

题目所用apk下载地址:

http://pan.baidu.com/share/link?shareid=644965540&uk=839654349

 

2 第一小问更改方法

首先,我们需要将apk反编译为smali文件。这里推荐使用apkIDE。

2.1 确定要修改的地方

显然,哪里用了包名,哪里就需要修改:

①AndroidManifest.xml:package, application name, contentProvider。

②smali文件中:所有com/qihoo/test改为com/qihoo/crack/StubApplication、所有com.qihoo.test改为com.qihoo.crack.StubApplication。

③目录结构:将原目录com.qihoo.test改为com.qihoo.crack,然后在这个目录里面新建子目录StubApplication,最后将原来属于test目录的所有文件copy到StubApplication中。

至此,在smali中的修改工作就告一段落了。但仅仅这样是不行的,因为在APK中会调用libqihooTest.so中的native函数packageNameCheck()。这个函数是使用动态注册的方式进行注册的,在JNI_OnLoad函数中完成注册功能,使得原APK中的com.qihoo.test.MainActivity.packageNameCheck()同so中的packageNameCheck()函数相关联。我们可以把libqihootest.so拖到ida中查看其中的JNI_OnLoad函数,就可以发现该函数会调用如下JNI方法:

jclass testClass = (*env)->FindClass(env, “com/qihoo/test/Mainactivity”);

Findclass的字符串参数使用硬编码写在so中。如果更改后的包名短于原来的包名,那么我们可以使用winhex直接修改这个so,不过这个方法明显不适合于本程序,所以只能另辟蹊径了。

 

2.2 通过packageNameCheck函数检查

前面的分析发现在libqihootest.so中的JNI_OnLoad函数中会调用FindClass(env, “com/qihoo/test/Mainactivity”),而我们更改过后的smali文件中是没有这个类的。所以如果不设法解决这个问题,程序肯定无法正常运行。

分析到此,解决方法就出来了:

1)在原来的smali文件中创建一个test.MainActivity类(注意是在com.qihoo目录下新建目录test,再在test目录下新建MainActivity类),然后将native方法都移植到这一个类中。

2)想法跳过JNI_OnLoad函数:也就是说,我们既需要运行libqihootest.so中的packageNameCheck等native函数,又不运行JNI_OnLoad函数。

 

我选择第二种。下面来详细分析如何实现第二种方法。

我们知道,一般情况下JNI_OnLoad函数是在使用System.loadLibrary载入so的时候第一个运行的native函数,而如果使用javah方式(静态注册)编写native代码的话,就可以省略JNI_OnLoad函数,所以我们有必要弄清JNI_OnLoad的实现机制。

System.loadLibrary也是一个native方法,它的调用的过程是:

Dalvik/vm/native/java_lang_Runtime.cpp:

Dalvik_java_lang_Runtime_nativeLoad ->Dalvik/vm/Native.cpp:dvmLoadNativeCode

dvmLoadNativeCode

打开函数dvmLoadNativeCode,可以找到以下代码:

 

handle = dlopen(pathName, RTLD_LAZY); //获得指定库文件的句柄,

//这个库文件就是System.loadLibrary(pathName)传递的参数

…..

vonLoad = dlsym(handle, "JNI_OnLoad"); //获取该文件的JNI_OnLoad函数的地址

   if (vonLoad == NULL) { //如果找不到JNI_OnLoad,就说明这是用javah风格的代码了,那么就推迟解析

 LOGD("No JNI_OnLoad found in %s %p, skipping init",pathName, classLoader); //这句话我们在logcat中经常看见!

}else{

….

}

 

从上面的代码可以看出:System.loadLibrary函数首先会通过dlopen获取so文件的句柄,然后使用dlsym获取该JNI_OnLoad函数的地址,如果该地址为空,就说明没有此函数(这并不是错误)——隐喻就是so库使用javah的编码方式,此时不需要解析so中的函数,而是等java层调用native函数的时候再解析。

 

分析到此,我们就已经找到绕过JNI_OnLoad函数的方法了:参照System.loadLibrary的方式,使用dlopen、dlsym函数直接调用libqihootest.so中的packageNameCheck函数

C代码如下:

/*callQihooSo.c*/

 

#include <string.h>

#include <stdio.h>

#include <jni.h>

#include <dlfcn.h>  //使用dlopen等函数的头文件

#include <android/log.h>

 

#define LOG_TAG "360TEST2"

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

 

/*这里我直接使用javah的方式编写native代码*/

JNIEXPORT  Java_com_qihoo_crack_StubApplication_MainActivity_packageNameCheck( JNIEnv* env,  jobject obj){

void* filehandle =dlopen("/data/data/com.qihoo.crack.StubApplication/lib/libqihooTest.so", RTLD_LAZY ); //获取libqihooTest.so的句柄

      if(filehandle){

        void (*packageNameCheck)(JNIEnv *,jobject);

        packageNameCheck = (void (*)(JNIEnv *,jobject)) dlsym(filehandle, "packageNameCheck"); //找到.so文件中的函数

        if(packageNameCheck){

          packageNameCheck(env, obj); //传递参数      }

        else{

          LOGI("get packageNameCheck func failed!");

        }

        LOGI("success!");

      }else{

          LOGI("get file handle failed!");

      }

     return ;

}

 

JNIEXPORT   Java_com_qihoo_crack_StubApplication_MainActivity_applicatioNameCheck( JNIEnv* env,

                                                  jobject obj){

      void*  filehandle = dlopen("/data/data/com.qihoo.crack.StubApplication/lib/libqihooTest.so", RTLD_LAZY );

      if(filehandle){

        void (*applicatioNameCheck)(JNIEnv *,jobject);

        applicatioNameCheck = (void (*)(JNIEnv *,jobject)) dlsym(filehandle, "applicatioNameCheck"); //找到.so文件中的函数

        if(applicatioNameCheck){

        applicatioNameCheck(env, obj); //传递参数

          return ;

        }

        else{

          LOGI("get applicatioNameCheck func failed! ");

        }

        LOGI("success!");

      }else{

          LOGI("get file handle failed!");

      }

    return ;

}

Android.mk如下:

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_LDLIBS := -L . -ldl -llog  #一定要加入ldl这个库,dyopen等函数需要

LOCAL_MODULE := callQihooSo

include $(BUILD_SHARED_LIBRARY)

接着像正常编译动态库文件一样编译。编译完成后将libcallQihooSo.so和libqihooTest.so一起放到反编译文件夹的lib/armeabi目录中,然后将MainAcitivity.smali中的System.loadLibrary(“qihooTest”),改为System.loadLibrary(“callQihooSo”),回编译、签名即可。

2.3 总结

第一种方法个人觉得实用性不高,所以就不加以详细介绍了。第二种方法本质上就是一个调用第三方库的问题。只是有一点不同的就是:一般情况下调用第三方库需要在java层使用System.loadLibrary将第三方库文件加载到内存中,然后就可以直接使用第三方库中的函数,而不需要dlopen等函数了(详情参考http://blog.csdn.net/jiuyueguang/article/details/9450597)。

但本题是不能使用System.loadLibrary加载libqihooTest.so的,所以只能使用dlopen机制实现了。

 

3 第二小问的实现方法

主要原理就是参考文档:http://blogs.360.cn/blog/proxydelegate-application/

该文档介绍了Proxy/delegation Application框架的原理和实现。这里详细地描述下它的实现过程。

3.1 创建一个新的android工程

创建该工程的目的是为了得到实现这个框架的smali文件(反编译此apk),然后将相关的smali文件添加到题目apk反编译出来的smali文件夹的合适位置(避免我们直接写smali文件,减少工作量)。所以,为了方便文件的移植,我们新建工程的包名命名为“com.qihoo.crack.StubApplication”,工程的结构图如下图所示:

 

3.2 开始编写代码

首先,创建一个ProxyApplication类:

package com.qihoo.crack.StubApplication;

 

import java.lang.reflect.Field;

import java.lang.reflect.InvocationTargetException;

import java.lang.reflect.Method;

import java.util.ArrayList;

 

import android.app.Application;

import android.content.Context;

import android.content.pm.ApplicationInfo;

import android.content.pm.PackageManager;

import android.content.pm.PackageManager.NameNotFoundException;

import android.os.Bundle;

import android.text.InputFilter.AllCaps;

import android.util.Log;

 

public abstract class ProxyApplication extends Application{

       protected abstract void initProxyApplication();

       private static Context pContext = null; //保存ProxyApp的mContext,后面有用

       private static String TAG = "proxy";

       @Override

       public void onCreate() {

              // TODO Auto-generated method stub

              super.onCreate();

              String className = "android.app.Application"; //默认的Application名              String key = "DELEGATE_APPLICATION_CLASS_NAME";

              try {

                     ApplicationInfo appInfo = getPackageManager().getApplicationInfo(super.getPackageName(), PackageManager.GET_META_DATA);

                     Bundle bundle = appInfo.metaData;

                     if(bundle != null && bundle.containsKey(key)){

                            className = bundle.getString(key);

                            if(className.startsWith(".")){

                                   className = super.getPackageName() + className;

                            }

                     }

                    

                     Class delegateClass = Class.forName(className, true, getClassLoader());

                     Application delegate = (Application) delegateClass.newInstance();

                    

          //获取当前Application的applicationContext

Application proxyApplication = (Application)getApplicationContext();

                    

/*使用反射一一替换proxyApplicationContext,这是本程序的重难点*/

                     //首先更改proxy的mbaseContext中的成员mOuterContext

                     Class contextImplClass = Class.forName("android.app.ContextImpl");

                     Field mOuterContext = contextImplClass.getDeclaredField("mOuterContext");

                     mOuterContext.setAccessible(true);

                     mOuterContext.set(pContext, delegate);

                    

                     //再获取context的mPackageInfo变量对象

                     Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");

                     mPackageInfoField.setAccessible(true);

                     Object mPackageInfo = mPackageInfoField.get(pContext);

                     Log.d(TAG, "mPackageInfo: "+ mPackageInfo);

                    

                     //修改mPackageInfo中的成员变量mApplication

                     Class loadedApkClass = Class.forName("android.app.LoadedApk");  //mPackageInfo是android.app.LoadedApk类

                     Field mApplication = loadedApkClass.getDeclaredField("mApplication");

                     mApplication.setAccessible(true);

                     mApplication.set(mPackageInfo, delegate);

                    

                     //然后再获取mPackageInfo中的成员对象mActivityThread

                     Class activityThreadClass = Class.forName("android.app.ActivityThread");

                     Field mAcitivityThreadField = loadedApkClass.getDeclaredField("mActivityThread");

                     mAcitivityThreadField.setAccessible(true);

                     Object mActivityThread = mAcitivityThreadField.get(mPackageInfo);

                    

                     //设置mActivityThread对象中的mInitialApplication

                     Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");

                     mInitialApplicationField.setAccessible(true);

                     mInitialApplicationField.set(mActivityThread, delegate);

                    

                     //最后是mActivityThread对象中的mAllApplications,注意这个是List

                     Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");

                     mAllApplicationsField.setAccessible(true);

                     ArrayList<Application> al = (ArrayList<Application>)mAllApplicationsField.get(mActivityThread);

                     al.add(delegate);

                     al.remove(proxyApplication);

                    

                    

                     //设置baseContext并调用onCreate

                     Method attach = Application.class.getDeclaredMethod("attach", Context.class);

                     attach.setAccessible(true);

                     attach.invoke(delegate, pContext);

                     delegate.onCreate();

                                                       

                                         

              } catch (NameNotFoundException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

              } catch (ClassNotFoundException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

              } catch (InstantiationException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

              } catch (IllegalAccessException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

              } catch (NoSuchFieldException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

              } catch (NoSuchMethodException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

              } catch (IllegalArgumentException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

              } catch (InvocationTargetException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

              }     

       }

 

       @Override

       public String getPackageName() {

              // TODO Auto-generated method stub

              return "Learning And Sharing!";

       }

       @Override

       protected void attachBaseContext(Context base) {

              // TODO Auto-generated method stub

              super.attachBaseContext(base);

              pContext = base;

              Log.d(TAG, "attachBaseContext");

              initProxyApplication();

       }

}

这个代码是严格按照参考文档的框架写的。所以应当参照该文档阅读这些代码。这里主要说一说我在替换API层所有Application引用时遇到的困难。

由于我起先并不了解Android的context相关知识,所以对这一块完全是云里雾里。给大牛们留过小字条,也写过邮件,不过,大牛们都比较忙,所以一直没能得到解答。直到前段时间,请教了群里的“沧海一声呵”朋友(他才大一,你敢信?!!),才得到解决。

以下部分大牛们可以略过啦,现假设读者也同我一样是个android初学者。那么,要想理解和解决“替换API层的所有Application引用”,我们必须深刻理解android的Context机理。这方面的资料可以参考:

http://blog.csdn.net/qinjuning/article/details/7310620

以及我的另一篇博文:

http://www.cnblogs.com/wanyuanchun/p/3828603.html

当然,仅仅这篇文档,是不能让我们完全理解context的,我们还需要通过自己阅读分析Android关于context的源码来加以理解。比如在上面的代码中有一句:

//修改mPackageInfo中的成员变量mApplication

                     Class loadedApkClass = Class.forName("android.app.LoadedApk");  //mPackageInfo是android.app.LoadedApk类

 

如果我们不阅读源码的话,是不可能知道mPackageInfo是android.app.LoadedApk类,而非想当然的android.app.PackageInfo类。

好了,由于篇幅有限,就不过多延伸了。下面继续介绍框架实现。

ProxyApplication类完成之后,就是编写MyProxyApplication类了。该类继承至ProxyApplication,代码很简单:

package com.qihoo.crack.StubApplication;

 

import android.app.Application;

import android.content.Context;

import android.content.ContextWrapper;

import android.content.pm.ApplicationInfo;

import android.content.pm.PackageManager;

import android.content.pm.PackageManager.NameNotFoundException;

import android.os.Bundle;

import android.util.Log;

 

public class MyProxyApplication extends ProxyApplication{

       @Override

       protected void initProxyApplication() {

              // TODO Auto-generated method stub

              //在这里替换surrounding,实现自定义的classloader等功能

              Log.d("proxy", "initProxyApplication");

       }

}

由于题目只是要求加载Delegation Application,所以我们只在initProxyApplication函数中打印log即可。

最后就是修改AndroidManifest.xml文档了,修改后的文档为:

 

<application

        android:name="com.qihoo.crack.StubApplication.MyProxyApplication"

        android:allowBackup="true"

        android:icon="@drawable/ic_launcher"

        android:label="@string/app_name"

        android:theme="@style/AppTheme" >

        <meta-data

            android:name="DELEGATE_APPLICATION_CLASS_NAME"

            android:value="com.qihoo.crack.StubApplication" >   #注意,这里一定要填写正确,否则当我们检测当前application的时候,就会发现得到的application要么是默认的,要么是MyProxyApplication!

        </meta-data>

        <activity

            android:name="com.qihoo.crack.StubApplication.MainActivity"

            android:label="@string/app_name" >

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

 

                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

    </application>

 

到此我们的proxyDemo APK已经编写完毕,将其打包成APK之后,反编译这个APK,然后提取出里面的MyProxyApplication.smali和ProxyApplication.smali文档,放到题目APK的smali/com/qihoo/crack/StubApplication目录中。再按照同样的方式修改题目APK的AndroidManifest.xml,编译、签名,生成APK即可。

最终效果图如下:

  

注意:第二个图,是错误的!正确的显示结果应该是com.qihoo.crack.StubApplication!错误原因是由于我当时在更改AndroidManifest.xml的时候,将META-DATA里面的value值写错了~~详情可见上面红字部分。

总结

根据我个人的理解,此题第二问的应用范围还是很广的,如下文提及的APK加壳方案:

http://blog.csdn.net/androidsecurity/article/details/8678399

OK,技术方面就说到这里,作为一个初学者,我想谈谈一点技术之外的话题。

众所周知,解决一个问题,并不是唯一的目的,通过解决问题来学习知识才是我们追求的目标。同样的,我们在分享自己解决某个问题的方法技巧时,最好多花点时间叙述“我为什么要这么做”,而不是仅仅提及“我用什么方法解决了什么问题”。因为只有这样,才能做到真正的知识分享,我们才能向国外那样拥有很好的学习氛围(这个大家应该是深有体会吧~~)。所以我在这里厚颜代表广大的初学者们向各位大牛请求:在分享方法技术的时候,请多花点时间讲解“我为什么要这么做”,以及“该如何学到这方面的知识”吧!对于你们来说可能会耗费半小时的时间,但对新手来说可能就是半个月都不止了….

0 0