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

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

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 0x90s. 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.

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

More About