Cracking UnCrackable Android apps: step-by-step guide (part 1)
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.
- 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.
- 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 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.
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.
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.
apktool d -s UnCrackable-Level1.apk -o decoded
d stands for
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
apktool b decoded -d -o Uncrackable-Level1-repackaged-with-d-option.apk
b stands for
-d is very useful, as one would have to alter
AndroidManifest.xml manually to add
app:debuggable=true attribute to
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.
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
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
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"
am is an
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
adb forward tcp:4321 jdwp:PID
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".
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
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
jdb is a tool provided by Java - a
-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
(echo suspend && cat) | jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=4321
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
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
Resume the execution:
> resume All threads resumed. > Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,251 bci=0 main
Print all local variables in current stack frame using
> resume All threads resumed. > Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,251 bci=0 main locals Method arguments: flag = true Local variables: main
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 resume All threads resumed. > Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,251 bci=0 main locals Method arguments: flag = false Local variables: main
The place of interest is hit. Change the flag with the
set command and
set <value> = <expr> assigns new value to field/variable/array element):
main set flag = true flag = true = true main resume All threads resumed. > Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,251 bci=0 main
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
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
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=0 main
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
locals command of the
jdb. Several iterations might be required.
main locals Method arguments: Local variables: anObject = "RAW" main cont > Breakpoint hit: "thread=main", java.lang.String.equals(), line=997 bci=0 main locals Method arguments: Local variables: anObject = "UTF-8" main cont > Breakpoint hit: "thread=main", java.lang.String.equals(), line=997 bci=0 main locals Method arguments: Local variables: anObject = "I want to believe" main
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.
For more engineering insights shared by Mews tech team: