The freeware version of IDA Pro unfortunately does not support the ARM processor type.
With Ghidra, strings can be obtained by simply loading the DEX file and selecting Window ->
Defined stringsin the menu.
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.
With Dextra, you can dump all the strings using the following command:
dextra-Sclasses.dex
The output from Dextra can be manipulated using standard Linux commands, for example, using grepto search for certain keywords.
It is important to know, the list of strings obtained using the above tools can be very big, as it also includes the various class and package names used in the application. Going through the complete list, specially for big binaries, can be very cumbersome. Thus, it is recommended to start with keyword-based searching and go through the list only when keyword search does not help. Some generic keywords which can be a good starting point are - password, key, and secret.
Other useful keywords specific to the context of the app can be obtained while you are using the app itself. For instance, imagine that the app has as login form, you can take note of the displayed placeholder or title text of the input fields and use that as an entry point for your static analysis.
Native Code
In order to extract strings from native code used in an Android application, you can use GUI tools such as Ghidra or Cutter or rely on CLI-based tools such as thestringsUnix utility (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.
Cross References
Java and Kotlin
There are many RE tools that support retrieving Java cross references. For many of the GUI-based ones, this is usually done by right clicking on the desired function and selecting the corresponding option, e.g.Show References toin Ghidra orFind Usageinjadx.
Native Code
Similarly to Java analysis, you can also use Ghidra to analyze native libraries and obtain cross references by right clicking the desired function and selectingShow References to.
API Usage
The Android platform provides many in-built libraries for frequently used functionalities in applica- tions, for example cryptography, Bluetooth, NFC, network or location libraries. Determining the presence of these libraries in an application can give us valuable information about its nature.
For instance, if an application is importingjavax.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”.
Similarly, the above approach can be used to determine where and how an application is using NFC. For instance, an application using Host-based Card Emulation for performing digital pay- ments must use theandroid.nfcpackage. Therefore, a good stating point for NFC API analysis would be to consult the Android Developer Documentation to get some ideas and start search- ing for critical functions such as processCommandApdu from the android.nfc.cardemulation.
HostApduServiceclass.
Network Communication
Most of the apps you might encounter connect to remote endpoints. Even before you perform any dynamic analysis (e.g. traffic capture and analysis), you can obtain some initial inputs or entry
Typically these domains will be present as strings within the binary of the application. One way to achieve this is by using automated tools such asAPKEnum or MobSF. Alternatively, you can grepfor the domain names by using regular expressions. For this you can target the app binary directly or reverse engineer it and target the disassembled or decompiled code. The latter option has a clear advantage: it can provide you withcontext, as you’ll be able to see in which context each domain is being used (e.g. class and method). “
From here on you can use this information to derive more insights which might be of use later during your analysis, e.g. you could match the domains to the pinned certificates or theNetwork Security Configuration file or perform further reconnaissance on domain names to know more about the target environment. When evaluating an application it is important to check the Net- work Security Configuration file, as often (less secure) debug configurations might be pushed into final release builds by mistake.
The implementation and verification of secure connections can be an intricate process and there are numerous aspects to consider. For instance, many applications use other protocols apart from HTTP such as XMPP or plain TCP packets, or perform certificate pinning in an attempt to deter MITM attacks but unfortunately having severe logical bugs in its implementation or an inherently wrong security network configuration.
Remember that in most of the cases, just using static analysis will not be enough and might even turn to be extremely inefficient when compared to the dynamic alternatives which will get much more reliable results (e.g. using an interceptor proxy). In this section we’ve just slightly touched the surface, please refer to the section “Basic Network Monitoring/Sniffing” in the “Android Basic Security Testing” chapter and also check the test cases in the “Android Network Communication”
chapter.
Manual (Reversed) Code Review Reviewing Decompiled Java Code
Following the example from “Decompiling Java Code”, we assume that you’ve successfully de- compiled and opened the UnCrackable App for Android Level 1in IntelliJ. As soon as IntelliJ has indexed the code, you can browse it just like you’d browse any other Java project. Note that many of the decompiled packages, classes, and methods have weird one-letter names; this is because the bytecode has been “minified” with ProGuard at build time. This is a basic type ofobfuscation that makes the bytecode a little more difficult to read, but with a fairly simple app like this one, it won’t cause you much of a headache. When you’re analyzing a more complex app, however, it can get quite annoying.
When analyzing obfuscated code, annotating class names, method names, and other identifiers as you go along is a good practice. Open theMainActivityclass in the packagesg.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 thata.averifies user input, so we’ll refactor the code to reflect this.
Right-click the class name (the firstaina.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.anow becomesValidator.a. Follow the same procedure to rename the static methodatocheck_input.
Congratulations, you just learned the fundamentals of static analysis! It is all about theorizing, annotating, and gradually revising theories about the analyzed program until you understand it completely or, at least, well enough for whatever you want to achieve.
Next, Ctrl+click (or Command+click on Mac) on thecheck_inputmethod. This takes you to the method definition. The decompiled method looks like this:
publicstatic booleancheck_input(String string) { byte[]arrby=Base64.decode((String)\
"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0);
byte[]arrby2=newbyte[]{};
try{
arrby=sg.vantagepoint.a.a.a(Validator.b("8d127684cbc37c17616d806cf50473cc"),arrby);
arrby2=arrby;
}sa
catch(Exception exception) {
Log.d((String)"CodeCheck", (String)("AES error:"+exception.getMessage()));
}
if(string.equals(newString(arrby2))) { return true;
}
return false;
}
So, you have a Base64-encoded String that’s passed to the functionain the package
sg.vantagepoint.a.a(again, everything is calleda) along with something that looks suspiciously
like a hex-encoded encryption key (16 hex bytes = 128bit, a common key length). What exactly does this particularado? Ctrl-click it to find out.
public classa{
publicstatic byte[]a(byte[]object,byte[]arrby) {
object=newSecretKeySpec((byte[])object,"AES/ECB/PKCS7Padding");
Cipher cipher=Cipher.getInstance("AES");
cipher.init(2, (Key)object);
returncipher.doFinal(arrby);
} }
Now you’re getting somewhere: it’s simply standard AES-ECB. Looks like the Base64 string stored inarrby1incheck_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!
A faster way to get the decrypted string is to add dynamic analysis. We’ll revisitUnCrackable App for Android Level 1later to show how (e.g. in the Debugging section), so don’t delete the project yet!
Reviewing Disassembled Native Code
Following the example from “Disassembling Native Code” we will use different disassemblers to review the disassembled native code.
radare2
Once you’ve opened your file in radare2 you should first get the address of the function you’re looking for. You can do this by listing or getting information 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”:
$ r2-AHelloWord-JNI/lib/armeabi-v7a/libnative-lib.so ...
[0x00000e3c]>is~Java
003 0x00000e78 0x00000e78 GLOBAL FUNC 16 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI
The method can be found at address 0x00000e78. To display its disassembly simply run the following commands:
[0x00000e3c]>e emu.str=true;
[0x00000e3c]>s 0x00000e78 [0x00000e78]>af [0x00000e78]>pdf
╭(fcn)sym.Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI 12
│ sym.Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI(int32_t arg1);
│ ;arg int32_t arg1 @ r0
│ 0x00000e78 ~ 0268 ldr r2, [r0] ;arg1
│ ;-- aav.0x00000e79:
│ ;UNKNOWN XREF from aav.0x00000189(+0x3)
│ 0x00000e79 unaligned
│ 0x00000e7a 0249 ldr r1, aav.0x00000f3c ;[0xe84:4]=0xf3c aav.0x00000f3c
│ 0x00000e7c d2f89c22 ldr.w r2, [r2, 0x29c]
│ 0x00000e80 7944 add r1, pc ;"Hello from C++"section..rodata
╰ 0x00000e82 1047 bx r2
Let’s explain the previous 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 addresss 0x00000e78, where our target function is located.
We do this so that the following commands apply to this address.
• pdfmeansprint disassembly of function.
Using radare2 you can quickly run commands and exit by using the flags-qc '<commands>'. From the previous steps we know already what to do so we will simply put everything together:
$ r2-qc'e emu.str=true; s 0x00000e78; af; pdf'HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so
╭(fcn)sym.Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI 12
│ sym.Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI(int32_t arg1);
│ ;arg int32_t arg1 @ r0
│ 0x00000e78 0268 ldr r2, [r0] ;arg1
│ 0x00000e7a 0249 ldr r1, [0x00000e84] ;[0xe84:4]=0xf3c
│ 0x00000e7c d2f89c22 ldr.w r2, [r2, 0x29c]
│ 0x00000e80 7944 add r1, pc ;"Hello from C++"section..rodata
╰ 0x00000e82 1047 bx r2
Notice that in this case we’re not starting with the-Aflag not runningaaa. Instead, we just tell radare2 to analyze that one function by using the analyze functionaf 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.
The workflow can be further improved by usingr2ghidra-dec, a deep integration of Ghidra decom- piler for radare2. r2ghidra-dec generates decompiled C code, which can aid in quickly analyzing the binary.
IDA Pro
We assume that you’ve successfully opened lib/armeabi-v7a/libnative-lib.so in IDA pro.
Once the file is loaded, click into the “Functions” window on the left and pressAlt+tto 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 func- tion.
Not a lot of code there, but you should analyze it. The first thing you need to know is that the first argument passed to every JNI function is a JNI interface pointer. An interface pointer is a pointer to a pointer. This pointer points to a function table: an array of even more pointers, each of which points to a JNI interface function (is your head spinning yet?). The function table is initialized by the Java VM and allows the native function to interact with the Java environment.
With that in mind, let’s have a look at each line of assembly code.
LDR R2, [R0]
Remember: the first argument (in R0) is a pointer to the JNI function table pointer. The LDR instruction loads this function table pointer into R2.
LDR R1, =aHelloFromC
This instruction loads into R1 the PC-relative offset of the string “Hello from C++”. Note that this string comes directly after the end of the function block at offset 0xe84. Addressing relative to the program counter allows the code to run independently of its position in memory.
LDR.W R2, [R2,#0x29C]
This instruction loads the function pointer from offset 0x29C into the JNI function pointer table pointed to by R2. This is theNewStringUTFfunction. 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:
jstring (*NewStringUTF)(JNIEnv*,const char*);
The function takes two arguments: the JNIEnv pointer (already in R0) and a String pointer. Next, the current value of PC is added to R1, resulting in the absolute address of the static string “Hello from C++” (PC + offset).
ADD R1, PC
Finally, the program executes a branch instruction to theNewStringUTFfunction pointer loaded into R2:
BX R2
When this function returns, R0 contains a pointer to the newly constructed UTF string. This is the final return value, so R0 is left unchanged and the function returns.
Ghidra
After opening the library in Ghidra we can see all the functions defined in theSymbol Treepanel under Functions. The native library for the current application is relatively very small. There are three user defined functions: 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 functionJava_sg_- vantagepoint_helloworldjni_MainActivity_stringFromJNIare already discussed in detail in previous sections. In this section we can look into the decompilation of the function.
Inside the current function there is a call to another function, whose address is obtained by ac- cessing an offset in theJNIEnvpointer (found asplParm1). 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.
Automated Static Analysis
You should use tools for efficient static analysis. They allow the tester to focus on the more complicated business logic. A plethora of static code analyzers are available, ranging from open source scanners to full-blown enterprise-ready scanners. The best tool for the job depends on budget, client requirements, and the tester’s preferences.
Some static analyzers rely on the availability of the source code; others take the compiled APK
even though they can help us focus on potential problems. Review each finding carefully and try to understand what the app is doing to improve your chances of finding vulnerabilities.
Configure the static analyzer properly to reduce the likelihood of false positives and maybe only select several vulnerability categories in the scan. The results generated by static analyzers can otherwise be overwhelming, and your efforts can be counterproductive if you must manually investigate a large report.
There are several open source tools for automated security analysis of an APK.
• Androbugs
• JAADAS
• MobSF
• QARK