# Class loaders in the middle: confusing Android static analyzers

Jean-Marie Mineau, Jean-François Lalande

► To cite this version:

Jean-Marie Mineau, Jean-François Lalande. Class loaders in the middle: confusing Android static analyzers. Digital Threats: Research and Practice, 2025, 6 (3), pp.1-19. 10.1145/3754457. hal-05186153

HAL Id: hal-05186153

https://centralesupelec.hal.science/hal-05186153v1

Submitted on 25 Jul 2025

HAL is a multi-disciplinary open access
archive for the deposit and dissemination of sci-
entific research documents, whether they are pub-
lished or not. The documents may come from
teaching and research institutions in France or
abroad, or from public or private research centers.

L'archive ouverte pluridisciplinaire HAL, est
destinée au dépôt et à la diffusion de documents
scientifiques de niveau recherche, publiés ou non,
émanant des établissements d'enseignement et de
recherche français ou étrangers, des laboratoires
publics ou privés.

<figure><img src="image_1.png" /></figure>

Distributed under a Creative Commons Attribution 4.0 International License
Class loaders in the middle: confusing Android static analyzers

JEAN-MARIE MINEAU and JEAN-FRANCOIS LALANDE, CentraleSupélec, Inria, France

When executing a mobile application, Android executes either the classes provided by the developer or the ones provided by the operating system. The dynamic linking and loading of the different classes is a complex task that may be exploited by an attacker. In particular, if the developer adds a class whose name collides with another class of Android, they may confuse a reverse engineer. In this paper, we explore the possible collisions that can occur between classes defined multiple times at different locations, i.e., multiple times in the APK file or, at the same time, in the APK and the operating system. We highlight three attacks that we call shadow attacks. In particular, we show that static analysis tools used by a reverse engineer choose the shadow implementation for most of the evaluated tools, and output a wrong result. In particular, the flow analysis of Androguard or Flowdroid can be fooled by an attacker. In a dataset of 49 975 applications, we also explored if shadow attacks are used in the wild and found that most of the time, there is no malicious behavior behind them. The main results are that 23.52 % of applications shadow a class of the SDK and 3.11 % a hidden class of the system.

CCS Concepts: • **Security and privacy** → Malware and its mitigation; *Mobile platform security*; *Software security engineering*; • **Software and its engineering** → **Software reverse engineering**.

Additional Key Words and Phrases: Android, static analysis, class loading, code obfuscation

**ACM Reference Format:**

Jean-Marie Mineau and Jean-Francois Lalande. 2024. Class loaders in the middle: confusing Android static analyzers. In *Proceedings of TODO (ACM Digital Threats)*. ACM, New York, NY, USA, 20 pages. https://doi.org/XXXXXXX.XXXXXXX

# 1 INTRODUCTION

Android applications are distributed using markets of applications. The market maintainers have the difficult task to discover suspicious applications and delete them if they are effectively malicious applications. For such a task, some automated analysis is performed, but sometimes, a manual investigation is required. A reverse engineer is in charge of studying the application: they usually perform a static analysis and a dynamic analysis. The reverse engineer uses in the first phase static analysis tools in order to access and review the code of the application. If this first phase is not accurately driven, for example if they fail to access a critical class, they may decide that a malicious application is safe. Additionally, as stated by Li *et al*. [10] in their conclusions, such a task is complexified by dynamic code loading, reflective calls, native code, and multi-threading which cannot be easily handled statically. Nevertheless, even if we do not consider these aspects, determining statically how the regular class loading system of Android is working is a difficult task.

Class loading occurs at runtime and is handled by the components of Android Runtime (ART), even when the application is partially or fully compiled ahead of time. Nevertheless, at the development stage, Android Studio handles the resolution of the different classes that can be internal to the application. When building, the code is linked to the standard library i.e. the code contained in *android.jar*. In this article, we call these classes

Authors' address: Jean-Marie Mineau, jean-marie.mineau@centralesupelec.fr; Jean-Francois Lalande, jean-francois.lalande@irisa.fr, CentraleSupélec, Inria, av. de la Boulaie, Cesson Sévigné, France, F-35510.

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from permissions@acm.org.

ACM Digital Threats, TODO, TODO

© 2024 ACM.

ACM ISBN 978-1-4503-XXXXX-X/18/06

https://doi.org/XXXXXXX.XXXXXXX
“Development SDK classes”. android.jar is not added to the application because its classes will be available at runtime in other .jar files. To distinguish those classes found at runtime from Dev SDK classes, we call them Android SDK classes. When releasing the application, the building process of Android Studio can manage different versions of the Android SDK, reported in the Manifest as the “SDK versions”. Indeed, some parts of the core Android SDK classes can be embedded in the application, for retro compatibility purposes: by comparing the specified minimum SDK version and the target SDK version, the code of extra Android SDK classes is stored in the APK file. As a consequence, it is frequent to find inside applications some classes that come from the com.android packages. At runtime each smartphone runs a unique version of Android, but, as the application is deployed on multiple versions of Android, it is difficult to predict which classes will be loaded from the Android SDK classes or from the APK file itself. This complexity increases with the multi-DEX format of recent APK files that can contain several bytecode files.

Going back to the problem of a reverse engineer studying a suspicious application statically [15], the reverse engineer uses tools to disassemble the application [13] and track the flows of data in the bytecode. As an example, for a spyware potentially leaking personal information, the reverse engineer can unpack the application with Apktool and, after manually locating a method that they suspect to read sensitive data (by reading the unpacked bytecode), they can compute with FlowDroid [3] if there is a flow from this method to methods performing HTTP requests. During these steps, the reverse engineer faces the problem of resolving statically which class is loaded from the APK file and the Android SDK classes. If they, or the tools they use, choose the wrong version of the class, they may obtain wrong conclusions about the code. Thus, the possibility of shadowing classes could be exploited by an attacker in order to obfuscate the code.

In this paper, we study how Android handles the loading of classes in the case of multiple versions of the same class. Such collisions can exist inside the APK file or between the APK file and Android SDK classes. We intend to understand if a reverse engineer would be impacted during a static analysis when dealing with such an obfuscated code. Because this problem is already enough complex with the current operations performed by Android, we exclude the case where a developer recodes a specific class loader or replaces a class loader by another one, as it is often the case for example in packed applications [4]. We present a new technique that “shadows” a class i.e. embeds a class in the APK file and “presents” it to the reverse engineer instead of the legitimate version. The goal of such an attack is to confuse them during the reversing process: at runtime the real class will be loaded from another location of the APK file or from the Android SDK, instead of the shadow version. This attack can be applied to regular classes of the Android SDK or to hidden classes of Android [7, 9]. We show how these attacks can confuse the tools of the reverse engineer when he performs a static analysis. In order to evaluate if such attacks are already used in the wild, we analyzed 49 975 applications from 2023 that we extracted randomly from AndroZoo [1]. Our main result is that 23.52 % of these applications contain shadow collisions against the SDK and 3.11 % against hidden classes. Our investigations conclude that most of these collisions are not voluntary attacks, but we highlight one specific malware sample performing strong obfuscation revealed by our detection of one shadow attack.

The paper is structured as follows. Section 2 reviews the state of the art about loading of Android classes and the tools to perform reverse engineering on applications. Then, Section 3 investigates the internal mechanisms about class loading and presents how a reverse engineer can be confused by these mechanisms. In Section 4, we design obfuscation techniques and we show their effect on static analysis tools. Finally, Section 5 evaluates if these obfuscation techniques are used in the wild, by searching inside 49 975 APKs if they exploit these techniques. Section 6 discusses the limits of this work and Section 7 concludes the paper.
## 2 STATE OF THE ART

Class loading. Class loading mechanisms have been studied in the general context of the Java language. Gong [6] describes the JDK 1.2 class loading architecture and capabilities. One of the main advantages of class loading is the type safety property that prevents type spoofing. As explained by Liang and Bracha [11], by capturing events at runtime (new loaders, new class) and maintaining constraints on the multiple loaders and their delegation hierarchy, authors can avoid confusion when loading a spoofed class. This behavior is now implemented in modern Java virtual machines. Later Tazawa and Hagiya [16] proposed a formalization of the Java Virtual Machine supporting dynamic class loading in order to ensure type safety. Those works ensure strong safety for the Java Virtual Machine, in particular when linking new classes at runtime. Although Android has a similar mechanism, the implementation is not shared with the JVM of Oracle. Additionally, in this paper, we do not focus on spoofing classes at runtime, but on confusion that occurs when using a static analyzer used by a reverse engineer that tries to understand the code loading process offline.

Contributions about Android class loading focus on using the capabilities of class loading to extend Android features or to prevent reverse engineering of Android applications. For instance, Zhou et al. [20] extend the class loading mechanism of Android to support regular Java bytecode and Kritz and Maly [8] propose a new class loader to automatically load modules of an application without user interactions.

Regarding reverse engineering, class loading mechanisms are frequently used by packers for hiding all or parts of the code of an application [4]. The problem to be solved consists in locating secondary .dex files that can be unciphered just before being loaded. Dynamic hook mechanisms should be used to intercept the bytecode at load time. These techniques can be of some help for the reverse engineer, but they require to instrument the source code of AOSP or the application itself. The engineering cost is high and anti-debugging techniques can slow down the process. Thus, a reverse engineer always starts by studying statically an application using static analysis tools [10], and will eventually go to dynamic analysis [5] if further costly extra analysis is needed (for example, if they spot the use of a custom class loader). Performing a static analysis of an application can be time-consuming if the programmer uses obfuscation techniques such as native code, packing techniques, value encryption, or reflection. Such techniques can partially hide the Java bytecode from a static analysis investigation as they modify it at runtime. For example, packers exploit the class loading capability of Android to load new code. They also combine the loading with code generation from ciphered assets or code modification from native code calls [12] to increase the difficulty of recovery of the code. Because parts of the original code will be only available at runtime, deobfuscation approaches propose techniques that track DEX structures when manipulated by the application [19, 18, 17]. All those contributions are directly related to the class loading mechanism of Android.

Deobfuscating an application is the first problem the reverse engineer has to solve. Nevertheless, even if all classes of the code are recovered by the reverse engineer, understanding which classes are really loaded by Android brings an additional problem. The reverse engineer can have the feeling that what he sees in the bytecode is what is loaded at runtime, whereas the system can choose alternative implementations of a class. Our goal is to show that some tools mentioned in the literature [10] can suffer from attacks exploiting confusion inside regular class loading mechanisms of Android.

Hidden APIs. Li et al. did an empirical study of the usage and evolution of hidden APIs [9]. They found that hidden APIs are added and removed in every release of Android, and that they are used both by benign and malicious applications. More recently, He et al. [7] did a systematic study of hidden service API related to security. They studied how the hidden API can be used to bypass Android security restrictions and found that although Google countermeasures are effective, they need to be implemented inside the system services and not the hidden API due to the lack of in-app privilege isolation: the framework code is in the same process as the user code, meaning any restriction in the framework can be bypassed by the user.
```java
class A {
    public static void f() {
        B b = new B();
        b.do_something();
    }
}
```

<caption>Listing 1. Class instantiation</caption>

*Static analysis tools.* Static analysis tools are used to perform operations on an APK file, for example extracting its bytecode or information from the Manifest file. Because of the complexity of Android, few tools have followed all the evolutions of the file format and are robust enough to analyze all applications without crashing [14]. The tools can share the backend used to manipulate the code. For example, Apktool is often called in a subprocess to extract the bytecode. Another example is Soot [2], a Java framework that allows to manipulate the bytecode from an object representation of instructions. This framework enables advanced features such as inserting or removing bytecode instructions but can require a lot of memory and time to perform its operations. The most known tool built on top of Soot is FlowDroid [3], which enables to compute information flows statically into the code.

Because these tools are used by reverse engineers, we will evaluate the accuracy of the provided results in the case of an application developer exploits the possible confusions that brings the class loading mechanisms of Android.

## 3 ANALYZING THE CLASS LOADING PROCESS

For building obfuscation techniques based on the confusion of tools with class loaders, we manually studied the code of Android that handles class loading. In this section, we report the inner workings of ART and we focus on the specificities of class loading that can bring confusion. Because the class loading implementation has evolved over time during the multiple iterations of the Android operating system, we mainly describe the behavior of ART from Android version 14 (SDK 34).

### 3.1 Class loaders

When ART needs to access a class, it queries a ClassLoader to retrieve its implementation. Each class has a reference to the ClassLoader that loaded it, and this class loader is the one that will be used to load supplementary classes used by the original class. For example in Listing 1, when calling A.f(), the ART will load B with the class loader that was used to load A. This behavior has been inherited from Java but Android replaced the original Java ClassLoaders with stub classes and added its own ClassLoader classes.

The left part of Figure 1 shows the different class loaders specific to Android in white and the stubs of the original Java class loaders in grey. The main difference between the original Java class loaders and the ones used by Android is that they do not support the Java bytecode format. Instead, the Android-specific class loaders load their classes from (many) different file formats specific to Android. Usually, when used by a programmer, the classes are loaded from memory or from a file using the DEX format (.dex). When used directly by ART, the classes are usually stored in an application file (.apk) or in an optimized format (OAR/ODEX).

### 3.2 Delegation

The order in which classes are loaded at runtime requires special attention. All the specific Android class loaders (DexClassLoader, InMemoryClassLoader, etc.) have the same behavior (except DelegateLastClassLoader) but they handle specificities for the input format. Each class loader has a delegate class loader, represented in the right part of Figure 1 by black plain arrows. This delegate is a concept specific to class loaders and has nothing to
<figure><img src="image_2.png" /><figcaption>Fig. 1. The class loading hierarchy of Android: gray – Java-based, white – Android-based</figcaption></figure>

do with class inheritance. Unless a specific class loader is provided when instantiating a new class loader, the class loaders will delegate to the singleton class *BootClassLoader*. When a class loader needs to load a class, except for *DelegateLastClassLoader*, it will first ask the delegate, i.e. *BootClassLoader*, and if the delegate does not find the class, the class loader will try to load the class on its own. *DelegateLastClassLoader* behaves slightly differently: it will first delegate to *BootClassLoader* then, it will check its files and finally, it will delegate to its actual delegate (given when instantiating the *DelegateLastClassLoader*). A normal class loader would prioritize the classes of its delegate over its own.

At runtime, Android instantiates for each application three instances of class loaders described previously: *bootClassLoader*, the unique instance of *BootClassLoader*, and two instances of *PathClassLoader*: *systemClassLoader* and *appClassLoader*. *bootClassLoader* is responsible for loading Android **platform classes**. It is the direct delegate of the two other class loaders instantiated by Android. *appClassLoader* points to the application .apk file, and is used to load the classes inside the application *systemClassLoader* is a *PathClassLoader* pointing to './', the working directory of the application, which is '/' by default. The documentation of *ClassLoader.getSystemClassLoader* reports that this class loader is the default context class loader for the main application thread. In reality, the platform classes are loaded by *bootClassLoader* and the classes from the application are loaded from *appClassLoader*. *systemClassLoader* is never used.

In addition to the class loaders instantiated by ART when starting an application, the developer of an application can use class loaders explicitly by calling to ones from the Android SDK, or by recoding custom class loaders that inherit from the *ClassLoader* class. At this point, modeling accurately the complete class loading algorithm becomes impossible: the developer can program any algorithm of their choice. For this reason, this case is excluded from this paper and we focus on the default behavior where the context class loader is the one pointing to the
```python
def get_mutli_dex_classeses_dex_name(index: int):
    if index == 0:
        return "classes.dex"
    else:
        return f"classes{index + 1}.dex"

def load_class(class_name: str):
    if is_platform_class(class_name):
        return load_from_boot_class_loader(class_name)
    else:
        index = 0
        dex_file = get_mutli_dex_classeses_dex_name(index)
        while file_exists_in_apk(dex_file) and \
             not class_found_in_dex_file(class_name, dex_file):
            index += 1
            if file_exists_in_apk(dex_file):
                return load_from_file(dex_file, class_name)
        else:
            raise ClassNotFoundError()
```

<caption>Listing 2. Default Class Loading Algorithm for Android Applications</caption>

.apk file and where its delegate is BootClassLoader. With such a hypothesis, the delegation process can be modeled by the pseudo-code of method load_class given in Listing 2.

In addition, it is important to distinguish the two types of platform classes handled by BootClassLoader and that both have priority over classes from the application at runtime:

* the ones available in the **Android SDK** (normally visible in the documentation);

* the ones that are internal and that should not be used by the developer. We call them **hidden classes** [7, 9] (not documented).

As a preliminary conclusion, we observe that the class loading mechanism defines a priority and that an attacker could use it to prioritize an implementation over another one. This could mislead the reverse engineer if they use the one that has the lowest priority. To determine if a class is impacted by the priority given to BootClassLoader, we need to obtain the list of classes that are part of Android i.e. the platform classes. We discuss in the next section how to obtain these classes from the emulator.

## 3.3 Determining platform classes

Figure 2 shows how classes of Android are used in the development environment and at runtime. In the development environment, Android Studio uses `android.jar` and the specific classes written by the developer. After compilation, only the classes of the developer, and sometimes extra classes computed by Android Studio are zipped in the APK file, using the multi-dex format. At runtime, the application uses `BootClassLoader` to load the platform classes from Android. Until our work, previous works [7, 9] considered both Android SDK and hidden classes to be in the file `/system framework framework.jar` found in the phone itself, but we found that the classes loaded by `bootClassLoader` are not all present in `framework.jar`. For example, He et al. [7] counted 495 thousand APIs (fields and methods) in Android 12, based on Google documentation on restriction for non SDK interfaces<fnref n="1" />. However, when looking at the content of `framework.jar`, we only found 333 thousand APIs.

<footnote n="1">¹https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces</footnote>
<figure><img src="image_3.png" /><figcaption>Fig. 2. Location of SDK classes during development and at runtime</figcaption></figure>

<table><caption>Table 1. Number of platform classes and their categories</caption><thead><tr><th rowspan="2">SDK version</th><th rowspan="2">SDK Classes</th><th colspan="2">Number of classes</th></tr><tr><th>Hidden Classes</th><th>Platform Classes</th></tr></thead><tbody><tr><td>32</td><td>5079</td><td>31 777</td><td>36 854</td></tr><tr><td>33</td><td>5280</td><td>37 210</td><td>42 485</td></tr><tr><td>34</td><td>5720</td><td>43 218</td><td>48 935</td></tr></tbody></table>

Indeed, classes such as `com.android.okhttp.OkHttpClient` are loaded by `bootClassLoader`, listed by Google, but not in `framework.jar`.

By looking at the `BOOTCLASSPATH` environment variable in an emulator, we found 25 `.jar` files, including `framework.jar`, in the `BOOTCLASSPATH` of the standard emulator for Android 12 (SDK 32), 31 for Android 13 (SDK 33), and 35 for Android 14 (SDK 34), containing respectively a total of 499 837, 539 236 and 605 098 API methods and fields. Table 1 gives the number of SDK classes (classes found in `android.jar`), hidden classes (classes found in the emulator but not in `android.jar`), and platform classes (classes found in the emulator). Except for a very small number of annotations and anonymous classes (2 to 5 classes depending on the SDK), all SDK classes are included in the platform classes. Table 2 summarizes the discrepancies we found between Google's list and the platform classes we found in Android emulators. Note also that some methods may also be found *only* in the documentation. Our manual investigations suggest that the documentation is not well synchronized with the evolution of the platform classes and that Google has almost solved this issue in API 34.

We conclude that it can be dangerous to trust the documentation and that gathering information from the emulator or phone is the only reliable source. Gathering the precise list of classes and the associated bytecode is not a trivial task.
<table><caption>Table 2. Comparison for API methods between documentation and emulators</caption><thead><tr><th rowspan="2">SDK version</th><th colspan="4">Number of API methods</th></tr><tr><th>Documented</th><th>In emulator</th><th>Only documented</th><th>Only in emulator</th></tr></thead><tbody><tr><td>32</td><td>495 713</td><td>499 837</td><td>1060</td><td>5184</td></tr><tr><td>33</td><td>537 427</td><td>539 236</td><td>1258</td><td>3067</td></tr><tr><td>34</td><td>605 106</td><td>605 098</td><td>26</td><td>18</td></tr></tbody></table>

```cpp
std::string DexFileLoader::GetMultiDexClassesDexName(size_t index) {
    return (index == 0) ?
        "classes.dex" :
        StringFormat("classes%zu.dex", index + 1);
}
```

<caption>Listing 3. The method generating the .dex filenames from the AOSP</caption>

## 3.4 Multiple DEX files

For the application class files, Android uses its specific format called DEX: all the classes of an application are loaded from the file `classes.dex`. With the increasing complexity of Android applications, the need arrised to load more methods than the DEX format could support in one `.dex` file. To solve this problem, Android started storing classes in multiple files named `classesX.dex` as illustrated by the Listing 3 that generates the filenames read by class loaders. Android starts loading the file `GetMultiDexClassesDexName(0)` (`classes.dex`), then `GetMultiDexClassesDexName(1)` (`classes2.dex`), and continues until finding a value n for which `GetMultiDexClassesDexName(n)` does not exist. Even if Android emits a warning message when it finds more than 100 `.dex` files, it will still load any number of `.dex` files that way. This change had the unintended consequence of permitting two classes with the same name but different implementations to be stored in the same `.apk` file using two `.dex` files.

Android explicitly performs checks that prevent several classes from using the same name inside a `.dex` file. However, this check does not apply to multiple `.dex` files in the same `.apk` file: a `.dex` can contain a class with a name already used by another class in another `.dex` file of the application. Of course, such a situation should not happen when multiple `.dex` files have been generated properly by Android Studio. Nevertheless, for an attacker controlling the process, this issue raises the question of which class is selected when several classes sharing the same name are present in `.apk` files.

We found that Android loads the class whose implementation is found first when looking in the order of multiple `.dex` files, as generated by the method `GetMultiDexClassesDexName`. We will show later in Section 4.2 that this choice is not the most intuitive and can fool analysis tools when reversing an application. As a conclusion, we model both the multi-dex and delegation behaviors in the pseudo-code of Listing 2.

## 4 OBFUSCATION TECHNIQUES

In this section, we present new obfuscation techniques that take advantage of the complexity of the class loading process. Then, in order to evaluate their efficiency, we reviewed some common Android reverse analysis tools to see how they behave when collisions occur between classes of the APK or between a class of the APK and classes of Android (Android SDK or hidden classes). We call this collision "**class shadowing**", because the attacker version of the class shadows the one that will be used at runtime. To evaluate if such shadow attacks are working, we handcrafted three applications implementing shadowing techniques to test their impact on static analysis
```java
public class Main {
    public static void main(Activity ac) {
        String personal_data = Taint.sourceforge();
        String obfuscated Personal_data = Obfuscation hide_flow(personal_data);
        Taint.sink(ac, obfuscated Personal_data);
    }
}

public class Obfuscation { // customized for each obfuscation technique
    public static String hide_flow(String personal_data) { ... }
}
```

<caption>Listing 4. Main body of test apps</caption>

tools. Then, we manually inspected the output of the tools in order to check its consistency with what Android is really doing at runtime. For example, for Apktool, we look at the output disassembled code, and for Flowdroid [3], we check that a flow between Taint.sourceforge() and Taint.sink() is correctly computed.

## 4.1 Obfuscation Techniques

From the results presented in Section 3, three approaches can be designed to hide the behavior of an application.

**Self shadow:** shadowing a class with another from APK. This method consists in hiding the implementation of a class with another one by exploiting the possible collision of class names, as described in Section 3.4 with multiple .dex files. If reverse engineers or tools ignore the priority order of a multi-dex file, they can take into account the wrong version of a class.

**SDK shadow:** shadowing a SDK class. This method consists in presenting to the reverse engineer a fake implementation of a class of the SDK. This class is embedded in the APK file and has the same name as the one of the SDK. Because BootClassLoader will give priority to the Android SDK at runtime, the reverse engineer or tool should ignore any version of a class that is contained in the APK. The only constraint when shadowing an SDK class is that the shadowing implementation must respect the signature of real classes. Note that, by introducing a custom class loader, the attacker could inverse the priority, but this case is out of the scope of this paper.

**Hidden shadow:** shadowing a hidden class. This method is similar to the previous one, except the class that is shadowed is a hidden class. A hidden class is a class found in the Android device that was not present in the Android SDK. We listed between 31 777 and 43 218 in Table 1. Because the ART will give priority to the internal version of the class, the version provided in the APK file will be ignored. Such shadow attacks are more difficult to detect by a reverse engineer, that may not know the existence of this specific hidden class in Android.

## 4.2 Impact on static analysis tools

We selected tools that are commonly used to unpack and reverse Android applications: Jadx<fnref n="2" />, a decompiler for Android applications, Apktool<fnref n="3" />, a disassembler/repackager of applications, Androguard<fnref n="4" />, one of the oldest Python package for manipulating Android applications, and Flowdroid [3] that performs taint flow analysis.

For evaluating the tools, we designed a single application that we can customize for different tests. Listing 4 shows the main body implementing:

<footnote n="2">²https://github.com/skylot/jadx</footnote>

<footnote n="3">³https://apktool.org/</footnote>

<footnote n="4">⁴https://github.com/androguard/androguard</footnote>
<table><caption>Table 3. Working attacks against static analysis tools: ●: working, ◦: works but producing warning or can be seen by the reverse engineer, ×: not working</caption><thead><tr><th rowspan="2">Tool</th><th rowspan="2">Version</th><th colspan="3">Shadow Attack</th></tr><tr><th>Self</th><th>SDK</th><th>Hidden</th></tr></thead><tbody><tr><td>Jadx</td><td>1.5.0</td><td>○</td><td>●</td><td>●</td></tr><tr><td>Apktool</td><td>2.9.3</td><td>○</td><td>●</td><td>●</td></tr><tr><td>Androguard</td><td>4.1.2</td><td>○</td><td>●</td><td>●</td></tr><tr><td>Flowdroid</td><td>2.13.0</td><td>●</td><td>×</td><td>●</td></tr></tbody></table>

• a possible flow to evaluate FlowDroid: a flow from a method `Taint.source()` to a method `Taint.sink(Activity, String)` through a method `Obfuscation hide_flow(String)`;

• a possible use of a SDK or hidden class inside the class `Obfuscation` to evaluate platform classes shadowing for other tools.

The first application we released is a control application that does not do anything special. It will be used for checking the expecting result of tools. The second implements self shadowing: the class `Obfuscation` is duplicated: one is the same as the in the control app (`Obfuscation hide_flow(String)` returns its arguments), and the other version returns a constant string. These two versions are embedded in several DEX of a multi-dex application. The third application tests SDK shadowing and needs an existing class of the SDK. We used `Pair` that we try to shadow. We put data in a `Pair` and reread the data from the `Pair`. The colliding `Pair` discards the data and returns null. The last application tests for Hidden API shadowing. Like for the third one, we similarly store data in `com.android.okhttp.Request` and then retrieve it. Again, the shadowing implementation discards the data.

We found that these static analysis tools do not consider the class loading mechanism, either because the tools only look at the content of the application file (e.g. a disassembler) or because they consider class loading to be a dynamic feature and thus out of their scope. In Table 3, we report on the types of shadowing that can be tricked each tool. A plain circle is a shadow attack that leads to a wrong result. A white circle indicates a tool emitting warnings or that displays the two versions of the class. A cross is a tool not impacted by a shadow attack. We explain in more detail in the following the results for each considered tool.

### 4.2.1 Jadx
Jadx is a reverse engineering tool that regenerates the Java source code of an application. It processes all the classes present in the application, but only save/display one class by name, even if two versions are present in multiple `.dex` files. Nevertheless, when multiple classes with the same name are found, Jadx reports it in a comment added to the generated Java source code. This warning stipulates that a possible collision exists and lists the files that contain the different versions of the class. Unfortunately, after reviewing the code of Jadx, we believe that the selection of the displayed class is an undefined behavior. At least for the version 1.5.0 that we tested, we found that Jadx selects the wrong implementation when a class with the same name is present. For example in `classes2 dex` and `classes3 dex`. We report it with a “○” because warnings are issued.

Shadowing Android SDK and hidden classes is possible in Jadx: there is only one implementation of the class in the application and Jadx does not have a list of the internal classes of Android: no warning is issued to the reverse engineer that the displayed class is not the one used by Android.

### 4.2.2 Apktool
Apktool generates Smali files, an assembler language for DEX bytecode. Apktool will store the disassembled classes in a folder that matches the `.dex` file that stores the bytecode. This means that when shadowing a class with two versions in two `.dex` files, the shadow implementations will be disassembled into
<figure><img src="image_4.png" /><figcaption>Fig. 3. Call Graphs of an application calling Main.bad() from a shadowed Obfuscation class.</figcaption></figure>

two directories. No indication is displayed that a collision is possible. It is up to the reverse engineer to have a chance to open the good one.

Similarly to Jadx, using an Android SDK or hidden class will not be detected by the tool that will unpack the fake shadow version.

### 4.2.3 Androguard
Androguard has different usages, with different levels of analysis. The documentation highlights the analysis commands that compute three types of objects: an APK object, a list of DEX objects, and an Analysis object. The APK and the list of .dex files are a one-to-one representation of the content of an application, and have the same issues that we discussed with Apktool: they provide the different versions of a shadow class contained in multiple .dex files.

The Analysis object is used to compute a method call graph and we found that this algorithm may choose the wrong version of a shadowed class when using the cross references that are computed. This leads to an invalid call graph as shown in Figure 3: the two methods doSomething() are represented in the graph, but the one linked to main() on the graph is the one calling the method good() when in fact the method bad() is called when running the application.

Androguard has a method .is_external() to detect if the implementation of a class is not provided inside the application and a method .is android_api() to detect if the class is part of the Android API. Regrettably, the documentation of .is android_api() explains that the method is still experimental and just checks a few package names. This means that although those methods are useful, the only indication of the use of an Android SDK or hidden classes is the fact that the class is not in the APK file. Because of that, like for Apktool and Jadx, Androguard has no way to warn the reverse engineer that the shadow of an Android SDK or hidden classes is not the class used when running the application.

### 4.2.4 Flowdroid
Flowdroid [3] is used to detect if an application can leak sensitive information. To do so, the analyst provides a list of source and sink methods. The return value of a method marked as source is considered sensitive and the argument of a method marked as sink is considered to be leaked. By analyzing the bytecode of an application, Flowdroid can detect if data emitted by source methods can be exfiltrated by a sink method. Flowdroid is built on top of the Soot [2] framework that handles, among other things, the class selection process.

We found that when selecting the classes implementation in a multi-dex APK, Soot uses an algorithm close to what ART is performing: Soot sorts the .dex bytecode file with a specified prioritizer (a comparison function
that defines an order for `.dex` files) and selects the first implementation found when iterating over the sorted files. Unfortunately, the prioritizer used by Soot is not exactly the same as the one used by the ART. The Soot prioritizer will give priority to `classes.dex` and then give priority to files whose name starts with `classes` over other files and finally will use the alphabetical order. This order is good enough for application with a small number of `.dex` files generated by Android Studio, but because it uses the alphabetical order and does not check the exact format used by Android, a malicious developer could hide the implementation of a class in `classes2.dex` by putting a false implementation in `classes0.dex`, `classes1.dex` or `classes12.dex`.

In addition to self shadowing, Flowdroid is sensitive to the use of platform classes, as it needs the bytecode of those classes to be able to track data flows. This is solved for SDK classes by providing `android.jar` to Flowdroid. Flowdroid gives priority to the classes from the SDK over the classes implemented in the application, thus defeating SDK shadow attacks. Unfortunately, `android.jar` only contains classes from the Android SDK, meaning that using hidden classes breaks the flow tracking. Solving this issue would require finding the bytecode of all the platform classes of the Android version targeted and as we said previously it requires extracting this information from the emulator.

## 4.3 Countermeasures

Countermeasures against shadow attacks depend on each tool and its objectives. The first important recommendation is to implement the class selection algorithm according to the algorithm described in Listing 2. It should solve any case of self-shadowing, except for tools like Apktool, which do not have to select a class for computing the result but show the whole application's content. For those tools, a clear warning should be added, pointing out that multiple implementations have been found and displaying the one that will be used at runtime.

Countermeasures against SDK shadow and Hidden shadow attacks are more complex to handle: it requires the list of platform classes on the target smartphone. The list of SDK classes can be extracted easily from `android.jar`, but hidden classes need to be obtained by another means. They could be listed directly from the AOSP tree of the Android source code, or obtained from Android documentation, or extracted from the phone itself. The first approach requires statically analyzing the source code, which can be difficult to achieve as several programming languages are used, and the code base is large and fragmented. As discussed earlier in the paper, the documentation can lack some classes. Consequently, the most reliable source is the smartphone itself. It should be noted that none of these methods can be generalized for all possible versions of Android, as the exact list will depend on the exact targeted device, possibly modified by the manufacturer. Thus, to counter shadow attacks, static analysis tools need to embed multiple lists of platform classes, one for each Android version. Then, the best heuristic would be to use the list of platform classes that is closest to the target SDK of the analyzed application.

Some tools like Flowdroid would require additional countermeasures: to compute the exact flow of data, Flowdroid also needs to analyze the code of platform classes. For the SDK classes, Flowdroid has already analyzed them, but the hidden classes have not. In addition to the data flow in hidden classes, Flowdroid needs a list of data sources and sinks from those classes.

We believe that analysis tools can handle shadow attacks. The implementation of the solution will differ depending on the nature tool and may not always require the same implementation effort.

## 4.4 Relation with obfuscation techniques

As described in the state of the art, reverse engineers face other techniques of obfuscation, such as packers or native code. These techniques rely on custom class loaders that load new parts of the application from ciphered assets or from the network. The reverse engineers have to study the application dynamically, to recover new classes, and eventually go back to a static phase to understand the behavior of the application. In this section, we compare shadow attacks with these techniques, and we discuss how they interact with them.
Advanced obfuscation techniques relying on packers have a higher impact on the difficulty of performing a static analysis compared to shadow attacks. Most of the time, the reverse engineer cannot deobfuscate the application without performing a dynamic analysis. For these reasons, approaches have been designed to assist the capture of the bytecode that is loaded dynamically, after the precise time where the deobfuscation methods have been executed [19, 18, 17]. On the contrary, a shadow attack can be easily defeated by implementing our algorithm in the static analysis tool, as discussed earlier in Section 4.3. Nevertheless, shadow attacks are stealthier than packers or native code. Packers can be easily spotted by artifacts left behind in the application or by detecting classes implementing a custom class loading mechanism. On the contrary, an extra class implementing a shadow attack, which would not be executed, could contain voluntarily few code compared to the executed class of Android. Such attacks would be more discreet than a packer that adds in the application a lot of possibly native code

Combining regular obfuscation techniques with shadow attacks can be achieved in two ways.

First, the attacker could hide the code of a packer or a native call by using a shadow attack. For example, by colliding a class of the SDK, a control flow analysis could be wrongly computed, leading to considering that part of the code is dead, which would mislead the reverse engineer about the use of this part that contains a packer. At runtime, this code would be triggered, unpacking new code.

Second, the attacker could use a packer to unpack code at runtime in a first phase. The reverse engineer would have to perform a dynamic analysis, for example, using a tool such as Dexhunter [19], to recover new DEX files that are loaded by a custom class loader. Then, the reverse engineer would go back to a new static analysis and could have the problem of solving shadow attacks, for example, if a class is defined multiple times in the loaded DEX files.

Because the interaction between shadow attacks and other obfuscation techniques often relies on a loading mechanism implemented by the developer, investigating these cases requires analyzing the Java bytecode that is handling the loading. This problem is left as future work.

We have seen that tools can be impacted by shadow attacks. We discussed the possible countermeasures and how shadow attacks interacts with other obfuscation techniques. In the next section, we will investigate if shadow attacks are used in the wild.

# 5 SHADOW ATTACKS IN THE WILD

In this section, we evaluate in the wild if applications that can be found in the Play store or other markets use one of the shadow techniques. Our goal is to explore the usage of shadow techniques in real applications. Because we want to include malicious applications (in case such techniques would be used to hide malicious code), we selected 50 000 applications randomly from AndroZoo [1] that appeared in 2023. Malicious applications are spot in our dataset by using a threshold of 3 over the number of VirusTotal engines reporting an application as a malware. This number is provided by Androzoo, for scans performed between january 2023 and january 2024 depending on the application. Some few applications over the total cannot be retrieved or parsed leading to a final dataset of 49 975 applications<fnref n="5" />. We automatically disassembled the applications to obtain the list of included classes. Then, we check if any shadow attack occurs in the APK itself or with platform classes of SDK 34.

## 5.1 Results

We report in the upper part of Table 4 the statistics about the whole dataset and the three shadow attacks: "self" when a class shadows another one in the APK, "SDK" when a class of the SDK shadows one of the APK, and "Hidden" when a hidden class of Android shadows one of the APK. We observe that, on average, a few classes are shadowed by another class. Note that the median value is 0 meaning that few apps shadow a lot of classes,

<footnote n="5">⁵http://dx.doi.org/10.5281/zenodo.15846481</footnote>
<table><caption>Table 4. Shadow classes compared to SDK 34 for a dataset of 49 975 applications</caption><thead><tr><th rowspan="2"></th><th rowspan="2">Number</th><th colspan="3">Applications</th><th rowspan="2">Avg Target SDK</th><th rowspan="2">Avg Min SDK</th><th colspan="2">Nb of shadow classes</th><th rowspan="2">Identical Code</th></tr><tr><th>%</th><th>% malware</th><th>Avg Target SDK</th><th>Avg Min SDK</th><th>Average</th><th>Median</th></tr></thead><tbody><tr><td colspan="10" style="text-align: center;">For all applications of the dataset</td></tr><tr><td>Self</td><td>49 975</td><td>100.0%</td><td>0.53%</td><td>32.1</td><td>21.7</td><td></td><td>2.1</td><td>0.0</td><td>74.8%</td></tr><tr><td>SDK</td><td>49 975</td><td>100.0%</td><td>0.53%</td><td>32.1</td><td>21.7</td><td></td><td>6.5</td><td>0.0</td><td>8.04%</td></tr><tr><td>Hidden</td><td>49 975</td><td>100.0%</td><td>0.53%</td><td>32.1</td><td>21.7</td><td></td><td>0.5</td><td>0.0</td><td>17.42%</td></tr><tr><td>Total</td><td>49 975</td><td>100.0%</td><td>0.53%</td><td>32.1</td><td>21.7</td><td></td><td>9.0</td><td>0.0</td><td>23.76%</td></tr><tr><td colspan="10" style="text-align: center;">For applications with at least 1 shadow case</td></tr><tr><td>Self</td><td>234</td><td>0.47%</td><td>5.98%</td><td>31.4</td><td>22.4</td><td></td><td>438.1</td><td>18.0</td><td>74.8%</td></tr><tr><td>SDK</td><td>11 755</td><td>23.52%</td><td>0.38%</td><td>32.4</td><td>22.0</td><td></td><td>27.6</td><td>5.0</td><td>8.04%</td></tr><tr><td>Hidden</td><td>1556</td><td>3.11%</td><td>0.71%</td><td>32.1</td><td>22.2</td><td></td><td>16.1</td><td>1.0</td><td>17.42%</td></tr><tr><td>Total</td><td>12 301</td><td>24.61%</td><td>0.42%</td><td>32.4</td><td>22.0</td><td></td><td>36.7</td><td>6.0</td><td>23.76%</td></tr></tbody></table>

but the majority of apps do not shadow anything. The number of applications shadowing a hidden API is low, which is an expected result as these classes should not be known by the developer. We observe a consequent number of applications, 23.52%, of applications that perform SDK shadowing. It can be explained by the fact that some classes that newly appear are embedded in the APK for end users that have old versions of Android: it is suggested by the average value of Min SDK which is 21.7 for the whole dataset: on average, an application can be run inside a smartphone with API 21, which would require to embed all new classes from 22 to 34. This hypothesis about missing classes is further investigated later in this section.

In the bottom part of Table 4, we give the same statistics but we excluded applications that do not perform any shadowing. For those pairs of shadow classes, we disassembled them using Apktool to perform a comparison using instructions represented in the Smali language. For self-shadow, we compare the pair. For the shadowing of the SDK or Hidden class, we compare the code found in the APK with implementations found in the emulator and `android.jar` of SDK 32, 33, and 34.

**Self-shadowing.** We observe a low number of applications doing self-shadow attacks. For each class that is shadowed, we compared its bytecode with the shadowed one. We observe that 74.8% are identical which suggests that the compilation process embeds the same class multiple times but makes variations in headers or metadata values. We investigate later in Section 5.2 the case of malicious applications.

**SDK shadowing.** For the shadowing of SDK classes, we observe a low ratio of identical classes. This result could lead to the wrong conclusion that developers embed malicious versions of the SDK classes, but our manual investigation shows that the difference is slight and probably due to compiler optimization. To go further in the investigation, in Figure 4 we represent these redefined classes with the following rules:

* The class is classified on the X axis in the figure according to the SDK it first appeared in.

* The class is counted as "green" (solid) if it first appeared in the SDK **after** the APK min SDK (retro compatibility purpose).

* The class is counted as "red" (hatched) if it first appeared in the SDK **before** the APK min SDK (which is useless for the application as the SDK version is always available).

We observe that the majority of classes are legitimate retro-compatibility additions of classes, especially after SDK 21 (which is the average min SDK, cf. Table 4). Abnormal cases are observed for classes that appeared in API versions 7 and before, 8, and 16. Table 5 reports the top ten classes that shadow the SDK for the three mentioned versions. For SDK before 7, it mainly concerns HTTP classes: for example, the class `HttpParams` is an interface,
<figure><img src="image_5.png" /><figcaption>Fig. 4. Redefined SDK classes, sorted by the first SDK they appeared in.</figcaption></figure>

containing limited bytecode that mostly matches the class already present on the emulator (98.03% of shadowed classes are identical). HttpConnectionParams on the other hand differs from the platform class and we observe only 4.99% of identical classes. Manual inspection of some applications revealed that the two main reasons are:

* instead of checking if the methods attributes are null inline like Android does, applications use the method org.apache.http.util.ArgЫ.notNull(). According to comments in the source code of Android<fnref n="6" />, the class was forked in 2007 from Apache 'httpcomponents' project. Looking at the history of the project, the use of ArgЫ.notNull() was introduced in 2012⁷. This shows that applications are embedding code from more recent version of this library without realizing their version will not be the used one.

* very small changes that we found can be attributed to the compilation process (e.g. swapping registers: v0 is used instead of v1 and v1 instead of v0), but even if we consider them different, they are very similar.

The remaining 4.99% of classes that are identical to the Android version are classes where the body of the methods is replaced by stubs that throw exception ("Stub!"). This code corresponds to what we found in android.jar but not the code we found in the emulator, which is surprising. Nevertheless, we decided to count them as identical, because android.jar is the official jar file for developer, and stubs are replaced in the emulator: it is intended by Google developers.

Other results of Table 5 can be similarly discussed: either they are identical with a high ratio, or they are different because of small variations. When substantial differences appear it is mainly because different versions of the same library have been used or an SDK class is embedded for retro-compatibility.

*Hidden shadowing*. For applications redefining hidden classes, on average, 16.1 classes are redefined (cf bottom part of Table 4). The top 3 packages whose code actually differs from the ones found in Android are java.util.stream, org.ccil.cowan.tagsoup and org.json:

* stream: when looking in more detail, we found that java.util.stream was only redefined by 6 applications. This package contains a lot of classes. The developer re-added all the classes in their application, leading to a high number of shadowing in total.

<footnote n="6">⁶https://cs.android.com/android/platform/superproject/main+/main:frameworks/base/core/java/org.apache/http/params/HttpConnectio
nParams.java; drc=3bdd327f8532a79b83f575cc62e8eb09a1f93f3d?</footnote>

<footnote n="7">⁷https://github.com/apache/httpcomponents-core/commit/9104a92ea79e338d876b1b60f5cd2b243ba7069f?</footnote>
<table><caption>Table 5. Top 10 of shadow classes of the SDK</caption><thead><tr><th>Class</th><th>occurrences</th><th>Identical ratio</th></tr></thead><tbody><tr><td colspan="3">redefined for SDK &le; 7</td></tr><tr><td>Lorg/apache/http/params/HttpParams;</td><td>1318</td><td>98.03%</td></tr><tr><td>Lorg/apache/http/params/HttpConnectionParams;</td><td>1202</td><td>4.99%</td></tr><tr><td>Lorg/apache/http/conn/ConnectTimeoutException;</td><td>1200</td><td>35.0%</td></tr><tr><td>Lorg/apache/http/params/CoreConnectionPNames;</td><td>1190</td><td>99.92%</td></tr><tr><td>Lorg/xmlpull/v1/xmlPullParser;</td><td>1111</td><td>52.57%</td></tr><tr><td>Lorg/apache/http/conn/scheme/SocketFactory;</td><td>1074</td><td>87.52%</td></tr><tr><td>Lorg/apache/http/conn/scheme/HostNameResolver;</td><td>1072</td><td>87.59%</td></tr><tr><td>Lorg/apache/http/conn/scheme/LayeredSocketFactory;</td><td>963</td><td>89.41%</td></tr><tr><td>Lorg/json/JSONException;</td><td>945</td><td>0.0%</td></tr><tr><td>Lorg/apache/http/conn/ssl/X509HostnameVerifier;</td><td>886</td><td>0.79%</td></tr><tr><td colspan="3">redefined for SDK = 8</td></tr><tr><td>Ljuavax/xml/namespace/QName;</td><td>297</td><td>0.0%</td></tr><tr><td>Ljuavax/xml/namespace/NamespaceContext;</td><td>226</td><td>98.23%</td></tr><tr><td>Landroid/net/http/SslError;</td><td>221</td><td>31.67%</td></tr><tr><td>Lorg/w3c/dom/UserDataHandler;</td><td>82</td><td>92.68%</td></tr><tr><td>Ljuavax/xml/transform/TransformerConfigurationException;</td><td>73</td><td>69.86%</td></tr><tr><td>Ljuavax/xml/transform/TransformerException;</td><td>73</td><td>0.0%</td></tr><tr><td>Lorg/w3c/dom/ls/LSException;</td><td>61</td><td>63.93%</td></tr><tr><td>Lorg/w3c/dom/TypeInfo;</td><td>54</td><td>88.89%</td></tr><tr><td>Lorg/w3c/dom/DOMConfiguration;</td><td>54</td><td>46.3%</td></tr><tr><td>Ljuavax/xml/transform/TransformerFactoryConfigurationError;</td><td>52</td><td>0.0%</td></tr><tr><td colspan="3">redefined for SDK = 16</td></tr><tr><td>Landroid/annotation/SuppressLint;</td><td>2634</td><td>98.48%</td></tr><tr><td>Landroid/annotation/TargetApi;</td><td>2634</td><td>98.48%</td></tr><tr><td>Landroid/media/MediaCodec$CryptoException;</td><td>11</td><td>18.18%</td></tr><tr><td>Landroid/media/MediaCryptoException;</td><td>10</td><td>20.0%</td></tr><tr><td>Landroid/view/accessibility/AccessibilityNodeProvider;</td><td>9</td><td>0.0%</td></tr><tr><td>Landroid/view/ActionProvider$VisibilityListener;</td><td>8</td><td>12.5%</td></tr><tr><td>Landroid/app/Notification$BigTextStyle;</td><td>7</td><td>0.0%</td></tr><tr><td>Landroid/app/Notification$Style;</td><td>7</td><td>0.0%</td></tr><tr><td>Landroid/util/LongSparseArray;</td><td>7</td><td>0.0%</td></tr><tr><td>Landroid/media/MediaPlayer$TrackInfo;</td><td>7</td><td>0.0%</td></tr></tbody></table>

* tagsoup: TagSoup is a library for parsing HTML.

* json: there is only one hidden class in org.json, redefined by 821 applications: JSONObject$1. This class is internal, and because of this, it does not appear in the SDK. Thus, this class falls in the category of hidden platform classes, instead of SDK.

All these hidden shadow classes are libraries included by the developers who probably did not know that they were already embedded in Android.

## 5.2 Shadowing in malware applications

The last column of Table 4 shows the proportion of applications considered as malware because we arbitrarily fixed a threshold of 3 positive detections from VirusTotal reports. For the whole dataset, we have 0.53% of applications considered as malware. We can see that an application that uses self-shadowing is 10 times more
```java
public class Reflection {
    private static final int ERROR_SET APPLICATION_FAILED = -20;
    private static final String TAG = "Reflection";
    // ...

    static {
        try {
            Method declaredMethod = Class.class.getDeclaredMethod("forName", String.class);
            Method declaredMethod2 = Class.class.getDeclaredMethod("getDeclaredMethod", String.class,
                Class[].class);
            Class cls = (Class) declaredMethodranvoke(null, "dalvik.system.VMRuntime");
            Method method = (Method) declaredMethod2invoke(cls, "getRuntime", null);
            setHiddenApiExemptions = (Method) declaredMethod2invoke(cls, "setHiddenApiExemptions",
                new Class[]{String[].class});
            sVmRuntime = methodinvoke(null, new Object[0]);
        } catch (Throwable th) {
            Log.e TAG, "reflect bootstrap failed:", th);
        }
        System.loadLibrary("free-reflection");
        // ...
    }
    // ...
}
```

<caption>Listing 5. Implementation of Reflection found un classes11.dex (shadows Listing 6)</caption>

```java
public class Reflection {
    private static final String DEX = "ZGV4CjAzNQCI4EprGS2pXI/v3OwlBrlfRnX5rmkKVdN0CwAAcA ...
        AoAAA==";
    private static final String TAG = "Reflection";

    private static native int unsealNative(int i);

    public static int unseal(Context context) {
        return (Build. VERSION. SDK_INT < 28 || BootstrapClass.exemptAll() || unsealByDexFile(
            context)) ? 0 : -1;
    }

    private static boolean unsealByDexFile(Context context) {
        // Decode DEX from base64 and load it as bytecode.
        // ...
    }
    // ...
}
```

<caption>Listing 6. Implementation of Reflection executed by ART (shadowed by Listing 5)</caption>

likely to be a malware, when the proportion of malware among application shadowing platform classes is the same as in the rest of the dataset. Thus, we manually reversed self-shadowing malware, and found that the self-shadowing does not look to be voluntary. The colliding classes are often the same implementation, occasionally with minor differences, like different versions of a library. Additionally, we noticed multiple times internal classes from com.google.android.gms.ads colliding with each other, but we believe that it is due to bad processing during the compilation of the application.
The most notable case we found was an application that still exists on the Google Play Store with the same package name<fnref n="8" />. This application contains a self-shadow class `me.weishu.reflection Reflection` that can be found in `github`, in the repository `tiann/FreeReflection`<fnref n="9" />. This class is used to disable Android restrictions on hidden API. At first glance, we believed the shadowing to be done voluntarily for obfuscation purposes. The shadow class that would be seen by a reverse engineer is given in Listing 5: it contains some Java bytecode performing reflection and loading a native library named “free-reflection” (the associated `.so` is missing). The shadowed class that is really executed is summarized in Listing 6. It contains a more obfuscated code: a DEX field storing `base64` encoded DEX bytecode that is later used to load some new code. When looking at this new code stored in the field, we found that it does almost the same thing as the code in the shadow class. Thus, we believe that the developer has upgraded their obfuscation techniques, replacing a native library by inline `base64` encoded bytecode. The shadow attack could be unintentional, but it strengthens the masking of the new implementation.

As a conclusion, we observed that:

* SDK shadowing is performed by 23.52 % of applications but are unintentional: these classes are embedded for retro-compatibility purpose or because the developer added a library already present in Android;

* Hidden shadowing rarely occurs and is mainly due to the usage of libraries that Android already contains;

* Malware perform more self-shadowing than goodware applications, and we found a sample where self-shadowing would clearly mislead the reverse engineer.

## 6 THREAT TO VALIDITY

During the analysis of the ART internals, we made the hypothesis that its different operating modes are equivalent: we analyzed the loading process for classes stored as non-optimized `.dex` format, and not for the pre-compiled `.oat`. It is a reasonable hypothesis to suppose that the two implementations have been produced from the same algorithm using two compilation workflows.

Similarly, we assumed that the platform classes are stored in the file listed in the `BOOTCLASSPATH` variable, but for optimization purposes, today classes are loaded from the `boot.art`, a file in an unstable, undocumented format. The `BOOTCLASSPATH` is still used when loading an application without the optimization, but this does not happen on normal devices. We confirm empirically our hypothesis on an Android Emulator, but we may have missed some edge cases.

The comparison of Smali code can lead to underestimated values, for example, if the compilation process performs minor modifications such as instruction reordering. The ratios reported in this study for the comparison of code are thus a lower bound and would be higher with a more precise comparison. In addition, platform classes are stored differently in older versions of Android and could not be easily retrieved. For this reason, we did not compared the classes found in applications to their versions older than SDK 32 to avoid producing unreliable statistics for those versions.

## 7 CONCLUSION

This paper has presented three shadow attacks that allow malware developers to fool static analysis tools when reversing an Android application. By including multiple classes with the same name or by using the same name as a class of the Android SDK, the developer can mislead a reverse engineer or impact the result of a flow analysis, such as the ones of Androguard or Flowdroid.

We explored if such shadow attacks are present in as dataset of 49 975 applications. We found that on average, 23.52 % of applications are shadowing the SDK, mainly for retro-compatibility purposes and library embedding. More dangerously, 3.11 % of applications are shadowing a hidden class, which could lead to unexpected execution

<footnote n="8">⁸SHA256: C46A65EA1A797119CCC03C579B61C94FE8161308A3B6A8F55718D6ADAD112546</footnote>

<footnote n="9">⁹https://github.com/tiann/FreeReflection</footnote>
as these classes can appear/disappear with the evolution of Android internals. Investigations for applications that defined classes multiple times suggest that the compilation process or the inclusion of different versions of the same library is the main explanation. Finally, when investigating malware samples, we found a specific sample containing a shadow attack that would hide a part of the critical code from a reverse engineer studying the application.

Future work concerns the correctness of bytecode analysis. For now, we rely on the Smali representation of the bytecode but the compilation process makes this comparison difficult. Another future work focuses on custom class loaders that an attacker can use to load classes with a specific mechanism not linked to any classical Android mechanism. We intend to trace dynamically all loadings by hooking specific methods of such a class loader, when detected. If such a custom class loader is found, it would be difficult to determine statically if a shadow class is loaded, thus requiring auditing it at runtime.

## ACKNOWLEDGMENTS

This work was supported by the ANR Research under the Plan France 2030 bearing the reference ANR-22-PECY-0007.

## REFERENCES

[1] Kevin Allix, Tegawendé F. Bissyandé, Jacques Klein, and Yves Le Traon. 2016. AndroZoo: Collecting Millions of Android Apps for the Research Community. In *13th Working Conference on Mining Software Repositories (MSR)*. (May 2016), 468–471.

[2] Steven Arzt, Siegfried Rasthofer, and Eric Bodden. 2013. Instrumenting android and java applications as easy as abc. In *Fourth International Conference on Runtime Verification*. Vol. 8174. Series Title: LNCS. Springer Berlin Heidelberg, Rennes, France, (Sept. 2013), 364–381. ISBN: 978-3-642-40786-4. DOI: 10.1007/978-3-642-40787-1_26.

[3] Steven Arzt, Siegfried Rasthofer, Christian Fritz, Eric Bodden, Alexandre Bartel, Jacques Klein, Yves Le Traon, Damien Octeau, and Patrick McDaniel. 2014. FlowDroid: precise context, flow, field, object-sensitive and lifecycle-aware taint analysis for android apps. In *ACM SIGPLAN Conference on Programming Language Design and Implementation*. Vol. 49. Issue: 6 ISSN: 03621340. ACM Press, Edinburgh, UK, (June 5, 2014), 259–269. DOI: 10.1145/2666356.2594299.

[4] Yue Duan, Mu Zhang, Abhishek Vasisht Bhaskar, Heng Yin, Xiaorui Pan, Tongxin Li, Xueqiang Wang, and Xiaofeng Wang. 2018. Things you may not know about android (un)packers: a systematic study based on whole-system emulation. In *24th Annual Network and Distributed System Security Symposium*. Issue: February.

[5] Manuel Egele, Theodoor Scholte, Engin Kirda, and Christopher Kruegel. 2012. A survey on automated dynamic malware-analysis techniques and tools. *ACM Computing Surveys*, 44, 2. ISBN: 0360-0300. DOI: 10.1145/2089125.2089126.

[6] Li Gong. 1998. Secure Java class loading. *IEEE Internet Computing*, 2, 6, (Nov. 1998), 56–61. Conference Name: IEEE Internet Computing. DOI: 10.1109/4236.735987.

[7] Yi He, Yacong Gu, Purui Su, Kun Sun, Yajin Zhou, Zhi Wang, and Qi Li. 2023. A Systematic Study of Android Non-SDK (Hidden) Service API Security. *IEEE Transactions on Dependable and Secure Computing*, 20, 2, (Mar. 2023), 1609–1623. Conference Name: IEEE Transactions on Dependable and Secure Computing. DOI: 10.1109/TDSC.2022.3160872.

[8] Pavel Kriz and Filip Malý. 2015. Provisioning of application modules to Android devices. In *2015 25th International Conference Radioelektronika (RADIOELEKTRONIKA)*. (Apr. 2015), 423–426. DOI: 10.1109/RADIOELEK.2015.7129009.

[9] Li Li, Tegawendé F. Bissyandé, Yves Le Traon, and Jacques Klein. 2016. Accessing Inaccessible Android APIs: An Empirical Study. In *2016 IEEE International Conference on Software Maintenance and Evolution (ICSME)*. (Oct. 2016), 411–422. DOI: 10.1109/ICSME.2016.35.

[10] Li Li, Tegawendé F. Bissyandé, Mike Papadakis, Siegfried Rasthofer, Alexandre Bartel, Damien Octeau, Jacques Klein, and Yves Le Traon. 2017. Static analysis of android apps: a systematic literature review. *Information and Software Technology*, 88, 67–95. DOI: 10.1016/j.infsof.2017.04.001.

[11] Sheng Liang and Gilad Bracha. 1998. Dynamic class loading in the Java virtual machine. *SIGPLAN Not.*, 33, 10, (Oct. 1998), 36–44. DOI: 10.1145/286942.286945.

[12] Yibin Liao, Jiakuan Li, Bo Li, Guodong Zhu, Yue Yin, and Ruoyan Cai. 2016. Automated detection and classification for packed android applications. In *International Conference on Mobile Services*. IEEE, San Francisco, USA, (June 2016), 200–203.

[13] Noah Mauthe, Ulf Kargén, and Nahid Shahmehri. 2021. A large-scale empirical study of android app decompilation. In *IEEE International Conference on Software Analysis, Evolution and Reengineering (SANER)*. 2021 IEEE International Conference on Software Analysis, Evolution and Reengineering (SANER). ISSN: 1534-5351. (Mar. 2021), 400–410. DOI: 10.1109/SANER50967.2021.00044.
[14] Jean-Marie Mineau and Jean-François Lalande. 2024. Evaluating the reusability of android static analysis tools. In *ICSR 2024 - 21st International Conference on Software and Systems Reuse* (LNCS). Vol. LNCS 14614. Medium: ICSR 2024. Springer, Limassol, Cyprus, (June 2024), 153-170. doi: 10.1007/978-3-031-66459-5_10.

[15] Ya Pan, Xiuting Ge, Chunrong Fang, and Yong Fan. 2020. A systematic literature review of android malware detection using static analysis. *IEEE Access*, 8, 116363-116379. doi: 10.1109/ACCESS.2020.3002842.

[16] Akihiko Tozawa and Masami Hagiya. 2002. Formalization and Analysis of Class Loading in Java. en. *Higher-Order and Symbolic Computation*, 15, 1, (Mar. 2002), 7-55. doi: 10.1023/A:1019912130555.

[17] Michelle Y Wong and David Lie. 2018. Tackling runtime-based obfuscation in android with tiro. In *USENIX Security Symposium* number 27. USENIX, Baltimore, USA, (Aug. 2018), 1247-1262.

[18] Lei Xue, Xiapu Luo, Le Yu, Shuai Wang, and Dinghao Wu. 2017. Adaptive unpacking of android apps. In *International Conference on Software Engineering* number 39. IEEE, Buenos Aires, Argentina, (May 2017), 358-369.

[19] Yueqian Zhang, Xiapu Luo, and Haoyang Yin. 2015. Dexhunter: toward extracting hidden code from packed android applications. In *European Symposium on Research in Computer Security* number 20. Springer, Vienna, Austria, (Nov. 2015), 293-311.

[20] Wenwen Zhou, Yang Yongzhi, and Jiejuan Wang. 2022. Dynamic Class Generating and Loading Technology in Android Web Application. In *2022 International Symposium on Networks, Computers and Communications (ISNCC)*. (July 2022), 1-6. doi: 10.1109/ISNCC55209.2022.9851782.

Received 29 November 2024; revised 19 May 2025; accepted 7 July 2025