Reinforcement method of Android APK

Where there are people, there is competition. The development of Android is accompanied by the development of reverse and secure reinforcement. Reverse workers can accelerate the speed of reverse through some very easy-to-use software, such as IDA, JEB, etc; Application developers will also use various means to prevent reverse workers from reversing their applications.

However, in most cases, it is impossible to prevent the reverse completely. We can only improve the difficulty of the reverse application by other means, so that the reverse workers need (can not bypass) to spend enough time to reverse the application successfully. In practice, we can adopt this idea for protection as long as it does not significantly affect the running speed of the application.

In this context, inflation and confusion came into being, which is our first way of protection. In this way, the method name and variable name in the code are named in a very confusing way, such as 0, O, O, l, I, 1, etc. In addition to this confusion, in order to improve the reverse workload, methods with the same or similar method names will be added to the code, or some unnecessary parent classes will be added to achieve the purpose of code expansion.

However, this method can not stop reverse workers. Later, developers found that DEX files can be loaded dynamically through the DexClassLoader class. In general, we call this kind of reinforcement as shell. Because this kind of reinforcement is dynamically loading DEX files, we can generally call it DEX shell. However, Android is mainly written by java code, and Java code is very easy to be reverse analyzed, so gradually put the code dynamically loaded with DEX into the so layer for operation. The code in the so layer is mainly c/c + + code. The reverse difficulty is much higher than Java, which improves the security of the application.

With the continuous popularization of this reinforcement method, this method can not stop most of the reverse work. Reinforcement personnel need a new reinforcement method to fight against the reverse. In the later stage, whether reinforcement or reverse, they all focused on the confrontation of so layer. At this time, they found a method of reinforcement using elf file format (the shared library of. So in Android is elf file format).

In so, we define a section in which we store some of our key function codes. We encrypt this part of the code through elf file format, and then decrypt the encrypted code when the elf file loads and executes the initialization function array.

Advantages of shelling or other reinforcement: it can protect its core code algorithm to a certain extent, improve the difficulty of cracking, piracy or secondary packaging, and prevent code injection, dynamic debugging and memory injection attacks.

Disadvantages of shell or other reinforcement: in theory, as long as protection is added, it may affect the compatibility and operation efficiency of the application.

Due to the limitations of Android phone's battery, CPU and other hardware, it is impossible for general applications to carry out very strong protection like PC.

Confusion and expansion

confusion

Theme: replace the original meaningful class name with meaningless characters, such as a, b, c or easily confused characters, such as 0, O, O, l, I and 1.

Parameter configuration: change the value of minifyEnabled under release to true to open confusion; Add shrinkResources true to turn on resource compression.

#The compression level is 0-7, and Android is generally 5 (the number of code iteration optimization)
-optimizationpasses 5

#Do not use mixed case class names
-dontusemixedcaseclassnames

#Log in case of confusion
-verbose

#No warning org greenrobot. greendao. Applications not applied in database package and its sub packages
-dontwarn org.greenrobot.greendao.database.**
-dontwarn rx.**
-dontwarn org.codehaus.jackson.**

......
#Keep the classes and class members of the jackson package and its sub packages from being confused
-keep class org.codehaus.jackson.** {*;}
#---------Important note-------
#-keep class class name {*;}
#-keepclassmembers class name {*;}
#A * indicates that the class name under the package is kept from being confused;
# -keep class org.codehaus.jackson.*
# The second * * means to keep the class names under the package and all its sub packages from being confused
# -keep class org.codehaus.jackson.**
#---------------------------------
#Keep the class name, methods and variables in the class from being confused
-keep class org.codehaus.jackson.** {*;|
#Do not confuse the class name of class ClassTwoOne and the public members and methods in the class
#Public can be replaced by other java attributes, such as private, public static, final, etc
#You can also make < init > represent construction methods, < methods > represent methods, and < Fields > represent members,
#These can also be preceded by java attributes such as public
-keep class com.dev.demo.two.ClassTwoOne {
    public *;
}
#Don't confuse types and constructors inside
-keep class com.dev.demo.ClassOne {
    public <init>();
}
#Do not confuse the class name and the constructor inside
-keep class com.dev.demo.ClassOne {
    public <init>();
}
#Do not confuse class names and constructors with int arguments
-keep class com.dev.demo.two.ClassTwoTwo {
    public <init>(int);
}
#Do not confuse public modified methods of classes with private modified variables
-keepclassmembers class com.dev.demo.two.ClassTwoThree {
    public <methods>;
    private <fields>;
}

#Do not confuse internal classes, you need to modify them with $
#Do not confuse the inner class ClassTwoTwoInner and all its members
-keep class com.dev.demo.two.ClassTwoTwo$ClassTwoTwoInner{*;}

More configuration References:

https://juejin.cn/post/6844903471095742472

https://www.huaweicloud.com/articles/ae151e2f60923097cefc473bd131addf.html

expand

Code confusion can increase the difficulty of reverse to a certain extent, but the increased workload for reverse workers is relatively small. Code expansion can increase the total amount of code, so that reverse workers must analyze all the code to get some final results. Code inflation is also one of the initial defense methods. The main idea is to write some garbage code to expand the amount of code, which may consume a lot of time for attackers in reverse analysis, so as to protect APK.

There are many kinds of implementation ideas for expansion code. For example, multiplication is changed to addition, addition is changed to self addition, and so on. As long as the code quantity is increased, it will not affect the realization of the function.

Here I wrote a simple automatic code generation project.

https://gitee.com/koifishly/function_generator

DEX shell

Before, the main code of Android was Java code, but in reverse analysis, Java code is easy to be analyzed. To solve this problem, we want to dynamically load our Java code (. DEX file) after the app runs. This method mainly uses the DexClassLoader class to realize dynamic loading. The DexClassLoader class supports dynamic loading apk or dex.

Dynamic loading APK

Dynamic loading apk is simply to load the compiled apk Apk files are put into a In the dex file. This The apk file is our real application. The following is the apk source apk The dev file is for another project dex file. This project is mainly to release the source apk at runtime, and then transfer the process to the source apk for execution.

According to the above schematic diagram, we need 3 objects

  • Source apk: apk to be shelled.
  • Shell apk: decrypt and restore apk and execute.
  • Encryption tool: combine the source apk and shell dex into a new dex and correct the new dex.

Project implementation demo code

IDE: Android Studio 4.1.3

Android version: 4.4 +

Project source code: nisosaikou/AndroidDEX shell - Code cloud - Open Source China (gitee.com)

Source APK

1. Write functional logic code normally. The code here is a simple ctf judgment code.

 2. Create a new APP class, which inherits from Application class and implements onCreate method.

 3. Generate a release version apk and save the apk.

Modify MainActivity The parent class of Java makes MainActivity inherit from Activity. Change the text display to run the source APK.

 

Shell APK

Proxy.java

Create a new Proxy class called Proxy, which inherits from Application class. This class is used to release and decrypt the original APK.

attachBaseContext()

Override the attachBaseContext method in the Application. This method will be executed before the onCreate method of the Activity.

The functions realized by this method mainly include:

Release the source apk contained in the shell dex.

Encrypt the released apk.

Copy the files in the lib directory in the source apk to the path of the current program (shell).

Create a new DexClassLoader and replace it with the DexClassLoader of the parent node.

DexClassLoader Inherited from BaseDexClassLoader,This is flexible. Each parameter can be customized. We usually use this to load customized parameters apk/dex/jar File.

Code example:

@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()

Load source apk resources

Get mainifest The startup class name of the source apk recorded in XML.

Set the ActivityThread information (Android. App. ActivityThread - > currentactivitythread).

Code example:

@Override
public void onCreate() {
    super.onCreate();
    // Source apk startup class
    String srcAppClassName = "";
    // Path of original 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 is configured in the xml file.
        }
        else {
            return;
        }
    }
    catch (Exception e)
    {
    }
 
    //Get the member property LoadedApk info of AppBindData class under ActivityThread class;
    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");
 
    // Empty the original loadedApkInfo
    RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, null);
 
    // Get Application of shell thread
    Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication");
    ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications");
    mAllApplications.remove(oldApplication);
    // Construct a new Application
    // 1. Update 2 classnames
    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. Register application
    Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication", loadedApkInfo, new Class[] { boolean.class, Instrumentation.class }, new Object[] { false, null });
 
    //Replace mInitialApplication in ActivityThread
    RefInvoke.setFieldOjbect("android.app.ActivityThread", "mInitialApplication", currentActivityThread, app);
    //Replace the previous content provider with the newly registered 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 function

It manages the execution of the main thread of the application process (equivalent to the main entry function of ordinary Java programs), and is responsible for scheduling and executing activities, broadcasts and other operations according to the requirements of AMS (through the iaapplicationthread interface, AMS is Client and ActivityThread.ApplicationThread is Server).

In the Android system, by default, all components in an application (such as Activity, BroadcastReceiver and Service) will be executed in the same process, and the [main thread] of the process is responsible for the execution.

In the Android system, if there is a special specification (through android:process), specific components can also run in different processes. No matter which process the components run in, by default, they are executed by the [main thread] of this process.

[main thread] the main thread is usually too busy to handle both UI events of Activity components and background Service work of Service. In order to solve this problem, the main thread can create multiple sub threads to handle background Service work, and concentrate on UI events.

Class structure reference

Call the currentActivityThread method to get the member variable scurrentialactivitythread in the ActivityThread.

Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});

 

Gets the mBoundApplication in the currentactivitythread.

Object mBoundApplication = RefInvoke.getFieldObject("android.app.ActivityThread", currentActivityThread, "mBoundApplication");

Get the member variable info in mBoundApplication.

Object loadedApkInfo = RefInvoke.getFieldObject("android.app.ActivityThread$AppBindData", mBoundApplication, "info");

By observing the LoadedApk class, you can find some important properties, which will be used below.

Leave the mApplication property in info blank.

RefInvoke.setFieldObject("android.app.LoadedApk", "mApplication", loadedApkInfo, null);

Remove munitialapplication from the linked list mllapplications under currentactivitythread. Uninitialapplication stores the initialized applications (current shell applications), and millapplications stores all applications.

Remove the current application from the existing application, and then add the newly built application to it.

Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication");ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications");mAllApplications.remove(oldApplication);

Construct a new Application


Update 2 classnames.

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;

Register the application (registered with the makeApplication method in LoadedApk).

Application app = (Application) RefInvoke.invokeMethod("android.apo.LoadedApk", "makeApplication", loadedApkInfo, new Class[] { boolean.class, Instrumentation.class }, new Object[] {false, null});

Replace mInitialApplication with the app just created.

RefInvoke.setFieldObject("android.app.ActivityThread", "mInitialApplication",
 currentActivityThread, app);

Update ContentProvider.

ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldObject("android.app.ActivityThread", currentActivityThread, "mProviderMap");

Iterator it = mProviderMap.values().iterator();
while (it.hasNext()) {
    Object providerClientRecord = it.next();
    Object localProvider = RefInvoke.getFieldObject("android.app.ActivityThread$ProviderClientRecord", providerClientRecord, "mLocalProvider");
    RefInvoke.setFieldObject("android.content.ContentProvider", "mContext", localProvider, app);
}

Execute the onCreate method of the new app.

app.onCreate();

RefInvoke.java

Method called by Java reflection.

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[] pareType, Object[] pareValues) {
        try {
            Class obj_class = Class.forName(class_name);
            Method methodd = obj_class.getMethod(method_name, pareTyple);
            return method.invoke(null, pareValues);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static Object getFieldObject(String class_name, Object obj, String fieldName) {
        try {
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(fieldName);
            field.setAccessible(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void setFieldObject(String classname, String fieldName, Object obj, Object fieldValue) {
        try {
            Class obj_class = Class.forName(classname);
            Field field = obj_class.getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(obj, fieldValue);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static Object invokeMethod(String class_name, String method_name, Object obj, Class[] pareTyple, Object[] pareValues) {
        try {
            Class obj_class = Class.forName(class_name);
            Method method = obj_class.getMethod(method_name, pareTyple);
            return method.invoke(obj, pareValues);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?><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>

Questions about resources

So far, the source program can run, but apk will certainly use relevant resources, such as layout files, etc. we did not introduce how to deal with resources.

There are two major processing methods for resources. The first is to copy the resources in apk to the current program when the shell dex decompresses the source apk. The second is to replace the dex file in the shell apk with the resource file in the source apk. Because this paper does not focus on the processing of resources, the second method is to directly copy and replace resources.

Dex combination repair tool

Merge APK and shell DEX files to generate a new DEX file and correct the new DEX file header.

Shelling steps

  • src.apk: source APK.

  • des.apk: shell APK.

  • DexFixed.jar: Dex tool

  • classes.dex: des.apk's classes dex.

  • res: folder in the source APK.

  • resources.arsc: files in the source APK.

1. Use dexfixed The jar tool puts Src Apk and classes Dex is merged to generate a new Dex and replaced in the shell APK.

2. Replace classes. In shell APK dex,res,resources.arsc.

3. apk re sign.

4. Normal operation.

summary

The dex shell is a basic shell. It only encrypts the source APK and puts it into the dex file for release at run time. After the shell program decrypts the original APK and runs, we only need to dump the dexdump in memory. We can also use frida framework for shelling.

Dynamically load DEX (Java)

We used two projects to dynamically load APK. One project is responsible for loading APK and the other is responsible for business process. The core file of business process project is a dex file. We can consider only using the dex file as an attachment, and then dynamically load dex.

Project implementation demo code

Simply put, here is the link to git.

Source engineering

  • Create a new Android project with simple functions.

  • Create the assets folder.

Save the apk file after compilation dex file, put Save the dex file to the assets directory. Rename the dex file to origin dex (can be renamed to any file name).

Delete mainactivity java. Note: only the source file is deleted here, not the Activity.

Encrypted DEX

Create a new Java project to implement a simple encryption.

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 : <source file> <encrypted file>");
            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) {

            }
            bos.flush();
            //When closing, you only need to close the outermost stream
            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;
    }
}

Put the encrypted file into the assets directory just created.

The renamed file can be encrypted and then put into the assets directory, and then decrypted before loading dex.

Shell Engineering

Here, the shell project can be modified based on the source project without creating a new project.

Create proxyapplication Java and refinvoke java. The codes of these two classes are basically the same as those above. I won't repeat them here. Just look at the code.

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 loads Dex files:

DexClassLoader(dexPath, optimizedDirectory, libraryPath, parent)
 
dexPath: Where the target class is located APK perhaps jar Package,/.../xxx.jar
 
optimizedDirectory: from APK perhaps jar Extracted dex File storage path
 
libraryPath: native Library path, which can be null
 
parent: The parent class loader is generally the loader of the current class

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

Confirm to delete mainactivity Java, and then modify androidmanifest xml.

In this way, the original dex file can be decrypted during execution.

The DEX file in APK does not contain important code.

Dynamic load DEX (SO)

Based on the above, I thought that proxyapplication Java and refinvoke The main code in Java is moved to so to run, which is the main idea of our shell. The implementation method is the same as the above, but it is just run in lib.

Create an Android native project. As above, write some simple code in MainActivity. Encrypt the dex file and put it into the assets folder.

Create a new ProxyApplication class, inherit the Application, extract the code of loading Dex and put it into a new class AttackBaseContent.

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);
}

Create a new Koi Cpp file, which has a Java_org_koi_dexsoshell_AttachBaseContext_onAttach function, corresponding to the onAttach method under attachbasecontext class in Java.

native-lib.cpp

#include <jni.h>
#include <string>
 
// 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, "<init>",
                                               "(Ljava/io/File;Ljava/lang/String;)V");
    jmethodID mid_FileOutputStream_init = env->GetMethodID(clz_FileOutputStream, "<init>",
                                                           "(Ljava/io/File;)V");
    jmethodID mid_DexClassLoader_init = env->GetMethodID(clz_DexClassLoader, "<init>",
                                                         "(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 (...) {}
}

In this example, the operation of encryption and decryption can be modified according to the actual situation.

Some code in JNI can be extracted from JNI_OnLoad or initarray.

All strings in JNI can be processed without being directly exposed to the source code.

AndroidManifest.xml

Modify according to the above method.

Note: turn off minifyEnabled.

ELF file shell

Before learning this part, you need to be familiar with ELF file format.

ELF section encryption

Main ideas

When writing code: customize a code section (. mytext) (to be encrypted later, but not processed now), and then an initialization function (. init_array). In this function, find the address where the elf file is loaded into memory, and then find it according to the elf file format mytext section to decrypt the contents of this section.

Encryption: after the original apk is compiled, use the code written by yourself to encrypt the data in the target lib mytext for encryption.

Finally, sign.

code

Create an ndk project. Write a piece of code into a custom section koitext.

Use__ attribute__((section(".koitext")) to specify the section.

#include <jni.h>
#include <string>
 
#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;
}

Write an initialization function to find the base address of elf file and give custom koitext decryption.

Header file support:

#include<sys/types.h>
#include<unistd.h>
#include <sys/mman.h>
#include <elf.h>
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(); //Find our so file in the / proc/id/maps file and the address of the active so file
    ehdr=(Elf32_Ehdr *)base;
    text_addr=ehdr->e_shoff+base;//Address of encryption section
    nblock=ehdr->e_entry >>16;//Size of encrypted section
    nsize=ehdr->e_entry&0xffff;//Size of encrypted section
    printf("nblock = %d\n", nblock);
    //Modify memory permissions
    if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
        puts("mem privilege change failed");
    }
    //Decryption is for the encryption algorithm
    for(i=0;i<nblock;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");
}
//Obtain the starting address of the SO file loaded into the memory, and decrypt only when the starting address is found;
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);  //The module information of process mapping is saved in this file. View it in 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, "-");  //Split string, return - previous characters
            ret = strtoul(temp, NULL, 16);  //Get address
            break;
        }
    }
    _error:
    fclose(fp);
    return ret;
}

effect:

ida will prompt elf file error.

Section table parsing error.

Additional encryption code:

#include <stdio.h>
#include <cstdint>
#include <string.h>
 
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);
    //The location of the string section header table
    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); //Offset to string table
    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++) {
        //Compare by name of string table
        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\n", encrypt_foffset, encrypt_size);
    printf("nblock = %d, nsize = %d\n", nblock, nsize);

    // Write the address and size of the section to
    ehdr->e_entry = (encrypt_size << 16) + nsize;
    ehdr->e_shoff = encrypt_foffset; //Section address

    // encryption
    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;
}

Keywords: Android Web Security

Added by diesel on Wed, 15 Dec 2021 00:01:49 +0200