Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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/

JADX

$ git clone https://github.com/skylot/jadx.git
$ cd jadx
$ ./gradlew dist
$ sudo cp build/jadx/bin/jadx /usr/local/bin/
$ sudo cp build/jadx/bin/jadx-gui /usr/local/bin/
$ sudo cp build/jadx/lib/jadx-dev-all.jar /usr/local/lib/

Or use flatpak instead:

$ flatpak install flathub com.github.skylot.jadx
$ flatpak run com.github.skylot.jadx

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>

Guest additions

Fedora:

$ sudo dnf install virtualbox-guest-additions

Ubuntu:

Devices –> Insert Guest Additions CD image…

$ sudo apt autoremove virtualbox-guest-utils virtualbox-guest-x11 -y
$ sudo apt install build-essential dkms linux-headers-$(uname -r) perl bzip2 
$ findmnt | grep -i vbox
$ cd /run/media/gemesa/VBox_GAs_7.2.22
$ sudo ./VBoxLinuxAdditions.run
$ reboot

Settings –> Storage –> remove CD attachment

O-MVLL

Check NDK version supported by o-mvll: https://github.com/open-obfuscator/o-mvll/releases

Check available NDK versions: https://github.com/android/ndk/wiki/Unsupported-Downloads

$ sdkmanager "ndk;29.0.14206865"
$ wget https://github.com/open-obfuscator/o-mvll/releases/download/1.11.1/omvll_v1-11-1_linux_2026-10-29T11_58_24.tar.gz
$ tar -xzf omvll_v1-11-1_linux_2026-10-29T11_58_24.tar.gz

dex2jar

$ git clone https://github.com/ThexXTURBOXx/dex2jar
$ cd dex2jar
$ ./gradlew distZip
$ cd dex-tools/build/distributions
$ unzip dex-tools-2.4-SNAPSHOT.zip
$ cd dex-tools-2.4-SNAPSHOT
$ chmod +x *.sh
$ pwd
/home/gemesa/git-repos/dex2jar/dex-tools/build/distributions/dex-tools-2.4-SNAPSHOT

Add to ~/.zshrc:

export PATH=$PATH:/home/gemesa/git-repos/dex2jar/dex-tools/build/distributions/dex-tools-2.4-SNAPSHOT

References:

  • https://github.com/ThexXTURBOXx/dex2jar#usage

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

FileSource
Stock firmware for your modelsamfirms.com (couldnt find fw here) or Frija tool or samfw
Magisk 27.0 APKgithub.com/topjohnwu/Magisk/releases - https://github.com/topjohnwu/Magisk/releases/download/canary-27005/app-release.apk
TWRP recovery for your devicetwrp.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 PatchedXDA thread original post
Multidisabler Samsung 3.5 corsicanuXDA thread original post
Ambasadii Cruel Kernel v8.2Ambasadii Dev site or Direct Google Drive link
TWRP Bootlogo Patch v1.6 or v1.7XDA 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

  1. enable the following in developer options:
    • OEM unlocking
    • USB debugging
  2. 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
  3. 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

Rooting

Patch the AP File with Magisk

  1. extract SAMFW.COM_SM-G970F_TMH_G970FXXSGHWC2_fac.zip:
    • AP_xxx.tar.md5
    • BL_xxx.tar.md5
    • CP_xxx.tar.md5
    • CSC_xxx.tar.md5
    • HOME_CSC_xxx.tar.md5
  2. adb push AP_G970F*.tar.md5 /sdcard/Download/
  3. adb install app-release.apk (Magisk)
  4. Magisk –> Install –> Select and Patch a File –> select the AP file
  5. Magisk will create the patched magisk_patched-XXXXX.tar in the same folder
  6. 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

  1. open Odin 3.14.1-3B Patched
  2. uncheck β€œAuto Reboot”
  3. boot phone into Download Mode: hold Vol Down + Bixby, then connect USB
  4. 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 *(not HOME_CSC)
  5. click Start and wait for PASS (green)
  6. do not reboot yet

Flash TWRP recovery with Odin

  1. restart phone and go back to download mode
  2. restart Odin
  3. in Odin: uncheck all slots
  4. in Odin: clear the AP slot and load the TWRP .img.tar into AP
  5. confirm β€œAuto Reboot” is still unchecked
  6. click Start and wait for PASS (green)

Boot into TWRP

  1. 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
  2. you should now be in TWRP
  3. 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

  1. in TWRP tap Wipe –> Advanced Wipe –> Dalvik / ART Cache + Cache then Swipe to Wipe
  2. This clears cache and dalvik cache. Skipping this step is the most common cause of bootloops.

Format data (fixes storage size issue)

  1. in TWRP: Wipe –> Format Data
  2. type yes and confirm
  3. this fixes the issue where internal storage shows as 32GB instead of 128GB
  4. 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

  1. in TWRP: tap Reboot –> System
  2. complete the standard Android setup wizard
  3. reboot the phone
  4. the Magisk app will appear (it can take a few minutes)
  5. open Magisk
    • if it says β€œUpgrade to full Magisk to finish the setup. Download and install?” then click OK
  6. open Magisk –> tap OK when prompted to reboot and complete installation
  7. the phone reboots
  8. root is active

Redmi note 10 5G rooting guide

Prerequisites

  • Settings –> Xiaomi Account –> log in with Google account
  • set a password and make a note of the account ID
  • add a phone number at https://account.xiaomi.com/pass/serviceLogin?checkSafePhone=true
  • enable dev options: Settings –> About phone –> MIUI version (tap 7 times)
  • Settings –> Additional settings –> Developer options: enable OEM unlocking and USB debugging
  • insert a SIM, turn Wi-Fi off and mobile data on, then go to Developer options –> Mi Unlock status –> Add account and device, then remove SIM
  • add to /etc/udev/rules.d/51-android.rules:
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="usb", ATTR{idVendor}=="2717", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="usb", ATTR{idVendor}=="08ed", MODE="0666", GROUP="plugdev"
SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", MODE="0666", GROUP="plugdev"

then:


$ sudo udevadm control --reload-rules
$ sudo udevadm trigger

reference:


$ grep -E "^\w+\s*(Google|Xiaomi|MediaTek)" /usr/share/misc/usb.ids
08ed  MediaTek Inc.
0e8d  MediaTek Inc.
18d1  Google Inc.
2717  Xiaomi Inc.
  • install https://github.com/MiForge/MiUnlockTool (pipx install miunlock)
  • install https://github.com/ssut/payload-dumper-go

Unlock the bootloader

$ adb reboot bootloader
$ fastboot devices
$ miunlock

How would you like to log in?

  1 - Browser (default)
  2 - Terminal
  3 - QR code

Choose > 2

Xiaomi Account ID, Email, or Phone (ex> +31XXXXXXXXX)
> XXXXXXXXXX
Password> *********

=== 2FA Verification Required ===


Attempts remaining: 5

Code sent to Email successfully.

Check your Email for the code.
Enter code (or type 'r' to resend): XXXXXX

Login successful

Account Region: NL

dataCenterZone: Europe

(Enter to continue, m to change dataCenterZone) > 

[ Ensure your Xiaomi device is in fastboot mode ] < 


Device Codename: camellia


notice: An unlocked device is an easy target for malware which may damage your device or cause financial loss.


The device will clear user data when unlocked


Press 'Enter' to continue β€” unlock(encryptData)

code: 20036
description: Please unlock 168 hours later. And do not add your account in MIUI again, otherwise you will wait from scratch
$ fastboot reboot

Download the ROM

  • check SW version: Settings –> About phone –> MIUI version: 14.0.4.0(TKSEUXM)
  • download from: https://miuirom.org/phones/redmi-note-10-5g –> https://miuirom.org/phones/redmi-note-10-5g#EEA
  • check if the mirror points to a legit xiaomi link, e.g. bn.d.miui.com
  • if fastboot ROM is not available, download recovery ROM instead: miui_CAMELLIANEEAGlobal_V14.0.4.0.TKSEUXM_7491c725d3_13.0.zip
  • unzip then payload-dumper-go -p boot payload.bin

Finish unlocking (after 168 hours)

$ adb reboot bootloader
$ miunlock
  • set up the phone, enable dev options

Magisk

  • download APK from https://github.com/topjohnwu/Magisk/releases
$ adb push Magisk-v31.1.apk /sdcard/Download/
  • install the APK via the UI (adb install requires dev options –> Install via USB but that requires a SIM card)
$ adb push boot.img /sdcard/Download/
  • Magisk –> Install –> Select and Patch a File –> boot.img
$ adb pull /sdcard/Download/magisk_patched-31100_jIweM.img
$ adb reboot bootloader
$ fastboot flash boot magisk_patched-31100_jIweM.img
$ fastboot reboot

KernelSU/KernelSU-Next

Config

Magisk

Config

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:

JADX

Use case

Decompile APK and .dex files.

Explanation

  • decompile APK and .dex files into Java sources
  • decode AndroidManifest.xml

Usage

GUI:

$ jadx-gui app/build/outputs/apk/release/app-release-unsigned.apk

CLI:

$ jadx -d out-release app/build/outputs/apk/release/app-release-unsigned.apk
INFO  - loading ...
INFO  - processing ...
ERROR - finished with errors, count: 4
$ head -n 5 out-release/resources/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    android:compileSdkVersion="36"

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:

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
0x00000900    1     16 sym.imp.__cxa_finalize
0x00000910    1     16 sym.imp.__cxa_atexit
0x00000920    1     16 sym.imp.__register_atfork
0x00000950    1     16 sym.imp.__openat_2
0x00000960    1     16 sym.imp.close
$ r2 -q -c 'iE' app-debug/lib/arm64-v8a/libnative-lib.so 
HINT: Relocs not applied, some references may not resolve: use -e bin.relocs.apply=true
nth paddr      vaddr      bind   type size lib name                                        demangled
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
6   0x000007f0 0x000007f0 GLOBAL FUNC 48       Java_com_example_test_Test_stringFromNative
8   0x00000854 0x00000854 GLOBAL FUNC 104      _Z8do_probev                                do_probe()
9   0x000008bc 0x000008bc GLOBAL FUNC 36       Java_com_example_test_Test_probe

References:

dex2jar

Use case

Convert .dex to .jar, then run parts (e.g. decryption functions) of the app on the host PC.

Explanation

See dex2jar.

Usage

$ cd test/test-app
$ ./gradlew assembleDebug
$ cd ../..
$ cp test/test-app/app/build/outputs/apk/debug/app-debug.apk .
$ d2j-dex2jar.sh app-debug.apk -o app-debug.jar
dex2jar app-debug.apk -> app-debug.jar
$ javap -cp app-debug.jar -p com.example.test.TextCipher
public class com.example.test.TextCipher {
  private static final byte KEY;
  public com.example.test.TextCipher();
  public static java.lang.String decrypt(java.lang.String);
  public static java.lang.String encrypt(java.lang.String);
  private static byte[] xor(byte[]);
}
$ java -cp app-debug.jar docs/src/scripts/dex2jar/TextCipherReflectionRunner.java 
Exception in thread "main" java.lang.VerifyError: Expecting a stackmap frame at branch target 29
Exception Details:
  Location:
    com/example/test/TextCipher.xor([B)[B @10: if_icmpge
  Reason:
    Expected stackmap frame at this location.
  Bytecode:
    0000000: 2bbe bc08 4e03 3d1c 2bbe a200 132d 1c2b
    0000010: 1c33 105a 8291 5484 0201 a7ff ed2d b0  

	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:467)
	at java.base/java.lang.Class.forName(Class.java:458)
	at TextCipherReflectionRunner.main(TextCipherReflectionRunner.java:6)
$ java -noverify -cp app-debug.jar docs/src/scripts/dex2jar/TextCipherReflectionRunner.java
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
Ej82NjV6PCg1N3ouMj96GwoRew==
Hello from the APK!
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class TextCipherReflectionRunner {
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("com.example.test.TextCipher");

        Constructor<?> ctor = c.getDeclaredConstructor();
        ctor.setAccessible(true);                           // needed only when it is private
        Object cipher = ctor.newInstance();

        Method enc = c.getDeclaredMethod("encrypt", String.class);
        enc.setAccessible(true);
        Object result = enc.invoke(cipher, "Hello from the APK!");
        System.out.println(result);

        Method dec = c.getDeclaredMethod("decrypt", String.class);
        dec.setAccessible(true);
        Object back = dec.invoke(cipher, result);
        System.out.println(back);
    }
}

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))
}

View on GitHub.

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]);
}

View on GitHub.

LIEF

Use case

Cross-platform library to parse, modify and abstract ELF, PE and MachO formats.

Explanation

See LIEF and lief.re.

Usage

$ pip install lief
$ cd test/test-app 
$ ./gradlew assembleDebug
$ apktool d app/build/outputs/apk/debug/app-debug.apk
$ ls app/build/outputs/apk/debug/app-debug/lib/arm64-v8a
libnative-lib.so
$ cp app/build/outputs/apk/debug/app-debug/lib/arm64-v8a/libnative-lib.so .

List JNI functions:

import lief
lib = lief.parse("libnative-lib.so")
for s in lib.exported_functions:
    if s.name.startswith("Java_") or s.name == "JNI_OnLoad":
        print(hex(s.address), s.name)
$ python docs/src/scripts/lief/jni-functions.py 
0x7f0 Java_com_example_test_Test_stringFromNative
0x8bc Java_com_example_test_Test_probe

Check hardening:

import lief
lib = lief.parse("libnative-lib.so")
print("PIE:", lib.is_pie)
print("NX:", lib.has_nx)
print("Canary:", any(f.name == "__stack_chk_fail" for f in lib.imported_functions))
$ python docs/src/scripts/lief/hardening.py
PIE: False
NX: True
Canary: False

Inject frida gadget:

import lief
lib = lief.parse("libnative-lib.so")
lib.add_library("libgadget.so.so")
lib.write("libnative-lib.so")
$ wget https://github.com/frida/frida/releases/download/17.26.2/frida-gadget-17.26.2-android-arm64.so.xz
$ xz -d frida-gadget-17.26.2-android-arm64.so.xz
$ mv frida-gadget-17.26.2-android-arm64.so libgadget.so
$ readelf -d libnative-lib.so | grep NEEDED                                              
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
$ python docs/src/scripts/lief/frida-gadget.py
$ readelf -d libnative-lib.so | grep NEEDED   
 0x0000000000000001 (NEEDED)             Shared library: [libgadget.so.so]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]

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:

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:

R8

Use case

R8 optimizes and obfuscates the Java code in release builds.

Explanation

  • Code shrinking
  • Method inlining
  • Class merging
  • Obfuscation

Usage

build.gradle.kts:

...
    buildTypes {
        release {
            optimization {
                enable = true
            }
        }
    }
...

gradle.properties:

...
android.r8.gradual.support=true
...

Before:

$ jadx -d out-debug app/build/outputs/apk/debug/app-debug.apk
INFO  - loading ...
INFO  - processing ...
ERROR - finished with errors, count: 109 
$ find out-debug/sources -name '?.java' -o -name '??.java' | head -n 5
out-debug/sources/androidx/appcompat/R.java
out-debug/sources/androidx/appcompat/resources/R.java
out-debug/sources/androidx/constraintlayout/widget/R.java
out-debug/sources/androidx/core/R.java
out-debug/sources/androidx/core/ktx/R.java
$ jadx-gui app/build/outputs/apk/debug/app-debug.apk

After:

$ jadx -d out-release app/build/outputs/apk/release/app-release-unsigned.apk
INFO  - loading ...
INFO  - processing ...
ERROR - finished with errors, count: 4
$ find out-release/sources -name '?.java' -o -name '??.java' | head -n 5
out-release/sources/defpackage/mf.java
out-release/sources/defpackage/he.java
out-release/sources/defpackage/ac.java
out-release/sources/defpackage/f4.java
out-release/sources/defpackage/di.java
$ head out-release/sources/defpackage/mf.java
package defpackage;

/* JADX INFO: compiled from: r8-map-id-eafe73029e20466c05c0462ab0e0500d885d3ab8cf2325c1fbce556b08b9b19f */
/* JADX INFO: loaded from: classes.dex */
public interface mf {
}
$ jadx-gui app/build/outputs/apk/release/app-release-unsigned.apk

References:

  • https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#kotlin

obfuscator-llvm

Use case

Obfuscate native code with LLVM IR level obfuscation passes.

Explanation

Maintained version of the original obfuscator-llvm.

Supported obfuscation passes:

Usage

General:

$ git clone git@github.com:llvm/llvm-project.git llvm-project-21
$ git switch release/21.x
$ cmake -S llvm -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_PARALLEL_LINK_JOBS=1 -DLLVM_ENABLE_PROJECTS="clang" -DCMAKE_INSTALL_PREFIX=/opt/llvm
$ ninja -C build -j 12
$ sudo ninja install
$ sudo dnf install xxd
$ git clone https://github.com/eshard/obfuscator-llvm.git
$ cd obfuscator-llvm
$ mkdir -p build && cd build
$ cmake -G "Ninja" -DLLVM_DIR=/opt/llvm/lib/cmake ..
$ ninja -j$(nproc)
$ sudo dnf install qemu-user-static-aarch64
$ sudo dnf -y --forcearch=aarch64 --installroot=/opt/a64 --releasever=$(rpm -E %fedora) --use-host-config install glibc-devel gcc
$ sudo dnf install lld
LLVM=/opt/llvm/bin
PLUGIN=$HOME/git-repos/obfuscator-llvm/build/libLLVMObfuscator.so
SYSROOT=/opt/a64
T="--target=aarch64-linux-gnu --sysroot=$SYSROOT"

$LLVM/clang $T -O1 -S -emit-llvm test_sub.c -o test_sub_a64.ll
$LLVM/opt -load-pass-plugin "$PLUGIN" -passes='function(substitution)' -S test_sub_a64.ll -o test_sub_a64_obf.ll
$LLVM/llc --relocation-model=pic -filetype=obj test_sub_a64_obf.ll -o test_sub_a64_obf.o
$LLVM/clang $T -fuse-ld=lld test_sub_a64_obf.o -o test_sub_a64_obf

file test_sub_a64_obf
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  int a = argc > 1 ? atoi(argv[1]) : 7;
  int b = argc > 2 ? atoi(argv[2]) : 3;
  printf("or=%d", a | b);
  return 0;
}
; ModuleID = 'test_sub.c'
source_filename = "test_sub.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32"
target triple = "aarch64-unknown-linux-gnu"

@.str = private unnamed_addr constant [6 x i8] c"or=%d\00", align 1

; Function Attrs: nofree nounwind uwtable
define dso_local noundef i32 @main(i32 noundef %0, ptr nofree noundef readonly captures(none) %1) local_unnamed_addr #0 {
  %3 = icmp sgt i32 %0, 1
  br i1 %3, label %4, label %9

4:                                                ; preds = %2
  %5 = getelementptr inbounds nuw i8, ptr %1, i64 8
  %6 = load ptr, ptr %5, align 8, !tbaa !16
  %7 = tail call i64 @strtol(ptr noundef nonnull captures(none) %6, ptr noundef null, i32 noundef 10) #3, !inline_history !19
  %8 = trunc i64 %7 to i32
  br label %9

9:                                                ; preds = %2, %4
  %10 = phi i32 [ %8, %4 ], [ 7, %2 ]
  %11 = icmp sgt i32 %0, 2
  br i1 %11, label %12, label %17

12:                                               ; preds = %9
  %13 = getelementptr inbounds nuw i8, ptr %1, i64 16
  %14 = load ptr, ptr %13, align 8, !tbaa !16
  %15 = tail call i64 @strtol(ptr noundef nonnull captures(none) %14, ptr noundef null, i32 noundef 10) #3, !inline_history !19
  %16 = trunc i64 %15 to i32
  br label %17

17:                                               ; preds = %9, %12
  %18 = phi i32 [ %16, %12 ], [ 3, %9 ]
  %19 = or i32 %18, %10
  %20 = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str, i32 noundef %19)
  ret i32 0
}

; Function Attrs: nofree nounwind
declare noundef i32 @printf(ptr noundef readonly captures(none), ...) local_unnamed_addr #1

; Function Attrs: mustprogress nocallback nofree nounwind willreturn
declare i64 @strtol(ptr noundef readonly, ptr noundef captures(none), i32 noundef) local_unnamed_addr #2

attributes #0 = { nofree nounwind uwtable "frame-pointer"="non-leaf-no-reserve" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="generic" "target-features"="+fp-armv8,+neon,+outline-atomics,+v8a,-fmv" }
attributes #1 = { nofree nounwind "frame-pointer"="non-leaf-no-reserve" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="generic" "target-features"="+fp-armv8,+neon,+outline-atomics,+v8a,-fmv" }
attributes #2 = { mustprogress nocallback nofree nounwind willreturn "frame-pointer"="non-leaf-no-reserve" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="generic" "target-features"="+fp-armv8,+neon,+outline-atomics,+v8a,-fmv" }
attributes #3 = { nounwind }

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9}
!llvm.ident = !{!10}
!llvm.errno.tbaa = !{!11}

!0 = !{i32 1, !"ptrauth-elf-got", i32 0}
!1 = !{i32 1, !"ptrauth-init-fini", i32 0}
!2 = !{i32 1, !"ptrauth-init-fini-address-discrimination", i32 0}
!3 = !{i32 1, !"ptrauth-sign-personality", i32 0}
!4 = !{i32 1, !"aarch64-elf-pauthabi-platform", i32 268435458}
!5 = !{i32 1, !"aarch64-elf-pauthabi-version", i32 0}
!6 = !{i32 8, !"PIC Level", i32 2}
!7 = !{i32 7, !"PIE Level", i32 2}
!8 = !{i32 7, !"uwtable", i32 2}
!9 = !{i32 7, !"frame-pointer", i32 4}
!10 = !{!"clang version 24.0.0git (https://github.com/llvm/llvm-project.git 56ed7f824a62ec3261ecdf57c51627dc7d58591c)"}
!11 = !{!12, !13, i64 0}
!12 = !{!"__libc_errno", !13, i64 0}
!13 = !{!"int", !14, i64 0}
!14 = !{!"omnipotent char", !15, i64 0}
!15 = !{!"Simple C/C++ TBAA"}
!16 = !{!17, !17, i64 0}
!17 = !{!"p1 omnipotent char", !18, i64 0}
!18 = !{!"any pointer", !14, i64 0}
!19 = distinct !{null}
undefined8 main(int param_1,long param_2)

{
  uint uVar1;
  uint uVar2;
  long lVar3;
  
  if (param_1 < 2) {
    uVar1 = 7;
  }
  else {
    lVar3 = strtol(*(char **)(param_2 + 8),(char **)0x0,10);
    uVar1 = (uint)lVar3;
  }
  if (param_1 < 3) {
    uVar2 = 3;
  }
  else {
    lVar3 = strtol(*(char **)(param_2 + 0x10),(char **)0x0,10);
    uVar2 = (uint)lVar3;
  }
  printf("or=%d",((uVar2 ^ 0xffffffff) & 0xef27f53c | uVar2 & 0x10d80ac3) ^
                 ((uVar1 ^ 0xffffffff) & 0xef27f53c | uVar1 & 0x10d80ac3) |
                 (~uVar2 | uVar1 ^ 0xffffffff) ^ 0xffffffff);
  return 0;
}
; ModuleID = 'test_sub_a64.ll'
source_filename = "test_sub.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32"
target triple = "aarch64-unknown-linux-gnu"

@.str = private unnamed_addr constant [6 x i8] c"or=%d\00", align 1

; Function Attrs: nofree nounwind uwtable
define dso_local noundef i32 @main(i32 noundef %0, ptr nofree noundef readonly captures(none) %1) local_unnamed_addr #0 {
  %3 = icmp sgt i32 %0, 1
  br i1 %3, label %4, label %9

4:                                                ; preds = %2
  %5 = getelementptr inbounds nuw i8, ptr %1, i64 8
  %6 = load ptr, ptr %5, align 8, !tbaa !16
  %7 = tail call i64 @strtol(ptr noundef nonnull captures(none) %6, ptr noundef null, i32 noundef 10) #3, !inline_history !19
  %8 = trunc i64 %7 to i32
  br label %9

9:                                                ; preds = %4, %2
  %10 = phi i32 [ %8, %4 ], [ 7, %2 ]
  %11 = icmp sgt i32 %0, 2
  br i1 %11, label %12, label %17

12:                                               ; preds = %9
  %13 = getelementptr inbounds nuw i8, ptr %1, i64 16
  %14 = load ptr, ptr %13, align 8, !tbaa !16
  %15 = tail call i64 @strtol(ptr noundef nonnull captures(none) %14, ptr noundef null, i32 noundef 10) #3, !inline_history !19
  %16 = trunc i64 %15 to i32
  br label %17

17:                                               ; preds = %12, %9
  %18 = phi i32 [ %16, %12 ], [ 3, %9 ]
  %19 = xor i32 %18, -1
  %20 = xor i32 %10, -1
  %21 = xor i32 -282593988, -1
  %22 = and i32 %19, -282593988
  %23 = and i32 %18, %21
  %24 = and i32 %20, -282593988
  %25 = and i32 %10, %21
  %26 = or i32 %22, %23
  %27 = or i32 %24, %25
  %28 = xor i32 %26, %27
  %29 = or i32 %19, %20
  %30 = xor i32 %29, -1
  %31 = or i32 -282593988, %21
  %32 = and i32 %30, %31
  %33 = or i32 %28, %32
  %34 = or i32 %18, %10
  %35 = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str, i32 noundef %33)
  ret i32 0
}

; Function Attrs: nofree nounwind
declare noundef i32 @printf(ptr noundef readonly captures(none), ...) local_unnamed_addr #1

; Function Attrs: mustprogress nocallback nofree nounwind willreturn
declare i64 @strtol(ptr noundef readonly, ptr noundef captures(none), i32 noundef) local_unnamed_addr #2

attributes #0 = { nofree nounwind uwtable "frame-pointer"="non-leaf-no-reserve" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="generic" "target-features"="+fp-armv8,+neon,+outline-atomics,+v8a,-fmv" }
attributes #1 = { nofree nounwind "frame-pointer"="non-leaf-no-reserve" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="generic" "target-features"="+fp-armv8,+neon,+outline-atomics,+v8a,-fmv" }
attributes #2 = { mustprogress nocallback nofree nounwind willreturn "frame-pointer"="non-leaf-no-reserve" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="generic" "target-features"="+fp-armv8,+neon,+outline-atomics,+v8a,-fmv" }
attributes #3 = { nounwind }

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9}
!llvm.ident = !{!10}
!llvm.errno.tbaa = !{!11}

!0 = !{i32 1, !"ptrauth-elf-got", i32 0}
!1 = !{i32 1, !"ptrauth-init-fini", i32 0}
!2 = !{i32 1, !"ptrauth-init-fini-address-discrimination", i32 0}
!3 = !{i32 1, !"ptrauth-sign-personality", i32 0}
!4 = !{i32 1, !"aarch64-elf-pauthabi-platform", i32 268435458}
!5 = !{i32 1, !"aarch64-elf-pauthabi-version", i32 0}
!6 = !{i32 8, !"PIC Level", i32 2}
!7 = !{i32 7, !"PIE Level", i32 2}
!8 = !{i32 7, !"uwtable", i32 2}
!9 = !{i32 7, !"frame-pointer", i32 4}
!10 = !{!"clang version 24.0.0git (https://github.com/llvm/llvm-project.git 56ed7f824a62ec3261ecdf57c51627dc7d58591c)"}
!11 = !{!12, !13, i64 0}
!12 = !{!"__libc_errno", !13, i64 0}
!13 = !{!"int", !14, i64 0}
!14 = !{!"omnipotent char", !15, i64 0}
!15 = !{!"Simple C/C++ TBAA"}
!16 = !{!17, !17, i64 0}
!17 = !{!"p1 omnipotent char", !18, i64 0}
!18 = !{!"any pointer", !14, i64 0}
!19 = distinct !{null}

Android:

See ANDROID_NDK.md.

References:

  • https://github.com/eshard/obfuscator-llvm
  • https://github.com/za233/Polaris-Obfuscator/tree/main
  • https://github.com/Apich-Organization/ensia

obfuscator-llvm

Use case

Obfuscate native code with LLVM IR level obfuscation passes. Actively maintained by Build38.

Explanation

Supported obfuscation passes are available here.

Usage

$ cd test/test-app
$ source ./omvll.env
$ ./gradlew clean assembleRelease
# See the API documentation at https://obfuscator.re/o-mvll/
import omvll
import os
from functools import lru_cache

THIRD_PARTY_MODULES = []

EXCLUDED_FUNCTIONS = []

SENSITIVE_FUNCTIONS = []

class MyConfig(omvll.ObfuscationConfig):

    omvll.config.global_mod_exclude = THIRD_PARTY_MODULES
    omvll.config.global_func_exclude = EXCLUDED_FUNCTIONS
    omvll.config.shuffle_functions = False
    omvll.config.inline_jni_wrappers = False

    omvll.config.output_folder = os.path.dirname(os.path.abspath(__file__))

    def __init__(self):
        super().__init__()

    def obfuscate_string(self, mod: omvll.Module, func: omvll.Function,
                         string: bytes):
        return omvll.StringEncOptGlobal()

    def obfuscate_constants(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.OpaqueConstantsBool(True)

    def obfuscate_arithmetic(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.ArithmeticOpt(2)

    def obfuscate_variable_access(self, mod: omvll.Module, func: omvll.Function,
                                  var: omvll.GlobalVariable):
        return None
        return omvll.VarAccessOpt(
            omvll.ObfuscationConfig.default_config(
                self, mod, func, [], [], SENSITIVE_FUNCTIONS, 10))

    def obfuscate_struct_access(self, mod: omvll.Module, func: omvll.Function,
                                struct: omvll.Struct):
        return None
        return omvll.StructAccessOpt(
            omvll.ObfuscationConfig.default_config(
                self, mod, func, [], [], SENSITIVE_FUNCTIONS, 10))

    def flatten_cfg(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.ControlFlowFlatteningOpt(
            omvll.ObfuscationConfig.default_config(
                self, mod, func, [], [], SENSITIVE_FUNCTIONS, 10))

    def break_control_flow(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.BreakControlFlowOpt(
            omvll.ObfuscationConfig.default_config(
                self, mod, func, [], [], SENSITIVE_FUNCTIONS, 5))

    def basic_block_duplicate(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.BasicBlockDuplicateWithProbability(5)

    def basic_block_split(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.BasicBlockSplitWithProbability(10)

    def function_outline(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.FunctionOutlineWithProbability(5)

    def shuffle_ops(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.ShuffleOpsOpt(min_block_size=4)

    def indirect_branch(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.ObfuscationConfig.default_config(
            self, mod, func, [], [], SENSITIVE_FUNCTIONS, 5)

    def indirect_call(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.ObfuscationConfig.default_config(
            self, mod, func, [], [], SENSITIVE_FUNCTIONS, 5)

    def anti_hooking(self, mod: omvll.Module, func: omvll.Function):
        return None
        return omvll.AntiHookOpt(
            omvll.ObfuscationConfig.default_config(
                self, mod, func, [], [], SENSITIVE_FUNCTIONS, 0))


@lru_cache(maxsize=1)
def omvll_get_config() -> omvll.ObfuscationConfig:
    return MyConfig()
export ANDROID_HOME=$HOME/Android/Sdk
export NDK_VERSION=29.0.14206865
export LD_LIBRARY_PATH=$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/linux-x86_64/lib64
export OMVLL_CONFIG=$(pwd)/omvll_config.py
export OMVLL_PYTHONPATH=$HOME/projects/omvll/Python-3.10.7/Lib
$ cat omvll-logs/omvll-module-logs/aarch64/native-lib.cpp-omvll-128971.log 
[omvll::AntiHook] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::AntiHook] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::FunctionOutline] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::StringEncoding] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::StringEncoding] Processing string Hello from JNI !  Compiled with ABI arm64-v8a.
Invoke subprocess: /home/gemesa/Android/Sdk/ndk/29.0.14206865/toolchains/llvm/prebuilt/linux-x86_64/bin/clang-21 -S -emit-llvm -target aarch64-none-linux-android28 -target aarch64-none-linux-android28 -std=c++17 -o /home/gemesa/git-repos/android-security-handbook/test/test-app/cache/omvll-cache-aarch64-none-linux-android28-2a1de4.ll /home/gemesa/git-repos/android-security-handbook/test/test-app/cache/omvll-cache-aarch64-none-linux-android28-2a1de4.cpp 
[omvll::StringEncoding] Changes  applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::OpaqueFieldAccess] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::BasicBlockDuplicate] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::BasicBlockDuplicate] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::ControlFlowFlattening] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::ControlFlowFlattening] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::BasicBlockSplit] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::BasicBlockSplit] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::BreakControlFlow] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::OpaqueConstants] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::OpaqueConstants] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::Arithmetic] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::Arithmetic] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::ShuffleOps] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::ShuffleOps] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::IndirectCall] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::IndirectCall] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::IndirectBranch] Executing on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp
[omvll::IndirectBranch] Changes not applied on module /home/gemesa/git-repos/android-security-handbook/test/test-app/app/src/main/cpp/native-lib.cpp

Phantom pass

See https://shadowshell.io/phantom-pass/.

Ghidra

Use case

Deobfuscate bogus control flow (bcf).

Usage

Script Manager –> Manage Script Directories –> add/enable $USER_HOME/ghidra_scripts

$ cp deobf-bcf.py $HOME/ghidra_scripts/
deobf-bcf.py> Running...
x    @ 00130db0
y    @ 00130db4
x.1  @ 00130db8
y.2  @ 00130dbc
main: patched 16 opaque branch(es)
deobf-bcf.py> Finished!
# Remove obfuscator-llvm bogus control flow (bcf) from listing and decompiler views.
# https://github.com/eshard/obfuscator-llvm/blob/main/bogus/BogusControlFlow.cpp
#
# predicates: (y < 10 || x * (x + 1) % 2 == 0), always true
# https://github.com/eshard/obfuscator-llvm/blob/ad26aa21b90cc5c5118d32114b674d1455f54168/bogus/BogusControlFlow.cpp#L43
#
# Ghidra cannot prove the "always true" part so we give it x, y concrete values:
# 0 (.bss) and let the decompiler constant fold.
#
# Step 1:
#   0 fill .bss and mark x, y as constants. The decompiler folds
#   the predicates and hides the bogus blocks.
#
# Step 2:
#   The decompiler lists the hidden blocks ("Removing unreachable block").
#   Any branch that can go to a hidden block is essentially a one way branch:
#     - target hidden -> never taken -> replace with nop
#     - fall-through hidden -> always taken -> replace with b target
#
# @category Deobfuscation

import re
from ghidra.app.cmd.function import CreateFunctionCmd
from ghidra.app.decompiler import DecompileOptions
from ghidra.app.decompiler.flatapi import FlatDecompilerAPI
from ghidra.program.flatapi import FlatProgramAPI
from ghidra.app.plugin.assembler import Assemblers
from ghidra.program.model.data import MutabilitySettingsDefinition as Mutability

# x, y, x.1, y.2, ...
OPAQUE_NAME = re.compile(r"^[xy]([._]\d+)?$")
# /* WARNING: Removing unreachable block (ram,0x00110968) */
DEAD_BLOCK = re.compile(r"Removing unreachable block \(\w+,0x([0-9a-fA-F]+)\)")


# step 1


def find_opaque_globals():
    opaque = []
    for sym in currentProgram.getSymbolTable().getAllSymbols(True):
        if not OPAQUE_NAME.match(sym.getName()):
            continue  # not named x, y, x.1, y.2, ...
        if not sym.getAddress().isMemoryAddress():
            continue  # e.g. a local variable named x (stack/register, not a global)
        opaque.append(sym)
    return opaque


def mark_constant(sym):
    addr = sym.getAddress()

    # Ghidra models .bss as uninitialized and the decompiler will not
    # read a value from it. We 0 fill it like the loader does at runtime.
    block = getMemoryBlock(addr)
    if not block.isInitialized():
        currentProgram.getMemory().convertToInitialized(block, 0)

    # mark x/y constant (Ghidra defines them as undefined4)
    Mutability.DEF.setChoice(getDataAt(addr), Mutability.CONSTANT)
    print("%-4s @ %s" % (sym.getName(), addr))


# step 2


def dead_blocks(func):
    decomp = FlatDecompilerAPI(FlatProgramAPI(currentProgram, monitor))
    # Without an options object the decompiler might leave out the
    # "Removing unreachable block" warnings.
    decomp.initialize()
    opts = DecompileOptions()
    opts.grabFromProgram(currentProgram)
    # the warning comments are on by default (uncomment if they are missing)
    # opts.setHeadCommentIncluded(True)
    # opts.setWARNCommentIncluded(True)
    decomp.getDecompiler().setOptions(opts)

    try:
        c_code = decomp.decompile(func, 60)  # 60 s timeout
    finally:
        decomp.dispose()
    # /* WARNING: Removing unreachable block (ram,0x00110968) */
    dead = set()
    for hex_addr in DEAD_BLOCK.findall(c_code):
        dead.add(int(hex_addr, 16))  # "00110810" -> 0x110810
    return dead


def patch_branches(func, dead):
    asm = Assemblers.getAssembler(currentProgram)
    # collect conditional jumps (tbz, cbz, b.gt, ...)
    # skip plain b, bl, ret
    branches = []
    for insn in currentProgram.getListing().getInstructions(func.getBody(), True):
        flow = insn.getFlowType()
        if flow.isConditional() and flow.isJump():
            branches.append(insn)

    patched = 0
    for insn in branches:
        addr = insn.getAddress()
        target = insn.getFlows()[0].getOffset()  # jump target
        fall = insn.getFallThrough().getOffset()  # fallthrough target

        if fall in dead and target not in dead:
            # The fallthrough is never taken -> it always jumps.
            #   0x1107f8  ldr   w8,[x22]          ; w8 = x
            #   0x1107fc  adrp  x23,0x120000
            #   0x110800  ldr   x23,[x23,#->y]    ; x23 = &y
            #   0x110804  sub   w9,w8,#1          ; w9 = x - 1
            #   0x110808  mul   w8,w8,w9          ; w8 = x * (x - 1)
            #   0x11080c  tbz   w8,#0,0x11081c    ; bit 0 is 0 (even) -> always jumps
            #   0x110810  ldr   w8,[x23]          ; dead
            #   0x110814  cmp   w8,#9             ; dead
            #   0x110818  b.gt  0x1107f8          ; dead
            #   0x11081c  cmp   w21,#2            ; jump target
            # becomes
            #   0x11080c  b     0x11081c
            asm.assemble(addr, "b 0x%x" % target)
        elif target in dead and fall not in dead:
            # The jump is never taken -> it always falls through.
            #   0x11096c  cset  w8,lt             ; w8 = predicate = 1 (always)
            #   ...
            #   0x110998  cbz   w8,0x1109c8       ; w8 == 0? never -> never jumps
            #   0x11099c  mov   w0,w19            ; live
            #   0x1109a0  mov   w1,w21            ; live
            #   0x1109a4  bl    mix               ; live
            # becomes
            #   0x110998  nop
            asm.assemble(addr, "nop")
        else:
            # Both paths live.
            # Both paths dead.
            continue
        patched += 1

    # Recompute the function from its entry point. Dead blocks are dropped.
    CreateFunctionCmd.fixupFunctionBody(currentProgram, func, monitor)
    print("%s: patched %d opaque branch(es)" % (func.getName(), patched))


def main():
    opaque = find_opaque_globals()
    if not opaque:
        print("No x, y globals found.")
        return
    for sym in opaque:
        mark_constant(sym)

    func = getFunctionContaining(currentAddress)
    if func is None:
        print("Move the cursor in the target function.")
        return

    dead = dead_blocks(func)
    if not dead:
        print("%s: no unreachable blocks found" % func.getName())
        return

    patch_branches(func, dead)


main()
#include <stdio.h>
#include <stdlib.h>

__attribute__((noinline)) static int mix(int a, int b) {
  int r = 0;
  for (int i = 0; i < b; i++) {
    if (i & 1)
      r += a ^ i;
    else
      r -= a & i;
  }
  return r;
}

int main(int argc, char **argv) {
  int a = argc > 1 ? atoi(argv[1]) : 7;
  int b = argc > 2 ? atoi(argv[2]) : 3;
  int r;

  if (a > b)
    r = a - b;
  else
    r = mix(a, b);

  printf("r=%d\n", r);
  return 0;
}
#!/usr/bin/env bash
set -euo pipefail

LLVM=/opt/llvm/bin
PLUGIN=$HOME/git-repos/obfuscator-llvm/build/libLLVMObfuscator.so
SYSROOT=/opt/a64
T="--target=aarch64-linux-gnu --sysroot=$SYSROOT"

SRC=test_bcf.c
OUT=test_bcf_a64

export LLVM_OBF_SEED=0xA04252B187478C00A40BC6D81D1A8A52

$LLVM/clang $T -O1 -fPIC -S -emit-llvm $SRC -o ${OUT}.ll
$LLVM/llc -filetype=obj ${OUT}.ll -o ${OUT}.o
$LLVM/clang $T -fuse-ld=lld ${OUT}.o -o ${OUT}

$LLVM/opt -load "$PLUGIN" -load-pass-plugin "$PLUGIN" \
	-bcf_prob=100 -bcf_loop=1 \
	-passes='function(bogus)' -S ${OUT}.ll -o ${OUT}_obf.ll

$LLVM/llc -filetype=obj ${OUT}_obf.ll -o ${OUT}_obf.o
$LLVM/clang $T -fuse-ld=lld ${OUT}_obf.o -o ${OUT}_obf

echo "opaque globals in IR:"
grep -E '^@(x|y)(\.[0-9]+)? ' ${OUT}_obf.ll || true

$LLVM/clang $T -shared -fuse-ld=lld ${OUT}_obf.o -o lib${OUT}_obf.so

Before:

undefined8 main(int param_1,long param_2)

{
  int iVar1;
  int iVar2;
  int param1;
  long lVar3;
  
  do {
    if ((x * (x + -1) & 1U) == 0) break;
  } while (9 < y);
  if (param_1 < 2) {
    iVar1 = 7;
  }
  else {
    if (((x * (x + -1) & 1U) != 0) && (9 < y)) goto LAB_0011087c;
    while( true ) {
      lVar3 = strtol(*(char **)(param_2 + 8),(char **)0x0,10);
      iVar1 = (int)lVar3;
      if (((x * (x + -1) & 1U) == 0) || (y < 10)) break;
LAB_0011087c:
      strtol(*(char **)(param_2 + 8),(char **)0x0,10);
    }
  }
  do {
    if ((x * (x + -1) & 1U) == 0) break;
  } while (9 < y);
  if (param_1 < 3) {
    iVar2 = 3;
  }
  else {
    if (((x * (x + -1) & 1U) != 0) && (9 < y)) goto LAB_00110918;
    while( true ) {
      lVar3 = strtol(*(char **)(param_2 + 0x10),(char **)0x0,10);
      iVar2 = (int)lVar3;
      if (((x * (x + -1) & 1U) == 0) || (y < 10)) break;
LAB_00110918:
      strtol(*(char **)(param_2 + 0x10),(char **)0x0,10);
    }
  }
  do {
    if ((x * (x + -1) & 1U) == 0) break;
  } while (9 < y);
  param1 = iVar1 - iVar2;
  if (param1 == 0 || iVar1 < iVar2) {
    if ((x * (x + -1) & 1U) != 0 && 9 < y) goto LAB_001109c8;
    while ((param1 = mix(iVar1,iVar2), (x * (x + -1) & 1U) != 0 && (9 < y))) {
LAB_001109c8:
      mix(iVar1,iVar2);
    }
  }
  else {
    do {
      if ((x * (x + -1) & 1U) == 0) break;
    } while (9 < y);
  }
  if (((x * (x + -1) & 1U) != 0) && (9 < y)) goto code_r0x00110a24;
  while ((printf("r=%d\n",param1), (x * (x + -1) & 1U) != 0 && (9 < y))) {
code_r0x00110a24:
    printf("r=%d\n",param1);
  }
  return 0;
}

After:

/* WARNING: Removing unreachable block (ram,0x00110968) */

undefined8 main(int param_1,long param_2)

{
  int iVar1;
  int iVar2;
  int param1;
  long lVar3;
  
  if (param_1 < 2) {
    iVar1 = 7;
  }
  else {
    lVar3 = strtol(*(char **)(param_2 + 8),(char **)0x0,10);
    iVar1 = (int)lVar3;
  }
  if (param_1 < 3) {
    iVar2 = 3;
  }
  else {
    lVar3 = strtol(*(char **)(param_2 + 0x10),(char **)0x0,10);
    iVar2 = (int)lVar3;
  }
  param1 = iVar1 - iVar2;
  if (param1 == 0 || iVar1 < iVar2) {
    param1 = mix(1,iVar1,iVar2);
  }
  printf("r=%d\n",param1);
  return 0;
}

Ghidra

Use case

Deobfuscate encrypted strings with emulator.

Usage

Script Manager –> Manage Script Directories –> add/enable $USER_HOME/ghidra_scripts

$ cp emu-enc-str.py $HOME/ghidra_scripts/
emu-enc-str.py> Running...
0x130be9: decrypted string: "secret"
0x130bf0: decrypted string: "hidden"
emu-enc-str.py> Finished!
#include <stdio.h>

#define STR_LEN 6

// "secret"
const unsigned char enc_str1[STR_LEN] = {0x29, 0x3f, 0x39, 0x28, 0x3f, 0x2e};
// "hidden"
const unsigned char enc_str2[STR_LEN] = {0x5f, 0x5e, 0x53, 0x53, 0x52, 0x59};

char dec_str1[STR_LEN + 1];
char dec_str2[STR_LEN + 1];

__attribute__((noinline)) void decrypt_str1(char *dst,
                                            const unsigned char *src) {
  for (int i = 0; i < STR_LEN; i++)
    dst[i] = src[i] ^ 0x5A;
  dst[STR_LEN] = '\0';
}

__attribute__((noinline)) void decrypt_str2(char *dst,
                                            const unsigned char *src) {
  for (int i = 0; i < STR_LEN; i++)
    dst[i] = src[i] ^ 0x37;
  dst[STR_LEN] = '\0';
}

int main(void) {
  decrypt_str1(dec_str1, enc_str1);
  decrypt_str2(dec_str2, enc_str2);

  printf("%s\n", dec_str1);
  printf("%s\n", dec_str2);
  return 0;
}
#!/usr/bin/env bash
set -euo pipefail

LLVM=/opt/llvm/bin
PLUGIN=$PWD/../build/libLLVMObfuscator.so
SYSROOT=/opt/a64
T="--target=aarch64-linux-gnu --sysroot=$SYSROOT"

SRC=test_enc_str.c
OUT=test_enc_str_a64

$LLVM/clang $T -fuse-ld=lld -O1 $SRC -o ${OUT}
$LLVM/llvm-strip ${OUT}
# @category Deobfuscation

from ghidra.app.emulator import EmulatorHelper

"""
undefined8 FUN_001108b8(void)

{
  FUN_00110868(&DAT_00130be9,&DAT_001005d4);
  FUN_00110890(&DAT_00130bf0,&DAT_001005da);
  puts(&DAT_00130be9);
  puts(&DAT_00130bf0);
  return 0;
}
"""

# fmt: off
calls = [(0x00110868, 0x00130BE9, 0x001005D4),
         (0x00110890, 0x00130BF0, 0x001005DA)]
# fmt: on

for func, dst, src in calls:
    emu = EmulatorHelper(currentProgram)
    emu.writeRegister("x0", dst)
    emu.writeRegister("x1", src)
    # arbitrary address
    emu.writeRegister("x30", 0x1000)
    # arbitrary address
    emu.writeRegister("sp", 0x70000000)
    emu.writeRegister("pc", func)
    emu.setBreakpoint(toAddr("0x1000"))
    emu.run(monitor)

    data = emu.readMemory(toAddr(hex(dst)), 6)
    text = "".join(chr(b & 0xFF) for b in data)
    print('%s: decrypted string: "%s"' % (hex(dst), text))
    emu.dispose()

Ghidra

Use case

Deobfuscate ollvm encrypted strings with emulator.

Usage

Script Manager –> Manage Script Directories –> add/enable $USER_HOME/ghidra_scripts

$ cp emu-enc-str.py $HOME/ghidra_scripts/
emu-enc-str.py> Running...
0x130a20: decrypted string: "secret"
0x130a27: decrypted string: "hidden"
emu-enc-str.py> Finished!
#include <stdio.h>

static const unsigned char str1[] = "secret";
static const unsigned char str2[] = "hidden";

int main(void) {
  printf("%s\n", str1);
  printf("%s\n", str2);
  return 0;
}
#!/usr/bin/env bash
set -euo pipefail

LLVM=/opt/llvm/bin
PLUGIN=$HOME/git-repos/obfuscator-llvm/build/libLLVMObfuscator.so
SYSROOT=/opt/a64
T="--target=aarch64-linux-gnu --sysroot=$SYSROOT"

SRC=test_enc_str.c
OUT=test_enc_str_a64

$LLVM/clang $T -O1 -fPIC -S -emit-llvm $SRC -o ${OUT}.ll
$LLVM/llc -filetype=obj ${OUT}.ll -o ${OUT}.o
$LLVM/clang $T -fuse-ld=lld ${OUT}.o -o ${OUT}

$LLVM/opt -load "$PLUGIN" -load-pass-plugin "$PLUGIN" \
	-passes='string-encryption' -S ${OUT}.ll -o ${OUT}_obf.ll

$LLVM/llc -filetype=obj ${OUT}_obf.ll -o ${OUT}_obf.o
$LLVM/clang $T -fuse-ld=lld ${OUT}_obf.o -o ${OUT}_obf

$LLVM/clang $T -shared -fuse-ld=lld ${OUT}_obf.o -o lib${OUT}_obf.so
# @category Deobfuscation

from ghidra.app.emulator import EmulatorHelper

"""
void tcyrHvkBDbRhVVf(void)

{
  YTKkx8dqnaDxN8a(&str1,7,0x5a);
  YTKkx8dqnaDxN8a(&str2,7,0x16);
  return;
}
"""

# fmt: off
calls = [(0x00110774, 0x00130A20, 7, 0x5A),
         (0x00110774, 0x00130A27, 7, 0x16)]
# fmt: on

for func, dst, size, key in calls:
    emu = EmulatorHelper(currentProgram)
    emu.writeRegister("x0", dst)
    emu.writeRegister("x1", size)
    emu.writeRegister("x2", key)
    # arbitrary address
    emu.writeRegister("x30", 0x1000)
    emu.writeRegister("pc", func)
    emu.setBreakpoint(toAddr("0x1000"))
    emu.run(monitor)

    data = emu.readMemory(toAddr(hex(dst)), size - 1)
    text = "".join(chr(b & 0xFF) for b in data)
    print('%s: decrypted string: "%s"' % (hex(dst), text))
    emu.dispose()

Ghidra

Use case

Deobfuscate omvll encrypted strings with emulator.

Usage

Prereqs: build an obfuscated APK.

Script Manager –> Manage Script Directories –> add/enable $USER_HOME/ghidra_scripts

$ cp emu-enc-str.py $HOME/ghidra_scripts/
$ cp test/test-app/app/build/outputs/apk/release/app-release-unsigned.apk .
$ apktool d app-release-unsigned.apk
emu-enc-str.py> Running...
0x1088c0: decrypted string: "Hello from JNI !  Compiled with ABI arm64-v8a."
emu-enc-str.py> Finished!
# @category Deobfuscation

from ghidra.app.emulator import EmulatorHelper

"""
void _INIT_0(void)

{
  ulong uVar1;
  undefined1 local_8 [8];
  
  uVar1 = 0;
  do {
    (&DAT_001088c0)[uVar1] = *(byte *)((ulong)local_8 | uVar1 & 7) ^ (&DAT_001088c0)[uVar1];
    uVar1 = uVar1 + 1;
  } while (uVar1 != 0x2f);
  return;
}
"""

# fmt: off
ctors = [(0x0010062C, 0x001088C0, 0x2F)]
# fmt: on

for func, dst, size in ctors:
    emu = EmulatorHelper(currentProgram)
    # arbitrary addresses
    emu.writeRegister("sp", 0x2000000)
    emu.writeRegister("x30", 0x1000)
    emu.writeRegister("pc", func)
    emu.setBreakpoint(toAddr("0x1000"))
    emu.run(monitor)

    data = emu.readMemory(toAddr(hex(dst)), size - 1)
    text = "".join(chr(b & 0xFF) for b in data)
    print('%s: decrypted string: "%s"' % (hex(dst), text))
    emu.dispose()

Alternatively, the encrypted data can be decrypted in-place.

emu-enc-str-overwrite.py> Running...
0x1088c0: decrypted string: "Hello from JNI !  Compiled with ABI arm64-v8a."
emu-enc-str-overwrite.py> Finished!
# @category Deobfuscation

from ghidra.app.emulator import EmulatorHelper

"""
void _INIT_0(void)

{
  ulong uVar1;
  undefined1 local_8 [8];
  
  uVar1 = 0;
  do {
    (&DAT_001088c0)[uVar1] = *(byte *)((ulong)local_8 | uVar1 & 7) ^ (&DAT_001088c0)[uVar1];
    uVar1 = uVar1 + 1;
  } while (uVar1 != 0x2f);
  return;
}
"""

# fmt: off
ctors = [(0x0010062C, 0x001088C0, 0x2F)]
# fmt: on

for func, dst, size in ctors:
    emu = EmulatorHelper(currentProgram)
    emu.enableMemoryWriteTracking(True)
    # arbitrary addresses
    emu.writeRegister("sp", 0x2000000)
    emu.writeRegister("x30", 0x1000)
    emu.writeRegister("pc", func)
    emu.setBreakpoint(toAddr("0x1000"))
    emu.run(monitor)

    data = emu.readMemory(toAddr(hex(dst)), size - 1)
    text = "".join(chr(b & 0xFF) for b in data)
    print('%s: decrypted string: "%s"' % (hex(dst), text))


mem = currentProgram.getMemory()
ranges = list(
    emu.getTrackedMemoryWriteSet().intersect(mem.getLoadedAndInitializedAddressSet())
)
for r in ranges:
    mem.setBytes(
        r.getMinAddress(), emu.readMemory(r.getMinAddress(), int(r.getLength()))
    )
emu.dispose()

Before:

                             //
                             // .data
                             // SHT_PROGBITS  [0x88c0 - 0x88ee]
                             // ram:001088c0-ram:001088ee
                             //
                             
                             DAT_001088c0                                    XREF[4]:     001000f8(*), _INIT_0:00100654(R), 
                                                                                          _INIT_0:00100664(W), 
                                                                                          _elfSectionHeaders::00000550(*)  
        001088c0 21              undefin    21h
                             
                             DAT_001088c1                                    XREF[2]:     _INIT_0:00100654(R), 
                                                                                          _INIT_0:00100664(W)  
        001088c1 0a              undefin    0Ah
        001088c2 04              ??         04h
        001088c3 d7              ??         D7h
        001088c4 30              ??         30h    0
        001088c5 9d              ??         9Dh
        001088c6 23              ??         23h    #
        001088c7 50              ??         50h    P
        001088c8 06              ??         06h
        001088c9 02              ??         02h
        001088ca 48              ??         48h    H
        001088cb f1              ??         F1h
        001088cc 11              ??         11h
        001088cd f4              ??         F4h
        001088ce 65              ??         65h    e
        001088cf 03              ??         03h
        001088d0 49              ??         49h    I
        001088d1 4f              ??         4Fh    O
        001088d2 2b              ??         2Bh    +
        001088d3 d4              ??         D4h
        001088d4 32              ??         32h    2
        001088d5 cd              ??         CDh
        001088d6 2c              ??         2Ch    ,
        001088d7 4e              ??         4Eh    N
        001088d8 0c              ??         0Ch
        001088d9 0b              ??         0Bh
        001088da 48              ??         48h    H
        001088db cc              ??         CCh
        001088dc 36              ??         36h    6
        001088dd c9              ??         C9h
        001088de 2d              ??         2Dh    -
        001088df 02              ??         02h
        001088e0 28              ??         28h    (
        001088e1 2d              ??         2Dh    -
        001088e2 21              ??         21h    !
        001088e3 9b              ??         9Bh
        001088e4 3e              ??         3Eh    >
        001088e5 cf              ??         CFh
        001088e6 28              ??         28h    (
        001088e7 14              ??         14h
        001088e8 5d              ??         5Dh    ]
        001088e9 42              ??         42h    B
        001088ea 1e              ??         1Eh
        001088eb 83              ??         83h
        001088ec 3e              ??         3Eh    >
        001088ed 93              ??         93h
        001088ee 45              ??         45h    E

After:

                             //
                             // .data
                             // SHT_PROGBITS  [0x88c0 - 0x88ee]
                             // ram:001088c0-ram:001088ee
                             //
                             
                             DAT_001088c0                                    XREF[4]:     001000f8(*), _INIT_0:00100654(R), 
                                                                                          _INIT_0:00100664(W), 
                                                                                          _elfSectionHeaders::00000550(*)  
        001088c0 48              undefin    48h
                             
                             DAT_001088c1                                    XREF[2]:     _INIT_0:00100654(R), 
                                                                                          _INIT_0:00100664(W)  
        001088c1 65              undefin    65h
        001088c2 6c              ??         6Ch    l
        001088c3 6c              ??         6Ch    l
        001088c4 6f              ??         6Fh    o
        001088c5 20              ??         20h     
        001088c6 66              ??         66h    f
        001088c7 72              ??         72h    r
        001088c8 6f              ??         6Fh    o
        001088c9 6d              ??         6Dh    m
        001088ca 20              ??         20h     
        001088cb 4a              ??         4Ah    J
        001088cc 4e              ??         4Eh    N
        001088cd 49              ??         49h    I
        001088ce 20              ??         20h     
        001088cf 21              ??         21h    !
        001088d0 20              ??         20h     
        001088d1 20              ??         20h     
        001088d2 43              ??         43h    C
        001088d3 6f              ??         6Fh    o
        001088d4 6d              ??         6Dh    m
        001088d5 70              ??         70h    p
        001088d6 69              ??         69h    i
        001088d7 6c              ??         6Ch    l
        001088d8 65              ??         65h    e
        001088d9 64              ??         64h    d
        001088da 20              ??         20h     
        001088db 77              ??         77h    w
        001088dc 69              ??         69h    i
        001088dd 74              ??         74h    t
        001088de 68              ??         68h    h
        001088df 20              ??         20h     
        001088e0 41              ??         41h    A
        001088e1 42              ??         42h    B
        001088e2 49              ??         49h    I
        001088e3 20              ??         20h     
        001088e4 61              ??         61h    a
        001088e5 72              ??         72h    r
        001088e6 6d              ??         6Dh    m
        001088e7 36              ??         36h    6
        001088e8 34              ??         34h    4
        001088e9 2d              ??         2Dh    -
        001088ea 76              ??         76h    v
        001088eb 38              ??         38h    8
        001088ec 61              ??         61h    a
        001088ed 2e              ??         2Eh    .
        001088ee 00              ??         00h

msynth

Use case

Code deobfuscation framework to simplify Mixed Boolean-Arithmetic (MBA) expressions.

Explanation

Python framework built on top of Miasm.

Supported simplification methods:

Usage

$ git clone https://github.com/mrphrazer/msynth.git
$ cd msynth
$ python -m venv msynth-env
$ source msynth-env/bin/activate
$ pip install -r requirements.txt
$ pip install .
$ unzip -d database -q database/3_variables_constants_7_nodes.txt.zip

Example 0

The test binary is generated with obfuscator-llvm:

undefined8 main(int param_1,long param_2)

{
  uint uVar1;
  uint uVar2;
  long lVar3;
  
  if (param_1 < 2) {
    uVar1 = 7;
  }
  else {
    lVar3 = strtol(*(char **)(param_2 + 8),(char **)0x0,10);
    uVar1 = (uint)lVar3;
  }
  if (param_1 < 3) {
    uVar2 = 3;
  }
  else {
    lVar3 = strtol(*(char **)(param_2 + 0x10),(char **)0x0,10);
    uVar2 = (uint)lVar3;
  }
  printf("or=%d",((uVar2 ^ 0xffffffff) & 0xef27f53c | uVar2 & 0x10d80ac3) ^
                 ((uVar1 ^ 0xffffffff) & 0xef27f53c | uVar1 & 0x10d80ac3) |
                 (~uVar2 | uVar1 ^ 0xffffffff) ^ 0xffffffff);
  return 0;
}
from miasm.expression.expression import ExprId, ExprInt
from msynth import Synthesizer

uVar1 = ExprId("uVar1", 32)
uVar2 = ExprId("uVar2", 32)
k = lambda v: ExprInt(v, 32)

e = ((uVar2 ^ k(0xffffffff)) & k(0xef27f53c) | uVar2 & k(0x10d80ac3)) ^ \
    ((uVar1 ^ k(0xffffffff)) & k(0xef27f53c) | uVar1 & k(0x10d80ac3)) | \
    (~uVar2 | uVar1 ^ k(0xffffffff)) ^ k(0xffffffff)

s = Synthesizer()
left, right = e.args
print(s.simplify(s.simplify(left) | s.simplify(right)))
$ python simplify.py           
uVar1 | uVar2

Example 1

Simplify the following code:

...
  *(undefined8 *)(unaff_x19 + 0x840) = 0;
...
  (*(code *)((*(ulong *)(unaff_x19 + 0x840) ^ 0x32a6ad) +
            (*(ulong *)(unaff_x19 + 0x840) & 0x32a6ad) * 2))();
...
# https://github.com/cea-sec/miasm/blob/master/doc/expression/expression.ipynb

"""
...
  *(undefined8 *)(unaff_x19 + 0x840) = 0;
...
  (*(code *)((*(ulong *)(unaff_x19 + 0x840) ^ 0x32a6ad) +
            (*(ulong *)(unaff_x19 + 0x840) & 0x32a6ad) * 2))();
...
"""

"""
  (x ^ 0x32a6ad) + (x & 0x32a6ad) * 2
"""

from miasm.expression.expression import ExprId, ExprInt
from msynth import Synthesizer

k = lambda v: ExprInt(v, 64)

x = ExprId("x", 64)
e = (x ^ k(0x32a6ad)) + (x & k(0x32a6ad)) * k(2)
e.replace_expr({x: k(0)})
s = Synthesizer()

print(s.simplify(e))
$ python simplify2.py
x + 0x32A6AD

Example 2

# https://github.com/mrphrazer/msynth/blob/dfd0f9e2cd0890658fe752aa207a2cfda2aef8d7/tests/synthesis/test_synthesizer_more.py#L84

from miasm.expression.expression import ExprId, ExprInt
from msynth import Synthesizer
from msynth.synthesis.oracle import SynthesisOracle
from msynth.synthesis.grammar import Grammar
from msynth.synthesis.mutations import Mutator

def obf(i0):
    i1 = 1
    while i1 != 0:
        i2 = i0 ^ i1
        i1 = ((i0 & i1) * 2)
        i0 = i2
    return i0

xs = [0, 1, 2, 3]

samples = {}
for x in xs:
    inputs = (ExprInt(x, 32),) # tuple
    output = ExprInt(obf(x), 32)
    samples[inputs] = output

oracle = SynthesisOracle(samples)

x = ExprId("x", 32)
grammar = Grammar(32, [x])
mutator = Mutator(grammar)

synthesizer = Synthesizer()
state, _ = synthesizer.iterated_local_search(mutator, oracle)

print(state.get_expr_simplified())
$ python simplify-loop.py
x + 0x1

References:

  • https://github.com/mrphrazer/msynth/blob/main/scripts/synthesize_expression.py

miasm

Use case

Reverse engineering framework in Python.

Explanation

See miasm and miasm.re.

Examples:

Usage

$ pip install miasm
from miasm.expression.expression import ExprId, ExprInt
from miasm.expression.simplifications import expr_simp

x = ExprId("x", 64)
k = lambda v: ExprInt(v, 64)

e = (x ^ k(0x32a86d)) + ((x & k(0x32a86d)) * k(2))
e = e.replace_expr({x: k(0)})

print(expr_simp(e))
$ python simplify.py
0x32A86D

References:

  • https://github.com/cea-sec/miasm/blob/master/doc/expression/expression.ipynb
  • https://github.com/cea-sec/miasm/blob/master/example/expression/simplification_add.py

bpftrace

Use case

Trace syscalls via eBPF.

Explanation

See bpftrace and bpftrace docs.

Usage

$ wget https://github.com/bpftrace/bpftrace/releases/download/v0.27.1/bpftrace-aarch64
$ chmod +x bpftrace-aarch64
$ adb push bpftrace-aarch64 /data/local/tmp/

Check what events are available:

$ adb shell
bluejay:/ $ su
bluejay:/ # cd /data/local/tmp
bluejay:/data/local/tmp # zcat /proc/config.gz | grep -E 'FTRACE_SYSCALLS|HAVE_SYSCALL_TRACEPOINTS'
CONFIG_HAVE_SYSCALL_TRACEPOINTS=y
# CONFIG_FTRACE_SYSCALLS is not set

CONFIG_FTRACE_SYSCALLS is not set so we do not have per-syscall (syscalls:*) events:

bluejay:/data/local/tmp # ./bpftrace-aarch64 -l 'tracepoint:syscalls:*' | head
./bpftrace-aarch64: stat /bugreports: No such file or directory
./bpftrace-aarch64: stat /adb_keys: No such file or directory
WARNING: Could not read functions from /sys/kernel/tracing/available_filter_functions. If this is expected, use --traceable-functions to set this path manually. This file is needed in order to determine which functions can safely be traced without causing issues in the kernel.

Only raw syscalls:

bluejay:/data/local/tmp # ./bpftrace-aarch64 -l 'tracepoint:raw_syscalls:*' | head
./bpftrace-aarch64: stat /bugreports: No such file or directory
./bpftrace-aarch64: stat /adb_keys: No such file or directory
WARNING: Could not read functions from /sys/kernel/tracing/available_filter_functions. If this is expected, use --traceable-functions to set this path manually. This file is needed in order to determine which functions can safely be traced without causing issues in the kernel.
tracepoint:raw_syscalls:sys_enter
tracepoint:raw_syscalls:sys_exit

Trace reboot (args):

$ cat reboot.bt 
tracepoint:raw_syscalls:sys_enter /args.id == 142/ {
  printf("%s uid=%d magic1=0x%x magic2=0x%x cmd=0x%x arg=0x%lx\n",
         comm, uid, (uint32)args.args[0], (uint32)args.args[1],
         (uint32)args.args[2], args.args[3]);
}
$ apkanalyzer manifest application-id KernelSU_Next_v3.4.2-spoofed_33355-release.apk 
kqvdie.zeykvw.hpbcyd
bluejay:/data/local/tmp # dumpsys package kqvdie.zeykvw.hpbcyd | grep appId                                                                                                                         
    appId=10297
bluejay:/data/local/tmp # ./bpftrace-aarch64 reboot.bt
./bpftrace-aarch64: stat /bugreports: No such file or directory
./bpftrace-aarch64: stat /adb_keys: No such file or directory
WARNING: Could not read functions from /sys/kernel/tracing/available_filter_functions. If this is expected, use --traceable-functions to set this path manually. This file is needed in order to determine which functions can safely be traced without causing issues in the kernel.
Attached 1 probe
libksud.so uid=10297 magic1=0xdeadbeef magic2=0xcafebabe cmd=0x0 arg=0x7fdf0d6d4c
libksud.so uid=10297 magic1=0xdeadbeef magic2=0xcafebabe cmd=0x0 arg=0x7fdcdce99c
libksud.so uid=0 magic1=0xdeadbeef magic2=0xfafafafa cmd=0x555e2 arg=0x7fe1a4ea84

KernelSU-Next source references:

References:

stackplz

Use case

eBPF-based stack tracing tool. Supports both native and Java stacks.

Explanation

See stackplz and medium.com.

Usage

Add to .zshrc:

# stackplz
export PATH=$PATH:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/
$ git clone https://github.com/SeeFlowerX/ebpf
$ git clone https://github.com/SeeFlowerX/ebpfmanager
$ git clone https://github.com/gemesa/stackplz
$ cd stackplz
$ git switch dev-gemesa
$ sudo dnf install golang
$ make
$ chmod +x bin/stackplz_arm64
$ adb push bin/stackplz_arm64 /data/local/tmp/stackplz
$ test/test-app
$ ./gradlew assembleDebug
$ adb install app/build/outputs/apk/debug/app-debug.apk
$ apkanalyzer manifest application-id app/build/outputs/apk/debug/app-debug.apk        
com.example.test
$ adb shell
bluejay:/ $ su
bluejay:/ # cd /data/local/tmp
bluejay:/data/local/tmp # ./stackplz -n com.example.test -s openat:f0 --stack -f w:/stackplz_marker
[*] save maps to maps_24175.txt
[*] save maps to maps_24204.txt
[*] save maps to maps_24206.txt
config file -> user/config/config_syscall_aarch64.json
hook syscall count:1
ConfigMap{stackplz_pid=2547,thread_whitelist=0}
uid => whitelist:[10299];blacklist:[]
pid => whitelist:[24175,24204,24206];blacklist:[]
tid => whitelist:[];blacklist:[]
start 2 modules
[24175|24175|om.example.test] openat(dirfd=-100, *pathname=0x77287b05e7(/stackplz_marker), flags=0x0, mode=0o000) LR:0x79f58ae7a8 PC:0x79f5908b88 SP:0x7fc37391c0, Backtrace:
  #00 pc 00000000000e8b88  /apex/com.android.runtime/lib64/bionic/libc.so (__openat+8)
  #01 pc 000000000008e7a4  /apex/com.android.runtime/lib64/bionic/libc.so (__openat_2+68)
  #02 pc 000000000000088c  /data/app/~~pneqkkELYGHetfvFMZCW-g==/com.example.test-5CD7uQXiiN-t6u6JdHOYIg==/base.apk!libnative-lib.so (offset 0xcb4000) (do_probe()+56)
  #03 pc 00000000000008d0  /data/app/~~pneqkkELYGHetfvFMZCW-g==/com.example.test-5CD7uQXiiN-t6u6JdHOYIg==/base.apk!libnative-lib.so (offset 0xcb4000) (Java_com_example_test_Test_probe+20)
  #04 pc 0000000000288270  /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144)
  #05 pc 0000000000270fc0  /apex/com.android.art/lib64/libart.so (art_quick_invoke_static_stub+640)
  #06 pc 00000000006a4f50  /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall<false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, bool, art::JValue*)+2484)
  #07 pc 000000000047cc38  /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp<false>(art::interpreter::SwitchImplContext*)+684)
  #08 pc 000000000048d438  /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8)
  #09 pc 0000000000001154  /data/app/~~pneqkkELYGHetfvFMZCW-g==/com.example.test-5CD7uQXiiN-t6u6JdHOYIg==/base.apk
  #10 pc 00000000006a4f0c  /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall<false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, bool, art::JValue*)+2416)
  #11 pc 000000000047cc38  /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp<false>(art::interpreter::SwitchImplContext*)+684)
  #12 pc 000000000048d438  /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8)
  #13 pc 0000000000000f7c  /data/app/~~pneqkkELYGHetfvFMZCW-g==/com.example.test-5CD7uQXiiN-t6u6JdHOYIg==/base.apk
  #14 pc 00000000006a4f0c  /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall<false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, bool, art::JValue*)+2416)
  #15 pc 000000000047cc38  /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp<false>(art::interpreter::SwitchImplContext*)+684)
  #16 pc 000000000048d438  /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8)
  #17 pc 0000000000000ed8  /data/app/~~pneqkkELYGHetfvFMZCW-g==/com.example.test-5CD7uQXiiN-t6u6JdHOYIg==/base.apk
  #18 pc 000000000048c844  /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+1216)
  #19 pc 00000000002883a8  /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88)
  #20 pc 000000000246c840  /memfd:jit-cache (deleted) (offset 0x2000000)

[24175|24175|om.example.test] openat(dirfd=-100, *pathname=0x77287b05e7, flags=0x0, mode=0o000, ret=-2)

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