Android APK的加固方法
有人的地方就有競爭,在Android的發展過程中就伴隨著逆向和安固加固的發展。逆向工作者可以通過一些非常好用的軟件,如IDA、JEB等,來加快逆向的速度;應用開發工作者也會通過各種手段來阻止逆向工作者對自己的應用進行逆向。
但是往往情況下逆向是不可能做到百分百阻止的,所以只能通過其他的手段來提高自己應用被逆向時的難度,讓逆向工作者需要(可不繞過)花費足夠多的時間才能把應用逆向成功。在實際情況下,只要不明顯影響應用運行速度,我們都可以采用這種思想來進行保護。
在這種背景下,膨脹與混淆就應運而生了,這就是我們最開始的一種保護方式。這種方式將代碼中的方法名和變量名用非常易于混淆的方式進行命名,如0、o、O、l、I、1等組合命名方式。除了這種混淆方式外,為了提高逆向工作量,會在代碼中加入相同或者類似方法名的方法或者增加一些沒有必要的父類來達到代碼膨脹的目的。
但是這種方式不能阻止逆向工作者的腳步,后來開發者發現可以通過DexClassLoader這個類來進行DEX文件的動態加載。我們一般情況下稱呼這種加固方式為加殼,由于這種是動態加載DEX文件,所以我們一般可以稱為DEX殼。但是Android 主要是由 Java 代碼編寫的,而 Java 代碼是非常容易被逆向分析的,所以漸漸地將動態加載DEX的代碼放入 so 層進行運行,so 層的代碼主要為c/c++代碼,逆向的難度比 Java 會高很多,提高了應用的安全性。
隨著這種加固方式的不斷普及,這種方式已經不能阻擋大多數的逆向工作了,加固人員就需要一種新的加固方式來和逆向進行對抗。后期不管是加固還是逆向,都將目光放在了 so 層的對抗,此時就發現了一種利用elf文件格式(Android中.so的共享庫為elf文件格式)來進行加固的方法。
在 so 中,自己定義一個節,在這個節中存放我們的一些關鍵功能代碼,通過elf文件格式將這部分的代碼進行加密,然后在elf文件加載執行初始化函數數組時,將被加密的代碼解密出來。
加殼或者其他加固的優勢:能在一定程度上保護自己核心代碼算法,提高破解、盜版或者二次打包的難度,還可以防范代碼注入、動態調試、內存注入攻擊。
加殼或者其他加固的劣勢:從理論上來說,只要加了保護都可能會影響應用的兼容性、運行效率。
由于受Android手機的電池、CPU等硬件的限制,一般的應用都不可能像PC上進行強度非常大的保護。
1、混淆、膨脹
混淆
主題思想:用沒有意義的字符,如a、b、c或者易于混淆的字符,如0、o、O、l、I、1代替原本的有意義的類名。
參數配置:將release下minifyEnabled的值改為true,打開混淆;加上shrinkResources true,打開資源壓縮。

#壓縮級別0-7,Android一般為5(對代碼迭代優化的次數)
-optimizationpasses 5
#不使用大小寫混合類名
-dontusemixedcaseclassnames
#混淆時記錄日志
-verbose
#不警告org.greenrobot.greendao.database包及其子包里面未應用的應用
-dontwarn org.greenrobot.greendao.database.**
-dontwarn rx.**
-dontwarn org.codehaus.jackson.**
......
#保持jackson包以及其子包的類和類成員不被混淆
-keep class org.codehaus.jackson.** {*;}
#--------重要說明-------
#-keep class 類名 {*;}
#-keepclassmembers class 類名{*;}
#一個*表示保持了該包下的類名不被混淆;
# -keep class org.codehaus.jackson.*
#二個**表示保持該包以及它包含的所有子包下的類名不被混淆
# -keep class org.codehaus.jackson.**
#------------------------
#保持類名、類里面的方法和變量不被混淆
-keep class org.codehaus.jackson.** {*;}
#不混淆類ClassTwoOne的類名以及類里面的public成員和方法
#public 可以換成其他java屬性如private、public static 、final等
#還可以使表示構造方法、表示方法、表示成員,
#這些前面也可以加public等java屬性限定
-keep class com.dev.demo.two.ClassTwoOne {
public *;
}
#不混淆類名,以及里面的構造函數
-keep class com.dev.demo.ClassOne {
public ();
}
#不混淆類名,以及參數為int 的構造函數
-keep class com.dev.demo.two.ClassTwoTwo {
public (int);
}
#不混淆類的public修飾的方法,和private修飾的變量
-keepclassmembers class com.dev.demo.two.ClassTwoThree {
public ;
private ;
}
#不混淆內部類,需要用$修飾
#不混淆內部類ClassTwoTwoInner以及里面的全部成員
-keep class com.dev.demo.two.ClassTwoTwo$ClassTwoTwoInner{*;}
更多混淆配置參考:
https://juejin.cn/post/6844903471095742472
https://www.huaweicloud.com/articles/ae151e2f60923097cefc473bd131addf.html
膨脹
代碼混淆能在一定程度上增加逆向的難度,但是給逆向工作者增加的工作量是比較小的,代碼膨脹就能夠增加總的代碼量,讓逆向工作者必須分析全部的代碼才能得到最終的一些結果。代碼膨脹也是初始防御的方法之一,主要思想是編寫一些垃圾代碼來擴充代碼量,這樣在逆向分析時就可能會消耗攻擊者大量的時間,從而達到保護APK的目的。
膨脹代碼有很多種實現的思想。比如乘法改加法、加法改自加等等,只要把代碼量變大,不影響功能的實現就可以了。
這里自己寫了一個簡單的自動生成代碼的工程。
https://gitee.com/koifishly/function_generator
2、DEX殼
之前Android的主要代碼為Java代碼,但是在逆向分析中,Java代碼是很容易被分析出來的。為了解決這個問題,我們就希望在app運行起來后動態加載我們Java代碼(.dex文件)。這種方法主要利用了DexClassLoader這個類來實現動態加載。DexClassLoader類支持動態加載.apk或者.dex。
動態加載APK
動態加載apk簡單來說,就是將已經編譯好的.apk文件放入到一個.dex文件中。這個.apk文件為我們真正的應用程序,以下就稱呼這個apk為源APK;.dex文件為另外一個工程的.dex文件,這個工程主要是為了在運行時釋放出源APK,然后將流程轉到源APK執行。

根據上面的原理圖,我們需要3個對象。
- 源APK:需要加殼的apk。
- 殼APK:將apk解密還原并執行。
- 加密工具:將源apk和殼dex進行組合成新的dex并且修正新的dex。
項目實現demo代碼
IDE:Android Studio 4.1.3
Android版本:4.4+
項目源碼:nisosaikou/AndroidDEX殼 - 碼云 - 開源中國 (gitee.com)
源APK
1、正常編寫功能邏輯代碼。這里的代碼為簡單的ctf 判斷代碼。

2、新建類APP類并且這個類繼承于類Application,實現onCreate方法。

3、生成一個release版本apk,把這個apk保存起來。
修改MainActivity.java的父類,使得MainActivity繼承于Activity。將文本顯示修改為運行的是源APK。

殼APK
Proxy.java
新建一個代理類叫Proxy,繼承于類Application。這個類用來釋放和解密原始的APK。
attachBaseContext()
重寫Application中的attachBaseContext方法。這個方法會在 Activity 的 onCreate 方法之前執行。
方法實現的功能主要有:
把殼dex中包含的源apk釋放出來。
把釋放的apk進行解密。
把源apk中的lib目錄中的文件復制到當前程序(殼)的路徑下。
創建一個新的DexClassLoader,替換到父節點的DexClassLoader。
DexClassLoader 繼承自BaseDexClassLoader,這個比較靈活,每個參數都可以自定義,我們一般用這個來加載自定義的apk/dex/jar文件。
代碼例子:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// the getDir method will create a directory in /data/user/0(uid)/packagename/
// the dx directory holds the file of the source apk
File relesaeDir = this.getDir("dx", MODE_PRIVATE);
mSouceAPKLibAbsolutePath = this.getDir("lx", MODE_PRIVATE).getAbsolutePath();
mSourceAPKReleaseDir = relesaeDir.getAbsolutePath();
mSourceAPKAbsolutePath = mSourceAPKReleaseDir + "/" + mSouceAPKName;
// create the source apk
// if the source apk exist, do nothing, otherwise create the source apk file.
File sourceApk = new File(mSourceAPKAbsolutePath);
if (!sourceApk.exists()){
try{
sourceApk.createNewFile();
} catch (Exception e) {
Log.e(TAG, "failed to create file.");
}
// the source apk file is empty, you need to read source apk file from the dex
// file of the shell apk and save it.
byte[] shellDexData;
// get dex of shell apk.
shellDexData = getShellDexFileFromShellApk();
// get the source apk and decrypt it.
// copy the libs in the decrypted apk file to the lib directory.
getSourceApkFile(shellDexData);
}
// Configure dynamic load environment
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});
String packageName = this.getPackageName();
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mPackages");
WeakReference weakReference = (WeakReference) mPackages.get(packageName);
DexClassLoader newDexClassLoader = new DexClassLoader(mSourceAPKAbsolutePath, mSourceAPKReleaseDir, mSouceAPKLibAbsolutePath, (ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk", weakReference.get(), "mClassLoader"));
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", weakReference.get(), newDexClassLoader);
}
onCreate()
加載源apk資源。
獲取manifest.xml中記錄的源apk的啟動類名。
設置ActivityThread信息(android.app.ActivityThread->currentActivityThread)。
代碼例子
@Override
public void onCreate() {
super.onCreate();
// 源apk啟動類
String srcAppClassName = "";
// 原apk所在路徑
try
{
ApplicationInfo applicationInfo = this.getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = applicationInfo.metaData;
if (bundle != null && bundle.containsKey(SRC_APP_MAIN_ACTIVITY)) {
srcAppClassName = bundle.getString(SRC_APP_MAIN_ACTIVITY);//className 是配置在xml文件中的。
}
else {
return;
}
}
catch (Exception e)
{
}
//獲取ActivityThread類下AppBindData類的成員屬性 LoadedApk info;
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});
Object mBoundApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mBoundApplication");
Object loadedApkInfo = RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "info");
// 將原來的loadedApkInfo置空
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, null);
// 獲取殼線程的Application
Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication");
ArrayList mAllApplications = (ArrayList) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications");
mAllApplications.remove(oldApplication);
// 構造新的Application
// 1.更新 2處className
ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.LoadedApk", loadedApkInfo, "mApplicationInfo");
ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "appInfo");
appinfo_In_LoadedApk.className = srcAppClassName;
appinfo_In_AppBindData.className = srcAppClassName;
// 2.注冊application
Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication", loadedApkInfo, new Class[] { boolean.class, Instrumentation.class }, new Object[] { false, null });
//替換ActivityThread中的mInitialApplication
RefInvoke.setFieldOjbect("android.app.ActivityThread", "mInitialApplication", currentActivityThread, app);
//替換之前的 內容提供者為剛剛注冊的app
ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mProviderMap");
Iterator it = mProviderMap.values().iterator();
while (it.hasNext()) {
Object providerClientRecord = it.next();
Object localProvider = RefInvoke.getFieldOjbect("android.app.ActivityThread$ProviderClientRecord", providerClientRecord, "mLocalProvider");
RefInvoke.setFieldOjbect("android.content.ContentProvider", "mContext", localProvider, app);
}
app.onCreate();
}
ActivityThread功能
它管理應用進程的主線程的執行(相當于普通Java程序的main入口函數),并根據AMS的要求(通過IApplicationThread接口,AMS為Client、ActivityThread.ApplicationThread為Server)負責調度和執行activities、broadcasts和其它操作。
在Android系統中,在默認情況下,一個應用程序內的各個組件(如Activity、BroadcastReceiver、Service)都會在同一個進程(Process)里執行,且由此進程的【主線程】負責執行。
在Android系統中,如果有特別指定(通過android:process),也可以讓特定組件在不同的進程中運行。無論組件在哪一個進程中運行,默認情況下,他們都由此進程的【主線程】負責執行。
【主線程】既要處理Activity組件的UI事件,又要處理Service后臺服務工作,通常會忙不過來。為了解決此問題,主線程可以創建多個子線程來處理后臺服務工作,而本身專心處理UI畫面的事件。
類結構參考

調用currentActivityThread方法獲取ActivityThread中的成員變量sCurrentActivityThread。
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});

獲取sCurrentActivityThread中的mBoundApplication。
Object mBoundApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mBoundApplication");

獲取mBoundApplication中成員變量info。
Object loadedApkInfo = RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "info");

觀察LoadedApk這個類,能發現一些重要的屬性,這個下面會用到。

將info中的mApplication屬性置空。
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, null);
在sCurrentActivityThread下的鏈表mAllApplications中移除mInitialApplication。mInitialApplication存放初始化的應用(當前殼應用),mAllApplications存放的是所有的應用。
把當前的應用,從現有的應用中移除掉,然后再把新構建的加入到里面去。
Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication");
ArrayList mAllApplications = (ArrayList) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications");
mAllApplications.remove(oldApplication);

構造新的Application
更新2處className。
ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.LoadedApk", loadedApkInfo, "mApplicationInfo");
ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "appInfo");
appinfo_In_LoadedApk.className = srcAppClassName;
appinfo_In_AppBindData.className = srcAppClassName;
注冊application(用LoadedApk中的makeApplication方法注冊)。
Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication", loadedApkInfo, new Class[] { boolean.class, Instrumentation.class }, new Object[] { false, null });

替換mInitialApplication為剛剛創建的app。
RefInvoke.setFieldOjbect("android.app.ActivityThread", "mInitialApplication", currentActivityThread, app);
更新ContentProvider。
ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mProviderMap");
Iterator it = mProviderMap.values().iterator();
while (it.hasNext()) {
Object providerClientRecord = it.next();
Object localProvider = RefInvoke.getFieldOjbect("android.app.ActivityThread$ProviderClientRecord", providerClientRecord, "mLocalProvider");
RefInvoke.setFieldOjbect("android.content.ContentProvider", "mContext", localProvider, app);
}
執行新app的onCreate方法。
app.onCreate();
RefInvoke.java
Java反射調用的方法。
package org.koi.dexloader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class RefInvoke {
public static Object invokeStaticMethod(String class_name, String method_name, Class[] pareTyple, Object[] pareVaules){
try {
Class obj_class = Class.forName(class_name);
Method method = obj_class.getMethod(method_name,pareTyple);
return method.invoke(null, pareVaules);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static Object getFieldOjbect(String class_name,Object obj, String filedName){
try {
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(filedName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void setFieldOjbect(String classname, String filedName, Object obj, Object filedVaule){
try {
Class obj_class = Class.forName(classname);
Field field = obj_class.getDeclaredField(filedName);
field.setAccessible(true);
field.set(obj, filedVaule);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Object invokeMethod(String class_name, String method_name, Object obj ,Class[] pareTyple, Object[] pareVaules){
try {
Class obj_class = Class.forName(class_name);
Method method = obj_class.getMethod(method_name,pareTyple);
return method.invoke(obj, pareVaules);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.koi.dexloader">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".Proxy"
android:theme="@style/Theme.DexLoader">
<meta-data
android:name="APPLICATION_CLASS_NAME"
android:value="org.koi.ctf20200802.APP"/>
<activity android:name="org.koi.ctf20200802.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
application>
manifest>
關于資源的問題
到目前為止,源程序能夠運行起來了,但是apk在運行的時候肯定會用到相關的資源,如布局文件等等,我們并沒有介紹如何處理資源。
資源有2中大的處理方法。第一種是在殼dex解壓出源apk時,把apk中的資源復制到現在程序下。第二種是替換殼apk中dex文件時,順便用源apk中的資源文件替換到殼apk中。因為本文不重點討論資源的處理問題,所以采用第二種方法,直接復制替換資源即可。
Dex組合修復工具
將APK和殼DEX文件合并,生成一個新的DEX文件,并且校正新的DEX文件頭。
加殼步驟
- src.apk:源APK。
- des.apk:殼APK。
- DexFixed.jar:Dex工具。
- classes.dex:des.apk中的classes.dex。
- res:源APK中的文件夾。
- resources.arsc:源APK中的文件。
1、用DEXFixed.jar工具把src.apk和classes.dex進行合并,生成一個新的Dex,替換到殼APK中。

2、替換殼APK中的classes.dex、res、resources.arsc。
3、apk重新簽名。
4、正常運行。

總結
dex殼是比較基礎的殼,只是將源APK加密后放入dex文件中,在運行時進行釋放。我們只需要在殼程序解密出原始的APK運行后,在內存中把dexdump下來就可以了,我們也可以用frida框架進行脫殼。
動態加載DEX(Java)
我們在上面動態加載APK時是采用了兩個工程,一個工程負責加載APK,一個負責業務流程,業務流程工程核心文件就是一個dex文件,可以考慮只將dex文件作為附件,然后進行動態加載dex。
項目實現demo代碼
簡單來說,這里存放git的鏈接。
源工程
- 新建一個簡單功能的 Android 工程。
- 創建assets文件夾。

保存編譯之后apk文件中的.dex文件,把.dex文件保存到assets目錄下。dex文件重命名為origin.dex(可以重命名為任意文件名)。
刪除MainActivity.java。注意:這里只刪除源文件,不要刪除Activity。
加密DEX
新建一個Java工程實現一個簡單的加密。
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Main {
public static void main(String[] args) {
if (args.length != 2)
{
System.out.println("jar : ");
return;
}
String sourceFile = args[0];
String encryptedFile = args[1];
try {
FileInputStream fis = new FileInputStream(sourceFile);
BufferedInputStream bis = new BufferedInputStream(fis);
FileOutputStream fos = new FileOutputStream(encryptedFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
byte[] buffer = new byte[10240];
int acount = 0;
while((acount = bis.read(buffer)) != -1) {
byte[] encryptedData = encrypt(buffer);
bos.write(encryptedData,0, acount);
}
bos.flush();
//關閉的時候只需要關閉最外層的流就行了
bos.close();
bis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] encrypt(byte[] sourceData)
{
for (int i = 0; i < sourceData.length; i++){
sourceData[i] ^= 273;
}
return sourceData;
}
}
把得到的加密文件放入剛剛創建的assets目錄下。
把重命名后的文件可以通過加密后再放入assets目錄下,然后再加載dex前進行解密。

殼工程
這里殼工程就在源工程的基礎上修改就可以了,不需要在新建一個工程。
分別創建ProxyApplication.java和RefInvoke.java。這兩個類的代碼和上面基本一樣,這里就不贅述了,直接看代碼。
ProxyApplication.java
package org.koi.ctf20210813;
import android.app.Application;
import android.content.Context;
import android.util.ArrayMap;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import dalvik.system.DexClassLoader;
public class P extends Application {
private final static String encryptedFileName = "flag";
private final static String package_name = "org.koi.ctf20210813";
private final static String activity_thread = "android.app.ActivityThread";
private final static String current_activity_thread = "currentActivityThread";
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
File cacheDir = getCacheDir();
if (!cacheDir.exists()){
cacheDir.mkdirs();
}
File outFile = new File(cacheDir, "out.dex");
InputStream is = getAssets().open(encryptedFileName);
FileOutputStream fos = new FileOutputStream(outFile);
byte[] buffer = new byte[1024];
int byteCount;
while ((byteCount = is.read(buffer)) != -1) {
buffer = decrypt(buffer);
fos.write(buffer, 0, byteCount);
}
fos.flush();
is.close();
fos.close();
String file_abs_path = outFile.getAbsolutePath();
Object currentActivityThread = I.invokeStaticMethod(activity_thread, current_activity_thread, new Class[]{}, new Object[]{});
ArrayMap mPackages = (ArrayMap)I.getFieldOjbect(activity_thread, currentActivityThread, "mPackages");
WeakReference weakReference = (WeakReference) mPackages.get(package_name);
ClassLoader parent = (ClassLoader)I.getFieldOjbect("android.app.LoadedApk", weakReference.get(), "mClassLoader");
DexClassLoader dLoader = null;
File dexOpt = base.getDir("dexOpt", base.MODE_PRIVATE);
dLoader = new DexClassLoader(file_abs_path, dexOpt.getAbsolutePath(), null, parent);
I.setFieldOjbect("android.app.LoadedApk", "mClassLoader", weakReference.get(), dLoader);
} catch (IOException e) {
e.printStackTrace();
}
}
public static byte[] decrypt(byte[] sourceData)
{
for (int i = 0; i < sourceData.length; i++){
sourceData[i] ^= 273;
}
return sourceData;
}
@Override
public void onCreate() {
super.onCreate();
}
}
DexClassLoader加載Dex文件:
DexClassLoader(dexPath, optimizedDirectory, libraryPath, parent) dexPath:目標類所在的APK或者jar包,/.../xxx.jar optimizedDirectory:從APK或者jar解壓出來的dex文件存放路徑 libraryPath:native庫路徑,可以為null parent:父類裝載器,一般為當前類的裝載器、
RefInvoke.java
package org.koi.ctf20210813;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class I {
public static Object invokeStaticMethod(String class_name, String method_name, Class[] pareTyple, Object[] pareVaules){
try {
Class obj_class = Class.forName(class_name);
Method method = obj_class.getMethod(method_name,pareTyple);
return method.invoke(null, pareVaules);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static Object getFieldOjbect(String class_name,Object obj, String filedName){
try {
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(filedName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void setFieldOjbect(String classname, String filedName, Object obj, Object filedVaule){
try {
Class obj_class = Class.forName(classname);
Field field = obj_class.getDeclaredField(filedName);
field.setAccessible(true);
field.set(obj, filedVaule);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Object invokeMethod(String class_name, String method_name, Object obj ,Class[] pareTyple, Object[] pareVaules){
try {
Class obj_class = Class.forName(class_name);
Method method = obj_class.getMethod(method_name,pareTyple);
return method.invoke(obj, pareVaules);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
AndroidManifest.xml
確認刪除MainActivity.java,然后修改AndroidManifest.xml。


這樣在執行時能解密原來dex文件。
APK中的DEX文件中,不包含重要代碼。

動態加載DEX(SO)
在上面的基礎上,想到可以把ProxyApplication.java和RefInvoke.java中的主要代碼移到so中來運行,這就是我們這種殼的主要思路。和上面的實現方式是一樣的,只是換到lib中運行而已。
創建一個Android 原生工程,和上面一樣,在MainActivity中寫一些簡單代碼。把dex文件加密后放入assets文件夾中。
新建ProxyApplication類,繼承Application,把加載Dex這部分代碼提取出來放入到一個新的類AttachBaseContent中。
ProxyApplication.java
import android.app.Application;
import android.content.Context;
public class P extends Application {
static {
System.loadLibrary("ctf20210814");
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
attachBase(base);
}
@Override
public void onCreate() {
super.onCreate();
}
public static native void attachBase(Context base);
}
這里新建一個koi.cpp文件,其中有一個Java_org_koi_dexsoshell_AttachBaseContext_onAttach函數,對應Java中AttachBaseContext類下的onAttach方法。
native-lib.cpp
#include
#include
// only update here
#define ENCRYPTED_FILE_NAME "flag"
#define DECRYPTED_FILE_NAME "ot.dex"
#define PACKAGE_NAME "org.koi.ctf20210814"
extern "C"
JNIEXPORT void JNICALL
Java_org_koi_ctf20210814_P_attachBase(JNIEnv *env, jclass clazz, jobject base) {
jclass clz_File = env->FindClass("java/io/File");
jclass clz_Context = env->FindClass("android/content/Context");
jclass clz_AssetManager = env->FindClass("android/content/res/AssetManager");
jclass clz_InputStream = env->FindClass("java/io/InputStream");
jclass clz_FileOutputStream = env->FindClass("java/io/FileOutputStream");
jclass clz_ActivityThread = env->FindClass("android/app/ActivityThread");
jclass clz_ArrayMap = env->FindClass("android/util/ArrayMap");
jclass clz_WeakReference = env->FindClass("java/lang/ref/WeakReference");
jclass clz_LoadedApk = env->FindClass("android/app/LoadedApk");
jclass clz_DexClassLoader = env->FindClass("dalvik/system/DexClassLoader");
jmethodID mid_File_init = env->GetMethodID(clz_File, "",
"(Ljava/io/File;Ljava/lang/String;)V");
jmethodID mid_FileOutputStream_init = env->GetMethodID(clz_FileOutputStream, "",
"(Ljava/io/File;)V");
jmethodID mid_DexClassLoader_init = env->GetMethodID(clz_DexClassLoader, "",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V");
jmethodID mid_Context_getCacheDir = env->GetMethodID(clz_Context, "getCacheDir",
"()Ljava/io/File;");
jmethodID mid_Context_getAssets = env->GetMethodID(clz_Context, "getAssets",
"()Landroid/content/res/AssetManager;");
jmethodID mid_Context_getDir = env->GetMethodID(clz_Context, "getDir",
"(Ljava/lang/String;I)Ljava/io/File;");
jmethodID mid_AssetManager_open = env->GetMethodID(clz_AssetManager, "open",
"(Ljava/lang/String;)Ljava/io/InputStream;");
jmethodID mid_File_exists = env->GetMethodID(clz_File, "exists", "()Z");
jmethodID mid_File_mkdirs = env->GetMethodID(clz_File, "mkdirs", "()Z");
jmethodID mid_File_getAbsolutePath = env->GetMethodID(clz_File, "getAbsolutePath",
"()Ljava/lang/String;");
jmethodID mid_InputStream_read = env->GetMethodID(clz_InputStream, "read", "([B)I");
jmethodID mid_InputStream_close = env->GetMethodID(clz_InputStream, "close", "()V");
jmethodID mid_InputStream_available = env->GetMethodID(clz_InputStream, "available", "()I");
jmethodID mid_FileOutputStream_write = env->GetMethodID(clz_FileOutputStream, "write",
"([BII)V");
jmethodID mid_FileOutputStream_flush = env->GetMethodID(clz_FileOutputStream, "flush", "()V");
jmethodID mid_FileOutputStream_close = env->GetMethodID(clz_FileOutputStream, "close", "()V");
jmethodID mid_ActivityThread_currentActivityThread = env->GetStaticMethodID(clz_ActivityThread,
"currentActivityThread",
"()Landroid/app/ActivityThread;");
jmethodID mid_ArrayMap_get = env->GetMethodID(clz_ArrayMap, "get",
"(Ljava/lang/Object;)Ljava/lang/Object;");
jmethodID mid_WeakReference_get = env->GetMethodID(clz_WeakReference, "get",
"()Ljava/lang/Object;");
jfieldID fid_ActivityThread_mPackages = env->GetFieldID(clz_ActivityThread, "mPackages",
"Landroid/util/ArrayMap;");
jfieldID fid_LoadedApk_mClassLoader = env->GetFieldID(clz_LoadedApk, "mClassLoader",
"Ljava/lang/ClassLoader;");
try {
jobject cacheDir = env->CallObjectMethod(base, mid_Context_getCacheDir);
if (!env->CallBooleanMethod(cacheDir, mid_File_exists)) {
env->CallBooleanMethod(cacheDir, mid_File_mkdirs);
}
jstring str = env->NewStringUTF(DECRYPTED_FILE_NAME);
jobject outFile = env->NewObject(clz_File, mid_File_init, cacheDir, str);
jobject AssetManager = env->CallObjectMethod(base, mid_Context_getAssets);
jstring out_file_name = env->NewStringUTF(ENCRYPTED_FILE_NAME);
jobject is = env->CallObjectMethod(AssetManager, mid_AssetManager_open, out_file_name);
jobject fos = env->NewObject(clz_FileOutputStream, mid_FileOutputStream_init, outFile);
jint file_size = env->CallIntMethod(is, mid_InputStream_available);
jbyteArray buffer = env->NewByteArray(file_size);
env->CallIntMethod(is, mid_InputStream_read, buffer); //read
jbyte* p_bt_ary = (jbyte*)env->GetByteArrayElements(buffer, 0);
// here you can add decryption function.
for (jint i = 0; i < file_size; ++i) {
p_bt_ary[i] ^= 273;
}
env->SetByteArrayRegion(buffer, 0, file_size, p_bt_ary);
env->CallVoidMethod(fos, mid_FileOutputStream_write, buffer, 0, file_size);
env->DeleteLocalRef(buffer);
env->CallVoidMethod(fos, mid_FileOutputStream_flush);
env->CallVoidMethod(is, mid_InputStream_close);
env->CallVoidMethod(fos, mid_FileOutputStream_close);
jstring file_abs_path = (jstring) env->CallObjectMethod(outFile, mid_File_getAbsolutePath);
jobject currentActivityThread = env->CallStaticObjectMethod(clz_ActivityThread,
mid_ActivityThread_currentActivityThread);
jobject mPackages = env->GetObjectField(currentActivityThread,
fid_ActivityThread_mPackages);
jstring package_name = env->NewStringUTF(PACKAGE_NAME);
jobject weakReference = env->CallObjectMethod(mPackages, mid_ArrayMap_get, package_name);
jobject loadedApk = env->CallObjectMethod(weakReference, mid_WeakReference_get);
jobject parent = env->GetObjectField(loadedApk, fid_LoadedApk_mClassLoader);
jstring jstr_dexOpt = env->NewStringUTF("dexOpt");
jobject dexOpt = env->CallObjectMethod(base, mid_Context_getDir, jstr_dexOpt, 0);
jstring dexOpt_abs_path = (jstring) env->CallObjectMethod(dexOpt, mid_File_getAbsolutePath);
jstring str_null = env->NewStringUTF("");
jobject dLoader = env->NewObject(clz_DexClassLoader, mid_DexClassLoader_init, file_abs_path,
dexOpt_abs_path, str_null, parent);
env->SetObjectField(loadedApk, fid_LoadedApk_mClassLoader, dLoader);
} catch (...) {}
}
這個例子中,進行加密解密的操作,可以根據實際情況進行修改。
JNI中有部分代碼可以提取到JNI_OnLoad或者initarray中進行處理。
JNI中的所有字符串可以進行一些處理,不直接暴露在源碼中。
AndroidManifest.xml
按照上面的方法進行修改。
注意:關閉minifyEnabled。
3、ELF文件殼
在學習這部分內容之前需要熟悉ELF的文件格式。
ELF節加密
主要思想
編寫代碼時:自定義一個代碼節(.mytext)(以后要進行加密,現在沒有處理),然后一個初始化函數(.init_array),在這個函數中找到elf文件加載到內存中的地址,然后根據elf文件格式找到.mytext節,對這個節的內容進行解密。
加密:在原始apk編譯好后,利用自己寫的代碼,把目標lib中的.mytext進行加密。
最后進行簽名。
代碼
創建一個ndk項目。編寫一段代碼放入自定義的(代碼)節.koitext中。
用__attribute__((section(".koitext")))來指定節。
#include
#include
#define SECTION_NAME ".koitext"
#define JNIHIDDEN __attribute__((visibility("hidden")))
// save the result.
int fw[40] = {13, 18, 14, 64, 11, 65, 16, 14, 20, 14, 11, 14, 18,
61, 12, 13, 60, 60, 20, 62, 16, 61, 61, 64, 63, 63, 15, 18, 12, 63, 14, 64,
13, 18, 14, 64, 11, 65, 16, 14};
int fs[38];
void str2ints (const char* fw, int* results);
char* jstring2charAry(JNIEnv* env, jstring jstr);
extern "C"
JNIEXPORT __attribute__((section(SECTION_NAME))) jboolean JNICALL
Java_org_koi_ctf20210821_MainActivity_checkflag(JNIEnv *env, jobject thiz, jstring flag) {
char fg[]="flag{helloboy_ewri346hHeewr34dr}";
str2ints(jstring2charAry(env, flag), fs);
for (int i = 0; i < strlen(fg); ++i) {
if(fw[i] != fs[i] )
return false;
}
return true;
}
__attribute__((section(SECTION_NAME))) void str2ints (const char* fw, int* results)
{
for (int mX4WyHKgmwSPY1V = 0; mX4WyHKgmwSPY1V < 32; mX4WyHKgmwSPY1V++){results[mX4WyHKgmwSPY1V] = fw[mX4WyHKgmwSPY1V];}
for (int _ZKdmdmEjiQ_Ouw = 0; _ZKdmdmEjiQ_Ouw < 32; _ZKdmdmEjiQ_Ouw++){results[_ZKdmdmEjiQ_Ouw] = results[_ZKdmdmEjiQ_Ouw] + 3;}
for (int zbTK_I56tB0GevN = 0; zbTK_I56tB0GevN < 32; zbTK_I56tB0GevN++){results[zbTK_I56tB0GevN] = results[zbTK_I56tB0GevN] + 10;}
for (int DfeXBWD6dcNPXKo = 0; DfeXBWD6dcNPXKo < 32; DfeXBWD6dcNPXKo++){results[DfeXBWD6dcNPXKo] = results[DfeXBWD6dcNPXKo] - 58;}
for (int jqJhXnPQwPYi2G6 = 0; jqJhXnPQwPYi2G6 < 32; jqJhXnPQwPYi2G6++){results[jqJhXnPQwPYi2G6] = results[jqJhXnPQwPYi2G6] - 66;}
for (int xA7fCVlKruHZC4Y = 0; xA7fCVlKruHZC4Y < 32; xA7fCVlKruHZC4Y++){results[xA7fCVlKruHZC4Y] = results[xA7fCVlKruHZC4Y] + 66;}
for (int sGVbaq_poAxfJ3O = 0; sGVbaq_poAxfJ3O < 32; sGVbaq_poAxfJ3O++){results[sGVbaq_poAxfJ3O] = results[sGVbaq_poAxfJ3O] + 8;}
for (int EIGWrEGI6UaAjH8 = 0; EIGWrEGI6UaAjH8 < 32; EIGWrEGI6UaAjH8++){results[EIGWrEGI6UaAjH8] = results[EIGWrEGI6UaAjH8] + 49;}
for (int nHJAUmNRoQs5M9k = 0; nHJAUmNRoQs5M9k < 32; nHJAUmNRoQs5M9k++){results[nHJAUmNRoQs5M9k] = results[nHJAUmNRoQs5M9k] + 11;}
for (int NzhuxVIobubHcRM = 0; NzhuxVIobubHcRM < 32; NzhuxVIobubHcRM++){results[NzhuxVIobubHcRM] = results[NzhuxVIobubHcRM] - 64;}
for (int Wa46hlZr0UFGqFu = 0; Wa46hlZr0UFGqFu < 32; Wa46hlZr0UFGqFu++){results[Wa46hlZr0UFGqFu] = results[Wa46hlZr0UFGqFu] + 4;}
}
JNIHIDDEN __attribute__((section(SECTION_NAME))) char* jstring2charAry(JNIEnv* env, jstring jstr)
{
jclass jcls_String = env->FindClass("java/lang/String");
jmethodID jmid_toCharArray = env->GetMethodID(jcls_String, "toCharArray", "()[C");
jmethodID jmid_length = env->GetMethodID(jcls_String, "length", "()I");
jcharArray charArray = (jcharArray)env->CallObjectMethod(jstr, jmid_toCharArray);
jint len = env->CallIntMethod(jstr, jmid_length);
char* pString = new char[len];
pString[len] = 0;
jboolean fals = false;
for (int i = 0; i < len; ++i) {
pString[i] = env->GetCharArrayElements(charArray, &fals)[i];
}
return pString;
}
寫一個初始化的函數,用來查找elf文件的基址以及給自定義的.koitext解密。
頭文件支持:
#include
#include
#include
#include
void init_native_Add() __attribute__((constructor));
unsigned long getLibAddr();
// loaded so file
#define SO_LIB_FILE_NAME "libctf20210821.so"
void init_native_Add(){
char name[15];
unsigned int nblock;
unsigned int nsize;
unsigned long base;
unsigned long text_addr;
unsigned int i;
Elf32_Ehdr *ehdr;
Elf32_Shdr *shdr;
base=getLibAddr(); //在/proc/id/maps文件中找到我們的so文件,活動so文件地址
ehdr=(Elf32_Ehdr *)base;
text_addr=ehdr->e_shoff+base;//加密節的地址
nblock=ehdr->e_entry >>16;//加密節的大小
nsize=ehdr->e_entry&0xffff;//加密節的大小
printf("nblock = %d", nblock);
//修改內存權限
if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
puts("mem privilege change failed");
}
//進行解密,是針對加密算法的
for(i=0;i
char *addr=(char*)(text_addr+i);
*addr=~(*addr);
}
if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){
puts("mem privilege change failed");
}
puts("Decrypt success");
}
//獲取到SO文件加載到內存中的起始地址,只有找到起始地址才能夠進行解密;
unsigned long getLibAddr(){
unsigned long ret=0;
char name[] = SO_LIB_FILE_NAME;
char buf[4096];
char *temp;
int pid;
FILE *fp;
pid=getpid();
sprintf(buf,"/proc/%d/maps",pid); //這個文件中保存了進程映射的模塊信息 cap /proc/id/maps 查看
fp=fopen(buf,"r");
if(fp==NULL){
puts("open failed");
goto _error;
}
while (fgets(buf,sizeof(buf),fp)){
if(strstr(buf,name)){
temp = strtok(buf, "-"); //分割字符串,返回 - 之前的字符
ret = strtoul(temp, NULL, 16); //獲取地址
break;
}
}
_error:
fclose(fp);
return ret;
}
效果:

ida會提示elf文件錯誤。

節表解析錯誤。


附加加密代碼:
#include
#include
#include
typedef uint32_t Elf32_Addr; // Program address
typedef uint32_t Elf32_Off; // File offset
typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;
enum {
EI_MAG0 = 0, // File identification index.
EI_MAG1 = 1, // File identification index.
EI_MAG2 = 2, // File identification index.
EI_MAG3 = 3, // File identification index.
EI_CLASS = 4, // File class.
EI_DATA = 5, // Data encoding.
EI_VERSION = 6, // File version.
EI_OSABI = 7, // OS/ABI identification.
EI_ABIVERSION = 8, // ABI version.
EI_PAD = 9, // Start of padding bytes.
EI_NIDENT = 16 // Number of bytes in e_ident.
};
struct Elf32_Ehdr {
unsigned char e_ident[EI_NIDENT]; // ELF Identification bytes
Elf32_Half e_type; // Type of file (see ET_* below)
Elf32_Half e_machine; // Required architecture for this file (see EM_*)
Elf32_Word e_version; // Must be equal to 1
Elf32_Addr e_entry; // Address to jump to in order to start program
Elf32_Off e_phoff; // Program header table's file offset, in bytes
Elf32_Off e_shoff; // Section header table's file offset, in bytes
Elf32_Word e_flags; // Processor-specific flags
Elf32_Half e_ehsize; // Size of ELF header, in bytes
Elf32_Half e_phentsize; // Size of an entry in the program header table
Elf32_Half e_phnum; // Number of entries in the program header table
Elf32_Half e_shentsize; // Size of an entry in the section header table
Elf32_Half e_shnum; // Number of entries in the section header table
Elf32_Half e_shstrndx; // Sect hdr table index of sect name string table
unsigned char getFileClass() const { return e_ident[EI_CLASS]; }
unsigned char getDataEncoding() const { return e_ident[EI_DATA]; }
};
// Program header for ELF32.
struct Elf32_Phdr {
Elf32_Word p_type; // Type of segment
Elf32_Off p_offset; // File offset where segment is located, in bytes
Elf32_Addr p_vaddr; // Virtual address of beginning of segment
Elf32_Addr p_paddr; // Physical address of beginning of segment (OS-specific)
Elf32_Word p_filesz; // Num. of bytes in file image of segment (may be zero)
Elf32_Word p_memsz; // Num. of bytes in mem image of segment (may be zero)
Elf32_Word p_flags; // Segment flags
Elf32_Word p_align; // Segment alignment constraint
};
// Section header.
struct Elf32_Shdr {
Elf32_Word sh_name; // Section name (index into string table)
Elf32_Word sh_type; // Section type (SHT_*)
Elf32_Word sh_flags; // Section flags (SHF_*)
Elf32_Addr sh_addr; // Address where section is to be loaded
Elf32_Off sh_offset; // File offset of section data, in bytes
Elf32_Word sh_size; // Size of section, in bytes
Elf32_Word sh_link; // Section type-specific header table index link
Elf32_Word sh_info; // Section type-specific extra information
Elf32_Word sh_addralign; // Section address alignment
Elf32_Word sh_entsize; // Size of records contained within the section
};
long get_file_size(FILE* pf);
int main()
{
char elf_name[64] = "C:\\Users\\koi\\Desktop\\libnative-lib.so";
char want2encrypt_section_name[] = ".mytext";
FILE* pf_elf = fopen(elf_name, "rb");
long sz_file = get_file_size(pf_elf);
char* file_buf = new char[sz_file];
fread(file_buf, sz_file, 1, pf_elf);
Elf32_Ehdr* ehdr = (Elf32_Ehdr*)(file_buf);
// 字符串節頭表的位置
Elf32_Shdr* shdrstr = (Elf32_Shdr*)(file_buf + ehdr->e_shoff + sizeof(Elf32_Shdr) * ehdr->e_shstrndx);
char* sh_str = (char*)(file_buf + shdrstr->sh_offset);//偏移到字符串表
Elf32_Shdr* shdr = (Elf32_Shdr*)(file_buf + ehdr->e_shoff);
int encrypt_foffset = 0;
int encrypt_size = 0;
for (int i = 0; i < ehdr->e_shnum; i++, shdr++)
{
//根據字符串表的名稱比較
if (strcmp(sh_str + shdr->sh_name, want2encrypt_section_name) == 0)
{
encrypt_foffset = shdr->sh_offset;
encrypt_size = shdr->sh_size;
break;
}
}
char* content = (char*)(file_buf + encrypt_foffset);
int block_size = 16;
int nblock = encrypt_size / block_size;
int nsize = encrypt_foffset / 4096 + (encrypt_foffset % 4096 == 0 ? 0 : 1);
printf("base = 0x%x, length = 0x%x", encrypt_foffset, encrypt_size);
printf("nblock = %d, nsize = %d", nblock, nsize);
//將節的地址和大小寫入
ehdr->e_entry = (encrypt_size << 16) + nsize;
ehdr->e_shoff = encrypt_foffset; //節的地址
//加密
for (int i = 0; i < encrypt_size; i++) {
content[i] = ~content[i];
}
strcat(elf_name, "_m");
FILE* m_elf_file = fopen(elf_name, "wb");
fwrite(file_buf, sz_file, 1, m_elf_file);
return 0;
}
long get_file_size(FILE* pf)
{
long cur_pos = ftell(pf);
fseek(pf, 0, SEEK_END);
long sz_file = ftell(pf);
fseek(pf, cur_pos, SEEK_SET);
return sz_file;
}