HelloWorld, Goodbye Protection: Disassembling a Broken DEX Challenge

2 weeks ago 2

Alok Kumar Mishra

Recently, I took on a CTF-style Android reversing challenge that looked deceptively simple at first glance, however proved to be a little mind boggling and a learning experience, compared to the other challenges I do. The task?
Modify the text displayed on the home screen of the app.

Sounds like a breeze, right? Just grab the APK, fire up ApkTool, tweak the Smali, recompile, and done.
But — plot twist — this app is described as ApkTool Resistant.

Here’s the exact description of the challenge:

“A very basic HelloWorld app with no native code or complex encryption. Just a broken dex file inside. Can you disassemble it and change the line on the main screen? :)”

At this point, I was intrigued. No native code? No obfuscation? Just a broken .dex file? This was clearly a test of tool knowledge and creative problem-solving. So I downloaded the APK and got to work…

Initial Analysis

Upon launching the application, I noticed just a black screen, with a title “HelloWorld” and a text box which read “Hello world!”. (Keep in mind, the differences in both)

HelloWorld App — It has just a single screen which is the main screen.

Examining the APK

For analyzing the application, I decompiled the APK using JADX-gui.
I found a basic AndroidManifest.xml file. However, the manifest file gave me my first step for analysis: The launch activity.

Press enter or click to view image in full size

The marked intent defines this activity as the main/launch activity.

So far so good, we’ve got our first target: com.v7878.helloworld.MainActivity

Let’s checkout this class in the source code.

Press enter or click to view image in full size

Source code of com.v7878.helloworld.MainActivity

But wait, we got an empty class! How’s the app even running with a blank activity? It’s not even definining an extended class…

Here’s where the twist lies. This app very cleverly utilizes sun.misc.UnSafe to unpack everything at runtime and execute it. Not only the MainActivity, most of the classes present in the application will be blank, and will be populated and executed at runtime. In the same parent directory v7878, we can notice a class named “SunUnsafe” in com.v7878.sun.unsafe.

Class com.v7878.sun.unsafe.SunUnsafe

But wait… What’s this sun.misc.Unsafe mystery? Well I got you covered. A blog uncovering the mystery by Baeldung, has it all you need to know about this particular package.

The crux we get is, it’s a hidden package (available in Java JDK, but not in Android JDK directly), which is accessed using reflection. Unsafe allows you to load and run raw bytecode in memory without leaving readable traces in your app.

Let’s open the source and try to decipher what’s going on. For that, we’d be taking a look at it’s raw smali code than the translated Java code, to understand things better.

Press enter or click to view image in full size

Exporting the smali

As soon as we open the smali file, we notice the expected reflection usage in the class object initializer method (clinit), to initialise theUnsafe field from the class Lsun/misc/Unsafe, storing it into register v0. Then a truthy value is parsed to set it as accessible and a getter invoke is used to store the retrieved instance, which is then checked by check-cast and put into field a.

Press enter or click to view image in full size

Class com.v7878.sun.unsafe.SunUnsafe initilalizer method

Now, we know the retrieved instance is stored into field a, we are now concerned where is this field a is used. For that, we’ll just be using the signature of the field a, to find the getters of the field, in the decompiled smali directory. We used Apktool_2.11.1 and Notepad++ for decompiling the APK into smali, and recursively searching for the getter instances of the field a.

Press enter or click to view image in full size

Decompilation through Apktool

Press enter or click to view image in full size

Recursive searching for the field getters

As the field a is a public static field here, it’s usage may be spread over multiple classes, however if it had been a private method, we wouldn’t have required to do a recursive search and rather could’ve searched in the same class it is defined. Let’s look at the occurences:

Press enter or click to view image in full size

All usages are in the same class

Nevertheless, we found all of it’s getter usages in the same class. Let’s start from the first usage…

Press enter or click to view image in full size

1st getter usage of the field

Apparently, the method it’s used in, is retrieving the addressSize of the instance field a, and returning it. Let us similarly find the usage of the method through it’s signature, again with a recursive search.

Press enter or click to view image in full size

Class object initializer method (<clinit>)

Apparently, the usage is found in another class (com/v7878/helloworld/Factory.smali). If we look carefully at the class initialization, we observe this class has extended AppComponentFactory. Taking a quick reference at the Android developer page , we get to know:

Allows application to override the creation of activities. This can be used to perform things such as dependency injection or class loader changes to these classes.

We can conclude, this is the class responsible for dynamic class loading at runtime in memory. Moving ahead, while taking a quick look at the class initializer method, we notice something that we’ve been searching for long…

Press enter or click to view image in full size

<clinit> method — com/v7878/helloworld/Factory.smali

Let’s try to understand this highlighted block of code, and what it does, stepwise:

  • It creates a new byte array of length 8 (0x8) (new-array v4, v4, [B).
  • Wraps it using a ByteBuffer and sets little endian byte order.
  • Reads a long value from memory via:

invoke-static {v2, v3}, Lcom/v7878/sun/unsafe/SunUnsafe;->d(J)J

(which is the core where memory is read reflectively.)

  • That long value is placed at offset 0 in the byte array.
  • A new String is created from the byte array.
  • That string (which prints as "Hello world!") is then printed. (println)

Now, upon seeing this, the immediate idea that strikes my mind is, why not just hardcode this stuff… I mean, we can entirely skip the buffer creation and memory processes, and define a string of our own choice to be printed, maybe? Let’s try doing that.

Press enter or click to view image in full size

Hardcoded the string print logic

We replaced the entire block with our hardcoded string, to be printed. Let’s try to compile this and launch the app to see if the changes appear as expected.

Press enter or click to view image in full size

Compiling app after smali modification

As soon as I was done compiling it, I went to see the compiled apk, but…

Press enter or click to view image in full size

Size difference between the original and recompiled apk

Alas! There was a suspicious size difference between the compiled apk and the original one. In general, the recompiled apk after modification has a slightly greater size due to optimization changes, however here it’s over a half megabyte decreased. Let’s checkout what’s changed:

Press enter or click to view image in full size

Side-by-side view for analyzing the unexpected size reduction

The compiled dex in the modified APK, has a much reduced size than expected, where we just replaced a few lines of code. This is where the part “Resistance to ApkTool” comes in. When we compiled our modified application with ApkTool, it applies few defaulted optimizations, which in general are for dex optimization and a smooth compilation. However, in our case, it acts opposite and rather nullifies most of the class codes that are dynamically loaded in memory, and corrupts the application. Let’s try running this application:

Press enter or click to view image in full size

App crashlog pulled over ADB

As expected, the app failed to run with the above crashlog (pulled from ADB). Looking at the first 4 lines of the log, it points towards memory corruption, and the rest of the part signifies an illegal class extension (“int” is a built-in datatype, invalid for class identifiers, while the error states Class “int” extends itself). Now, what to do if we can’t even compile with ApkTool after our changes…

There are 2 possible routes to go from:

1st is to modify apktool to skip all sort of compilation optimizations, and recompiling the apk afterwards.

2nd one is to go with the unconventional method of hex editing. Think about the issue, it’s with compilation optimizations, that is, when we are trying to recompile a modified smali directory. But, we know the string is hardcoded into the app, then why can’t we modify it directly through hex editing?

Press enter or click to view image in full size

classes.dex extracted from untouched APK

First off we rename the untouched APK to change it’s extension to ZIP (as we know, all Android packages are a type of zip archive). Then we extract the classes.dex file from the ZIP, which we have to apply hex editing on.

Press enter or click to view image in full size

HxD Editor

Moving ahead, we open the classes.dex, with an open-sourced hex editor utility, HxD. You could see the first 7 characters in the decoded text, which indicate the DEX version (039).

Let’s move to our target — to find and replace the string text.

Search window — HxD

We search for the exact ASCII string case-sensitively for the previously discussed reasons. Let’s see the output results:

Press enter or click to view image in full size

Search results — HxD

We found the exact string we were searching for, at the hex address 0x000F866A, if matched from row and column of the start of the string.

Now, before moving on to doing the replacement, we must learn a few things about hex strings here.

First off, the structure of ASCII strings in hex. DEX strings are represented using the string_data_item format, which begins with a ULEB128-encoded length field—this indicates the number of UTF-16 code units (or simply, the number of characters for standard ASCII). This is followed by the UTF-8 encoded bytes of the string (i.e., the hexadecimal representation of each ASCII character), and finally terminated with a null byte (0x00). Therefore, the general format of a string in the hex dump is:
<length (ULEB128)> + <UTF-8 bytes of string> + <null byte>
As an example, a 4-character ASCII string would typically occupy 6 bytes: 1 byte for the length, 4 bytes for the characters, and 1 byte for the null terminator.

Secondly, the strings here are in alphabetical order, and while replacement, our replacement string too must retain the alphabetical order.

Thirdly, all strings here are mapped to their hex values, and are explicitly case sensitive (the hex value for “A” is 0x41 and the one for “a” is 0x61), this must be taken care of, while replacement.

Lastly, the length of the strings. We must retain the length of the hex strings, no matter what. The length of the replaced string should be equal to that of the current string. Incase the replaced string is shorter, we pad the rest of the space with 0x00 (null) value in hex, and for longer strings, in general, it cannot be replaced with a shorter hex string, as it may overwrite bytes of next string or even length/null bytes, and we must find a longer string to replace it with, or alter memory reference values to point to the replaced string instead of current string in the display.

I learned a few of these, the hard way, falling into errors and reading documentation, hence clearing them here right away before moving on, so you don’t repeat the same mistakes.

Now, it’s time to choose a string to replace “Hello world!” with. I solved this challenge on 17th of March, and at that time, Pavel Du Rove, the founder of Telegram, was quite much into controversies surrounding his arrest and return to home. So I decided to keep something relating to that matter. As observed, we must select a string that comes between “HORIZONTAL_DIMENSION” and “Horizontal” (Note how uppercase ascii letters precede lowercase ascii letters, due to hex value mapping of them. Hence, we too, must follow hex values of characters to find the suitable replacement.)

I just thought of “HelpfulPavel” to be the suitable replacement, as it fits the length of the original string “Hello world!” too. I quickly calculated the hex equivalent of the replacement string, which came out to be :

48 65 6C 70 66 75 6C 50 61 76 65 6C

Now, let’s overwrite this over the original hex values of current string.

Now, all we have to do is save the modified dex file, and replace the modified dex file inside the untouched APK , with the original one.

Press enter or click to view image in full size

Modified dex file

Press enter or click to view image in full size

Modified app with the dex file

Now, we need to install the app and check if our changes are applied or not. But before that, a primary step, that’s a necessity for android apps to be installed, is signing the app. Not going into much detail, as it’s not in the scope of this blog, I used “uber-apk-signerby “PatrickFav”, for signing the apk everytime I tested it, as the APK remians unsigned whenever we modify it, and Android doesn’t allow any APK without a signed certificate to be installed.

Let’s now install the signed apk:

Press enter or click to view image in full size

As we already installed a previous version of this app, we are now using the -r flag to replace the existing one with this updated one.

But as soon as I launched the APK, it crashed again. What could be the reason now, the dex file size is also the same and we have also kept in mind the string replacement rule while hex editing…

Time to view the crashlog again:

Press enter or click to view image in full size

Crashlog after launching the hex edited APK

Aha! The bad checksum error. Here comes the final part of the process, dealing with malformed dex (Remember the term “Broken DEX” in the challenge?).

Try to remember the first thing we noticed when we opened the dex file with HxD… The dex version (039), right?
Did the other bytes ahead of it, were also intended to convey something to us about the Dex?

Yes! Infact, the first 32 bytes if the dex are all concerning to it’s integrity as follows:

Offset Size Field Data Type Purpose
--------------------------------------------------------------

0x00–0x07 8 bytes magic char[8] File ID & version

0x08–0x0B 4 bytes checksum uint32 Adler32 from 0x0C to EOF

0x0C–0x1F 20 bytes signature uint8[20] SHA-1 from 0x20 to EOF

The first 32 bytes of a DEX file form its header prelude, ensuring integrity, version recognition, and basic verification before deeper parsing.

Magic (0x00–0x07)
Identifies the file as a DEX file and specifies the format version (e.g., dex\n035\0). It’s crucial for tools like the Android Runtime (ART) to detect and validate the file format.

Checksum (0x08–0x0B)
A 4-byte Adler-32 checksum computed over the entire file starting from byte 0x0C to the end. It offers a fast integrity check to detect accidental corruption.

Signature (0x0C–0x1F)
A 20-byte SHA-1 hash calculated from byte 0x20 to EOF. It ensures content authenticity and tamper resistance, used during class loading and verification stages.

This structure ensures that the DEX file is authentic, uncorrupted, and compatible with the expected version of the Android runtime.

The issue with the current dex file is, the improper 4-byte Adler-32 checksum, which is calculated from 0x0C to the end of the file to maintain the integrity of file content, and stored at offset 0x08–0x0B.

This causes the runtime failure with a bad checksum for our aplication. To fix this, we need to replace the improper checksum, with a recomputed checksum from 0x0C onwards, so it aligns with the file and the app doesn’t throw a runtime error. Not going into much detail, we have got the implementations to the automation of dex repair, in the open source community itself, by user “anestisb”. The scripts automates the calculation and fixing of checksums and magic values, if they’re badly aligned. Moreover, Python3 has got it’s own advantages having in-built libraries like hashlib (for SHA-1) and zlib.alder32, which make the process of automation much simpler, and one of my great friends, “Abhi”, has ported it’s implementation in Python3 as well, for one of his projects.

In all, you may chose to fix it manually using Python, or just use the available scripts to do so, and fix the dex file. As soon as I fixed the dex file, signed it, and launched it again, the app launched, and displayed our modified text without any issues, as expected.

Successful launch of the app with modified text

Caution: Handling Applications Using sun.misc.Unsafe

If during reverse engineering you encounter the use of sun.misc.Unsafe, treat the application with extreme caution. Unsafe allows direct access to memory and low-level system operations, bypassing standard Java safety mechanisms. This capability is frequently leveraged for obfuscation, anti-debugging, and potentially malicious behaviors.

Running such apps on a personal or host system can expose you to severe risks, including memory corruption, silent payload execution, and system compromise.

Do not execute these applications directly. Always analyze them within isolated environments such as:

— A sandboxed emulator

— A rooted test device with no sensitive data

— A virtual machine with strict network controls

Presence of Unsafe is a strong indicator of non-standard behavior. Proceed only after thorough inspection.

As an example to the practical usage of Unsafe , you could take a look at the application “Creati AI Photo Generator”, which utilizes this package in HiddenApiBypass.It allows apps or modules to access Android’s internal (hidden) APIs that are normally restricted from use starting Android 9+. sun.misc.Unsafe allows direct memory access, bypassing Java’s memory safety. With Unsafe, HiddenApiBypass can:

  • Directly manipulate method handles or class flags.
  • Patch the hiddenApiEnforcementPolicy flags at runtime in ART.
  • Bypass verification checks when reflective access is attempted.

Press enter or click to view image in full size

Implementation of Unsafe API in HiddenApiBypass library (Creati AI Photo Generator)

As it doesn’t come under the scope of this blog, you could read the internal working of this library in the official blog.

Finally, we got over with this challenge. This challenge was shared at crackmy.app (https://crackmy.app/crackmes/broken-dex-file-1-resistance-to-apktool-7817), however it got removed as of now, so I’ll be sharing the file, for practice/reference.

  1. Original Untouched APK (MD5: 4c6c2e79e6118cf085890ed48ad159c6)

We’ve reached to the end of the blog…

This being my first technical blog, I really hope, it would’ve been worthy of your time, and genuinely thank you for spending your precious time to read this.

Please don’t hesistate to share your feedback, comments, suggestions and improvements of all sorts, in the comments. I welcome them all and am open to improvement as much as possible.

Read Entire Article