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.dex
in 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 dex2jar
or enjarify
.dex2jar
and 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/src
directory in a file browser and drag the sg
directory into the now empty Java
folder in the IntelliJ project view (hold the "alt" key to copy the folder instead of moving it).System.load
method. 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.JavaVM
and JNIEnv
. Both of them are pointers to pointers to function tables:JavaVM
provides an interface to invoke functions for creating and destroying a JavaVM. Android allows only one JavaVM
per process and is not really relevant for our reversing purposes.JNIEnv
provides access to most of the JNI functions which are accessible at a fixed offset through the JNIEnv
pointer. This JNIEnv
pointer 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.This app is not exactly spectacular, all it does is show a label with the text "Hello from C++". This is the app Android generates by default when you create a new project with C/C++ support, which is just enough to show the basic principles of JNI calls.
apkx
.HelloWord-JNI/src
directory. 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 onCreate
method:public native String stringFromJNI
at 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:libnative-lib.so
. When System.loadLibrary
is 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 JNIEnv
data 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 greadelf
tool, which you can install via Macports or Homebrew. The following example uses greadelf
:stringFromJNI
native method is called.libnative-lib.so
into 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 -A
to run the aaa
command right after loading the binary in order to analyze all referenced code.-A
might 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 aa
or 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.Code analysis is not a quick operation, and not even predictable or taking a linear time to be processed. This makes starting times pretty heavy, compared to just loading the headers and strings information like it’s done by default.People that are used to IDA or Hopper just load the binary, go out to make a coffee and then when the analysis is done, they start doing the manual analysis to understand what the program is doing. It’s true that those tools perform the analysis in background, and the GUI is not blocked. But this takes a lot of CPU time, and r2 aims to run in many more platforms than just high-end desktop computers.
The freeware version of IDA Pro unfortunately does not support the ARM processor type.
Loading an APK file directly into Ghidra might lead to inconsistencies. Thus it is recommended to extract the DEX file by unzipping the APK file and then loading it into Ghidra.
grep
to 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.getInstance
function, 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.nfc
package. 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 processCommandApdu
from the android.nfc.cardemulation.HostApduService
class.MainActivity
class in the package sg.vantagepoint.uncrackable1
. The method verify
is 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.a
verifies user input, so we'll refactor the code to reflect this.a
in 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). a.a
now becomes Validator.a
. Follow the same procedure to rename the static method a
to check_input
.check_input
method. This takes you to the method definition. The decompiled method looks like this:a
in 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 a
do? Ctrl-click it to find out.arrby1
in check_input
is 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!i
about the symbols s
(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 0x00000e78
is 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.pdf
means print disassembly of function.-qc '<commands>'
. From the previous steps we know already what to do so we will simply put everything together:-A
flag not running aaa
. Instead, we just tell radare2 to analyze that one function by using the analyze function af
command. 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.so
in IDA pro. Once the file is loaded, click into the "Functions" window on the left and press Alt+t
to open the search dialog. Enter "java" and hit enter. This should highlight the Java_sg_vantagepoint_helloworld_ MainActivity_stringFromJNI
function. Double-click the function to jump to its address in the disassembly Window. "Ida View-A" should now show the disassembly of the function.LDR
instruction loads this function table pointer into R2.NewStringUTF
function. 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:NewStringUTF
function pointer loaded into R2:FUN_001004d0
, FUN_0010051c
, and 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_stringFromJNI
are already discussed in detail in previous sections. In this section we can look into the decompilation of the function.JNIEnv
pointer (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.cut
, grep
, sort
etc, to parse the proc filesystem information.lsof
with 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/net
or just by inspecting the /proc/<pid>/net
directories (for some reason not process specific). There are multiple files present in these directories, of which tcp
, tcp6
and udp
might be considered relevant from the tester's perspective.rem_address
: remote address and port number pair (in hexadecimal representation).tx_queue
and 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.netstat
command, 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:netstat
output 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).Recv-Q
and 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 (CLOSED
)./proc/<pid>/maps
contains 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.adb
command line tool was introduced in the "Android Basic Security Testing" chapter. You can use its adb jdwp
command to list the process IDs of all debuggable processes running on the connected device (i.e., processes hosting a JDWP transport). With the adb forward
command, 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.suspend
command into jdb:?
prints the complete list of commands. Unfortunately, the Android VM doesn't support all available JDWP features. For example, the redefine
command, 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.a
displays the "This in unacceptable..." message box. This method creates an AlertDialog
and sets a listener class for the onClick
event. 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 setCancelable
method is called.android.app.Dialog.setCancelable
and resume the app.setCancelable
method. You can print the arguments passed to setCancelable
with the locals
command (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 resume
command.setCancelable
with the argument false
. Set the variable to true
with the set
command and resume.flag
to true
each 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.equals
of the java.lang.String
class 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 locals
command.onCreate
method. Uncrackable1 app triggers anti-debugging and anti-tampering controls within the onCreate
method. That's why setting a breakpoint on the onCreate
method just before the anti-tampering and anti-debugging checks are performed is a good idea.onCreate
method 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.a
method of the class sg.vantagepoint.a.c
./system/xbin
and 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.a
method.System.getenv
method with the "Force Step Into" feature.a
method, 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.a
method gets the directory names, it will search for the su
binary within these directories. To defeat this check, step through the detection method and inspect the variable content. Once execution reaches a location where the su
binary 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".File.exists
should return false
.a
of class sg.vantagepoint.uncrackable1.a
. Set a breakpoint on method a
and "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.String.equals
method call.adb install
to install it on your device or on an emulator.gdbserver --attach
command 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 gdbserver
:gdbserver
is 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 abd forward
command:gdb
included 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_stringFromJNI
before resuming the app. Unfortunately, this isn't possible at this point in the execution because libnative-lib.so
isn'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.suspend
command into jdb:libnative-lib.so
. In jdb, set a breakpoint at the java.lang.System.loadLibrary
method and resume the process. After the breakpoint has been reached, execute the step up
command, which will resume the process until loadLibrary
returns. At this point, libnative-lib.so
has been loaded.gdbserver
to 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 -STOP
command 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 methods
command and resume execution. jdb will dump all method entries and exits from that point onwards.ddms
command.ptrace
system call to attach to the target process, once anti-debugging measures become active it will stop working./sys/kernel/debug/tracing
directory holds all control and output files related to ftrace. The following files are found in this directory:frida-trace
offers 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-trace
is 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 open
function using frida-trace
is demonstrated below, where -U
connects to the USB device and -i
specifies the function to be included in the trace.frida-trace
generates one little JavaScript handler file per matched function in the auto-generated __handlers__
folder, which Frida then injects into the process. You can edit these files for more advanced usage such as obtaining the return value of the functions, their input parameters, accessing the memory, etc. Check Frida's JavaScript API for more details.open
function in libc.so
is located in is __handlers__/libc.so/open.js
, it looks as follows:onEnter
takes care of logging the calls to this function and its two input parameters in the right format. You can edit the onLeave
event to print the return values as shown above.Note that libc is a well-known library, Frida is able to derive the input parameters of itsopen
function 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.
open
function independently. By using such a color scheme, the output can be easily visually segregated for each thread.