前段时间忙于考试,所以发了第三题的详细分析后就一直拖到现在才终于有空写第二题的详细分析文档~~由于第三题是个体力活,并不涉及太多的方法原理,所以当时给出的“详细分析文档”充其量是一个记录文档。痛定思痛,决定好好写写第二题的详细分析方法和原理!
本文主要参考了
http://bbs.pediy.com/showthread.php?t=187803,非常感谢帖子里面的几位大牛提出的方法思路!
不过在那个帖子中,大牛只说了应该怎么做,却没有说为什么这么做,也没有给出具体的实现方法,所以我就写了这篇文档来为各位大牛添块砖加片瓦
。
新人初学,有错误的地方,望大家指正!
360APK包与类更改分析
1 题目要求
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="@string/app_name" >
</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即可。
最终效果图如下:
总结
根据我个人的理解,此题第二问的应用范围还是很广的,如下文提及的APK加壳方案:
http://blog.csdn.net/androidsecurity/article/details/8678399
OK,技术方面就说到这里,作为一个初学者,我想谈谈一点技术之外的话题。
众所周知,解决一个问题,并不是唯一的目的,通过解决问题来学习知识才是我们追求的目标。同样的,我们在分享自己解决某个问题的方法技巧时,最好多花点时间叙述“我为什么要这么做”,而不是仅仅提及“我用什么方法解决了什么问题”。因为只有这样,才能做到真正的知识分享,我们才能向国外那样拥有很好的学习氛围(这个大家应该是深有体会吧~~)。所以我在这里厚颜代表广大的初学者们向各位大牛请求:在分享方法技术的时候,请多花点时间讲解“我为什么要这么做”,以及“该如何学到这方面的知识”吧!对于你们来说可能会耗费半小时的时间,但对新手来说可能就是半个月都不止了….
注:本帖由看雪论坛志愿者PEstone 重新将文档整理排版,若和原文有出入,以原作者附件为准
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课