Level 2 looks and feels like CRACKME level 1, but it is more difficult to crack for two reasons:
- First, the place where the secret is stored. This time, it is hard-coded in native code.
- Second, the app also employs a technique that makes attaching a debugger to it a bit harder.
The plan for getting to the secret is the same though: make the app debuggable, get to the place you want to debug and see the registers that store the secret.
Reconnaissance
Convert .apk
to .jar
:
d2j-dex2jar.sh -f UnCrackable-Level2.apk
examine the source code buy launching JD-GUI and opening the result of the previous command in it:
java -jar /opt/jd-gui/jd-gui.jar &
Examine MainActivity
.

static { System.loadLibrary("foo"); }
means that the system loads a native library. MainActivity
initializes that library, which is indicated by lines:
private native void init();
and
protected void onCreate(Bundle paramBundle) {
init();
...
}
Examining the source code of MainActivity
further, one can notice that secret validation is done by CodeCheck
class – it’s a
method:
package sg.vantagepoint.uncrackable2;
public class CodeCheck {
private native boolean bar(byte[] paramArrayOfbyte);
public boolean a(String paramString) {
return bar(paramString.getBytes());
}
}
A
method delegates to the native method bar
(loaded from the native library that is initialized in MainActivity
). At this point it is evident that validation is done in native code. It is impossible to debug Java code with jdb
here – to set a breakpoint on the Java’s compare
method – as was done in Level 1 CRACKME, because, well, there is no Java method. Debugging must be done for native code – with gdb
and addressing concrete memory addresses as breakpoints. What needs to be debugged here is that native library – "foo"
library.
Debug native library
Extract the native library with apktool
:
apktool d UnCrackable-Level2.apk -o decoded
native libraries are going to resize in decoded/lib
directory under directories that are named after the CPU architecture that libraries are compiled for:
droid@droid:\~/dev/hack/mstg-temp$ l decoded/lib/
total 24K
4.0K drwxr-xr-x 6 droid droid 4.0K Apr 26 14:21 .
4.0K drwxr-xr-x 6 droid droid 4.0K Apr 26 14:21 ..
4.0K drwxr-xr-x 2 droid droid 4.0K Apr 26 14:21 arm64-v8a
4.0K drwxr-xr-x 2 droid droid 4.0K Apr 26 14:21 armeabi-v7a
4.0K drwxr-xr-x 2 droid droid 4.0K Apr 26 14:21 x86
4.0K drwxr-xr-x 2 droid droid 4.0K Apr 26 14:21 x86_64
One needs to find out what library “flavor” the device, on which the app is to be run, uses.
BE AWARE: having Android Studio say x86_64
emulator on the emulator image proved to be misleading. As I’ve found out, what is loaded on such x86_64
emulator is x86
library. To find out which library is actually loaded when the app is run: 1) run the app; 2) find out its PID
; 3) see inside the /proc/PID/maps
file; 4) search for app name to see what libraries are loaded. They will have an architecture designator in their names. It is crucial to get it right in order to successfully follow this guide all the way through. The next section is dedicated to that.
Know your architecture
See what emulators are available:
droid@droid:\~/dev/hack/mstg-temp$ $ANDROID_HOME/emulator/emulator -list-avds
Nexus_10_API_29
Nexus_10_API_29_2
Pixel_2_API_29
Pixel_2_XL_API_29
Launch one:
$ANDROID_HOME/emulator/emulator -avd Pixel_2_API_29 &
Install vanilla soon-to-be-cracked app:
adb install UnCrackable-Level2.apk
Launch the app on the device. Find its PID
:
droid@droid:\~/dev/hack/mstg-temp$ adb shell ps | grep mstg
u0_a139 12312 1774 1903428 116844 0 0 S owasp.mstg.uncrackable2
u0_a139 12336 12312 1845480 24320 0 0 S owasp.mstg.uncrackable2
There are two processes (more on this later). Choose either one and while being root
on the device’s shell, run the following:
generic_x86:/ # cat /proc/12336/maps | grep uncrack
c15c6000-c15c9000 r-xp 00000000 fd:20 23507 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/lib/x86/libfoo.so
c15c9000-c15ca000 r--p 00002000 fd:20 23507 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/lib/x86/libfoo.so
c15ca000-c15cb000 rw-p 00003000 fd:20 23507 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/lib/x86/libfoo.so
c1cb4000-c1cb9000 r--p 00000000 fd:20 23537 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/oat/x86/base.odex
c1cb9000-c1cba000 r-xp 00005000 fd:20 23537 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/oat/x86/base.odex
c1cba000-c1d7c000 r--s 00000000 fd:20 23538 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/oat/x86/base.vdex
c1d7c000-c1d7d000 r--p 00006000 fd:20 23537 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/oat/x86/base.odex
c1d7d000-c1d7e000 rw-p 00007000 fd:20 23537 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/oat/x86/base.odex
d3134000-d316d000 r--s 00099000 fd:20 23463 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/base.apk
dea76000-dea80000 r--s 000d2000 fd:20 23463 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/base.apk
dea8a000-dea97000 rw-p 00000000 00:00 0 [anon:dalvik-/data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/oat/x86/base.art]
ec1de000-ec1df000 r--p 00005000 fd:20 23540 /data/app/owasp.mstg.uncrackable2-CKZ-8gOexaIDJQWvGE78Qg==/oat/x86/base.art
From which the answer for the library architecture is x86
.
See inside the native library
Open decoded/lib/x86/libfoo.so
with the Cutter
tool to see the machine code (assembler code) for the native library. Cutter
comes as a ready-to-be-used binary that I’ve placed in /usr/bin
directory on my machine:
Cutter-v1.10.2-x64.Linux.AppImage &
When in Cutter
, function
tab/window describes all the functions used in the library, so it is a great place to start the examination of it.

There are two functions on the list that are quite interesting – strncmp
and ptrace
. One can assume that strncmp
is used to compare user input with the secret. That makes it a candidate for a debugging.
The other function worth mentioning is ptrace
. It is a Linux system call used for debugging. It attaches itself to the Android process (read as Android app). It is used this way as an anti-debugging measure to avoid reverse-engineering of the app, since only one process can be attached to Android process. Indeed, later in this guide, if ptrace
was not eliminated, one couldn’t have attached gdbserver
to the app’s process:
134|generic_x86:/data/local/tmp # gdbserver :8888 --attach 12312Cannot attach to process 12312: Operation not permitted (1), process 12312 is already traced by process 12336Exiting
Get rid of ptrace
To make a library not to call ptrace
, one has to identify the places (addresses) it is called at, and “NOP it”. NOP stands for no operation. It is a technique of re-writing the undesirable machine instruction, making the program to do nothing instead. On x86
CPUs it is done by using a series of 0x90
s. As a side note, on ARM
CPUs it will not work, but there is another technique for NOPing there. As a reminder, the majority of hand-held devices are built with ARM
CPUs inside. This guide deals with x86
CPUs as the app is run in an emulator on x86
machine.
Using cutter
GUI tool, find the places in the program where ptrace
is called. Go to Functions
tab/window, find ptrace
there in the list, right-click it and choose Show X-Refs
(or just use the shortcut: X
). It will bring up the window that contains a list of call sites. Double clicking on one of them will show that call site in the Disassembly
tab/window:

The addresses of interest are 0x00000777
and 0x000007a7
. These calls must be NOP-ed out, in order to have Android process not being attached to some other process, and as a consequence, allow attaching a debugger. To do that, radare2 can be used.
radare2 -w decoded/lib/x86/libfoo.so
where -w
opens file in a writable mode.
droid@droid:~/dev/hack/mstg-temp$ radare2 -w decoded/lib/x86/libfoo.so
-- There is no F5 key in radare2 yet
[0x00000600]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x00000600]> wx 9090909090 @ 0x00000777
[0x00000600]> wx 9090909090 @ 0x000007a7
[0x00000600]> quit
At this moment libfoo.so
is patched and doesn’t contain a call to ptrace
system call.
Bypass security dialogs
Examination of the source code for MainActivity
shows that a security dialog that prohibits further interaction with the app (assuming you are on an emulator that is considered a rooted device), is invoked by a
method. Delete the body of the method by editing MainActivity.smali
file (decoded/smali/sg/vantagepoint/uncrackable2/MainActivity.smali
). Empty method body should look like this:
.method private a(Ljava/lang/String;)V
.locals 3
return-void
.end method
Re-pack the app
At this point the app is patched to allow the next stage of cracking – it doesn’t have security dialogs and it is possible to attach another process to it (to attach a debugger). Re-pack decoded/
directory with apktool
:
apktool b decoded/ -d -o UnCrackable-Level2-patched.apk
Don’t forget to sign the apk as well.
Delete the app that is installed on the device, and install its patched version:
adb install UnCrackable-Level2-patched.apk
Debug with gdb
It’s going to be a remote debugging with gdb
and the use of gdbserver
. The setup is simple: gdbserver
will reside on the device and be hooked up to the app’s process, while gdb
will be running on the local machine and be communicating with gdbserver
using TCP protocol and port forwarding.
Interestingly enough, my installation of gdb
didn’t have a gdbserver
, so:
sudo apt-get install gdbserver
Identify the location of gdbserver
:
droid@droid:\~/dev/hack/mstg-level-2$ whereis gdbserver
gdbserver: /usr/bin/gdbserver
Copy it to the device:
adb push /usr/bin/gdbserver /data/local/tmp/gdbserver
On the device’s shell, run gdbserver
so it configures itself for the system:
droid@droid:\~$ adb shell
generic_x86:/ $ cd /data/local/tmp
generic_x86:/data/local/tmp $ ls
gdbserver
generic_x86:/data/local/tmp $ gdbserver --version
GNU gdbserver (GDB) 7.11
Copyright (C) 2016 Free Software Foundation, Inc.
gdbserver is free software, covered by the GNU General Public License.
This gdbserver was configured as "i686-linux-android"
Find out the PID
of the app:
droid@droid:~/dev/hack/mstg-level-2$ adb shell ps | grep uncrack
u0_a140 13547 1774 1896668 117000 0 0 S owasp.mstg.uncrackable2
There is only one process, instead of two. Which means, NOP-ing of ptrace
was done successfully.
Attach gdbserver
to the app’s process (requires root):
generic_x86:/data/local/tmp # gdbserver :8888 --attach 13547
Attached; pid = 13547
Listening on port 8888
Port 8888
is chosen randomly and for ease of typing and remembering. Forward port 8888
of the device to the port 8888
of the local machine:
adb forward tcp:8888 tcp:8888
At this point, gdbserver
is running on the device, is attached to the app’s process and awaits commands on port 8888
of the device. Also, local machine port 8888
links to that port 8888
on the device. The last bit is to start gdb
on local machine and attach ourselves to port 8888
:
(gdb) target remote :8888
After running the upper command, there will be a lot of output in the shell, and the device shell will print Remote debugging from host 127.0.0.1
.
Debug native library
To set breakpoints during debugging, one has to know where to set them, which means knowing memory addresses of interest. In an everyday development happening in IDE, that task boils down to setting breakpoints on lines of source code. IDE in turn sets breakpoints on memory addresses thanks to a mapping source file line -> assembler code
.
How to find out the addresses of interest? Addresses seen in GUI of cutter
are relative addresses, relative to the address a library is loaded at. But one needs concrete addresses for debugging. For this, one needs to know the address range the library is loaded into. With this, an already familiar command should help (run on the device shell as root):
generic_x86:/ # cat /proc/13547/maps | grep uncrack
c171a000-c171d000 r-xp 00000000 fd:20 23474 /data/app/owasp.mstg.uncrackable2-AGO4Z6o29sNYr03UJRqm3w==/lib/x86/libfoo.so
c171d000-c171e000 r--p 00002000 fd:20 23474 /data/app/owasp.mstg.uncrackable2-AGO4Z6o29sNYr03UJRqm3w==/lib/x86/libfoo.so
c171e000-c171f000 rw-p 00003000 fd:20 23474 /data/app/owasp.mstg.uncrackable2-AGO4Z6o29sNYr03UJRqm3w==/lib/x86/libfoo.so
d085b000-d090b000 r--p 00000000 00:00 0 [anon:dalvik-classes.dex extracted in memory from /data/app/owasp.mstg.uncrackable2-AGO4Z6o29sNYr03UJRqm3w==/base.apk]
d3134000-d316d000 r--s 0004d000 fd:20 23043 /data/app/owasp.mstg.uncrackable2-AGO4Z6o29sNYr03UJRqm3w==/base.apk
dea76000-dea80000 r--s 000de000 fd:20 23043 /data/app/owasp.mstg.uncrackable2-AGO4Z6o29sNYr03UJRqm3w==/base.apk
The line of interest is:
c171a000-c171d000 r-xp 00000000 fd:20 23474 /data/app/owasp.mstg.uncrackable2-AGO4Z6o29sNYr03UJRqm3w==/lib/x86/libfoo.so
c171a000-c171d000
part tells the address space the library is loaded into. It is this lower bound c171a00
from which relative addresses displayed by cutter
are offset.
As a reminder of what is to be achieved here: breakpoint to strncmp
function call and dumping the contents of the registers, hoping to see the secret. Relative library address of strncmp
is 0x00000ffb
, so set the breakpoint to lower bound of the address space + relative address
: 0xc171affb
:
(gdb) b *0xc171affbBreakpoint 1 at 0xc171affb(gdb) cContinuing.
The breakpoint is set. Time to type something into the input field and click that VERIFY button. However, we are not there yet. There is one more interesting piece we should pay attention to, just above strncmp
function call:

push 0x17
is a check for a length of the input, that must be 23 characters. So, type a 23 characters into the input field, otherwise the breakpoint won’t be hit.
Upon hitting the breakpoint, print addresses registers are using with info registers
command of gdb
. From the picture above, the registers of interests are esi
and eax
. Dump what they have with x/s <memory_address>
. One of them will contain your input String, the other, the secret: Thanks for all the fish
.
Outro
Both examples described in this guide are quite basic ones. They both follow the same approach:
- Make app debuggable.
- Comprehend the code and change it to achieve a particular goal.
- Repackage.
- Debug.
The field of software reverse engineering and tempering is a vast one – I hope this guide provides a good starting point for those interested in the topic.