Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Android APK Encryption with First-Generation Packers

Tech 1

A first-generation packer encrypts the entire source APK and appends it to a stub (shell) DEX file. The stub DEX contains decryption and dynamic loading code, which extracts and loads the original APK at runtime. As it involves encrypting the whole APK, this method is resource-intensive for large applications and was largely abandoned early on. Nevertheless, its core concepts remain foundational.

Encryption techniques can be subdivided into several areas:

  • String encryption within DEX files.
  • Static encryption and decryption of the complete DEX.
  • Resource encryption (XML and ARSC files, sometimes in hexadecimal).
  • Anti-decompilation tactics targeting tools like apktool, exploiting their inherent limitations.
  • Anti-debugging via Ptrace and TracePid value verification.
  • Custom DexClassLoader implementations for handling DEX protection and packing.
  • Disk-loading (where the decrypted DEX becomes visible in the APK's directory).

Application Launch Flow Analysis

Understanding the application startup sequence is crucial, as packers must intervene before the main execution begins. The key flow relevant to first-generation packers is:

  1. ActivityThread.main(): The application entry point.
  2. ActivityThread.handleBindApplication(): Instantiates the application.
  3. LoadedApk.makeApplication(): Creates the Application object.
  4. Instrumentation.newApplication(): Instantiates the Application class.
  5. Application.attachBaseContext(): The first callback available for the Application object.
  6. Application.onCreate(): Called after attachBaseContext.

After the Application startup flow concludes, the MainActivity's attachBaseContext() and onCreate() methods are invoked. Therefore, a packer must perform dynamic loading and classloader adjustments during the Application's initialization, typically within attachBaseContext() or onCreate(). In essence, the original application is concealed, and an outer "shell" program is wrapped around it. This shell decrypts and loads the original application via reflection.

Implementation Process

The implementation involves three main components:

  1. Source Application: A standard Android project compiled into an APK that will be hidden.
  2. Packing Tool: A separate program (e.g., in Python) that merges the source APK with the shell stub.
  3. Stub (Shell) Program: An Android project that decrypts, loads, and executes the original application.

The final packaged APK must have the stub program as its entry point.

Constructing the Packed APK

The tool concatenates the source APK data to the end of the stub's classes.dex file and appends the size of the APK data. It then updates the DEX file's header fields (file_size, checksum, signature) accordingly.

Example Python Packing Script (Core Logic):

import hashlib
import struct
import zlib

def pack_apk(stub_dex_path, source_apk_path, output_dex_path):
    with open(stub_dex_path, 'rb') as f:
        stub_data = f.read()
    with open(source_apk_path, 'rb') as f:
        apk_data = f.read()

    # Optional: Process (encrypt/compress) the APK data here
    processed_apk = process_data(apk_data)
    apk_size = len(processed_apk)

    # Build the new DEX: Stub + APK + APK size (8 bytes, little-endian)
    new_dex = stub_data + processed_apk + struct.pack('<Q', apk_size)

    # Update DEX header fields (offsets are simplified for illustration)
    # Update file size (offset 0x20)
    total_len = len(new_dex)
    new_dex = new_dex[:0x20] + struct.pack('<I', total_len) + new_dex[0x24:]
    # Update SHA-1 signature (offset 0x0C)
    sha1_digest = hashlib.sha1(new_dex[0x20:]).digest()
    new_dex = new_dex[:0x0C] + sha1_digest + new_dex[0x20:]
    # Update Adler-32 checksum (offset 0x08)
    checksum = zlib.adler32(new_dex[0x0C:])
    new_dex = new_dex[:0x08] + struct.pack('<I', checksum) + new_dex[0x0C:]

    with open(output_dex_path, 'wb') as f:
        f.write(new_dex)

The Stub (Shell) Program

The shell's Application class (e.g., ProxyApplication) overrides attachBaseContext() to extract and load the original APK.

1. Reading and Extracting the Original APK

public class ProxyApplication extends Application {
    private String extractedApkPath;

    @Override
    protected void attachBaseContext(Context ctx) {
        super.attachBaseContext(ctx);
        try {
            // 1. Read the shell's own APK and locate the DEX
            ZipFile apkZip = new ZipFile(ctx.getApplicationInfo().sourceDir);
            ZipEntry dexEntry = apkZip.getEntry("classes.dex");
            InputStream is = apkZip.getInputStream(dexEntry);
            byte[] combinedData = readStreamFully(is);
            is.close();
            apkZip.close();

            // 2. Extract the original APK from the end of the DEX
            long apkSize = ByteBuffer.wrap(combinedData, combinedData.length - 8, 8)
                                     .order(ByteOrder.LITTLE_ENDIAN).getLong();
            int startIndex = combinedData.length - 8 - (int) apkSize;
            byte[] originalApkData = Arrays.copyOfRange(combinedData, startIndex, combinedData.length - 8);
            // Optional: Decrypt/decompress originalApkData here

            // 3. Write the original APK to a private directory
            File outDir = ctx.getDir("extracted", Context.MODE_PRIVATE);
            File apkFile = new File(outDir, "original.apk");
            FileOutputStream fos = new FileOutputStream(apkFile);
            fos.write(originalApkData);
            fos.close();
            apkFile.setReadOnly(); // Required for Android 14+
            extractedApkPath = apkFile.getAbsolutePath();
        } catch (Exception e) {
            throw new RuntimeException("Failed to extract APK", e);
        }
    }

    private byte[] readStreamFully(InputStream is) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        byte[] data = new byte[4096];
        int nRead;
        while ((nRead = is.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, nRead);
        }
        return buffer.toByteArray();
    }
}

2. Loading the Original APK and Hooking the ClassLoader

After extraction, the stub must load the DEX from the original APK and ensure the system uses the correct ClassLoader to find the original app's classes (like its MainActivity). The key is to replace the mClassLoader field within the LoadedApk object associated with the current process.

private void loadOriginalApp(Context ctx) {
    try {
        // Create directories for optimized DEX and native libraries
        File optDir = ctx.getDir("odex", Context.MODE_PRIVATE);
        File libDir = ctx.getDir("libs", Context.MODE_PRIVATE);
        ClassLoader parentLoader = getClassLoader(); // The shell's PathClassLoader

        // Load the original APK's DEX
        DexClassLoader originalLoader = new DexClassLoader(
                extractedApkPath,
                optDir.getAbsolutePath(),
                libDir.getAbsolutePath(),
                parentLoader
        );

        // Use reflection to replace the LoadedApk's class loader
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
        Object activityThread = currentActivityThread.invoke(null);

        Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        ArrayMap<String, ?> mPackages = (ArrayMap<String, ?>) mPackagesField.get(activityThread);

        String packageName = ctx.getPackageName();
        WeakReference<?> wr = (WeakReference<?>) mPackages.get(packageName);
        Object loadedApk = wr.get();

        Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
        Field mClassLoaderField = loadedApkClass.getDeclaredField("mClassLoader");
        mClassLoaderField.setAccessible(true);
        mClassLoaderField.set(loadedApk, originalLoader);

        // Preload the main activity class to ensure it's found
        originalLoader.loadClass("com.example.originalapp.MainActivity");

    } catch (Exception e) {
        Log.e("ProxyApplication", "Failed to load original app", e);
    }
}

3. Replacing the Application Instance (Optional)

If the original APK has its own custom Application class, the stub needs to replace the current Application instance with the original one to ensure its lifecycle methods (onCreate(), etc.) are called. This involves more complex reflection to manipulate ActivityThread and LoadedApk fields.

private void replaceApplicationInstance() {
    try {
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentAt = activityThreadClass.getDeclaredMethod("currentActivityThread");
        Object at = currentAt.invoke(null);

        // Get the LoadedApk and clear its mApplication field
        Field mBoundAppField = activityThreadClass.getDeclaredField("mBoundApplication");
        mBoundAppField.setAccessible(true);
        Object boundAppData = mBoundAppField.get(at);
        Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
        Field infoField = appBindDataClass.getDeclaredField("info");
        infoField.setAccessible(true);
        Object loadedApk = infoField.get(boundAppData);

        Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
        Field appField = loadedApkClass.getDeclaredField("mApplication");
        appField.setAccessible(true);
        appField.set(loadedApk, null);

        // Update the application class name in AppInfo
        Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
        ApplicationInfo appInfo = (ApplicationInfo) appInfoField.get(boundAppData);
        appInfo.className = "com.example.originalapp.OriginalApplication"; // Original app's Application class

        // Create the new Application instance
        Method makeApp = loadedApkClass.getDeclaredMethod("makeApplication", boolean.class, Instrumentation.class);
        makeApp.setAccessible(true);
        Application originalApp = (Application) makeApp.invoke(loadedApk, false, null);

        // Replace the ActivityThread's mInitialApplication
        Field initAppField = activityThreadClass.getDeclaredField("mInitialApplication");
        initAppField.setAccessible(true);
        initAppField.set(at, originalApp);

        // Call the original Application's onCreate()
        originalApp.onCreate();
    } catch (Exception e) {
        Log.e("ProxyApplication", "Failed to replace Application", e);
    }
}

Key Implementation Notes

  • Android 14+ Restriction: DEX files loaded via DexClassLoader must be read-only. Ensure the extracted APK file has its permissions set accordingly (setReadOnly()).
  • ClassLoader Hierarchy: The standard approach replaces the mClassLoader in LoadedApk. An alternative method involves inserting a custom ClassLoader into the parent delegation chain between PathClassLoader and BootClassLoader.
  • Resource Loading: This basic implementation does not handle loading resources from the original APK. A complete solution would require creating a new Resources object and attaching the original APK's AssetManager.
  • Manifest Modification: The shell's AndroidManifest.xml must declare ProxyApplication as the android:name in the <application> tag. The MainActivity entry point should be changed to the original app's activity.

Common Unpacking Techniques

Common methods for analyzing or unpacking such packed applications include:

  1. Memory Dumping: Extracting decrypted DEX files from process memory.
  2. Cache Analysis: Retrieving optimized DEX/ODEX files from the application's cache directories.
  3. File Monitoring: Observing file system operations during app startup.
  4. Hooking: Intercepting key functions in the loading process (e.g., DexFile APIs).
  5. Custom ROMs: Using modified Android systems with built-in dumping capabilities.
  6. Dynamic Debugging: Debugging the stub's extraction and loading routines.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.