DexGuard 8.3 introduces a new strong outer protection layer: code virtualization. It protects parts of your codebase from static analysis by replacing the original code with a randomized instruction set interpreted by a generated virtual machine (VM). Code virtualization is an alternative to class encryption.
DexGuard applies various layers of code protection to shield it from illegitimate static code analysis. One of the most fundamental layers is name obfuscation, the substitution of human-readable names within the application with functionally equivalent random characters. ProGuard only applies name obfuscation to the actual code. DexGuard takes it a step further and also applies it to Android-specific data, files and resources. DexGuard also provides string encryption to shield data that is stored as simple strings in your APK, it hides standard library method calls by replacing them with reflections calls, transforms trivial arithmetic expressions into more complex formulations etc. All of these hardening techniques prevent attackers from simply clicking and dragging your APK into a decompiler and exploring the code to determine the role of the various classes, fields or methods.
At a higher level, DexGuard conceals the well-chosen human code structure and reorders it into a functionally equivalent but convoluted control flow. The obfuscated code confuses and crashes decompilers because it doesn’t resemble in any way to the code produced by standard compilers. It also makes it much harder to debug-step through a method to deduce its functionality as the control flow is redirected all over the place. This kind of obfuscation is a strong option to protect sensitive algorithms or protocol implementations.
The final step is to hide parts of your mobile app. This is what class encryption achieves: it effectively makes your code invisible for static analysis. For that reason, class encryption (and by extension, packing) is a very effective outer defense layer for your Android application. This is how it is works: a class is encrypted into a stream of seemingly meaningless bits. The class is only decrypted at runtime, when it is needed, and made available to the app (see Figure 1).
DexGuard 8.3 introduces code virtualization for Android applications. This code hardening technique has the same aim as class encryption: hiding (parts of) the application’s code to protect them against static analysis.
Code virtualization transforms your method body into a stream of instructions for a randomized virtual machine that is injected into your app. This transformation happens at build time. DexGuard carefully analyses how your method code behaves and generates small and efficient VMs, each with their own unique instruction set. Once the VMs have been generated, DexGuard re-implements your method on the new instruction set. Each time your method is called, the new implementation is loaded and an interpreter on the native VM executes the instructions.
Figure 1: the result of applying class encryption (left) and code virtualization (right)
The result is that your original method code is no longer to be found inside the application. Also, and contrary to class encryption, the original code is never reconstructed at runtime. Everything happens in application space (see Figure 1), which means that the overhead of class loading is avoided. The generated code is included in the standard DEX files that contain the code of the application and is optimized during installation by the Android system.
Code virtualization combines well with other DexGuard techniques like API call hiding, string encryption or control flow obfuscation. For maximal protection, you can even combine it with class encryption. Applying these different layers of protection ensures that unobfuscated method calls can not easily be detected and that no 1:1 correspondence can be found with previously released versions of your app.
To be able to decide whether to use class encryption or code virtualization (or both), it is crucial to be aware of the possibilities and restrictions of either hardening technique.
First, code virtualization enables you to protect classes than cannot be encrypted. The Android OS and Dalvik VM restrict which classes can be encrypted. For example, activities mentioned in the Android manifest need be available at all time. Code virtualization allows you to wrap these classes in a strong outer protection layer.
Second, class encryption and code virtualization have a markedly different impact on app performance. Class encryption introduces unavoidable computational overhead, caused by the loading of the bitstream to decrypt the encrypted classes and the process of making the decrypted classes available to your application. The impact of the decryption varies from application to application. For that reason, DexGuard allows you to determine where to apply class encryption so that you can throttle the overhead. Properties of the target devices also influence the performance of class encryption, such as I/O speed and available disk space.
Code virtualization shifts the computational overhead to a constant factor during runtime. In most cases, this doesn’t influence responsiveness in any way. Only code that contains many loops, recursive method calls or numeric operations generally slows down a lot. These constructs cannot be optimized by the AOT (ahead-of-time) or JIT (just-in-time) compilers because the virtualized code is effectively interpreted. For these cases, class encryption usually is the better option. Another solution is to target specific parts of the code, this enables you to better manage the overhead.
To illustrate the impact of either technique, we include the results of our tests with Google’s I/O Schedule sample app (Figure 2). We’ve applied both class encryption and code virtualization to the core of the app (the application code written by the developer, +-2.000 methods) and to the full app (including libraries, +-17.000 methods). The table shows the startup times as reported by Android’s ActivityManager and switching time between a handful of activities. The results clearly show that code virtualization has a constant performance impact but never really interferes with a responsive user experience. Class encryption, on the other hand, leads to spikes of noticeably hanging while decrypting chunks of the app. This is most apparent at startup at which point the app can take more than a full second to boot.
|Splash||Screen 1||Screen 2||Screen 3||Screen 4|
|Class encryption core||+0ms||+400ms||+0ms||+0ms||+0ms|
|Class encryption full||+1100ms||+200ms||+400ms||+0ms||+0ms|
|Code virtualization core||+0ms||+120ms||+100ms||+80ms||+50ms|
|Code virtualization full||+200ms||+300ms||+250ms||+320ms||+350ms|
Figure 2: Startup penalty for Activities compared to standard DexGuard obfuscation. Values are expressed in milliseconds as reported by ActivityManager.
Note that most application code isn’t performance critical or security sensitive, so the core columns in Figure 2 are the most relevant. This assumption also stands when testing the two techniques on gaming apps. Both techniques barely introduced any noticeable lag when their core (between 2.000 and 7.000 methods) was virtualized.
Code virtualization is available as part of the latest DexGuard release, DexGuard 8.3. You will find more details in the included documentation (newly introduced option -virtualizecode).