Workers IO workers.io
← blog

april 8, 2026 · security

Giving an Agent a Rooted Android Phone

So what actually happens if you hand an AI agent root access to an Android phone, plus a runtime hooking framework? In this case, it went straight to reverse-engineering Subway Surfers and figured out how to rack up unlimited coins.

by Shadan · engineer, workers io


I hooked up Claude Code to a rooted Android phone. With Android Debug Bridge (ADB), I could send input; UI Automator let me see what was on the screen; mitmproxy captured the network traffic; and Frida gave me the ability to mess with the app as it ran. With all of that in place, the agent could actually run a full mobile app pentest on its own.

Manual mobile security testing is slow. You open the app, poke around, flip over to Burp Suite to watch the traffic, and bounce between tools. Sometimes you fire up Frida to get around SSL pinning, or unpack the APK to hunt for secrets. Every time you switch tools or context, it adds up fast.

Agents are great at writing code, searching the web, and running shell commands. But they can’t see the device’s screen, tap buttons, or monitor the HTTPS traffic generated by those actions. So even if an agent knows what an API bug looks like, it can’t actually find one in a real app on its own. That gap between knowing and doing is where most automation falls short.

Give the agent a rooted phone.

The fix is pretty straightforward: give the agent a rooted Android device, connect it over ADB, and hand it the tools it needs.

  • Screen observation: Extract the UI hierarchy using UI Automator and parse it into a sequential list of interactive elements.
  • Input simulation: Transmit tap and text input events through ADB.
  • Network monitoring: Intercept HTTPS requests and responses using mitmproxy.
  • Runtime instrumentation: Utilize Frida to bypass SSL pinning, trace method calls, or modify application behavior during execution.
  • Security analysis: Perform checks on captured traffic to identify issues such as Insecure Direct Object References (IDORs), authentication weaknesses, and data exposure.

Once everything’s set up, the agent just loops: it looks at the screen, picks an action, does it, checks the network traffic, and figures out what to do next. This cycle keeps going until it’s covered what you want.

The Loop
OBSERVEdump UIACTadb inputINTERCEPTcapture HTTPSDECIDEnext actionrepeat

Root access is the real unlock here. Without it, you can’t install a trusted CA cert for traffic interception, run the Frida server, dig into other apps’ local storage, or get around their security. Root is what turns passive observation into actual, hands-on app testing.

Setting up the device

I usually use an Android emulator rooted with Magisk for this. You can do it on real hardware too, but emulators are just so much easier to spin up and throw away when you’re done.

rootAVD

rootAVD

patches the emulator’s ramdisk to add Magisk, and it works out of the box with standard Android Studio images.

# Clone rootAVD
git clone https://gitlab.com/newbit/rootAVD.git
cd rootAVD

# List available AVD images
./rootAVD.sh ListAllAVDs

# Patch the ramdisk for your target image
./rootAVD.sh system-images/android-34/google_apis_playstore/x86_64/ramdisk.img

After patching, start the emulator. Magisk should show as installed. You can verify:

adb shell su -c id
# uid=0(root) gid=0(root)

Trusting your proxy certificate

Normally, Android apps ignore user-installed certs. On a rooted device, you can fix that with AlwaysTrustUserCerts, a Magisk module that copies your certs into the system trust store every time the device boots. If the app does its own SSL pinning, you’ll still need Frida to get around it.

# Install mitmproxy's CA cert on the device
adb push ~/.mitmproxy/mitmproxy-ca-cert.cer /sdcard/cert.cer
# Install it as a user cert through Settings → Security → Install from storage

# Then install the Magisk module
# Download AlwaysTrustUserCerts.zip → push to device → install via Magisk → reboot

What the agent has access to

The agent doesn’t just call these tools one by one. Instead, it uses a skill that ties everything together into a single workflow. Here’s what’s actually happening under the hood.

Screen reading — ui.py

UI Automator dumps the screen as XML, but most of that is just non-interactive containers. The ui.py script filters it down to just the clickable elements, deduplicates by position, and gives each one an ID.

[1] "Sign In" btn @ (540,1200) bounds=[380,1150][700,1250] clickable
[2] "Email" input @ (540,400) bounds=[100,350][980,450] focusable
[3] "Password" input @ (540,600) bounds=[100,550][980,650] focusable
[4] "Forgot password?" link @ (540,750) bounds=[350,720][730,780] clickable

The agent takes that filtered list, picks something, and sends a tap or text input. It does one action per cycle, then checks the screen again.

Traffic interception — capture.py

mitmproxy logs every HTTP flow to a JSONL file. Each line is a request or response, with headers, a body preview, and a flow ID to tie it all together.

# Set the device proxy to the host
adb shell settings put global http_proxy 10.0.2.2:8080

# Start mitmproxy with the capture addon
ANDROID_APP_TESTER_OUT_DIR="$SESSION_DIR" \
ANDROID_APP_TESTER_PACKAGE="com.kiloo.subwaysurf" \
ANDROID_APP_TESTER_PRESERVE_AUTH=1 \
mitmdump --set block_global=false --listen-host 0.0.0.0 --listen-port 8080 \
  -s capture.py

Set PRESERVE_AUTH=1 to keep auth headers in the logs. That’s important, since those tokens are what you need to analyze during a pentest.

Traffic summarization — traffic.py

After each action, the agent needs to see what happened on the network. The script turns the JSONL file into a readable summary, filtered by time, so you can see what changed after each step.

python3 traffic.py \
  --input "$SESSION_DIR/traffic.jsonl" \
  --since-seconds 15 \
  --show-headers \
  --show-body

The agent looks at the last 15 seconds of network traffic — outgoing requests, responses, and headers. That feedback tells it what to do next, just like a human tester would.

Security analysis — analyze.py

Once the agent has gone through the main workflows and grabbed enough network data, it kicks off the analyzer script.

python3 analyze.py \
  --input "$SESSION_DIR/traffic.jsonl" \
  --mode full

This checks for:

  • IDORs — sequential IDs, UUIDs in paths, numeric query params that might belong to other users
  • Auth issues — missing tokens, JWT weaknesses, endpoints that work without auth
  • Data exposure — PII in responses, over-fetching, leaked keys.
  • Header security — missing HSTS, CSP, and CORS misconfiguration.

The analyzer can run in different modes — endpoints, idor, auth, exposure, or headers — so the agent can zero in on specific categories during analysis.

SSL pinning bypass — bypass.js

Some apps don’t trust the system cert store at all. They bundle their own certs or use OkHttp’s CertificatePinner. For those, we run Frida:

# Push frida-server to the device
adb push frida-server /data/local/tmp/
adb shell "su -c 'chmod 755 /data/local/tmp/frida-server'"
adb shell "su -c '/data/local/tmp/frida-server -D'" &

# Spawn the app with the SSL bypass
frida -U -f com.kiloo.subwaysurf -l bypass.js

The bypass script hooks into eight different SSL verification methods — TrustManagerImpl, OkHttp3’s CertificatePinner, SSLContext.init, WebViewClient, Conscrypt, and a few more. This covers most apps without needing extra tweaks.

SSL bypass is just the starting point. Frida can hook any method in the app: purchase verification, coin management, score tracking, and authentication checks. If a method exists in the binary, the agent can intercept it, read its arguments, and modify its return value.

How the agent actually tests

Everything above is set up. Here’s how the agent actually runs the test.

1. Observe

The agent runs ui.py to get a list of what’s on screen. But Subway Surfers is a Unity game, so it renders its own UI and the native accessibility tree just shows a single Game view element. The agent adapts by taking a screenshot and using vision to understand the game state instead.

2. Act

The agent taps to start a run and swipes to move the character into coin lanes. It can play the game: tap, swipe left, swipe right, jump, all using ADB input commands, one action per cycle.

3. Intercept

After a run, the agent checks for network traffic. Nothing interesting shows up, since the gameplay is entirely local. Coin pickups, score tracking, and collision detection all happen inside the IL2CPP binary. There’s no API to intercept, so the standard network-based approach doesn’t work here. The agent needs a different strategy.

4. Decide

The agent recognizes Subway Surfers is a Unity IL2CPP game. Instead of looking at HTTP traffic, it pivots to binary analysis: pull the APK, extract the IL2CPP metadata, dump class hierarchies with Frida, and hook the coin management methods directly. The network proxy becomes irrelevant, and Frida becomes the primary tool.

5. Repeat

Then it goes back to the start: observe, act, intercept, decide. This loop continues until the test is complete.

The path changes every time, since the agent adapts to what it sees instead of following a fixed script. In this case, it abandoned network interception entirely and switched to runtime hooking, a decision it made on its own.

The reverse-agent

Before performing any dynamic actions, a sub-agent pulls the APK from the device and analyzes its structure. For Subway Surfers, this means dealing with Unity’s IL2CPP compilation.

# Pull the APK (209MB base + 32MB arm64 split)
adb shell pm path com.kiloo.subwaysurf
# package:/data/app/~~PiSX0Fqu.../com.kiloo.subwaysurf-.../base.apk
# package:/data/app/~~PiSX0Fqu.../com.kiloo.subwaysurf-.../split_config.arm64_v8a.apk

adb pull /data/app/.../base.apk "$SESSION_DIR/"
adb pull /data/app/.../split_config.arm64_v8a.apk "$SESSION_DIR/"

The APK contains global-metadata.dat (15MB) — IL2CPP’s metadata file with every class name, method name, and field offset in plaintext — and libil2cpp.so (79MB), the compiled game binary. The reverse agent extracts both:

unzip -o base.apk "assets/bin/Data/Managed/Metadata/global-metadata.dat" -d extracted/
unzip -o split_arm64.apk "lib/arm64-v8a/libil2cpp.so" -d extracted/

Then it parses the metadata strings, searching for coin-related symbols. This is what it finds:

class WalletModel {  // SYBO.Subway.Core.ProfileData
    Dictionary<CurrencyType, ExpirableValue<SafeInt>> Currencies; // @ 0x38
    void AddCurrency(/* 4 params */);
    void SetCurrency(/* 3 params */);
    void SetCurrencySilently(/* 3 params */);
}

class RunSessionData {  // SYBO.Subway
    RunVariableGroup Coins; // @ 0x58
    void AddCoins(/* 1 param */);
}

class SafeInt {  // SYBO.Subway
    int _offset; // @ 0x10 (random XOR key)
    int _value;  // @ 0x14 (obfuscated: actual = _value - _offset)
}

At this point, the agent has the full wallet architecture. WalletModel manages persistent currencies using a SafeInt anti-cheat wrapper, RunSessionData.AddCoins handles in-run coin pickups, and there’s a CurrencyType enum (Coins=1, Keys=2, Hoverboards=3, ...). These findings tell the dynamic agent exactly where to hook.

With root, the agent can also check local storage directly. SharedPreferences, SQLite databases, and cache files are all accessible. For a Unity game, though, the interesting data lives in the IL2CPP binary itself rather than on disk.

When the session is done, the agent shuts down the proxy and stops frida-server. Since the proxy config persists after a reboot, you have to clean it up properly, or the device might lose internet access next time you use it.

kill "$(cat "$SESSION_DIR/mitmdump.pid")" 2>/dev/null || true
adb shell settings delete global http_proxy
adb shell "su -c 'pkill frida-server'" 2>/dev/null || true

Watching it work

I gave the agent a package name: com.kiloo.subwaysurf. It pulled the APK, identified the Unity IL2CPP architecture, extracted the metadata, and dumped the full class hierarchy via Frida. When it found RunSessionData.AddCoins and WalletModel.AddCurrency added hooks that multiply every coin pickup by 100x at two points in the pipeline — once at collection, once at wallet save.

The agent spawned the game with Frida attached and started a run. Here’s what the console looked like:

[*] RunSessionData.AddCoins hooked at 0x71bd893a20
[*] WalletModel.AddCurrency hooked at 0x71bd8f1c40
[*] WalletOnRunModel.SetCoins hooked at 0x71bd893b60
[*] AddCoins called: 1 → 100
[*] AddCoins called: 1 → 100
[*] AddCoins called: 1 → 100
[*] AddCoins called: 1 → 100
[*] GiveCurrencyRewardsFromRun: 400 coins
[*] AddCurrency: 400 → 40,000

Four coins were picked up during a short run. Each one is multiplied by 100 at pickup. Then the end-of-run reward (400 coins) was multiplied again at the wallet level — 400 became 40,000. Starting balance: 10,000. After one run: 50,000. After two runs: 2,050,000 coins.

The hooks themselves are minimal:

// Hook RunSessionData.AddCoins — multiply at pickup
Interceptor.attach(addCoinsAddr, {
  onEnter: function (args) {
    var original = args[1].toInt32();
    args[1] = ptr(original * 100);
  },
});

// Hook WalletModel.AddCurrency — multiply at wallet write
Interceptor.attach(addCurrencyAddr, {
  onEnter: function (args) {
    var amount = args[2].toInt32();
    args[2] = ptr(amount * 100);
  },
});

This is a fun experiment to demonstrate the agent’s ability to interact with and reverse-engineer real games — not a serious security finding. Subway Surfers is a single-player game, and coin manipulation only affects your own device. But the same techniques the agent used here — IL2CPP reversing, Frida hooking, bypassing client-side validation — apply equally to apps where the stakes are real: fintech, e-commerce, and authentication flows.

What does this get you?

Manual interaction is almost always the main bottleneck in mobile pentesting. The hard part isn’t the analysis—it’s the endless cycle of opening the app, clicking around, switching to the proxy, checking requests, and doing it all over again. I’ve spent more time than I’d like just repeating those steps.

This setup eliminates that manual bottleneck. The agent automates user actions, monitors its own network traffic, and decides what to do next. With root, it sees everything an engineer would—intercepted HTTPS requests, runtime hooks, and decompiled code. The skill ties it all together so the agent can run the whole test without you having to guide it step by step.

In the Subway Surfers test, the agent went from “here’s an APK” to modifying coin values in a single session. It identified the game engine, extracted the IL2CPP metadata, mapped the wallet architecture, and wrote Frida hooks to change the coin count—all on its own.

To try it yourself, install the skill:

npx skills add workersio/spec

more posts