Order Of Six Angles

Main Logo

A security researcher's blog about reverse-engineering, malware and malware analysis

Home | RU | Translations | Tools | Art | About

7 April 2020

tags: android - malware

Infecting android applications - The new way

Foreword

Idea authors: Erbol & Thatskriptkid

Author of drawing: @alphin.fault instagram

Author of the article and proof-of-concept code: Thatskriptkid

Proof-of-Concept Link

Target audience of the article - people who have an idea of the current way of infecting android applications through smali code patching and want to learn about a new and more effective way. If you are not familiar with the current infection practice, read my article - How to steal digital signature using Man-In-The-Disk, chapter - “Creating payload”. The technique described here was completely invented by us; there is no description of such a method in the Internet.

Our technique:

  1. Does not use bugs or android vulnerabilities
  2. Not intended for cracking applications (removing ads, licenses, etc.).
  3. Designed to add malicious code without any interference with the target application or its appearance.

Disadvantages of the current approach

The way to inject malicious code by decoding the application to smali code and patching it is the only and widely practiced method to date. smali/backsmali is the only tool used for this. It is the basis for all known apk infectors, for example:

  1. backdoor-apk.
  2. TheFatRat
  3. apkwash
  4. kwetza

Malware also uses smali/backsmali and patching. The work algorithm of the Trojan Android.InfectionAds.1:

Decoding and patching involves changing the original classesN.dex file. This leads to two problems:

  1. Overstepping the limit of 65536 methods in one DEX file if there is too much malicious code.
  2. The application can check the integrity of DEX files

DEX decoding/disassembling is a complex process that requires constant updating and highly dependent on the android version.

Almost all available infection/modification tools are written in Java and/or depend on JVM - this greatly narrows the scope of use and makes it impossible to launch the infectors on routers, embedded systems, systems without JVM, etc.

Description of a new approach

There are several types of starting applications in the android, one of which is called cold start. Cold start happens when application is started for the first time.

The execution of an application starts with the creation of an Application object. Most android applications have their own Application class, which should extends the main class android.app.Applciation. An example of a class:

package test.pkg;
import android.app.Application;
public class TestApp extends Application {

    public TestApp() {}

    @Override
    public void onCreate() {
        super.onCreate();
    }
}

The class test.pkg.TestApp should be registered in AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="Test"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:name="test.pkg.TestApp">
    </application>
</manifest>

The process of launching such an application:

The basic requirements for our infection techniques have been defined:

  1. Execution of malicious code, at application launch
  2. Saving all steps of the process of launching the original application

The injection of the malicious code took place at the stage of the cold start:Application Object creation->Application Object Constructor. A malicious Application class was created, injected in the APK and spelled out in AndroidManifest.xml, instead of the original one. To preserve the previous execution chain, it was inherited from test.pkg.TestApp.

Malicious Application class:

package my.malicious;
import test.pkg;
public class InjectedApp extends TestApp {

    public InjectedApp() {
        super();
        executeMaliciousPayload();
    }
}

Modified AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="Test"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:name="my.malicious.InjectedApp">
    </application>
</manifest>

The process of launching malicious code inside an infected application (modifications are marked in red):

Applied modifications:

  1. The class my.malicious.InjectedApp was added to the original APK
  2. In AndroidManifest.xml the line test.pkg.TestApp has been replaced with my.malicious.InjectedApp

The benefits of the new approach

It is possible to apply necessary modifications to the APK:

  1. Without AndroidManifest.xml decoding/encoding
  2. Without DEX dissasembling/assembling
  3. Without making changes to the original DEX files

These facts allow you to infect almost any existing application without restrictions. Adding your own class and modifying the manifest works much faster than decoding DEX. The malicious code injected by our technology starts immediately, as we are injected right at the beginning of the application launch process. The described infection technique doesn’t depend on the architecture and version of the android (with a few exceptions).

The PoC for demonstration was written in Go and is capable to be extended to a full featured tool. PoC is compiled into one binary file and does not use any runtime dependencies. Using Go allows using cross compilation to build an infector for almost any architecture and OS.

Testing of infected APK by PoC was on:

NOX player 6.6.0.8006-7.1.2700200616, Android 7.1.2 (API 25), ARMv7-32

NOX player 6.6.0.8006-7.1.2700200616, Android 5.1.1 (API 22), ARMv7-32

Android Studio Emulator, Android 5.0 (API 21), x86

Android Studio Emulator, Android 7.0 (API 24), x86

Android Studio Emulator, Android 9.0 (API 28), x86_64

Android Studio Emulator, Android 10.0 (API 29), x86

Android Studio Emulator, Android 10.0 (API 29), x86_64

Android Studio Emulator, Android API 30, x86

Xiaomi Mi A1

We managed to successfully infect a huge number of applications (for obvious reasons, the names are hidden). We managed to successfully infect applications that cannot be decoded using smali/backsmali, and therefore by existing tool.

Identifying necessary modifications in AndroidManifest.xml and patching

One of the modifications required for the infection is to replace the string in AndroidManifest.xml. It is possible to patch the string without decoding/encoding the manifest.

APKs contain the manifest in binary encoded form. The structure of the binary manifest is undocumented and represents a custom XML encoding algorithm from Google. For convenience, was created a description in Kaitai Struct that can be used as documentation.

AndroidManifest.xml structure (in brackets - size in bytes):

Two applications with different class names were developed to detect changes in the manifest, after patching the original Application class name to a malicious one. The applications were built into an APK and unpacked to produce binary manifests.

An example of the original manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.qoogle.service.outbound.thread.safe.eng.packages.packas.pack.level.random">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="MinDEX"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:name="test.pkg.TestApp">
    </application>

</manifest>

An example of a patched manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.qoogle.service.outbound.thread.safe.eng.packages.packas.pack.level.random">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="MinDEX"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:name="test.pkg.TestAppAAAAAAAAA">
    </application>

</manifest>

The length of the fully-qualified class name has increased by 9 characters. Both files were opened in HexCmp, to get the diff.

Changes to the manifest and explanation of reasons:

. .
field offset description diff_count explanation
header.file_len 0x4 Total file length 0x10 In the original manifest there was 0x2 bytes of alignment, in the modified version they are not required.
Strings in the binary manifest are stored in UTF-16 format, i.e. one character takes 0x2 bytes.
In total, we increased the string by 9 characters (0x12 bytes) minus 0x2 alignment bytes, it equals to 0x10 byte difference
header.string_table_len 0xC Length of array of strings 0x10 The string is in an array of strings. The explanation for the 0x10 byte difference is the same as for header.file_len.
string_offset_table.offset 0x7C Offset to the line following the modified 0x12 string_offset_table stores offset up to strings in an array of manifest strings. Since the length of the string has increased,
the line following it has been moved further by 0x12 bytes. Alignment is not taken into account here, as it
is located before the array of strings.

field offset description diff_count explanation
strings.len 0x2EA String length 0x9 The number of characters by which the string has increased

In the structure of the manifest given at the beginning, after strings follows padding to align resource_header. In the original manifest, the last line of uses-sdk ends on the offset 0x322 (orange), which means that two bytes of alignment (green) for resource_header have been added.

In the modified version, string_table ends in offset 0x334 (orange) and then immediately follows resource_header (red), which does not require alignment.

AndroidManifest.xml structure scheme, with an indication of fields to be patched, to replace the name of the original Applciation class with a malicious one (marked in red):

The Proof-of-Concept code developed for the article implements these modifications in the manifest.Patch() method.

Creating files to be injected in the target application

The second modification needed to infect is the injection of a class with malicious code. In order to save the original application startup chain, an Application class must be injected into the APK, the parent class of which must be the original Application class. At the stage of preparing the files to be injected, it is unknown. Therefore, when creating the class, it was necessary to use the name placeholder - z.z.z.

The initial state of the application and the DEX to be injected:

After receiving the original name of the Application class from the manifest, the placeholder was patched:

The infection process ends with the addition of the malicious DEX to the target application:

Since classes with malicious code can have different code, they were put into a separate DEX. This was also done to simplify the patching of the placeholder:

The class names in DEX are arranged alphabetically. The Application class name of the target application can start with any letter. For predictable string order, after the patching, the name of the placeholder was chosen to be z.z.z.

To prepare the files to be injected, a project was created in Android Studio, with three classes.

Class InjectedApp. Its full name:

aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa.InjectedApp

This name must meet two rules:

  1. It must be longer than any Application class name of any target application

  2. It must be higher in alphabetical order of any Application Class name of any application

The InjectedApp class that will be executed instead of the Application class of the target application:

package aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa;
import aaaaaaaaaaaa.payload;
import z.z.z;

public class InjectedApp extends z {

    public InjectedApp() {
        super();
        payload p = new payload();
        p.executePayload();
    }
}

The main goal of the class is to start executing a malicious code that is in another DEX:

        payload p = new payload();
        p.executePayload();

The payload class contains malicious code:

package aaaaaaaaaaaa;

import android.util.Log;

public class payload {

    public void executePayload() {
            Log.i("HELL", "Hello, I'm a malicious payload");
    }
}

The full name of the class must satisfy the following rule:

  1. It must be alphabetically higher than any Application class name of any application

To inject arbitrary malicious code, you must create a DEX file that must comply with the conditions:

  1. Contain a class with a name:

aaaaaaaaaaaaaa.payload

  1. The class must contain the method

public void executePayload()

A placeholder class z.z.z, whose full name will be patched to the full name of the Applciation class of the target application.

package z.z;

import android.app.Application;

public class z extends Application {
}

The class must comply with the conditions:

  1. The full name of the class must be alphabetically lower than the full names of the classes InjectedApp and payload

  2. The full name of the class must be shorter than any of the full names of the Application classes of any application

According to the developed injection scheme, the InjectedApp and payload classes were compiled into separate DEXs. For this purpose, Android Studio built the APK with Android Studio->Generate Signed Bundle/APK->release. The compiled .class files were created in the folder app\build\intermediates\javac\release\classes.

Compile .class files into DEX, using d8:

d8 --release --min-api 16 --no-desugaring InjectedApp.class --output .

d8 --release --min-api 16 --no-desugaring payload.class --output .

The resulting DEX should be added to the target application.

Identifying the necessary modifications in DEX and patching

After patching the placeholder z.z.z to the full name of the Application class of the target application, the DEX structure will change. To detect modifications, two applications with class names of different lengths were created in Android Studio.

The InjectedApp class, inherited from z.z.z, in the first application:

package aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa;
import aaaaaaaaaaaa.payload;
import z.z.z;

public class InjectedApp extends z {

    public InjectedApp() {
        super();
        payload p = new payload();
        p.executePayload();
    }
}

Class InjectedApp, inherited from z.z.zzzzzzzzzzzzzzzzzzzzzzzz in the second application:

package aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa;
import aaaaaaaaaaaa.payload;
import z.z.z;

public class InjectedApp extends zzzzzzzzzzzzzzzz {

    public InjectedApp() {
        super();
        payload p = new payload();
        p.executePayload();
    }
}

The length of the class name increased by 15 characters. Classes were compiled separately into DEX:

d8 --release --min-api 16 --no-desugaring InjectedApp.class --output .

Let’s open the resulting DEX in HexCMP:

Official documentation on the DEX structure

field offset description diff_count explanation
header_item.checksum 0x8 Checksum full Any change in DEX, the checksum is recalculated.
header_item.signature 0xC Hash full Any change in the DEX hash is recalculated
header_item.file_size 0x20 File size 0x10 String size increased by 0xF, plus 0x1 bytes of alignment.
header_item.map_off 0x34 map offset 0x10 the map goes after an array of strings, so the offset was increased, taking into account the alignment
header_item.data_size 0x68 data section size 0x10 The data section is located after an array of strings, so the offset was enlarged, taking into account the alignment
map.class_def_item.class_data_off 0xE8 offset to class data 0xF This structure does not require alignment, so the value increased by the number of added characters
map_list.debug_info_item 0x114 debug info offset Not important The field stores the data needed for the correct output when it is crashed. The field can be ignored.

field offset description diff_count explanation
string_data_item.utf16_size 0x1B3 string length 0xF Strings in DEX are stored in MUTF-8 format, where one character takes 1 byte.

Changes at the end of the file:

field offset description diff_count explanation
map.class_data_item.offset 0x29C offset to class data 0xF The structure class_data_item follows immediately after an array of strings and does not require alignment
map.annotation_set_item.entries.annotation_off_item 0x2A8 offset to annotations 0x10 The alignment is taken into account
map.map_list.offset 0x2B4 offset to map_list 0x10 The alignment is taken into account

The Proof-of-Concept code developed for the article implements these modifications in the mydex.Patch() method.

Results

To apply the necessary modifications, we have developed PoC, which works according to the algorithm:

  1. Unpacking APK files
  2. Parsing AndroidManifest.xml
  3. Finding the name of the Application class
  4. Patching original Application class name with aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa.InjectedApp
  5. Patching of the placeholder z.z.z with the original name of the Application class
  6. Adding two DEXs to APK (one with InjectedApp application class, another with malicious classes)
  7. Packing all files in new APK

Limitations of the new approach

This technique will not work with applications that meet all conditions simultaneously:

  1. minSdkVersion <= 20
  2. Do not use in dependencies library androidx.multidex:multidex or com.android.support:multidex.
  3. Runs on android versions lower than Android 5.0 (API level 21).

Thus, it is assumed that the application has one DEX file. The restriction applies because the android versions before Android 5.0 (API level 21) use the Dalvik virtual machine to run the code. By default, Dalvik only accepts a single DEX file in the APK. To get around this limitation, you should use the above libraries. Android versions after Android 5.0 (API level 21), instead of Dalvik, use the ART system, which natively supports multiple DEX files in an application, because when you install an application, it will compile all DEXs into one .oat file. See official documentation for details.

Further PoC improvements

  1. If an application does not have its own Application class, you should add InjectedApp to AndroidManifest.xml
  2. Adding your tags to AndroidManifest.xml
  3. APK signing
  4. Getting rid of AndroidManifest.xml decoding

FAQ

Q: Why not use underscores in the full name of InjectedApp, so it is almost guaranteed to be alphabetically above any name in the Application class of the target application?

A: Technically it’s possible, but there will be problems with Android 5 and there will be the following error:

D/AndroidRuntime( 3891): Calling main entry com.android.commands.pm.Pm
D/DefContainer( 3414): Copying /mnt/shared/App/20200629234847850.apk to base.apk
W/PackageManager( 1802): Failed parse during installPackageLI
W/PackageManager( 1802): android.content.pm.PackageParser$PackageParserException: /data/app/vmdl1642407162.tmp/base.apk (at Binary XML file line #48): Bad class name ________.__________._0000000000000000000000000000000000000000000000000000000000000000.InjectedApp in package XXXXXXXXXXXXXXXXXXXx
W/PackageManager( 1802):        at android.content.pm.PackageParser.parseBaseApk(PackageParser.java:885)
W/PackageManager( 1802):        at android.content.pm.PackageParser.parseClusterPackage(PackageParser.java:790)
W/PackageManager( 1802):        at android.content.pm.PackageParser.parsePackage(PackageParser.java:754)
W/PackageManager( 1802):        at com.android.server.pm.PackageManagerService.installPackageLI(PackageManagerService.java:10816)
W/PackageManager( 1802):        at com.android.server.pm.PackageManagerService.access$2300(PackageManagerService.java:236)
W/PackageManager( 1802):        at com.android.server.pm.PackageManagerService$6.run(PackageManagerService.java:8888)
W/PackageManager( 1802):        at android.os.Handler.handleCallback(Handler.java:739)
W/PackageManager( 1802):        at android.os.Handler.dispatchMessage(Handler.java:95)
W/PackageManager( 1802):        at android.os.Looper.loop(Looper.java:135)
W/PackageManager( 1802):        at android.os.HandlerThread.run(HandlerThread.java:61)
W/PackageManager( 1802):        at com.android.server.ServiceThread.run(ServiceThread.java:46)

Q: Why not inject Activity and write it in the manifest instead of the main one, because it also starts first? Yes, with this method, payload will run a little later, but it’s not critical.

A: There are two problems in this approach. The first is that there are applications that use a lot of tags activity-alias in the manifest that refer to the name of the main activity. In this case we will have to patch not one line in the manifest, but several. It also makes it difficult to parse and find the name of the desired Activity. The second is that the main Activity runs in the main UI thread, which imposes some restrictions on the malicious code.

Q: But you can’t use services in an Application class. What kind of malicious code can there be without services?

A: First of all, this restriction is introduced in the Android version starting with API 25. Secondly, this limitation applies to the android applications in general, not to the Application class specifically. Third, you can use services, but not ordinary services, but foreground.

Q: Your PoC is not working

A: In this case, make sure that:

  1. The original application works
  2. All file paths in PoC are correct
  3. There’s nothing unusual in apkinfector.log.
  4. The name of the original Application class in the patched InjectedApp.dex is really in its place.
  5. The target application uses its Application class. Otherwise, PoC inoperability is predictable.

If nothing helped, try to play with the -min-api parameter when compiling classes. If nothing worked, then create an issue on github.

Q: Why was the Application constructor selected for the infection and not the OnCreate() method?

A: The point is that there are applications that have an Application class that has the OnCreate() method with the final modifier. If you put your Application with OnCreate(), the android will generate an error:

06-28 07:27:59.770  2153  4539 I ActivityManager: Start proc 6787:xxxxxxxxx/u0a46 for activity xxxxxxxxx/.Main
06-28 07:27:59.813  6787  6787 I art     : Rejecting re-init on previously-failed class java.lang.Class<InjectedApp>:
 java.lang.LinkageError: Method void InjectedApp.onCreate() overrides final method in class LX/001; 
(declaration of 'InjectedApp' appears in /data/app/xxxxxxxxx-1/base.apk:classes2.dex)

Reasons for the error here

if (super_method->IsFinal()) {
          ThrowLinkageError(klass.Get(), "Method %s overrides final method in class %s",
                            virtual_method->PrettyMethod().c_str(),
                            super_method->GetDeclaringClassDescriptor());
          return false;
        }

The Android detects that the super method is final and gives out an error.

In Java, if you have not created any constructor, the compiler will create it for you (without parameters). If you have created a constructor with parameters, then the constructor without parameters is not automatically created. Since we call a constructor without parameters, you may think that there is a problem if the target application’s app class contains a constructor with parameters. But it is not correct because Android requires a default constructor. Otherwise, you get this error.

06-28 08:51:54.647  8343  8343 D AndroidRuntime: Shutting down VM
06-28 08:51:54.647  8343  8343 E AndroidRuntime: FATAL EXCEPTION: main
06-28 08:51:54.647  8343  8343 E AndroidRuntime: Process: xxxxxxxxx, PID: 8343
06-28 08:51:54.647  8343  8343 E AndroidRuntime: java.lang.RuntimeException: Unable to instantiate application xxxxxxxxx.YYYYYY: java.lang.InstantiationException: java.lang.Class<xxxxxxxxx.YYYYYY> has no zero argument constructor
06-28 08:51:54.647  8343  8343 E AndroidRuntime:        at android.app.LoadedApk.makeApplication(LoadedApk.java:802)
Вверх