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.
With VirtualBox, make sure to select Ubuntu64 OS in the settings and boot this ISO.
Set the following VM Display Settings:
Video Memory: 64MB.
Graphics Controller: VMSVGA.
Extended Features: Check Enable 3D Acceleration.
Choose the auto installation in the Advanced Options.
When the Android VM is installed, press
e
twice in the GRUB menu, and edit the boot command to the one below and pressEnter
thenb
:
grub edit> kernel /android-9.0-r2/kernel nomodeset xforcevesa root=/dev/ram0 SRC=/android-9.0-r2
We may replace
nomodeset xforcevesa
byvga=ask
to change the Android’s resolution.
- 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
[...]
- 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]):
Install Odin Flashing Tool.
Download
SuperSU
and copy it on the phone's storage.Power off the phone and launch the recovery mode pressing the buttons
Home + Power + Vol Down
.Connect the phone to the computer over USB.
In
Odin > AP
, select thetwrp-3.1.0-0-zeroflte.img.tar
file and clickStart
. The phone will reboot on the TWRP menu (if not, the TWRP menu can be opened usingHome + Power + Vol Up
).Select
Install
and choose theSuperSU
file, then reboot.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:
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
andresources.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 fromapt
might be broken and return anundefined 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 BEFOREzipalign
:
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’svenv
orpipx
), instead ofpip
. 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, orC:\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 isV
(Verbose), and the highest isS
(Silent):Filtering warnings with the following
logcat
command will then give us theW
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 ourlibdivajni.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
andclose
) 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
asroot
, we must firstly runadb root
to becomeroot
withadb
.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 :
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.
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
(code
→ s1
→ s_40
→ s2
):
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
), thentrue == 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‘sonCreate()
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 !