unzip UnCrackable-Level1.apk -d UnCrackable-Level1) and look at the content. In the standard setup, all the Java bytecode and app data is in the file
classes.dexin the app root directory (
UnCrackable-Level1/). This file conforms to the Dalvik Executable Format (DEX), an Android-specific way of packaging Java programs. Most Java decompilers take plain class files or JARs as input, so you need to convert the classes.dex file into a JAR first. You can do this with
dex2jarand automates extraction, conversion, and decompilation. Run it on the APK and you should find the decompiled sources in the directory
Uncrackable-Level1/src. To view the sources, a simple text editor (preferably with syntax highlighting) is fine, but loading the code into a Java IDE makes navigation easier. Let's import the code into IntelliJ, which also provides on-device debugging functionality.
app/src/main/java. Right-click and delete the default package "sg.vantagepoint.uncrackable1" created by IntelliJ.
Uncrackable-Level1/srcdirectory in a file browser and drag the
sgdirectory into the now empty
Javafolder in the IntelliJ project view (hold the "alt" key to copy the folder instead of moving it).
System.loadmethod. However, instead of relying on widely used C libraries (such as glibc), Android binaries are built against a custom libc named Bionic. Bionic adds support for important Android-specific services such as system properties and logging, and it is not fully POSIX-compatible.
JNIEnv. Both of them are pointers to pointers to function tables:
JavaVMprovides an interface to invoke functions for creating and destroying a JavaVM. Android allows only one
JavaVMper process and is not really relevant for our reversing purposes.
JNIEnvprovides access to most of the JNI functions which are accessible at a fixed offset through the
JNIEnvpointer is the first parameter passed to every JNI function. We will discuss this concept again with the help of an example later in this chapter.
HelloWord-JNI/srcdirectory. The main activity is found in the file
HelloWord-JNI/src/sg/vantagepoint/helloworldjni/MainActivity.java. The "Hello World" text view is populated in the
public native String stringFromJNIat the bottom. The keyword "native" tells the Java compiler that this method is implemented in a native language. The corresponding function is resolved during runtime, but only if a native library that exports a global symbol with the expected signature is loaded (signatures comprise a package name, class name, and method name). In this example, this requirement is satisfied by the following C or C++ function:
System.loadLibraryis called, the loader selects the correct version based on the device that the app is running on. Before moving ahead, pay attention to the first parameter passed to the current JNI function. It is the same
JNIEnvdata structure which was discussed earlier in this section.
Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI. On Linux systems, you can retrieve the list of symbols with
readelf(included in GNU binutils) or
nm. Do this on macOS with the
greadelftool, which you can install via Macports or Homebrew. The following example uses
stringFromJNInative method is called.
libnative-lib.sointo any disassembler that understands ELF binaries (i.e., any disassembler). If the app ships with binaries for different architectures, you can theoretically pick the architecture you're most familiar with, as long as it is compatible with the disassembler. Each version is compiled from the same source and implements the same functionality. However, if you're planning to debug the library on a live device later, it's usually wise to pick an ARM build.
HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so) in radare2 and in IDA Pro. See the section "Reviewing Disassembled Native Code" below to learn on how to proceed when inspecting the disassembled native code.
r2 -A HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so. The chapter "Android Basic Security Testing" already introduced radare2. Remember that you can use the flag
-Ato run the
aaacommand right after loading the binary in order to analyze all referenced code.
-Amight be very time consuming as well as unnecessary. Depending on your purpose, you may open the binary without this option and then apply a less complex analysis like
aaor a more concrete type of analysis such as the ones offered in
aa(basic analysis of all functions) or
aac(analyze function calls). Remember to always type
?to get the help or attach it to commands to see even more command or options. For example, if you enter
aa?you'll get the full list of analysis commands.
grepto search for certain keywords.
strings <path_to_binary>) or radare2's rabin2 (
rabin2 -zz <path_to_binary>). When using the CLI-based ones you can take advantage of other tools such as grep (e.g. in conjunction with regular expressions) to further filter and analyze the results.
javax.crypto.Cipher, it indicates that the application will be performing some kind of cryptographic operation. Fortunately, cryptographic calls are very standard in nature, i.e, they need to be called in a particular order to work correctly, this knowledge can be helpful when analyzing cryptography APIs. For example, by looking for the
Cipher.getInstancefunction, we can determine the cryptographic algorithm being used. With such an approach we can directly move to analyzing cryptographic assets, which often are very critical in an application. Further information on how to analyze Android's cryptographic APIs is discussed in the section "Android Cryptographic APIs".
android.nfcpackage. Therefore, a good stating point for NFC API analysis would be to consult the Android Developer Documentation to get some ideas and start searching for critical functions such as
MainActivityclass in the package
sg.vantagepoint.uncrackable1. The method
verifyis called when you tap the "verify" button. This method passes the user input to a static method called
a.a, which returns a boolean value. It seems plausible that
a.averifies user input, so we'll refactor the code to reflect this.
a.a) and select Refactor -> Rename from the drop-down menu (or press Shift-F6). Change the class name to something that makes more sense given what you know about the class so far. For example, you could call it "Validator" (you can always revise the name later).
Validator.a. Follow the same procedure to rename the static method
check_inputmethod. This takes you to the method definition. The decompiled method looks like this:
ain the package
sg.vantagepoint.a.a(again, everything is called
a) along with something that looks suspiciously like a hex-encoded encryption key (16 hex bytes = 128bit, a common key length). What exactly does this particular
ado? Ctrl-click it to find out.
check_inputis a ciphertext. It is decrypted with 128bit AES, then compared with the user input. As a bonus task, try to decrypt the extracted ciphertext and find the secret value!
iabout the symbols
is) and grepping (
~radare2's built-in grep) for some keyword, in our case we're looking for JNI related symbols so we enter "Java":
0x00000e78. To display its disassembly simply run the following commands:
e emu.str=true;enables radare2's string emulation. Thanks to this, we can see the string we're looking for ("Hello from C++").
s 0x00000e78is a seek to the address
s 0x00000e78, where our target function is located. We do this so that the following commands apply to this address.
-qc '<commands>'. From the previous steps we know already what to do so we will simply put everything together:
-Aflag not running
aaa. Instead, we just tell radare2 to analyze that one function by using the analyze function
afcommand. This is one of those cases where we can speed up our workflow because you're focusing on some specific part of an app.
lib/armeabi-v7a/libnative-lib.soin IDA pro. Once the file is loaded, click into the "Functions" window on the left and press
Alt+tto open the search dialog. Enter "java" and hit enter. This should highlight the
Java_sg_vantagepoint_helloworld_ MainActivity_stringFromJNIfunction. Double-click the function to jump to its address in the disassembly Window. "Ida View-A" should now show the disassembly of the function.
LDRinstruction loads this function table pointer into R2.
NewStringUTFfunction. You can look at the list of function pointers in jni.h, which is included in the Android NDK. The function prototype looks like this:
NewStringUTFfunction pointer loaded into R2:
Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI. The other symbols are not user defined and are generated for proper functioning of the shared library. The instructions in the function
Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNIare already discussed in detail in previous sections. In this section we can look into the decompilation of the function.
JNIEnvpointer (found as
plParm1). This logic has been diagrammatically demonstrated above as well. The corresponding C code for the disassembled function is shown in the Decompiler window. This decompiled C code makes it much easier to understand the function call being made. Since this function is small and extremely simple, the decompilation output is very accurate, this can change drastically when dealing with complex functions.
/proc. Procfs provides a directory-based view of a process running on the system, providing detailed information about the process itself, its threads, and other system-wide diagnostics. Procfs is arguably one of the most important filesystems on Android, where many OS native tools depend on it as their source of information.
sortetc, to parse the proc filesystem information.
lsofwith the flag
-p <pid>to return the list of open files for the specified process. See the man page for more options.
NAME: path of the file.
TYPE: type of the file, for example, file is a directory or a regular file.
/proc/netor just by inspecting the
/proc/<pid>/netdirectories (for some reason not process specific). There are multiple files present in these directories, of which
udpmight be considered relevant from the tester's perspective.
rem_address: remote address and port number pair (in hexadecimal representation).
rx_queue: the outgoing and incoming data queue in terms of kernel memory usage. These fields give an indication how actively the connection is being used.
uid: containing the effective UID of the creator of the socket.
netstatcommand, which also provides information about the network activity for the complete system in a more readable format, and can be easily filtered as per our requirements. For instance, we can easily filter it by PID:
netstatoutput is clearly more user friendly than reading
/proc/<pid>/net. The most relevant fields for us, similar to the previous output, are following:
Foreign Address: remote address and port number pair (port number can be replaced with the well-known name of a protocol associated with the port).
Send-Q: Statistics related to receive and send queue. Gives an indication on how actively the connection is being used.
State: the state of a socket, for example, if the socket is in active use (
ESTABLISHED) or closed (
/proc/<pid>/mapscontains the currently mapped memory regions and their access permissions. Using this file we can get the list of the libraries loaded in the process.
/data/data/<app_package_name>. The content of this directory has already been discussed in detail in the "Accessing App Data Directories" section.
adbcommand line tool was introduced in the "Android Basic Security Testing" chapter. You can use its
adb jdwpcommand to list the process IDs of all debuggable processes running on the connected device (i.e., processes hosting a JDWP transport). With the
adb forwardcommand, you can open a listening socket on your host computer and forward this socket's incoming TCP connections to the JDWP transport of a chosen process.
suspendcommand into jdb:
?prints the complete list of commands. Unfortunately, the Android VM doesn't support all available JDWP features. For example, the
redefinecommand, which would let you redefine a class' code is not supported. Another important restriction is that line breakpoints won't work because the release bytecode doesn't contain line information. Method breakpoints do work, however. Useful working commands include:
sg.vantagepoint.uncrackable1.MainActivity.adisplays the "This in unacceptable..." message box. This method creates an
AlertDialogand sets a listener class for the
onClickevent. This class (named
b) has a callback method will terminates the app once the user taps the OK button. To prevent the user from simply canceling the dialog, the
setCancelablemethod is called.
android.app.Dialog.setCancelableand resume the app.
setCancelablemethod. You can print the arguments passed to
localscommand (the arguments are shown incorrectly under "local variables").
setCancelable(true)was called, so this can't be the call we're looking for. Resume the process with the
setCancelablewith the argument
false. Set the variable to
setcommand and resume.
trueeach time the breakpoint is reached, until the alert box is finally displayed (the breakpoint will be reached five or six times). The alert box should now be cancelable! Tap the screen next to the box and it will close without terminating the app.
java.lang.Stringclass compares the string input with the secret string. Set a method breakpoint on
java.lang.String.equals, enter an arbitrary text string in the edit field, and tap the "verify" button. Once the breakpoint is reached, you can read the method argument with the
onCreatemethod. Uncrackable1 app triggers anti-debugging and anti-tampering controls within the
onCreatemethod. That's why setting a breakpoint on the
onCreatemethod just before the anti-tampering and anti-debugging checks are performed is a good idea.
onCreatemethod by clicking "Force Step Into" in Debugger view. The "Force Step Into" option allows you to debug the Android framework functions and core Java classes that are normally ignored by debuggers.
amethod of the class
/system/xbinand others). Since you're running the app on a rooted device/emulator, you need to defeat this check by manipulating variables and/or function return values.
System.getenvmethod with the "Force Step Into" feature.
amethod, not to the next executable line. This happens because you're working on the decompiled code instead of the source code. This skipping makes following the code flow crucial to debugging decompiled applications. Otherwise, identifying the next line to be executed would become complicated.
amethod gets the directory names, it will search for the
subinary within these directories. To defeat this check, step through the detection method and inspect the variable content. Once execution reaches a location where the
subinary would be detected, modify one of the variables holding the file name or directory name by pressing F2 or right-clicking and choosing "Set Value".
sg.vantagepoint.uncrackable1.a. Set a breakpoint on method
aand "Force Step Into" when you reach the breakpoint. Then, single-step until you reach the call to
String.equals. This is where user input is compared with the secret string.
adb installto install it on your device or on an emulator.
gdbserver --attachcommand causes gdbserver to attach to the running process and bind to the IP address and port specified in
comm, which in this case is a HOST:PORT descriptor. Start HelloWorldJNI on the device, then connect to the device and determine the PID of the HelloWorldJNI process (sg.vantagepoint.helloworldjni). Then switch to the root user and attach
gdbserveris listening for debugging clients on port
1234. With the device connected via USB, you can forward this port to a local port on the host with the
gdbincluded in the NDK toolchain.
StringFromJNI; it only runs once, at startup. You can solve this problem by activating the "Wait for Debugger" option. Go to Developer Options -> Select debug app and pick HelloWorldJNI, then activate the Wait for debugger switch. Then terminate and re-launch the app. It should be suspended automatically.
Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNIbefore resuming the app. Unfortunately, this isn't possible at this point in the execution because
libnative-lib.soisn't yet mapped into process memory, it's loaded dynamically during runtime. To get this working, you'll first use jdb to gently change the process into the desired state.
suspendcommand into jdb:
libnative-lib.so. In jdb, set a breakpoint at the
java.lang.System.loadLibrarymethod and resume the process. After the breakpoint has been reached, execute the
step upcommand, which will resume the process until
loadLibraryreturns. At this point,
libnative-lib.sohas been loaded.
gdbserverto attach to the suspended app. This will cause the app to be suspended by both the Java VM and the Linux kernel (creating a state of "double-suspension").
kill -STOPcommand and attach jdb to set a deferred method breakpoint on any initialization method. Once the breakpoint is reached, activate method tracing with the
trace go methodscommand and resume execution. jdb will dump all method entries and exits from that point onwards.
ptracesystem call to attach to the target process, once anti-debugging measures become active it will stop working.
/sys/kernel/debug/tracingdirectory holds all control and output files related to ftrace. The following files are found in this directory:
frida-traceoffers out-of-the-box support for Android/iOS native code tracing and iOS high level method tracing. If you prefer a GUI-based approach you can use tools such as RMS - Runtime Mobile Security which enables a more visual experience as well as include several convenience tracing options.
frida-traceis a CLI tool for dynamically tracing function calls. It makes tracing native functions trivial and can be very useful for collecting information about an application.
frida-trace, a Frida server should be running on the device. An example for tracing libc's
frida-traceis demonstrated below, where
-Uconnects to the USB device and
-ispecifies the function to be included in the trace.
libc.sois located in is
__handlers__/libc.so/open.js, it looks as follows:
onEntertakes care of logging the calls to this function and its two input parameters in the right format. You can edit the
onLeaveevent to print the return values as shown above.
openfunction and automatically log them correctly. But this won't be the case for other libraries or for Android Kotlin/Java code. In that case, you may want to obtain the signatures of the functions you're interested in by referring to Android Developers documentation or by reverse engineer the app first.
openfunction independently. By using such a color scheme, the output can be easily visually segregated for each thread.
frida-traceis a very versatile tool and there are multiple configuration options available such as:
-i "Java_*"(note the use of a glob
*to match all possible functions starting with "Java_").
-j((starting on frida-tools 8.0).
Java.enumerateMethods('*youtube*!on*')uses globs to take all classes that include "youtube" as part of their name and enumerate all methods starting with "on".
-j '*!*certificate*/isu'triggers a case-insensitive query (
i), including method signatures (
s) and excluding system classes (
pip install jnitraceand run it straightaway as follows:
-loption can be provided multiple times to trace multiple libraries, or
*can be provided to trace all libraries. This, however, may provide a lot of output.
NewStringUTFmade from the native code (its return value is then given back to Java code, see section "Reviewing Disassembled Native Code" for more details). Note how similarly to frida-trace, the output is colorized helping to visually distinguish the different threads.
-qemucommand line flag. You can use QEMU's built-in tracing facilities to log executed instructions and virtual register values. Starting QEMU with the
-dcommand line flag will cause it to dump the blocks of guest code, micro operations, or host instructions being executed. With the
-d_asmflag, QEMU logs all basic blocks of guest code as they enter QEMU's translation function. The following command logs all translated blocks to a file: