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 🙂
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.

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.

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 j
ava d
eb
ugger;
-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
.

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
:

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.