Frida Android Tutorial: frida-server, ADB and Runtime Hooking

Deepak Prasad
Tested on Kali GNU/Linux Rolling 2026.2 (kali-rolling)
Package frida==17.17.0
frida-tools==14.10.4 (Python venv)
frida-server 17.17.0 (android-x86)
Applies to Kali Linux with adb; rooted Android emulator or owned test device
Lab environment Pentest lab
Genymotion Desktop lab
Android Studio AVD lab
Privilege Normal user for Frida clients; root on the Android guest for frida-server (adb root or su)
Cert prep CompTIA PenTest+ (mobile specialized systems / dynamic analysis concepts)
Scope Connect with ADB, install a pinned Frida environment, run matching frida-server, enumerate and attach to InsecureShop, and build observe-only then controlled-modify Java hooks. Does not cover SSL pinning bypass (see Android SSL pinning bypass), full Frida Gadget repackaging, Drozer IPC labs, or iOS.
Short on time?Beta Get the gist of this 13-minute read in seconds, then ask follow-up questions

Frida lets you inspect and modify an Android app while it is running. This tutorial installs a matching frida-server on a rooted Genymotion device, connects from Kali, enumerates InsecureShop, and builds simple Java hooks for observing and modifying runtime values.

Use any rooted Android guest that adb can reach. This lab used Genymotion; the free Android Studio AVD path works the same way once adb devices shows device and you pick the matching frida-server ABI.

ADB provides the device transport and shell access. Android IPC and exported-component testing with Drozer belongs in a separate guide; this page stays on Frida attach, hook, and verify. Use only APKs and devices you own or are authorized to test.

IMPORTANT
Run Frida only against apps and devices in scope. Prefer intentionally vulnerable training APKs such as InsecureShop in an isolated lab. Put client scope in your rules of engagement. Do not instrument third-party production apps without written permission.

What Frida does on Android

Frida injects a JavaScript engine into a process so you can hook Java and native methods at runtime. On Android the usual lab path is:

Piece Role
ADB Device visibility, package install, push frida-server, start activities
frida / frida-tools on Kali Client that talks to the device over USB or TCP
frida-server on the guest Rooted helper process Frida attaches through

Static scanners such as MobSF and APKHunt find leads in the APK. Frida confirms what happens while the process runs.

text
  APK on disk                 Running process
  ┌─────────────────┐         ┌──────────────────────┐
  │ MobSF / APKHunt │  leads  │ Frida hooks / traces │
  │ static findings │ ──────► │ args, returns, logs  │
  └─────────────────┘         └──────────────────────┘

Frida Android lab requirements

You need:

  • A rooted emulator or owned rooted device (this lab: Genymotion Android 10 x86 — Genymotion Desktop lab; a Google APIs AVD with a matching frida-server ABI works the same way)
  • Working adb from Kali
  • A training APK you are allowed to instrument

Training target:

  • APK: InsecureShop v1.0 (intentionally vulnerable)
  • Package identifier: com.insecureshop

Create a lab folder and download the APK with curl:

bash
mkdir -p "${HOME}/frida-lab" && curl -fL -o "${HOME}/frida-lab/insecureshop.apk" "https://github.com/optiv/InsecureShop/releases/download/v1.0/InsecureShop.apk"

curl -fL fails on HTTP errors and follows redirects. Confirm the file size is non-zero before you install it.


Connect to Android with ADB

Pin the Genymotion TCP serial so later commands stay on the right device when more than one ADB target appears:

bash
export ANDROID_SERIAL=192.168.56.103:5555

Connect, then list devices:

bash
adb connect "${ANDROID_SERIAL}"
output
already connected to 192.168.56.103:5555

Confirm the transport with adb devices. The serial should show as device, not offline:

bash
adb devices
output
List of devices attached
192.168.56.103:5555	device

Read the ABI and Android release so the Frida server binary matches the guest:

bash
adb shell getprop ro.product.cpu.abi
output
x86

Map the property value to the Frida release asset name. The filename is not always a literal copy of ro.product.cpu.abi:

Android ABI (ro.product.cpu.abi) Frida frida-server asset
x86 android-x86
x86_64 android-x86_64
arm64-v8a android-arm64
armeabi-v7a android-arm

Check the Android release next. Genymotion in this lab reports API level 29 as release 10:

bash
adb shell getprop ro.build.version.release
output
10

Install the training APK when it is not already present:

bash
adb install -r "${HOME}/frida-lab/insecureshop.apk"
output
Performing Streamed Install
Success

Confirm the package identifier you will attach to later. A short grep filter keeps the pm list readable:

bash
adb shell pm list packages | grep insecureshop
output
package:com.insecureshop

Install Frida on Kali

The Frida core version reported by frida --version should match the frida-server release on Android. The separate frida-tools Python package has its own version number and does not need to equal the server version.

This lab pins the versions that produced the sample output below. Create a venv and install exactly those packages with pip:

bash
python3 -m venv "${HOME}/frida-lab/venv"

python3 -m venv is silent on success. Activate the environment and install the pinned packages:

bash
source "${HOME}/frida-lab/venv/bin/activate" && pip install 'frida==17.17.0' 'frida-tools==14.10.4'

Confirm the core version you will match on the device:

bash
frida --version
output
17.17.0

If you intentionally install a newer Frida release, download the matching frida-server rather than copying the version shown in this example.


Install and start frida-server

Download the matching frida-server for this Genymotion ABI (android-x86), decompress it, and keep the binary named frida-server:

bash
VER=$(frida --version) && curl -fL -o "${HOME}/frida-lab/frida-server-${VER}-android-x86.xz" "https://github.com/frida/frida/releases/download/${VER}/frida-server-${VER}-android-x86.xz"

Decompress the archive, rename the binary to frida-server, and mark it executable:

bash
xz -dk "${HOME}/frida-lab/frida-server-${VER}-android-x86.xz" && mv "${HOME}/frida-lab/frida-server-${VER}-android-x86" "${HOME}/frida-lab/frida-server" && chmod 755 "${HOME}/frida-lab/frida-server"

Restart adbd as root when the image allows it, then push the server binary before any chmod or start:

bash
adb root
output
adbd is already running as root

Push first so both the adb root path and the su -c fallback operate on a file that already exists on the device:

bash
adb push "${HOME}/frida-lab/frida-server" /data/local/tmp/frida-server
output
/root/frida-lab/frida-server: 1 file pushed, 0 skipped. 0.4 MB/s (53939692 bytes in 144.678s)

Your path and transfer speed will differ. On this eng-style Genymotion image, adb root worked, so mark the binary executable and start it without su:

bash
adb shell "chmod 755 /data/local/tmp/frida-server"

chmod is silent on success. Start frida-server as a daemon in the guest:

bash
adb shell "/data/local/tmp/frida-server -D"

If adb root instead says adbd cannot run as root in production builds, keep ADB as the normal shell user and run the same steps with su -c after the push:

bash
adb shell "su -c 'chmod 755 /data/local/tmp/frida-server'"

chmod through su is silent on success. Start the server the same way:

bash
adb shell "su -c '/data/local/tmp/frida-server -D'"

Confirm a live frida-server process with the guest ps listing:

bash
adb shell "ps -A | grep frida-server"
output
root          3747     1 1148076 170356 poll_schedule_timeout f2487bb9 S frida-server

Verify Frida with frida-ps

With frida-server running, list a sample of running processes over the USB/TCP transport (-U):

bash
frida-ps -U | head -n 20
output
 PID  Name
----  ---------------------------------------------------------------------------
2810  Files
1701  Phone
2217  Search
2356  WebView Shell
2638  abb
 489  adbd

Installed apps, including ones that are not running yet, show up with -Uai. Filter for the training package so the result stays deterministic across devices:

bash
frida-ps -Uai | grep -i insecureshop
output
5086  InsecureShop          com.insecureshop

Launch InsecureShop from the emulator UI first if the PID column shows a dash. A dash means the app is installed but not currently running. Use com.insecureshop with -N / -f below, not only the display name.


Attach to an Android app

Frida's attach flags answer different questions:

Flag Meaning
-N / --attach-identifier Attach by application identifier (com.insecureshop)
-n / --attach-name Attach by process display name (InsecureShop)
-f / --file Spawn the package, then inject before it fully resumes

Prefer the package identifier. It stays stable when the launcher label changes.

Clear app data so you land on the login screen, then launch the app:

bash
adb shell pm clear com.insecureshop
output
Success

Launch the app so a process exists for attach:

bash
adb shell monkey -p com.insecureshop -c android.intent.category.LAUNCHER 1

Allow storage permission if the dialog appears. Create hook-demo.js in the next section, then attach with -N com.insecureshop (or spawn with -f com.insecureshop when the process is not already running).


Hook a Java method

Start with an observe-only method hook before attempting more complex instrumentation. Do not jump straight to SSL pinning bypass. On InsecureShop, login calls android.util.Log.d with the typed username and password before Util.verifyUserNamePassword runs.

Save the method wrapper, replace implementation, then call the original with .call(this, …). That matches Frida's own Android examples and avoids recursive wrappers.

Save this script as hook-demo.js in the lab folder:

javascript
Java.perform(function () {
  var Log = Java.use('android.util.Log');
  var logD = Log.d.overload('java.lang.String', 'java.lang.String');
  logD.implementation = function (tag, msg) {
    if (tag === 'userName' || tag === 'password') {
      console.log('[*] Runtime Log.d tag=' + tag + ' msg=' + msg);
    }
    return logD.call(this, tag, msg);
  };

  var LoginActivity = Java.use('com.insecureshop.LoginActivity');
  var onLogin = LoginActivity.onLogin.overload('android.view.View');
  onLogin.implementation = function (view) {
    console.log('[*] LoginActivity.onLogin entered');
    var ret = onLogin.call(this, view);
    console.log('[*] LoginActivity.onLogin returned');
    return ret;
  };

  console.log('[*] Demo hooks installed');
});
Output

With InsecureShop already launched, attach by package identifier:

bash
frida -U -N com.insecureshop -l "${HOME}/frida-lab/hook-demo.js" -q -t 90

To spawn the package under Frida instead of attaching to a running process:

bash
frida -U -f com.insecureshop -l "${HOME}/frida-lab/hook-demo.js" -q -t 90

On the login screen (after attach or spawn), submit a wrong password first, then the known training credentials shopuser / !ns3csh0p. Sample output from this lab:

output
[*] Demo hooks installed
[*] LoginActivity.onLogin entered
[*] Runtime Log.d tag=userName msg=shopuser
[*] Runtime Log.d tag=password msg=badpass
[*] LoginActivity.onLogin returned
[*] LoginActivity.onLogin entered
[*] Runtime Log.d tag=userName msg=shopuser
[*] Runtime Log.d tag=password msg=!ns3csh0p
[*] LoginActivity.onLogin returned

Read that cycle as: method call → hook → observe arguments → original behavior continues. The hook observes the login path without changing authentication behavior.

text
  before hook                         after hook (observe-only)
  ┌──────────────────────┐            ┌──────────────────────────┐
  │ onLogin()            │            │ onLogin()                │
  │  └─ Log.d(user/pass) │            │  └─ Frida prints args    │
  │  └─ verify(...)      │            │  └─ original Log.d runs  │
  └──────────────────────┘            │  └─ verify(...) unchanged│
                                      └──────────────────────────┘

Inspect runtime values and modify controlled UI text

Extend the same script with a filtered TextView.setText hook. AboutUs sets a fixed marketing string when you tap About InsecureShop. Intercepting that string shows controlled modification without touching auth.

Add this block inside Java.perform before the “hooks installed” log line:

javascript
var TextView = Java.use('android.widget.TextView');
  var setText = TextView.setText.overload('java.lang.CharSequence');
  setText.implementation = function (text) {
    var s = (text === null) ? null : text.toString();
    if (s !== null && s.indexOf('intentionally designed vulnerable') !== -1) {
      console.log('[*] AboutUs setText original=' + JSON.stringify(s));
      var replacement = Java.use('java.lang.String').$new(
        'Frida lab note: AboutUs string intercepted at runtime'
      );
      console.log('[*] AboutUs setText modified =' + JSON.stringify(replacement.toString()));
      return setText.call(this, replacement);
    }
    return setText.call(this, text);
  };
Output

Re-attach with the updated script:

bash
frida -U -N com.insecureshop -l "${HOME}/frida-lab/hook-demo.js" -q -t 40

In a second terminal, start the exported About activity with adb shell am start -n com.insecureshop/.AboutUsActivity, then tap About InsecureShop so setText runs. The Frida session printed the original string and the replacement:

output
[*] Demo hooks installed
[*] AboutUs setText original="InsecureShop is an intentionally designed vulnerable android app built in Kotlin."
[*] AboutUs setText modified ="Frida lab note: AboutUs string intercepted at runtime"

The on-screen label showed the Frida replacement string. That is enough to prove you can observe and change values at runtime without touching SSL or auth bypass tooling.


Relate static findings to runtime evidence

Static reports already flag InsecureShop logging and exported activities. On this APK, decompiled LoginActivity calls Log.d("userName", …) and Log.d("password", …). Frida just printed those values live.

Manifest evidence for the About screen:

text
android:name="com.insecureshop.AboutUsActivity"
android:exported="true"

MobSF surfaces the exported activity during static triage. In the APKHunt test, manual review of APKHunt's decompiled manifest confirmed the same export even though APKHunt's component summary missed it.

Runtime confirmation here was:

  1. Frida saw the AboutUs setText call when the UI path ran.
  2. ADB can start AboutUsActivity without walking the normal menu: adb shell am start -n com.insecureshop/.AboutUsActivity.

Treat static output as leads. Use Frida when you need arguments, return values, or proof the code path actually runs.


Frida Gadget versus frida-server

Approach When it fits Trade-off
frida-server on a rooted emulator/device Lab intros, attach to many apps Needs root (or equivalent) on the guest
Frida Gadget embedded in a repackaged APK Non-rooted targets, single-app focus Rebuild/sign the APK; load config per app

Official Frida Android docs still steer beginners to the rooted/frida-server path. This article stays on that path. Gadget is the usual next step when root is off the table and the engagement allows repackaging.


Next step: Android IPC with Drozer

Frida answers what a running method sees and returns. When the question is exported activities, services, receivers, and content providers, use Drozer: it focuses on Android IPC and component attack surface rather than general method hooking.

For console and Agent install, port forwarding, attacksurface, and starting exported components, follow the Drozer Android pentesting tutorial. Keep this page on Frida runtime hooks; use ADB am start only for one-off known activities when you are not running Drozer.


Troubleshooting

Symptom Likely cause Fix
Failed to spawn / empty frida-ps -U frida-server not running or wrong ABI Map ro.product.cpu.abi with the table above; push the matching asset; start as root
Version mismatch / protocol errors Host frida --version ≠ on-device frida-server Pin and reinstall the matching release pair; ignore that frida-tools has a different package version
frida-ps: device not found ADB session missing or wrong serial adb devices; set ANDROID_SERIAL; adb connect host:5555 for Genymotion TCP
Permission denied starting frida-server Not root on guest adb root on eng/userdebug, or su -c when production builds refuse adb root
Process exits as soon as you attach App anti-tamper or crash in early init Prefer frida -U -f package …; keep hooks minimal; test on the training APK first
Architecture mismatch on push Wrong frida-server asset Genymotion x86 needs android-x86, not arm64
App spawns too quickly to attach Short-lived process Use -f so hooks load before resume
Emulator quirks / SELinux denials Guest policy blocks the binary Confirm frida-server runs as root; read adb logcat / audit messages; try another supported rooted image if policy blocks injection

Summary

This tutorial uses Frida core 17.17.0 with a matching frida-server, connects through ADB to a rooted Genymotion device, and attaches to InsecureShop by package identifier. Observe-only hooks on Log.d and LoginActivity.onLogin show arguments without changing authentication. A filtered TextView.setText hook then proves you can modify a controlled UI string at runtime.

Match frida --version to the on-device server, and map ro.product.cpu.abi to the correct Frida asset name before you push. Keep frida-tools pin separate from that core version. When you need IPC and exported-component enumeration rather than method hooks, switch to Drozer instead of stretching this page across two search intents.


References


Frequently Asked Questions

Why does Frida fail with a version mismatch?

The Frida core version from frida --version must match the frida-server binary on Android. The frida-tools package version can differ. Pin both Python packages for a reproducible lab, read frida --version, then download that exact frida-server-*-android-<abi> asset.

Do I need a rooted emulator for this tutorial?

For the frida-server path used here, yes. Official Frida Android docs recommend a rooted device or emulator for the simplest introduction. Non-rooted work usually means embedding Frida Gadget in a repackaged APK, which this article only summarizes.

What is a safe first Frida hook on InsecureShop?

Hook android.util.Log.d for the userName and password tags, or wrap LoginActivity.onLogin, save the overload, and call the original with .call(this, …). Avoid SSL pinning bypass scripts until you can attach, enumerate, and read simple method traffic reliably.

Should I attach with -n or -N?

Prefer -N (application identifier) with com.insecureshop. Use -n only when you intentionally match the process display name. Use -f when you need to spawn the package under Frida from the start.

Which frida-server file do I download for arm64-v8a?

Use the android-arm64 asset. Map arm64-v8a to android-arm64, armeabi-v7a to android-arm, x86 to android-x86, and x86_64 to android-x86_64.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Kennedy Muthii

Information Security Analyst

Accomplished professional proficient in Python, ethical hacking, Linux, cybersecurity, and OSINT. With a track record including winning a national cybersecurity contest, launching a startup in Kenya, and holding a degree in information science, he is currently engaged in cutting-edge research in ethical hacking.

  • Python (programming language)
  • Certified Ethical Hacker
  • White Hat (Computer Security)
  • Linux
  • Penetration Testing