Cracking UnCrackable Android apps: step-by-step guide (part 1)

Android/Flutter developer @mews, UNIX shell advocate, Linux and computer networking enthusiast | former nuclear engineer.

The best way to learn how something works is to break it apart – or build it yourself. I assume you’re an Android developer and you’re familiar with the later part, so let’s focus on the former one by cracking two Android apps, extracting a secret String from them.

Why cracking?

  • To patch bugs in software whose code-base you have no influence over.
  • To make your projects more secure by learning the ways that bad guys might compromise them.
  • To discover secrets 🙂
This is a blog-post version of my Cracking UnCrackable Android Apps webinar.

What this guide covers

There is a great online resource dedicated to mobile security: the Mobile Security Testing Guide (MSTG). I’m going to present here a solution for two Android CRACKMEs provided by it. What is a CRACKME?  Think of it as an app built purposefully to be cracked. MSTG provides several CREACKMEs with varying difficulty level; I’m going to go over the basic ones, level 1 and level 2 (in my next blog post).

Level 1: the secret is on the Java side, debugging Java code.

Level 2: the secret is on the native side, debugging and patching native library.

The MSTG repository also contains links to other solutions of the same CRACKME challenges – I encourage you to check them out after reading this guide.

The tools

  • adb – install apk, get shell
  • apktool – unpack/re-package apk
  • dex2jar – convert .dex files to .class files
  • jd-gui – java decompiler with GUI
  • jdb – Java debugger
  • gdb – GNU project debugger
  • cutter – binary patching with GUI
  • radare2 – binary patching

Level 1

Level 1 app is a simple one screen app, with an input field and a VERIFY button. Pressing the button will compare whatever is in the input field with the secret String. The main goal of the cracking challenge is to find out the value of that secret String.

The plan is to put the app into debug mode and debug it. The debugger will allow you to see the value of the secret String, as well as circumvent the safety mechanisms employed by the app.

See what you are dealing with: install and uninstall the app

List available emulators:

where the value of the environment variable $ANDROID_HOME is usually ~/Android/Sdk. If nothing comes up after running the previous command, create an emulator using avdmanager tool, or via Android Studio GUI.

$ANDROID_HOME/emulator/emulator -list-avds

Fire up the emulator:

$ANDROID_HOME/emulator/emulator -avd <name_of_the_avd> &

Install the app:

adb install UnCrackable-Level1.apk

Launching the app on the emulator will give a dialog "Root detected" and the app will exit upon closing the dialog with the dialog’s button. This is because the app has a root detection mechanism to prevent tempering and that emulator is considered to be a rooted device. We are going to bypass this dialog.

Android App Level 1 Root Decected

The main piece to cracking the app is to make it debuggable.  Currently, the installed app is not debuggable, so uninstall the app.  First, find out its package name:

adb shell pm list packages | grep mstg

Then, uninstall it:

adb uninstall owasp.mstg.uncrackable1

Make the app debuggable

An app should be debuggable if it is flagged as one in its AndroidManifest.xml file. Use apktool to unpack .apk, and then re-pack it with the altered manifest file.

Unpack .apk:

apktool d -s UnCrackable-Level1.apk -o decoded

where d stands for decode;

-s means do not decode resources (we don’t need them. All we need is a manifest);

-o – output directory.

Now, repack with -d option, that will automatically add debuggable="true" to the AndroidManifest.xml:

apktool b decoded -d -o Uncrackable-Level1-repackaged-with-d-option.apk

where b stands for build.

Option -d is very useful, as one would have to alter AndroidManifest.xml manually to add app:debuggable=true attribute to <application> tag.

Sign the app

The app needs to be signed, otherwise the installation of the unsigned app will fail. Use apksigner to check whether the app will pass the verification process during installation:

apksigner verify --print-certs --verbose <path_to_apk>

The following template can be used to sign the app:

apksigner sign -v --in UnCrackable-Level1-repackaged-with-d-option.apk --v2-signing-enabled --ks <path_to_keystore_file> --ks-key-alias $KEYSTORE_KEY_ALIAS --ks-pass env:KEYSTORE_PASSWORD --ks-type pkcs12

Signing the app is not in the scope of this post, hence no further explanation of the above command is provided. If you are not comfortable with signing on the command line (terminal), you can always sign your app in Android Studio. A keystore file can also be created with the help of Android Studio.

Install debuggable app

adb install UnCrackable-Level1-repackaged-with-d-option.apk

Upon launch, the app will show "App is debuggable" dialog – another hack-prevention mechanism. This is to be bypassed.

Android App Level 1 Debuggable

Bypass dialogs

There is little use of a debuggable app when one cannot reach the point which is to be debugged. In other words, the dialog "App is debuggable" prevents us from debugging a place in code that is called when the VERIFY button is pressed. To get to that stage, one has to bypass the dialog.

Decompile the app – see the source code

First, convert .apk to .jar:

d2j-dex2jar.sh -f UnCrackable-Level1.apk

Second, open GUI tool:

java -jar /opt/jd-gui/jd-gui.jar &

Third, with the GUI tool, open the .jar archive for source code inspection. After inspection, it is evident that dialog is set not to be dismissed on the click outside of it – only pressing the dialog button can close the dialog. The problem is that button’s click listener will exit the app. So, the way around it is simple: make dialog dismiss-able by clicking outside of it, i.e. change false to true in alertDialog.setCancelable``(false). It can be achieved with the debugger setting the variable during runtime.

Attach the debugger

Run the app in "wait for debugger" mode:

adb shell am start -D -n "owasp.mstg.uncrackable1/sg.vantagepoint.uncrackable1.MainActivity"

where am is an ActivityManager;

start starts a component;

-D enables debugging;

Next, a debugger must be connected to the app. The debugger resides on the local machine. To transfer debugging information from the device (emulator) to the local machine one should establish a connection – a socket connection. The setup is simple: using adb establish a socket connection between the app process and a socket on the localhost.

adb forward LOCAL REMOTE

or:

adb forward tcp:4321 jdwp:PID

where tcp:4321 stands for "use port 4321 on the localhost and TCP as a transport protocol". 4321 was chosen for ease of typing and remembering;

jdwp:PID stand for "use process id of the app and a JDWP as a transport protocol".

Find PID by running:

adb shell ps | grep mstg

On a UNIX machine, one can verify that there is a socket 4321 listening by running:

lsof -i -P -n | grep LISTEN

Describing options to lsof is out of the scope of this post.

If you have been following up to this point exercising all the commands on your machine, at this stage:

The app waits for a debugger to be attached to it.

There is a connection to the app’s process via localhost:4321.

All that’s left is to fire up that debugger (DO NOT RUN THIS COMMAND):

jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=4321

where jdb is a tool provided by Java – a java debugger;

-connect – establishes a connection to target VM using named connector com.sun.jdi.SocketAttach with argument values listed after :. Read this, if you want to know more about communication between debugger and VM.

The command attaches the jdb to listen to the localhost socket 4321 for data that is passed there by adb from the app. The problem with the above command and the reason I have asked you not to run it, is that as soon the debugger starts to listen to the socket, the app will resume: the code for showing the dialog will run and we don’t want that because we want to debug that code.

In order to suspend the execution of the app upon debugger connecting to it, pipe down suspend command to the jdb:

(echo suspend && cat) | jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=4321

where suspend is a legitimate command to the interactive jdb command. To see what jdb supports, run jdb – it will give the interactive shell – then run help.

The output will be:

droid@droid:~/dev/hack/mstg-level-1$ (echo suspend && cat) | jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=4321 Set uncaught java.lang.Throwable Set deferred uncaught java.lang.Throwable Initializing jdb ...  > All threads suspended.  >

and at that moment debugger will be attached and the app will be suspended prior to any of its code being run.

Alter variables during runtime

Set breakpoint at the line when the dialog is made not dismiss-able:

Initializing jdb ...  > All threads suspended.  > stop in android.app.Dialog.setCancelable Set breakpoint android.app.Dialog.setCancelable >

Take a note that it is android.app.Dialog.setCancelable and not android.app.AlertDialog.setCancelable.

Resume the execution:

> resume All threads resumed.  > Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,251 bci=0main[1]

Print all local variables in current stack frame using locals command:

> resume All threads resumed.  > Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,251 bci=0main[1] locals Method arguments: flag = true Local variables: main[1]

flag = true hints that this is not the place wanted – android.app.Dialog.setCancelable``() is called by something else somewhere else. What we want to see is flag = false.

resume till the desired output:

main[1] resume All threads resumed.  > Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,251 bci=0main[1] locals Method arguments: flag = false Local variables: main[1]

The place of interest is hit. Change the flag with the set command and resume (set <value> = <expr> assigns new value to field/variable/array element):

main[1] set flag = true flag = true = true main[1] resume All threads resumed.  > Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,251 bci=0main[1]

Continue with set flag = true and resume till the execution is not stopping at any breakpoint anymore. It should take two more times. After the app resumes, you should be able to close the dialog by clicking outside of it.

Get the secret

The technique is the same as for dismissal of dialogs – look into source code and try to see what can be taken advantage of. The secret is stored in the app, but is compared with the user’s input using java.lang.String.equals.

Code Level 1 Equals Call Site

So, set a breakpoint to java.lang.String.equals and see the parameters! The bad news is, if you try to set a breakpoint on equals method, you will quickly realize that it is quite a popular method – you will be getting lots of hits on it, most of which (except one) you don’t want. So, let’s set a breakpoint prior to where equals is called – see the line in a try... catch block in the image above. It would be javax.crypto.Cipher.doFinal:

Code Level 1 Cipher

Set the breakpoint first, then type something into the input field of the app and click the VERIFY button. The breakpoint should be hit after this.

> stop in javax.crypto.Cipher.doFinal(byte[]) Set breakpoint javax.crypto.Cipher.doFinal(byte[]) > Breakpoint hit: "thread=main", javax.crypto.Cipher.doFinal(), line=2,047 bci=0main[1]

It is now a good time to set a breakpoint on java.lang.String.equals method and resume the debugging.

After resuming the execution with cont and hitting equals breakpoint, set the parameters to equals with locals command of the jdb. Several iterations might be required.

main[1] locals Method arguments: Local variables: anObject = "RAW" main[1] cont > Breakpoint hit: "thread=main", java.lang.String.equals(), line=997 bci=0main[1] locals Method arguments: Local variables: anObject = "UTF-8" main[1] cont > Breakpoint hit: "thread=main", java.lang.String.equals(), line=997 bci=0main[1] locals Method arguments: Local variables: anObject = "I want to believe" main[1]

The secret is: I want to believe. Note, that there is no indication that that is the secret. It could be RAW. You just have to gather everything you got and check it.

The guide on cracking uncrackable Android apps on LEVEL 2 to be found HERE.

Android/Flutter developer @mews, UNIX shell advocate, Linux and computer networking enthusiast | former nuclear engineer.
Share:

More About