---
title: "Detecting Frida Stalker"
description: "Frida's Stalker spills its scratch registers after the red zone. This blog post shows how this mechanism can be used to detect if a function is stalked."
canonical_url: "https://www.romainthomas.fr/post/26-10-frida-stalker-detection/"
authors: ["Romain Thomas"]
date_published: "2026-10-31T00:00:00Z"
date_modified: "2026-10-31T18:19:26+01:00"
language: "en-US"
section: "post"
tags: ["android","frida","DBI","ARM64"]
categories: ["android","obfuscation"]
---

# Detecting Frida Stalker

> Frida's Stalker spills its scratch registers after the red zone. This blog post shows how this mechanism can be used to detect if a function is stalked.

## Introduction

Techniques to detect that a function is hooked by Frida (or by any other hook
engine) are well documented, and asking an AI about them provides meaningful and
relevant answers. Sometimes, a hook is not enough to understand the purpose of a
function and we need to take a closer look at what it executes. This is where
Frida's Stalker comes into play: it traces the code at the instruction level by
running an instrumented copy of it, while the original instructions stay untouched.

Detecting the Stalker is more challenging, and a well-designed DBI resists most
detections. This blog post shows a detection that a function can run on itself to
find out whether it is being stalked by Frida.

## The Detection

Any DBI needs scratch registers for its rewriting rules.
[QBDI](https://github.com/QBDI/QBDI) picks a register that the instrumented code does not use.
For instance, the rule that rewrites an arm64 `RET`
instruction is:

```text
# RET rewriting by QBDI
temp          := LR
DataBlock[PC] := temp
```

which could be lowered into:

```text
mov x11, lr
str x11, [x27, #600]
```

> **Note**
<div class="legacy-left">
Assuming that the DataBlock is addressed by <code>x27</code>
and that <code>x11</code> is free at the instrumentation point.
</div>


Here, QBDI pays the cost of finding a register that the instruction does not use
(`x11`) to store the `temp` variable, and it saves this register in its `DataBlock`.

QBDI could have used another strategy: saving and restoring registers on the
stack. Thanks to the early designers and authors of QBDI (Charles Hubain and
Cédric Tessier), using the stack for the needs of the DBI was
[not an option](https://qbdi.readthedocs.io/en/stable/execblock.html).
This choice keeps QBDI stealthy under strong side-effect and integrity checks.

On the other hand, Frida's Stalker uses the stack for the _same_ use case as the
`temp` allocation in the previous snippet. Instead of finding a free register
within the scope of the instrumented code, it uses predefined registers (e.g. `x16`
and `x17`) that are saved to and restored from the stack.

Assuming that the **non-instrumented** code uses the following stack layout:

![Native stack layout: the caller frame, the current frame between fp and sp with local variables and data, the 128-byte red zone from sp down to sp - 0x80, and unallocated memory below](./stack-layout-1.webp)

> **Note**
<div class="legacy-left">
The red zone is an area below the stack pointer that a function can use
without allocating a stack frame (e.g. 128 bytes on Apple arm64). On Windows ARM64,
its 16 bytes are (officially) reserved for analysis and dynamic patching.
Android does not define a red zone, but Frida skips 128 bytes anyway.
</div>


When the Stalker is running, every rewritten jump or call (e.g. `br`, `blr`) goes
to a stub that saves some predefined registers. The stack looks like this:

![Stack layout under the Stalker: the same frames and 128-byte red zone, then, right below sp - 0x80, the saved x16 and x17 followed by a GumCpuContext with the general-purpose, vector, and flag registers, left behind as Stalker artifacts](./stack-layout-2.webp)

The Stalker spills `x16` and `x17` (`x15` and `x17` for a syscall) right below
the red zone. When it switches to its own code, for instance to compile a new
block, it also saves some or all registers as a `GumCpuContext`.
The important point is that when Frida restores these registers,
it does not erase their copies.

On Android, compiled code never writes below the stack pointer, so these slots
only change under the Stalker. A function can therefore detect the Stalker in
three steps:

1. write a canary into `[sp - 0x90]` and `[sp - 0x88]`
2. execute an instruction that triggers Stalker rewrites (like an indirect branch)
3. compare the slots with the backup registers

| Rewritten instruction | `[sp - 0x90]` | `[sp - 0x88]` |
|-----------------------|---------------|---------------|
| `br`, `blr`, `ret`    | `x16`         | `x17`         |
| `svc`                 | `x15`         | `x17`         |

The code of this detection is available in this GitHub repo:  [romainthomas/frida-stalker-detection](https://github.com/romainthomas/frida-stalker-detection)

## \</last word\>

If you want to go further with the Stalker detection, you can check the
[SingPass RASP Analysis](/post/22-08-singpass-rasp-analysis#code-tracing)
blog post, which exposes another detection that I found while reverse engineering iXGuard.

Note that the detection explained in this blog post is rooted in some work done
on [DynaMIR](/project/dynamir)[^dynamir].

### Last Minute Update

I thought about this detection a while ago (i.e. at the beginning of this year).
In July, while I was playing with AI, I asked Opus 5 how it would detect the
Stalker, and my detection was not part of its answer :smirk:.

Before publishing this blog post[^cvp], and for the sake of completeness and
intellectual integrity, I asked Opus 5.5 the same question. The first two
detections of its answer are literally the stack-based detection that I describe.

This is just the reality of the pace at which AI models progress.

You can check the answers from OpenAI Daybreak[^astra] and Opus 5.5 here:

- [OpenAI Daybreak](./codex-daybreak-frida-stalker-detection.txt)
- [Claude Opus 5.5](./claude-opus-5.5-frida-stalker-detection.txt)

\Romain

[^astra]: The answer from Astra is not worth it.
[^dynamir]: More details to come soon™
[^cvp]: My personal account has also been CVP-approved, so I no longer face the
        safeguards for this kind of detection-based question.
