Introduction
This book teaches security analysis of Android applications (APK reversing, dynamic instrumentation, network traffic inspection and interception, etc.).
The source code is available here.
π§ The book is still under construction. New chapters will be added and the existing ones might be modified.
Prerequisites
Rust
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ rustup target add aarch64-linux-android
$ rustup target add x86_64-linux-android
yara-x
$ cargo install yara-x-cli
udev rules
Adding the udev rules manually might not be necessary on certain distros.
$ lsusb
...
Bus 001 Device 067: ID 18d1:4ee7 Google Inc. Nexus/Pixel Device (charging + debug)
...
$ echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4ee7", MODE="0660", GROUP="plugdev"' | sudo tee /etc/udev/rules.d/51-android.rules
$ sudo usermod -aG plugdev $USER
$ sudo udevadm control --reload
$ sudo udevadm trigger
Android Studio
Download Android Studio (android-studio-rabbit1-linux.tar.gz).
$ sudo dnf install zlib-ng-compat.i686 ncurses-libs.i686 bzip2-libs.i686
$ sudo tar -xzf android-studio-rabbit1-linux.tar.gz -C /opt/
$ sudo ln -sf /opt/android-studio/bin/studio /usr/local/bin/android-studio
Note: check the Android SDK version of your phone and select the linker accordingly (e.g. aarch64-linux-android36-clang).
$ adb shell getprop ro.build.version.sdk
37
$ echo 'export ANDROID_HOME=$HOME/Android/Sdk' >> ~/.zshrc
$ echo 'export PATH=$PATH:$ANDROID_HOME/platform-tools' >> ~/.zshrc
$ echo 'export PATH=$PATH:$ANDROID_HOME/emulator' >> ~/.zshrc
$ echo 'export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin' >> ~/.zshrc
$ echo 'export PATH=$PATH:$ANDROID_HOME/build-tools/$(ls $ANDROID_HOME/build-tools | tail -1)' >> ~/.zshrc
$ echo 'export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$(ls $ANDROID_HOME/ndk | tail -1)' >> ~/.zshrc
$ echo 'export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android36-clang' >> ~/.zshrc
$ echo 'export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android36-clang' >> ~/.zshrc
$ source ~/.zshrc
After startup install additional tools:
- Settings β> Languages & Frameworks β> Android SDK
- β> NDK (Side by side)
- β> Android SDK Command-line Tools (latest)
- β> CMake
References:
Emulator
Note: google_apis_playstore does not allow adb root.
$ sdkmanager "system-images;android-36;google_apis;x86_64"
$ avdmanager create avd -n test -k "system-images;android-36;google_apis;x86_64"
$ android emulator list
test
$ android emulator start test
Emulator process 2673237 started, log file location: '/home/gemesa/.android/test/emulator.log'
Waiting for virtual device 'test' to fully start (242 seconds left)
Virtual device successfully started as 'emulator-5554'
$ adb devices
List of devices attached
emulator-5554 device
$ adb shell getprop ro.build.version.sdk
36
$ adb root
$ adb shell
pipx
$ sudo dnf install pipx
frida
Note: always use matching frida client and server versions.
$ pip install colorama prompt-toolkit pygments websockets
$ pipx install frida-tools
$ # or rebuild and pull the latest frida
$ pipx reinstall frida-tools
$ frida --version
17.22.0
$ wget https://github.com/frida/frida/releases/download/17.22.0/frida-server-17.22.0-android-arm64.xz
Build from source (original version):
- https://developer.android.com/ndk/downloads
- https://github.com/android/ndk/wiki/Unsupported-Downloads
$ android sdk install "ndk;29.0.14206865"
$ echo 'export ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/29.0.14206865' >> ~/.zshrc
$ git clone https://github.com/frida/frida.git
$ git submodule update --init --recursive
$ mkdir build-android-arm64
$ cd build-android-arm64
$ ../configure --enable-gadget --enable-server --host=android-arm64
$ make
$ mkdir build-android-x86_64
$ cd build-android-x86_64
$ ../configure --enable-gadget --enable-server --host=android-x86_64
$ make
Build from source (patched version):
Same as above, except that the patches need to be applied first:
$ git clone git@github.com:gemesa/undetected-frida.git
$ git switch dev
$ ./apply-patches.sh ~/git-repos/frida ~/git-repos/undetected-frida
FRIDA_PREFIX=c7xz7rbrn75vc0k99mc6lvws929jezng
SESSION_SERVICE=5b2030ada95a4ab3e64bd346b4e357b2
Applying strongR-frida patches to subprojects/frida-core
patching file lib/base/rpc.vala
Hunk #1 succeeded at 36 (offset 19 lines).
Hunk #2 succeeded at 96 with fuzz 2 (offset 26 lines).
Hunk #3 succeeded at 125 (offset 26 lines).
patching file server/server.vala
Hunk #1 succeeded at 1 with fuzz 2.
Hunk #2 succeeded at 52 (offset 2 lines).
patching file src/linux/linux-host-session.vala
Hunk #1 succeeded at 64 (offset -64 lines).
patching file src/anti-anti-frida.py
patching file src/anti-anti-frida.py
Hunk #1 succeeded at 19 (offset 1 line).
patching file src/adb.vala
Hunk #1 succeeded at 1015 (offset 41 lines).
patching file src/anti-anti-frida.py
Hunk #1 succeeded at 19 with fuzz 1 (offset -8 lines).
Applying florida patches to subprojects/frida-core
patching file lib/base/linux.vala
Hunk #1 succeeded at 124 with fuzz 1 (offset 23 lines).
patching file src/frida-glue.c
Hunk #1 succeeded at 56 (offset 16 lines).
Applying florida patches to subprojects/frida-gum
patching file gum/gum.c
Hunk #1 succeeded at 302 (offset -2 lines).
Applying rycoh99 patches to subprojects/frida-core
patching file lib/gadget/gadget.vala
Hunk #1 succeeded at 1703 (offset -59 lines).
$ # build, see above
After build:
$ pip install lief
$ python3 ~/git-repos/frida/subprojects/frida-core/src/anti-anti-frida.py ~/git-repos/frida/build-android-arm64/subprojects/frida-core/server/frida-server
[*] Patch frida-agent: /home/gemesa/git-repos/frida/build-android-arm64/subprojects/frida-core/server/frida-server
[*] Patch `frida` to `oQpgx``
section=.rodata offset=0x163ef7 GLib-GIO -> OIG-biLG
section=.rodata offset=0x14dfb8 GDBusProxy -> yxorPsuBDG
section=.rodata offset=0x152bf3 GDBusProxy -> yxorPsuBDG
[*] Patch `gum-js-loop` to `gQaRmYSLzev`
[*] Patch `gmain` to `hurxN`
[*] Patch `gdbus` to `gWhDG`
Alternatively: download the prebuilt binaries. Note: the following patches are applied: strongR-frida florida rycoh99.
apktool
$ wget https://raw.githubusercontent.com/iBotPeaches/Apktool/refs/heads/main/scripts/linux/apktool
$ wget -O apktool.jar https://github.com/iBotPeaches/Apktool/releases/download/v3.0.3/apktool_3.0.3.jar
$ chmod +x apktool apktool.jar
$ sudo mv apktool apktool.jar /usr/local/bin/
Ghidra
$ curl -s "https://get.sdkman.io" | bash
$ sdk install gradle
$ sdk list java
$ sdk install java 25.0.3-tem
$ git clone https://github.com/NationalSecurityAgency/ghidra.git
$ cd ghidra
$ gradle -I gradle/support/fetchDependencies.gradle
$ gradle assembleAll
$ echo 'export PATH=$PATH:/home/gemesa/git-repos/ghidra/build/dist/ghidra_12.3_DEV' >> ~/.zshrc
$ echo 'export PATH=$PATH:/home/gemesa/git-repos/ghidra/build/dist/ghidra_12.3_DEV/support' >> ~/.zshrc
$ . ~/.zshrc
$ ghidraRun
PyGhidra:
$ sudo dnf install python3-devel
$ pip install pyghidra
$ pip install ghidra-stubs
References:
radare2
$ git clone https://github.com/radareorg/radare2
$ sudo dnf install patch
$ radare2/sys/install.sh
References:
VirtualBox
$ sudo dnf install gcc make perl kernel-devel kernel-headers
$ wget https://www.virtualbox.org/download/oracle_vbox_2016.asc
$ sudo rpm --import oracle_vbox_2016.asc
$ wget https://download.virtualbox.org/virtualbox/rpm/fedora/virtualbox.repo
$ sudo mv virtualbox.repo /etc/yum.repos.d/
$ sudo dnf install VirtualBox-7.2
$ sudo usermod -a -G vboxusers $USER
Fingerprint:
B9F8 D658 297A F3EF C18D 5CDF A2F6 83C5 2980 AECF
Oracle Corporation (VirtualBox archive signing key) <info@virtualbox.org>
References
Android Developers
OWASP Mobile Application Security
Tools
Pixel 6a rooting guide
Prerequisites
- enable the following in developer options:
- OEM unlocking
- USB debugging
- check: Settings β> ABout phone β> Build number: CP3A.261005.002.A1
- https://developers.google.com/android/images#bluejay
wget https://dl.google.com/dl/android/aosp/bluejay-cp3a.261005.002.a1-factory-79d88414.zip
$ adb reboot bootloader
$ fastboot flashing unlock
$ unzip bluejay-cp3a.261005.002.a1-factory-79d88414.zip
Archive: bluejay-cp3a.261005.002.a1-factory-79d88414.zip
creating: bluejay-cp3a.261005.002.a1/
extracting: bluejay-cp3a.261005.002.a1/image-bluejay-cp3a.261005.002.a1.zip
inflating: bluejay-cp3a.261005.002.a1/bootloader-bluejay-bluejay-17.0-15681480.img
inflating: bluejay-cp3a.261005.002.a1/radio-bluejay-g5123b-145971-260721-b-15910359.img
inflating: bluejay-cp3a.261005.002.a1/flash-all.bat
inflating: bluejay-cp3a.261005.002.a1/d3m2.ec.bin
inflating: bluejay-cp3a.261005.002.a1/proto11.ec.bin
inflating: bluejay-cp3a.261005.002.a1/evt.ec.bin
inflating: bluejay-cp3a.261005.002.a1/flash-base.sh
inflating: bluejay-cp3a.261005.002.a1/flash-all.sh
$ cd bluejay-cp3a.261005.002.a1
$ unzip image-bluejay-cp3a.261005.002.a1.zip
Archive: image-bluejay-cp3a.261005.002.a1.zip
inflating: android-info.txt
inflating: fastboot-info.txt
inflating: product.img
inflating: dtbo.img
inflating: vendor.img
inflating: system_ext.img
inflating: system_other.img
inflating: system.img
inflating: boot.img
inflating: vbmeta_system.img
inflating: super_empty.img
inflating: vendor_dlkm.img
inflating: pvmfw.img
inflating: vbmeta_vendor.img
inflating: vendor_boot.img
inflating: vbmeta.img
inflating: abl.img
inflating: bl1.img
inflating: bl2.img
inflating: bl31.img
inflating: gsa.img
inflating: ldfw.img
inflating: modem.img
inflating: pbl.img
inflating: tzsw.img
Restore boot.img
$ adb reboot bootloader
$ fastboot flash boot boot.img
$ fastboot reboot
KernelSU-Next
References:
- https://github.com/KernelSU-Next/KernelSU-Next/releases
Install KernelSU-Next manager
$ wget https://github.com/KernelSU-Next/KernelSU-Next/releases/download/v3.4.2/KernelSU_Next_v3.4.2-spoofed_33355-release.apk
$ wget https://github.com/KernelSU-Next/KernelSU-Next/releases/download/v3.4.2/KernelSU_Next_v3.4.2_33355-release.apk
$ apkanalyzer manifest application-id KernelSU_Next_v3.4.2-spoofed_33355-release.apk
kqvdie.zeykvw.hpbcyd
$ apkanalyzer manifest application-id KernelSU_Next_v3.4.2_33355-release.apk
com.rifsxd.ksunext
$ adb install KernelSU_Next_v3.4.2-spoofed_33355-release.apk
Performing Streamed Install
Success
Patch boot.img
$ adb push boot.img /sdcard/Download/
KernelSU-Next manager β> Install β> Select a file β> boot.img
$ adb pull /sdcard/Download/kernelsu_next_patched_20261015_182231.img
Flash boot.img
$ adb reboot bootloader
$ fastboot flash boot kernelsu_next_patched_20261015_182231.img
$ fastboot reboot
Enable su
KernelSU-Next manager β> Superuser β> Shell β> enable
$ adb shell
bluejay:/ $ su
bluejay:/ #
Magisk
References:
- https://github.com/topjohnwu/magisk
Install Magisk
$ wget https://github.com/topjohnwu/Magisk/releases/download/v30.7/Magisk-v30.7.apk
$ adb install Magisk-v30.7.apk
Patch boot.img
$ adb push boot.img /sdcard/Download/
Magisk β> Install β> Select and patch a file β> LETβS GO
$ adb pull /sdcard/Download/magisk_patched-30700_xpRGa.img
Flash boot.img
$ adb reboot bootloader
$ fastboot flash boot magisk_patched-30700_xpRGa.img
$ fastboot reboot
Enable su
$ adb shell
bluejay:/ $ su
bluejay:/ #
Magisk β> grant
Samsung S10e rooting guide
References
Downloads
| File | Source |
|---|---|
| Stock firmware for your model | samfirms.com (couldnt find fw here) or Frija tool or samfw |
| Magisk 27.0 APK | github.com/topjohnwu/Magisk/releases - https://github.com/topjohnwu/Magisk/releases/download/canary-27005/app-release.apk |
| TWRP recovery for your device | twrp.me/Devices/Samsung - https://eu.dl.twrp.me/beyond0lte/ - https://eu.dl.twrp.me/beyond0lte/twrp-3.7.0_9-2-beyond0lte.img.tar.html |
| Odin 3.14.1-3B Patched | XDA thread original post |
| Multidisabler Samsung 3.5 corsicanu | XDA thread original post |
| Ambasadii Cruel Kernel v8.2 | Ambasadii Dev site or Direct Google Drive link |
| TWRP Bootlogo Patch v1.6 or v1.7 | XDA thread original post (optional) |
Note 0: Ambasadii v8.2 is the current recommended kernel. CruelKernel v5.3 is an alternative but has an older security patch level. The Ambasadii project is now EOL.
Note 1: Magisk 27.0 is the highest version with stable support on these kernels. Magisk 28.x can be installed via in-app update after initial setup (might be unreliable).
$ ls
Ambasadii_Cruel-HVJ5-G97xF-v8.2-S-magisk.zip
app-release.apk
multidisabler-samsung-3.5-corsicanu.zip
'Odin3 v3.14.1_3B_PatcheD.zip'
SAMFW.COM_SM-G970F_TMH_G970FXXSGHWC2_fac.zip
twrp-3.7.0_9-2-beyond0lte.img.tar
TWRP_Bootlogo_patcher_v1.6.zip
Prerequisites
- enable the following in developer options:
- OEM unlocking
- USB debugging
- unlock the bootloader:
- power off the phone
- hold Vol Down + Bixby then connect USB cable to PC
- warning screen appears, long press Vol Up to unlock
- the phone wipes data and reboots
- after reboot:
- during setup:
- if it is stuck at Checking infoβ¦, go back and click Dont copy again
- go back into developer options
- check if OEM is unlocked
- enable USB debugging
- during setup:
Rooting
Patch the AP File with Magisk
- extract
SAMFW.COM_SM-G970F_TMH_G970FXXSGHWC2_fac.zip:AP_xxx.tar.md5BL_xxx.tar.md5CP_xxx.tar.md5CSC_xxx.tar.md5HOME_CSC_xxx.tar.md5
adb push AP_G970F*.tar.md5 /sdcard/Download/adb install app-release.apk(Magisk)- Magisk β> Install β> Select and Patch a File β> select the AP file
- Magisk will create the patched
magisk_patched-XXXXX.tarin the same folder adb pull /sdcard/Download/magisk_patched-XXXXX.tar
Note: Magisk produces a patched AP file that is much smaller than the original.
Flash FW + patched AP with Odin
- open Odin 3.14.1-3B Patched
- uncheck βAuto Rebootβ
- boot phone into Download Mode: hold Vol Down + Bixby, then connect USB
- load files into Odin slots:
- BL slot β>
BL_xxx.tar.md5 - AP slot β>
magisk_patched-XXXXX.tar - CP slot β>
CP_xxx.tar.md5 - CSC slot β>
CSC_xxx.tar.md5*(notHOME_CSC)
- BL slot β>
- click Start and wait for PASS (green)
- do not reboot yet
Flash TWRP recovery with Odin
- restart phone and go back to download mode
- restart Odin
- in Odin: uncheck all slots
- in Odin: clear the AP slot and load the TWRP
.img.tarinto AP - confirm βAuto Rebootβ is still unchecked
- click Start and wait for PASS (green)
Boot into TWRP
- with USB cable still connected, manually boot into TWRP recovery:
- hold Vol Down + Power β> after vibration, release Vol Down
- immediately hold Vol Up + Vol Down + Power (I accidentally released Vol Down for a sec and then it went to TWRP mode I think) or Vol Up + Bixby + Power
- I dont know if this is necessary: after the splash screen, release Power only, continue holding Vol Up + Bixby for ~1 second, then release
- you should now be in TWRP
- if TWRP prompts to keep system read-only, swipe to allow modifications
Note 1: Entering TWRP without a USB cable connected does not work reliably. Always keep the cable connected during this step.
Note 2: You will know you are at the right point to release buttons when you see the Samsung logo with red warning text (the unlocked bootloader warning), not the plain Samsung logo.
Wipe and install files in TWRP
Standard wipe
- in TWRP tap Wipe β> Advanced Wipe β> Dalvik / ART Cache + Cache then Swipe to Wipe
- This clears cache and dalvik cache. Skipping this step is the most common cause of bootloops.
Format data (fixes storage size issue)
- in TWRP: Wipe β> Format Data
- type
yesand confirm - this fixes the issue where internal storage shows as 32GB instead of 128GB
- after formatting: Reboot β> Recovery
Install zip files in TWRP (order matters)
adb push multidisabler-samsung-3.5-corsicanu.zip /sdcard/adb push Ambasadii_Cruel-HVJ5-G97xF-v8.2-S-magisk.zip /sdcard/adb push TWRP_Bootlogo_patcher_v1.6.zip /sdcard/
First boot and Magisk setup
- in TWRP: tap Reboot β> System
- complete the standard Android setup wizard
- reboot the phone
- the Magisk app will appear (it can take a few minutes)
- open Magisk
- if it says βUpgrade to full Magisk to finish the setup. Download and install?β then click OK
- open Magisk β> tap OK when prompted to reboot and complete installation
- the phone reboots
- root is active
KernelSU/KernelSU-Next
Config
- KernelSU 32601-2 (Manager: v3.3.0) / KernelSU-Next 33355-5 (Manager: v3.4.2-spoofed)
- ReZygisk v1.0.0
- TEESimulator-RS v6.0.1-307
- Play Integrity Fix [INJECT] v4.7.1-inject-s
- HMA-OSS oss-168
Magisk
Config
- Magisk 30.7 (30700)
- Play Integrity Fork v18
- Shamiko v1.2.5 (414)
- TEESimulator-RS v6.0.1-307
- Zygisk Next 1.5.0 (843-5217106-release)
apktool
Use case
Unpack and repack the APK.
Explanation
Decode an APKβs resources and disassemble the Dalvik bytecode into smali:
- read resources (e.g.
AndroidManifest.xml) - read or edit the smali disassembly
- rebuild the modified APK
Usage
$ apktool d test/test-app/app/build/outputs/apk/debug/app-debug.apk -o app-debug
I: Using Apktool 3.0.3 on app-debug.apk with 8 threads
I: Baksmaling classes2.dex...
I: Baksmaling classes.dex...
I: Baksmaling classes4.dex...
I: Baksmaling classes5.dex...
I: Baksmaling classes3.dex...
I: Loading resource table...
I: Decoding value resources...
I: Loading resource table from file: /home/gemesa/.local/share/apktool/framework/1.apk
I: Decoding file resources...
I: Generating values XMLs...
I: Decoding AndroidManifest.xml with resources...
I: Copying original files...
I: Copying assets...
I: Copying unknown files...
$ grep '<service' app-debug/AndroidManifest.xml
<service android:name="com.example.test.Worker1" android:process=":worker1"/>
<service android:name="com.example.test.Worker2" android:process=":worker2"/>
$ cat app-debug/smali_classes3/com/example/test/Worker1.smali
.class public Lcom/example/test/Worker1;
.super Lcom/example/test/TestService;
.source "Worker1.java"
# direct methods
.method public constructor <init>()V
.locals 0
.line 3
invoke-direct {p0}, Lcom/example/test/TestService;-><init>()V
return-void
.end method
$ apktool b app-debug -o app-debug-patched.apk
I: Using Apktool 3.0.3 on app-debug.apk with 8 threads
I: Smaling smali folder into classes.dex...
I: Smaling smali_classes3 folder into classes3.dex...
I: Smaling smali_classes2 folder into classes2.dex...
I: Smaling smali_classes4 folder into classes4.dex...
I: Smaling smali_classes5 folder into classes5.dex...
I: Building resources with aapt2...
I: Building apk file...
I: Importing assets...
I: Importing unknown files...
I: Built apk into: app-debug-patched.apk
$ keytool -genkeypair -keyalg RSA
$ zipalign -v -p 4 app-debug-patched.apk app-debug-patched-aligned.apk
$ keytool -genkeypair -keyalg RSA -keystore my-release-key.jks -alias mykey
$ apksigner sign --ks my-release-key.jks --out app-debug-patched-aligned-signed.apk app-debug-patched-aligned.apk
$ apksigner verify app-debug-patched-aligned-signed.apk
References:
- https://developer.android.com/build/building-cmdline#sign_manually
- https://github.com/ibotpeaches/apktool
- http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html
Ghidra
Use case
Disassemble and decompile native binaries.
Explanation
Analyze a binary and decompile the machine code into pseudocode (similar to C):
- list functions, strings, imports and cross-references
- read the disassembly and the decompiled code
- automate analysis with scripts (headless mode)
Usage
GUI:
PyGhidra headless:
# @runtime PyGhidra
# mkdir -p /tmp/ghidra_projects
# pyghidraRun -H /tmp/ghidra_projects headless -import /path/to/bin -postScript functions.py -scriptPath /path/to/script -deleteProject
import typing
if typing.TYPE_CHECKING:
from ghidra.ghidra_builtins import *
from itertools import islice
TAG = "## "
def out(msg):
print(TAG + str(msg))
for f in islice(currentProgram.getFunctionManager().getFunctions(True), 5):
out(f"{f.getEntryPoint()}: {f.getName()}")
$ apktool d test/test-app/app/build/outputs/apk/debug/app-debug.apk -o app-debug
...
$ mkdir -p /tmp/ghidra_projects
$ pyghidraRun -H /tmp/ghidra_projects headless -import app-debug/lib/arm64-v8a/libnative-lib.so -postScript functions.py -scriptPath ~/projects -deleteProject | grep -E '^##'
## 00100660: _FINI_1
## 00100670: FUN_00100670
## 00100678: _FINI_0
## 00100680: FUN_00100680
## 00100694: FUN_00100694
References:
- https://ghidra.re/ghidra_docs/api/ghidra/program/flatapi/FlatProgramAPI.html#currentProgram
- https://ghidra.re/ghidra_docs/api/ghidra/program/model/listing/Program.html
- https://ghidra.re/ghidra_docs/api/ghidra/program/model/listing/Program.html#getFunctionManager()
JNI
Android uses the JNI to call native functions from the managed code and vice versa. The following chapters are the most important ones to understand the calling convention:
Additionally, we need to retype the parameters passed to the native functions in order to be able to read and understand the native code. For this purpose, we can use this .gdt file.
For example, this is what Java_s_ni_ob (ni.ob on the Java side) looks like with and without the proper types applied.
Before:
void Java_s_ni_ob(_jclass *param_1,undefined4 param_2,_jmethodID *param_3,undefined4 param_4)
{
_jmethodID *p_Var1;
undefined4 uVar2;
undefined4 uVar3;
undefined1 *puVar4;
puVar4 = &stack0xfffffff8;
p_Var1 = (_jmethodID *)(**(code **)(*(int *)param_1 + 0x18))(param_1,"android/content/Intent");
uVar2 = (**(code **)(*(int *)param_1 + 0x84))
(param_1,p_Var1,"<init>","(Landroid/content/Context;Ljava/lang/Class;)V");
uVar2 = _JNIEnv::NewObject(param_1,p_Var1,uVar2,param_3,param_4,puVar4);
uVar3 = (**(code **)(*(int *)param_1 + 0x7c))(param_1,param_3);
uVar3 = (**(code **)(*(int *)param_1 + 0x84))
(param_1,uVar3,"startService",
"(Landroid/content/Intent;)Landroid/content/ComponentName;");
_JNIEnv::CallObjectMethod((_jobject *)param_1,param_3,uVar3,uVar2);
return;
}
After:
void Java_s_ni_ob(JNIEnv *env,jclass clazz,jobject obj,jobject obj2)
{
jclass p_Var1;
jmethodID p_Var2;
undefined4 uVar3;
undefined1 *puVar4;
puVar4 = &stack0xfffffff8;
p_Var1 = (*(*env)->FindClass)(env,"android/content/Intent");
p_Var2 = (*(*env)->GetMethodID)
(env,p_Var1,"<init>","(Landroid/content/Context;Ljava/lang/Class;)V");
uVar3 = _JNIEnv::NewObject((_jclass *)env,(_jmethodID *)p_Var1,p_Var2,obj,obj2,puVar4);
p_Var1 = (*(*env)->GetObjectClass)(env,obj);
p_Var2 = (*(*env)->GetMethodID)
(env,p_Var1,"startService",
"(Landroid/content/Intent;)Landroid/content/ComponentName;");
_JNIEnv::CallObjectMethod((_jobject *)env,(_jmethodID *)obj,p_Var2,uVar3);
return;
}
radare2
Use case
Disassemble and analyze native binaries. Good for scripting.
Explanation
Analyze a binary:
- list functions, strings, imports and cross-references
- read the disassembly
- automate analysis with one-liners
Usage
GUI:
CLI:
$ apktool d test/test-app/app/build/outputs/apk/debug/app-debug.apk -o app-debug
...
$ r2 -q -c 'aaa; afl' app-debug/lib/arm64-v8a/libnative-lib.so | head -5
HINT: Relocs not applied, some references may not resolve: use -e bin.relocs.apply=true
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Name local plt stubs from their got relocs (anal.plt)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Running plugin pre-analysis hooks
INFO: Analyze all functions arguments/locals (afva@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods (af @@ method.*)
INFO: Finding function preludes (aap)
INFO: Emulate functions to find computed references (aaef)
INFO: Recovering local variables (afva@@@F)
INFO: Type matching analysis for all functions (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Use -AA or aaaa to perform additional experimental analysis
INFO: Finding xrefs in noncode sections (e anal.in=io.maps.x; aav)
WARN: Skipping aav because base address is zero. Use -B 0x800000 or aav0
0x00000750 1 16 sym.imp.__cxa_finalize
0x00000760 1 16 sym.imp.__cxa_atexit
0x00000770 1 16 sym.imp.__register_atfork
0x00000660 1 16 entry0
0x000006c0 1 48 sym.Java_com_example_test_Test_stringFromNative
References:
frida
Use case
Dynamically instrument the target application
Explanation
Hook certain Java or native functions to:
- trace calls and print callstacks
- inspect or modify their arguments or return values
- change their implementation completely
Usage
Note: you need to set up frida first. Simplest case: rooted device/emulator + frida-server.
$ wget https://github.com/frida/frida/releases/download/17.19.0/frida-server-17.19.0-android-arm64.xz
$ xz -d frida-server-17.19.0-android-arm64.xz
$ chmod +x frida-server-17.19.0-android-arm64
$ adb push frida-server-17.19.0-android-arm64 /data/local/tmp/
$ adb shell
$ su
# cd /data/local/tmp
# ./frida-server-17.19.0-android-arm64 &
# exit
$ exit
$ frida-ps -U -a
PID Name Identifier
---- --------------- ---------------------------------------
3177 Camera com.android.camera2
5027 Chrome com.android.chrome
2997 Files com.google.android.documentsui
1783 Google com.google.android.googlequicksearchbox
1783 Google com.google.android.googlequicksearchbox
3791 Messages com.google.android.apps.messaging
3539 Personal Safety com.google.android.apps.safetyhub
2940 Phone com.google.android.dialer
4665 Photos com.google.android.apps.photos
1329 SIM Toolkit com.android.stk
1443 Settings com.android.settings
You can use frida-trace to auto-generate the default onEnter and onLeave handlers. These can be modified after if necessary.
Hook a native function (connect):
$ frida-trace -U -p 5027 -i "connect"
Instrumenting...
connect: Auto-generated handler at "/home/gemesa/git-repos/android-security-handbook/__handlers__/libc.so/connect.js"
Started tracing 1 function. Web UI available at http://localhost:44493/
Tip: Luma, the official Frida app, has frida-trace built in β with a Monaco handler editor and instruction-level tracing you can diff across runs. https://luma.frida.re/
/* TID 0x1405 */
35936 ms connect(sockfd=0x102, addr=0x73a9b303f300, addrlen=0x1c)
35937 ms | connect(sockfd=0x103, addr=0x73acaabdb69e, addrlen=0x6e)
35938 ms | connect(sockfd=0x103, addr=0x73acaabdb69e, addrlen=0x6e)
...
/home/gemesa/git-repos/android-security-handbook/__handlers__/libc.so/connect.js:
/*
* Auto-generated by Frida. Please modify to match the signature of connect.
* This stub is currently auto-generated from manpages when available.
*
* For full API reference, see: https://frida.re/docs/javascript-api/
*/
defineHandler({
onEnter(log, args, state) {
log(`connect(sockfd=${args[0]}, addr=${args[1]}, addrlen=${args[2]})`);
},
onLeave(log, retval, state) {
}
});
Hook a Java function (android.net.Uri!parse):
$ frida-trace -U -p 5027 -j 'android.net.Uri!parse'
Instrumenting...
Uri.parse: Auto-generated handler at "/home/gemesa/git-repos/android-security-handbook/__handlers__/android.net.Uri/parse.js"
Started tracing 1 function. Web UI available at http://localhost:41025/
Tip: Luma, the official Frida app, has frida-trace built in β with a Monaco handler editor and instruction-level tracing you can diff across runs. https://luma.frida.re/
/* TID 0x13a3 */
11338 ms Uri.parse("https://m.youtube.com/")
11339 ms <= "<instance: android.net.Uri, $className: android.net.Uri$StringUri>"
...
/home/gemesa/git-repos/android-security-handbook/__handlers__/android.net.Uri/parse.js:
/*
* Auto-generated by Frida. Please modify to match the signature of Uri.parse.
*
* For full API reference, see: https://frida.re/docs/javascript-api/
*/
defineHandler({
/**
* Called synchronously when about to call Uri.parse.
*
* @this {object} - The Java class or instance.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {array} args - Java method arguments.
* @param {object} state - Object allowing you to keep state across function calls.
*/
onEnter(log, args, state) {
log(`Uri.parse(${args.map(JSON.stringify).join(', ')})`);
},
/**
* Called synchronously when about to return from Uri.parse.
*
* See onEnter for details.
*
* @this {object} - The Java class or instance.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {NativePointer} retval - Return value.
* @param {object} state - Object allowing you to keep state across function calls.
*/
onLeave(log, retval, state) {
if (retval !== undefined) {
log(`<= ${JSON.stringify(retval)}`);
}
}
});
Anti-instrumentation/anti-hooking
Some hardened apps are capable of detecting the presence of frida. A cheap solution might be renaming frida-server and making it listen on a non-default port.
Host:
# mv ./frida-server-17.18.0-android-arm64 ol0
# ./ol0 -l 0.0.0.0:29436
Client:
$ frida-trace -H <host_ip>:29436 -p 5027 -j 'android.net.Uri!parse'
Client isolation
On some networks clients are isolated, meaning they cannot communicate to each other. Some possible solutions are listed below.
Convention:
- client: pc
- host: android
# wifi client isolation is enabled:
#
# client:
# adb forward tcp:29436 tcp:29436
# frida -H 127.0.0.1:29436 -n <app>
# host:
# ./ol0 -l 127.0.0.1:29436 &
# or:
# nohup ./ol0 -l 127.0.0.1:29436 > /dev/null 2>&1 &
#
# https://github.com/jpillora/chisel
# client:
# chisel server -p 8080 --reverse
# host:
# curl -LO https://github.com/jpillora/chisel/releases/download/v1.12.0/chisel_1.12.0_linux_arm64.gz
# gunzip chisel_1.12.0_linux_arm64.gz
# chmod +x chisel_1.12.0_linux_arm64
# chisel client CLIENT_IP:8080 R:29436:127.0.0.1:29436
# ./ol0 -l 127.0.0.1:29436 &
# or:
# nohup ./ol0 -l 127.0.0.1:29436 > /dev/null 2>&1 &
#
# USB tethering
#
# client:
# ip a --> enx... interface
# get gateway (android device) ip:
# ip route | grep enx
# frida -H ENX_GATEWAY_IP:29436 -n <app>
# host:
# Settings --> Network & Internet --> Hotspot & Tethering --> USB Tethering
# ./ol0 -l 0.0.0.0:29436 &
# or:
# nohup ./ol0 -l 0.0.0.0:29436 > /dev/null 2>&1 &
#
# wifi client isolation is disabled:
#
# client:
# frida -H <phone-wifi-ip>:29436 -n <app>
# host:
# ./ol0 -l 0.0.0.0:29436 &
# or:
# nohup ./ol0 -l 0.0.0.0:29436 > /dev/null 2>&1 &
Running frida-server in an improperly configured shell
In some cases (e.g. kernel exploits), we do not spawn a properly configured shell so we need to set up some environment variables manually.
# adb shell
# echo $BOOTCLASSPATH > /data/local/tmp/boot.txt
# echo $DEX2OATBOOTCLASSPATH > /data/local/tmp/dex2oat.txt
BOOTCLASSPATH="$(cat /data/local/tmp/boot.txt)" \
DEX2OATBOOTCLASSPATH="$(cat /data/local/tmp/dex2oat.txt)" \
ANDROID_DATA=/data \
ANDROID_ROOT=/system \
ANDROID_RUNTIME_ROOT=/apex/com.android.runtime \
ANDROID_TZDATA_ROOT=/apex/com.android.tzdata \
ANDROID_I18N_ROOT=/apex/com.android.i18n \
./ol0 -l 0.0.0.0:29436
Detection bypass
Memory dumping
Use case
Capture the mapped memory of a process to recover values from RAM.
Explanation
The list of mapped regions is available through /proc/<pid>/maps. The content of these regions can be read via /proc/<pid>/mem. It is a good idea to freeze the process first, then dump the memory. For now, we only dump rw-p regions.
There are other solutions available for dumping memory, e.g. hexdump, fridump or gdb and lldb. The main problem is that attaching to a process with frida, gdb and lldb can be detected.
Usage
$ cargo build --release --target aarch64-linux-android
$ adb push target/aarch64-linux-android/release/dumpmem /data/local/tmp/
Phone:
# ps -ef | grep test
u0_a220 5542 465 0 22:02:57 ? 00:00:02 com.example.test
u0_a220 5984 465 1 22:53:24 ? 00:00:00 com.example.test:worker2
u0_a220 5985 465 1 22:53:24 ? 00:00:00 com.example.test:worker1
root 6093 5955 0 22:55:00 pts/1 00:00:00 grep test
# ./dumpmem -p com.example.test -p com.example.test:worker1
Found PIDs: [5542, 5985]
Stopping PIDs: [5542, 5985]
Dumping rw-p regions...
Done: /data/local/tmp/memdump_5542
Dumping rw-p regions...
Done: /data/local/tmp/memdump_5985
Resuming PIDs: [5542, 5985]
# ls memdump_5542/bin | head -n 5
2000000-e000000.bin
26000000-32000000.bin
4a000000-4a002000.bin
57e9108b0000-57e9108b1000.bin
6ffb4000-702a0000.bin
# head -n 5 memdump_5542/dumpmem.log
02000000-0e000000 rw-p 00000000 00:00 0 [anon:dalvik-main space]
26000000-32000000 rw-p 00000000 00:00 0 [anon:dalvik-free list large object space]
4a000000-4a002000 rw-p 00000000 00:00 0
4a002000-4c002000 r--s 00000000 00:01 1040 /memfd:jit-zygote-cache (deleted)
4c002000-4e002000 r-xs 02000000 00:01 1040 [anon_shmem:dalvik-zygote-jit-code-cache]
Code
use std::{
fs::{self, File},
io::{self, Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
process::exit,
};
use std::sync::Mutex;
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
// https://docs.rs/clap/latest/clap/#example
use clap::Parser;
/// Dump the rw-p memory regions of an Android process.
#[derive(Parser)]
#[command(version, about, arg_required_else_help = true)]
struct Args {
/// Process names to dump, e.g. -p com.example.test -p com.example.test:worker1.
/// PIDs are also accepted: -p 1234.
#[arg(short, long)]
process: Vec<String>,
/// Output directory. The output is written to <output>/memdump_<pid>/.
#[arg(short, long, default_value = "/data/local/tmp")]
output: PathBuf,
}
static STOPPED_PIDS: Mutex<Vec<i32>> = Mutex::new(Vec::new());
// Resume during cleanup whatever happens.
struct Resumer(Vec<Pid>);
impl Drop for Resumer {
fn drop(&mut self) {
let raw: Vec<i32> = self.0.iter().map(|p| p.as_raw()).collect();
println!("Resuming PIDs: {raw:?}");
for &p in &self.0 {
_ = kill(p, Signal::SIGCONT);
}
STOPPED_PIDS.lock().unwrap().clear();
}
}
fn main() {
let args = Args::parse();
if let Err(e) = run(&args.process, &args.output) {
eprintln!("error: {e}");
exit(1);
}
}
fn run(names: &[String], out_base: &Path) -> io::Result<()> {
let mut pids: Vec<i32> = Vec::new();
for name in names {
let pid_raw = match name.parse::<i32>() {
Ok(pid) => pid,
Err(_) => pidof(name)?.ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, format!("{name} not running?"))
})?,
};
pids.push(pid_raw);
}
println!("Found PIDs: {pids:?}");
// https://crates.io/crates/ctrlc
ctrlc::set_handler(move || {
let stopped = STOPPED_PIDS.lock().unwrap();
println!("Resuming PIDs: {stopped:?}");
for &p in stopped.iter() {
_ = kill(Pid::from_raw(p), Signal::SIGCONT);
}
// https://man7.org/linux/man-pages/man7/signal.7.html
// https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
// 128 + SIGINT = 130
exit(130);
})
.expect("failed to install Ctrl+C handler");
*STOPPED_PIDS.lock().unwrap() = pids.clone();
let _resumer = Resumer(pids.iter().map(|&p| Pid::from_raw(p)).collect());
println!("Stopping PIDs: {pids:?}");
for &p in &pids {
kill(Pid::from_raw(p), Signal::SIGSTOP)?;
}
for &pid_raw in &pids {
// output0 = <output>/memdump_<pid>/bin/*.bin
// output1 = <output>/memdump_<pid>/dumpmem.log
let out = out_base.join(format!("memdump_{pid_raw}"));
let bin = out.join("bin");
let log_path = out.join("dumpmem.log");
fs::create_dir_all(&bin)?;
let mut log = File::create(&log_path)?;
let maps = fs::read_to_string(format!("/proc/{pid_raw}/maps"))?;
// Good practice: use write_all for bytes we already have.
log.write_all(maps.as_bytes())?;
println!("Dumping rw-p regions...");
let mut mem = File::open(format!("/proc/{pid_raw}/mem"))?;
// 55fc5b37a000-55fc5b425000 rw-p 00000000 00:00 0 [heap]
// 7fcd00021000-7fcd04000000 ---p 00000000 00:00 0
// https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map
for region in maps.lines().filter_map(parse_region) {
let (start, end) = region;
writeln!(log, "{start:x}-{end:x}")?;
let len = (end - start) as usize;
let mut buf = vec![0; len];
// Straight to the region and read it.
mem.seek(SeekFrom::Start(start))?;
match mem.read_exact(&mut buf) {
Ok(()) => {
let path = bin.join(format!("{start:x}-{end:x}.bin"));
fs::write(&path, &buf)?;
writeln!(log, "wrote {len} bytes -> {}", path.display())?;
}
Err(e) => {
writeln!(log, "skip {start:x}-{end:x}: {e}")?;
}
}
}
writeln!(log, "Done: {}", out.display())?;
println!("Done: {}", out.display());
}
Ok(())
}
fn pidof(name: &str) -> io::Result<Option<i32>> {
// https://doc.rust-lang.org/std/fs/fn.read_dir.html#examples
for entry in fs::read_dir("/proc")? {
let entry = entry?;
let fname = entry.file_name();
let Some(pid) = fname.to_str().and_then(|s| s.parse::<i32>().ok()) else {
continue;
};
// argv[0] = package name
if let Ok(cmdline) = fs::read(format!("/proc/{pid}/cmdline")) {
let argv0 = cmdline.split(|&b| b == 0).next().unwrap_or(&[]);
if argv0 == name.as_bytes() {
return Ok(Some(pid));
}
}
}
Ok(None)
}
// 55fc5b37a000-55fc5b425000 rw-p 00000000 00:00 0 [heap]
fn parse_region(line: &str) -> Option<(u64, u64)> {
let mut parts = line.split_whitespace();
let range = parts.next()?;
let perms = parts.next()?;
if perms != "rw-p" {
return None;
};
let (s, e) = range.split_once('-')?;
let start = u64::from_str_radix(s, 16).ok()?;
let end = u64::from_str_radix(e, 16).ok()?;
Some((start, end))
}
Memory search
Use case
Quickly search the previously dumped memory regions for a specific value in multiple encodings.
Explanation
We convert the search value to multiple byte patterns: ASCII, UTF-16 (LE/BE), 32/64-bit ints (LE/BE) and raw hex (+ reversed). Then we search for all of them in the dumped .bin files. yara-x does the search based on the rule generated by mkrule.
Alternatively, r2 could be used as well. But yr is significantly faster.
Usage
$ cargo build --release
$ target/release/mkrule 285735461 > rule.yar
$ cat rule.yar
rule searchmem
{
strings:
$ascii = "285735461" ascii nocase
$utf16_le = { 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00 }
$utf16_be = { 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 }
$int32_le = { 25 fa 07 11 }
$int32_be = { 11 07 fa 25 }
$int64_le = { 25 fa 07 11 00 00 00 00 }
$int64_be = { 00 00 00 00 11 07 fa 25 }
$hex = { 28 57 35 46 1? }
$hex_rev = { 1? 46 35 57 28 }
condition:
any of them
}
$ yr scan rule.yar memdump_5542/bin -s
searchmem memdump_5542/bin/2000000-e000000.bin
0x2d02bc:9:$ascii: 285735461
0x2d02e8:9:$ascii: 285735461
0x2d0304:9:$ascii: 285735461
0x2d04b4:18:$utf16_le: 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00
0x2d04d4:18:$utf16_le: 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00
0x2d04f4:18:$utf16_le: 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00
0x2d0515:18:$utf16_le: 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00
0x2d04b3:18:$utf16_be: 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31
0x2d04d3:18:$utf16_be: 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31
0x2d04f3:18:$utf16_be: 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31
0x2d0514:18:$utf16_be: 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31
0x2d0564:4:$int32_le: 25 fa 07 11
0x2d05e4:4:$int32_le: 25 fa 07 11
0x2d05a4:4:$int32_be: 11 07 fa 25
0x2d0630:4:$int32_be: 11 07 fa 25
0x2d05e4:8:$int64_le: 25 fa 07 11 00 00 00 00
0x2d062c:8:$int64_be: 00 00 00 00 11 07 fa 25
0x2d0644:5:$hex: 28 57 35 46 10
0x2d065c:5:$hex_rev: 10 46 35 57 28
$ cat memdump_5542/dumpmem.log | grep -E "0*2000000-0*e000000 rw-p"
02000000-0e000000 rw-p 00000000 00:00 0 [anon:dalvik-main space]
Code
use std::process::exit;
fn print_string(label: &str, body: String) {
// format specifiers:
// https://doc.rust-lang.org/std/fmt/
println!(" ${label:12} = {body}")
}
// deadbeef -> { DE AD BE EF }
fn hex(bytes: &[u8]) -> String {
let pairs: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
format!("{{ {} }}", pairs.join(" "))
}
// "285735461" --> ["28", "57", "35", "46", "1?"]
fn parse_hex(val: &str) -> Option<Vec<String>> {
let mut pairs: Vec<String> = Vec::new();
for i in (0..val.len()).step_by(2) {
let pair = val.get(i..(i + 2).min(val.len()))?;
if !pair.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let pair = pair.to_ascii_lowercase();
if pair.len() == 1 {
pairs.push(format!("{pair}?"));
} else {
pairs.push(pair);
}
}
Some(pairs)
}
/*
rule searchmem
{
strings:
$ascii = "1234" ascii nocase
$utf16_le = { 31 00 32 00 33 00 34 00 }
$utf16_be = { 00 31 00 32 00 33 00 34 }
$int32_big = { 00 00 04 D2 }
$int32_little = { D2 04 00 00 }
$int64_big = { 00 00 00 00 00 00 04 D2 }
$int64_little = { D2 04 00 00 00 00 00 00 }
$hex = { 12 34 }
$hex_reversed = { 34 12 }
condition:
any of them
}
*/
fn print_rule(val: &str) {
println!("rule searchmem");
println!("{{");
println!(" strings:");
print_string("ascii", format!("\"{val}\" ascii nocase"));
let utf16_le: Vec<u8> = val.encode_utf16().flat_map(u16::to_le_bytes).collect();
let utf16_be: Vec<u8> = val.encode_utf16().flat_map(u16::to_be_bytes).collect();
print_string("utf16_le", hex(&utf16_le));
print_string("utf16_be", hex(&utf16_be));
if let Ok(n) = val.parse::<u32>() {
print_string("int32_le", hex(&n.to_le_bytes()));
print_string("int32_be", hex(&n.to_be_bytes()));
}
if let Ok(n) = val.parse::<u64>() {
print_string("int64_le", hex(&n.to_le_bytes()));
print_string("int64_be", hex(&n.to_be_bytes()));
}
if let Some(pairs) = parse_hex(val) {
let mut rev = pairs.clone();
rev.reverse();
print_string("hex", format!("{{ {} }}", pairs.join(" ")));
print_string("hex_rev", format!("{{ {} }}", rev.join(" ")));
}
println!(" condition:");
println!(" any of them");
println!("}}");
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.len() != 1 || args[0].is_empty() {
println!("usage: mkrule <value>");
println!("e.g.: mkrule 1234 > rule.yar");
exit(1);
}
print_rule(&args[0]);
}
Burp
Use case
Inspect and intercept HTTPS traffic.
Explanation
Point the app at Burpβs proxy. This only reveals plain HTTP. HTTPS fails validation until Burpβs CA is trusted.
Since Android 14 the trust store lives in the immutable com.android.conscrypt APEX at /apex/com.android.conscrypt/cacerts. Each process has its own mount namespace, so the cert must be bind-mounted into zygote (so forked apps inherit it).
Usage
Burp:
- Proxy β> Proxy settings β> Import/export CA certificate β> Certificate in DER format
- Proxy β> Proxy settings β> Proxy listeners β> Bind to all interfaces (0.0.0.0)
$ openssl x509 -inform DER -in cacert.der -out cacert.pem
$ openssl x509 -inform PEM -subject_hash_old -in cacert.pem | head -1
9a5ba575
$ mv cacert.pem 9a5ba575.0
$ cat 9a5ba575.0 | openssl x509 -noout -subject -issuer
subject=C=PortSwigger, ST=PortSwigger, L=PortSwigger, O=PortSwigger, OU=PortSwigger CA, CN=PortSwigger CA
issuer=C=PortSwigger, ST=PortSwigger, L=PortSwigger, O=PortSwigger, OU=PortSwigger CA, CN=PortSwigger CA
$ adb push 9a5ba575.0 /data/local/tmp/
$ mkdir -p -m 700 /data/local/tmp/ca-copy
$ cp /apex/com.android.conscrypt/cacerts/* /data/local/tmp/ca-copy/
$ cp /data/local/tmp/9a5ba575.0 /data/local/tmp/ca-copy/
# overlay the legacy dir
$ mount -t tmpfs tmpfs /system/etc/security/cacerts
$ cp /data/local/tmp/ca-copy/* /system/etc/security/cacerts/
$ chown root:root /system/etc/security/cacerts/*
$ chmod 644 /system/etc/security/cacerts/*
$ chcon u:object_r:system_security_cacerts_file:s0 /system/etc/security/cacerts/*
# bind it over APEX
$ mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts
$ adb shell pgrep -f zygote
477
1022
$ nsenter --mount=/proc/477/ns/mnt -- /bin/mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts
$ nsenter --mount=/proc/1022/ns/mnt -- /bin/mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts
$ adb shell settings get global http_proxy
null
$ adb shell settings put global http_proxy 10.0.2.2:8080
$ adb shell settings get global http_proxy
10.0.2.2:8080
# reset later
$ adb shell settings put global http_proxy :0
$ adb shell am force-stop com.example.test
$ adb shell am start -n com.example.test/.MainActivity
Starting: Intent { cmp=com.example.test/.MainActivity }
# get the full name of the mangled check symbol
$ dexdump -d ./test/test-app/app/build/intermediates/dex/debug/mergeExtDexDebug/classes2.dex | grep check\$okhttp -A 1
026b3e: 6e30 d803 2100 |000f: invoke-virtual {v1, v2, v0}, Lokhttp3/CertificatePinner;.check$okhttp_release:(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V // method@03d8
026b44: 0e00 |0012: return-void
--
name : 'check$okhttp_release'
type : '(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V'
--
0268bc: |[0268bc] okhttp3.CertificatePinner.check$okhttp_release:(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V
0268cc: 1a00 cc1e |0000: const-string v0, "hostname" // string@1ecc
--
044026: 6e30 d803 7408 |0119: invoke-virtual {v4, v7, v8}, Lokhttp3/CertificatePinner;.check$okhttp_release:(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V // method@03d8
04402c: 6e10 4b04 0d00 |011c: invoke-virtual {v13}, Lokhttp3/ConnectionSpec;.supportsTlsExtensions:()Z // method@044b
$ adb shell ps -ef | grep test
u0_a220 4914 477 0 18:40:06 ? 00:00:05 com.example.test
u0_a220 4931 477 0 18:40:06 ? 00:00:00 com.example.test:worker1
u0_a220 4932 477 0 18:40:06 ? 00:00:00 com.example.test:worker2
$ frida -U -p 4914 -l cert-pin-bypass.js
____
/ _ | Frida 17.22.0 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . Prefer a GUI? Luma is the official Frida app, with a live REPL,
. . . . persistent sessions & collaboration. https://luma.frida.re/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
[Android Emulator 5554::PID::4914 ]-> Bypassed
// pin-bypass.js
Java.perform(function () {
var CP = Java.use("okhttp3.CertificatePinner");
// https://github.com/lysine-dev/okhttp/blob/75d8f91cfe2495b79d07b1dabe05789caa429ac2/okhttp/src/commonJvmAndroid/kotlin/okhttp3/CertificatePinner.kt#L157
CP["check$okhttp_release"].overload(
"java.lang.String",
"kotlin.jvm.functions.Function0",
).implementation = function () {
console.log("Bypassed");
return;
};
});
References:
- https://httptoolkit.com/blog/android-14-install-system-ca-certificate/
- https://source.android.com/docs/core/ota/modular-system/conscrypt
- https://www.redfoxsec.com/blog/installing-burp-suites-ca-as-a-system-certificate-on-android
- https://codeshare.frida.re/@silva95gustavo/okhttp3-certificate-pinner-bypass/
PCAPdroid
Use case
Capture network traffic per-app (not only HTTP/HTTPS). Optionally decrypt HTTPS.
Explanation
Capture packets via local VPN, dump them as .pcap + log the session keys.
Since Android 14 the trust store lives in the immutable com.android.conscrypt APEX at /apex/com.android.conscrypt/cacerts. Each process has its own mount namespace, so the cert must be bind-mounted into zygote (so forked apps inherit it).
Usage
$ wget https://github.com/emanuele-f/PCAPdroid/releases/download/v2.0.2/PCAPdroid_v2.0.2.apk
$ wget https://github.com/emanuele-f/PCAPdroid-mitm/releases/download/v2.4/PCAPdroid-mitm_v2.4_x86_64.apk
$ adb install PCAPdroid_v2.0.2.apk
$ adb install PCAPdroid-mitm_v2.4_x86_64.apk
PCAPdroid: Settings β> Enable TLS decryption (+ complete the setup guide)
$ cat /data/misc/user/0/cacerts-added/81c450f1.0 | openssl x509 -noout -subject -issuer
subject=CN=PCAPdroid CA, O=PCAPdroid
issuer=CN=PCAPdroid CA, O=PCAPdroid
$ mkdir -p -m 700 /data/local/tmp/ca-copy
$ cp /apex/com.android.conscrypt/cacerts/* /data/local/tmp/ca-copy/
$ cp /data/misc/user/0/cacerts-added/81c450f1.0 /data/local/tmp/ca-copy/
# overlay the legacy dir
$ mount -t tmpfs tmpfs /system/etc/security/cacerts
$ cp /data/local/tmp/ca-copy/* /system/etc/security/cacerts/
$ chown root:root /system/etc/security/cacerts/*
$ chmod 644 /system/etc/security/cacerts/*
$ chcon u:object_r:system_security_cacerts_file:s0 /system/etc/security/cacerts/*
# bind it over APEX
$ mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts
$ adb shell pgrep -f zygote
477
1022
$ nsenter --mount=/proc/477/ns/mnt -- /bin/mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts
$ nsenter --mount=/proc/1022/ns/mnt -- /bin/mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts
PCAPdroid:
Decryption rules β> App β> com.example.test
Traffic dump β> PCAP file
Start capturing
$ adb shell am force-stop com.example.test
$ adb shell am start -n com.example.test/.MainActivity
$ adb shell ps -ef | grep test
u0_a220 6372 477 7 20:20:13 ? 00:00:03 com.example.test
u0_a220 6391 477 1 20:20:13 ? 00:00:00 com.example.test:worker1
u0_a220 6392 477 1 20:20:13 ? 00:00:00 com.example.test:worker2
$ frida -U -p 6372 -l src/frida/cert-pin-bypass.js
____
/ _ | Frida 17.22.0 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . Prefer a GUI? Luma is the official Frida app, with a live REPL,
. . . . persistent sessions & collaboration. https://luma.frida.re/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
[Android Emulator 5554::PID::6372 ]-> Bypassed
// pin-bypass.js
Java.perform(function () {
var CP = Java.use("okhttp3.CertificatePinner");
// https://github.com/lysine-dev/okhttp/blob/75d8f91cfe2495b79d07b1dabe05789caa429ac2/okhttp/src/commonJvmAndroid/kotlin/okhttp3/CertificatePinner.kt#L157
CP["check$okhttp_release"].overload(
"java.lang.String",
"kotlin.jvm.functions.Function0",
).implementation = function () {
console.log("Bypassed");
return;
};
});
$ adb pull /sdcard/Download/PCAPdroid
$ ls PCAPdroid
PCAPdroid_04_Oct_20_20_34.keylog PCAPdroid_04_Oct_20_20_34.pcap
Wireshark:
Open β> Open Capture File
Edit β> Preferencesβ¦ β> Protocols β> TLS β> (Pre)-Master-Secret log filename
References:
- https://github.com/emanuele-f/pcapdroid/releases
- https://github.com/emanuele-f/PCAPdroid-mitm/releases
Test app
Use case
This app can be used to test tools such as:
Usage
$ cd test/test-app
$ ./gradlew assemble
$ adb install app/build/outputs/apk/debug/app-debug.apk
$ adb shell am start -n com.example.test/.MainActivity
$ adb shell ps -ef | grep test
u0_a217 3585 465 0 19:28:56 ? 00:00:01 com.example.test
u0_a217 3604 465 0 19:28:56 ? 00:00:00 com.example.test:worker1
u0_a217 3605 465 0 19:28:56 ? 00:00:00 com.example.test:worker2