360APK包與類更改分析
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,技術方面就說到這里,作為一個初學者,我想談談一點技術之外的話題。
眾所周知,解決一個問題,並不是唯一的目的,通過解決問題來學習知識才是我們追求的目標。同樣的,我們在分享自己解決某個問題的方法技巧時,最好多花點時間敘述“我為什么要這么做”,而不是僅僅提及“我用什么方法解決了什么問題”。因為只有這樣,才能做到真正的知識分享,我們才能向國外那樣擁有很好的學習氛圍(這個大家應該是深有體會吧~~)。所以我在這里厚顏代表廣大的初學者們向各位大牛請求:在分享方法技術的時候,請多花點時間講解“我為什么要這么做”,以及“該如何學到這方面的知識”吧!對於你們來說可能會耗費半小時的時間,但對新手來說可能就是半個月都不止了….