Server Driven Executioner as RASP

Sun, Apr 5, 2026 11-minute read

Server Driven Executioner as RASP

This architecture is inspired by Server-Driven UI (SDUI) — the pattern where the server tells the app what to render, and the app is just a renderer. We apply the same idea to security: the server tells the SDK what to check, the SDK just checks. No logic on device. No decisions on device.


Your RASP SDK ships inside an APK. An APK is a zip file. That’s the entire problem.

Every detection check, every condition, every “if rooted then block” — it’s all sitting in a package that anyone can unzip, decompile, and read. The question isn’t whether an attacker can reverse-engineer your detection logic. It’s how long it takes them. And that number is shrinking fast.

The Problem: LLMs Made Reverse Engineering Trivial

A year ago, bypassing a RASP SDK was a skilled craft. You needed to understand smali, navigate obfuscated call graphs, and manually trace detection flows through layers of indirection. It took days, sometimes weeks, for a competent reverse engineer.

That world is gone.

Today, an attacker pulls your APK, runs it through jadx, and feeds the decompiled source to an LLM. The LLM identifies your root detection class, maps the control flow, explains what each check does in plain English, and — here’s the kicker — generates a Frida script to bypass every check. Not in days. In minutes.

# This is not hypothetical. This is a Tuesday afternoon.
jadx -d output/ target.apk
# Feed SecurityCheck.java to any capable LLM
# "Analyze this class and generate Frida hooks to bypass all detection checks"
# LLM outputs a complete bypass script with explanations

ProGuard and R8 rename classes and strip metadata. That slows things down — from minutes to maybe an hour. The LLM doesn’t care that your root detection class is now called a.b.c. It reads the logic, sees Build.TAGS, sees /system/bin/su, sees getInstalledPackages with a check for Magisk — and it knows exactly what’s happening.

Ghidra handles the native layer. Feed the .so into an LLM with “find the anti-tamper checks,” and you get annotated pseudo-code with bypass suggestions.

The core issue is architectural: static detection logic burned into a distributable binary is a solved problem for attackers. It doesn’t matter how clever your obfuscation is. The logic is there. It can be extracted. And now AI makes that extraction nearly automatic.

Every RASP update follows the same cycle: ship → decompile → bypass → repeat. You’re playing defense on the attacker’s home turf.

The Flip: Make the SDK a Dumb Executioner

What if the SDK didn’t contain any detection logic at all?

Not less logic. Not obfuscated logic. No logic.

Here’s the architectural shift: strip the SDK down to two things:

  1. A static registry of callable Android APIs — the signal collectors. These are burned into the app. Build.TAGS, PackageManager.getInstalledPackages, file existence checks, /proc/mounts readers, native library inspectors. Just a flat list of things the SDK can do.

  2. A generic executioner — a lightweight engine that receives encrypted instructions from the server, decrypts them, executes the specified API calls, collects the raw results, and sends them back to the server. That’s it. No evaluation. No decision.

The SDK doesn’t know what “rooted” means. It doesn’t know that finding com.topjohnwu.magisk in the package list is bad. It doesn’t know that test-keys in Build.TAGS is suspicious. It has no conditions, no scoring, no “if X then Y.” It collects signals and ships them upstream. The server decides what to do.

All of the intelligence — the scoring, the risk evaluation, the response actions — lives on the server. The device is a sensor. The server is the brain.

Here’s the full architecture at a glance:

Server-Driven Executioner Architecture

Just like SDUI — the server sends instructions down, the device executes them and sends raw results back. The device never scores, never evaluates, never decides. All of that happens server-side.

The ML Gatekeeper: On-Device Guardrail

Before any execution policy reaches the executioner, it passes through a small on-device ML model — the gatekeeper.

This is a lightweight TFLite classifier trained server-side on thousands of known-good policy shapes. It ships with app updates. Its job: decide whether an incoming policy looks legitimate before the executioner touches it.

What the gatekeeper checks:

  • Structural validity — does the task list have the expected shape? Executioner count within bounds? Known API types only?
  • Parameter sanity — are file paths within expected directories? Are package names plausible Android identifiers? Are filter lists within trained ranges?
  • Distribution drift — does this task list look statistically similar to the ones this device has seen before? A sudden radical shift in composition gets held for re-attestation.

The gatekeeper outputs a confidence value. Below threshold? The task list gets rejected and the device falls back to its cached baseline. The rejection event gets reported to the server — so the server knows something is off.

Here’s why this matters: even if an attacker compromises the policy delivery channel (MITM, DNS hijack, certificate pinning bypass), they can’t push arbitrary execution policies to the device. The gatekeeper hasn’t seen policies shaped like that during training. It rejects them.

And here’s the subtle part: the gatekeeper is a neural network. An attacker can extract and inspect it, but understanding what a trained classifier considers “normal” is fundamentally harder than reading an if statement. It’s not a set of conditions to NOP — it’s a weight matrix.

What the Reverse Engineer Sees

An attacker decompiles your APK and finds… a bunch of Android API calls. Jumbled. No ordering. No context.

They see that the SDK can read Build.TAGS. Great. They see it can check for files in /system/bin/. Okay. They see it can query PackageManager. Sure.

But they have no idea:

  • Which of these APIs are currently being called
  • What the server does with the results
  • What combinations the server considers suspicious
  • What action the server takes in response
  • Whether some of these are honeypots designed to detect tampering

The execution policy that ties it all together? It’s not in the APK. It never was.

Server-Sent Execution Policy

Here’s what the server actually sends to the device at runtime — an encrypted task list. Each executioner gets an Android API to call and an optional filter. That’s all the device sees. No scoring. No thresholds. No “what to do if.”

{
  "policy_id": "v4.12",
  "executioners": [
    { "id": 1, "api": "file_exists",            "filter": ["specific system paths"]          },
    { "id": 2, "api": "system_property_read",    "filter": ["build metadata field"]           },
    { "id": 3, "api": "package_manager_scan",    "filter": ["known tampering tool packages"]  },
    { "id": 4, "api": "loaded_native_libraries", "filter": ["known hooking framework libs"]   }
  ],
  "ttl_seconds": 900
}

That’s the entire payload. Four executioners, each calling one Android API with a filter. The device runs them, collects raw results, and sends everything back to the server. The server does the scoring, the evaluation, and decides whether to allow, challenge, block, or force-logout. None of that logic ever touches the device.

And the 15-minute TTL means the device fetches a fresh task list regularly. Which might be completely different next time.

Why Not Just Dump Everything to the Server?

An obvious alternative: skip the executioner model entirely, collect every available signal from the device, and ship it all to the server. Let the server figure it out.

This doesn’t work at scale.

An Android device can expose hundreds of system properties, thousands of installed packages, dozens of running processes, mounted filesystems, loaded native libraries, accessibility services, developer settings — the list goes on. Collecting all of that, serializing it, encrypting it, and sending it upstream on every check would be expensive in bandwidth, battery, and server compute. Multiply that across millions of devices hitting your backend, and you’ve built yourself a very costly data pipeline that’s mostly noise.

The server-driven executioner model solves this by being surgical. The server sends a targeted task list — “check these 4 specific things” — and the device executes only those checks. The executioner filters results locally against the provided filters and sends back just the relevant, concise data points the server asked for.

The result: the device does minimal work (4 API calls instead of an exhaustive scan), the payload going upstream is tiny (a few fields, not a device dump), and the server receives exactly what it needs to make a decision — nothing more. Both sides stay fast. Both sides stay cheap.

The On-Device Executioner

Here’s a simplified Kotlin snapshot of the execution engine. This is the part that lives in the APK — and it’s intentionally generic:

// Simplified for illustration

class PolicyExecutioner(
    private val decryptor: PolicyDecryptor,
    private val gatekeeper: MLGatekeeper,       // TFLite — validates task list shape
    private val signalRegistry: SignalRegistry,  // maps API names → executors
    private val telemetry: TelemetryClient
) {
    suspend fun execute(encrypted: ByteArray): ExecutionResult {
        val taskList = decryptor.decrypt(encrypted) ?: return ExecutionResult.Failed

        // ML gatekeeper validates the task list before anything runs
        if (!gatekeeper.validate(taskList)) {
            telemetry.reportRejection(taskList.policyId)
            return ExecutionResult.Rejected
        }

        // Run each executioner, collect raw results — no evaluation
        val results = taskList.executioners.mapNotNull { exec ->
            signalRegistry.getExecutor(exec.api)
                ?.let { exec.id to it.execute(exec.filter) }
        }.toMap()

        // Send raw results to server — server scores and decides
        telemetry.report(results, taskList.policyId)

        return ExecutionResult.Reported
    }
}

Look at what this code does: decrypt, validate, execute, report. That’s it. No scoring. No conditions. No “if compromised then block.” The device collects raw API results and ships them to the server. The server does everything else. A reverse engineer reading this class learns nothing about what the app considers dangerous or what happens when it finds something.

The Telemetry Loop

After execution, the device sends back raw results. No interpretation, no scoring — just what each executioner found:

{
  "policy_id": "v4.12",
  "results": {
    "executioner_1": { "found": false, "latency_ms": 3 },
    "executioner_2": { "value": "release-keys", "latency_ms": 1 },
    "executioner_3": { "matches": 0, "latency_ms": 45 },
    "executioner_4": { "matches": 0, "latency_ms": 8 }
  },
  "gatekeeper_pass": true
}

The server receives this and does everything else — scoring, evaluation, decision. Block the next transaction? Force a logout? Allow silently? That logic lives entirely server-side, invisible to the device.

And when the server aggregates these raw results across millions of devices, patterns emerge:

  • A new root tool appears that doesn’t install to any known path? Unexpected null returns across a cluster of devices reveal it before a single signature is written.
  • A Frida-based hooking framework starts intercepting getInstalledPackages and returning empty lists? The server notices that a subset of devices suddenly report zero matches — statistically impossible — and pushes a new task list with an additional executioner within hours.
  • Certain executioners consistently return clean on compromised devices? They get deprioritized or replaced — without shipping an app update.

Raw results flow up, refined task lists flow down. The system learns continuously.

Why This Breaks the Attacker’s Playbook

No static target. The task list changes without an app update. An attacker who builds a bypass today might find it doesn’t work tomorrow — because the server pushed new executioners or changed the filters they weren’t hooking.

Invisible decision logic. The executioner is visible in the APK. But the scoring, the evaluation, and the response actions? Those live on the server. An attacker can see what the SDK is capable of checking, but not which checks are active, what the server does with the results, or what triggers a block. They’re fighting blind.

Hours, not months. A new KernelSU variant drops. Traditional RASP: update detection logic, test, release, wait for app store review, wait for users to update. Server-driven: push a new execution policy that checks for the new variant’s artifacts. Active across all devices in minutes.

Honeypot signals. Include detection checks in the execution policy that only trigger if someone is actively probing the SDK. A legitimate device will never hit them. An attacker running Frida and systematically hooking every API call will. Now you’re not just detecting compromise — you’re detecting the reverse engineer.

RASP as a Living System

The traditional RASP model — embed detection logic in the app, ship it, hope it holds — made sense when reverse engineering was a specialized skill that took real time and effort. That era is over. LLMs have democratized binary analysis. The tools are better, faster, and available to everyone.

Server-driven execution transforms RASP from “ship and pray” into a continuously adaptive system. The SDK is the muscle — a dumb, reliable executioner that faithfully runs whatever the server tells it to. The server is the brain — observing, learning, adapting.

And once you have this architecture, the next step is obvious: feed those aggregated raw results into ML models. Let the system automatically identify anomalous patterns that no human analyst wrote a condition for. Let it generate and A/B test task lists across device cohorts. Let it evolve.

The foundation isn’t a smarter APK. It’s a dumber one — with a very smart server behind it.


Key Takeaways

  • Inspired by SDUI. The same pattern that powers server-driven UIs — server sends instructions, device just renders — applied to security.
  • The device is a sensor, not a brain. The SDK collects raw signal results and sends them upstream. All scoring, evaluation, and decision-making happens server-side.
  • An on-device ML gatekeeper validates every incoming task list before execution — stopping tampered or rogue payloads even if the delivery channel is compromised.
  • Raw telemetry creates a feedback loop. Signal data from millions of devices lets the server detect new threats, identify bypasses, and push updated task lists — without an app release.
  • No scoring or decision logic ever touches the APK. Attackers can decompile the executioner, but they’ll never find the logic that determines what’s suspicious or what action to take.