DEV Community

Jack Sparrow
Jack Sparrow

Posted on

You Thought Frida Was Stealth When Not Attached? Dirty Pages Beg to Differ

All the classic Frida checks in 2025 — maps, thread names, CRC, ports — are dead.

Today I’m dropping the one that still kills 99 % of setups: Dirty-Page Detection.

Key point: As long as frida-server is running on the device — even if it never attaches to your app — you can detect it globally.

Why Dirty-Page Detection Is Almost Unbypassable

To listen for zygote forks and signals, frida-server inline-hooks three critical modules inside system_server:

  • libc.so
  • libselinux.so
  • libandroid_runtime.so

Inline-hooking the read-only .text section triggers Copy-On-Write.

The original file-backed pages become private dirty pages.

Even if Frida later restores the original bytes in the on_leave callback, the pages stay private forever.

That’s the permanent fingerprint we hunt.

Method 1 – smaps file detection

Sample code is used to demonstrate the thought process.

import re

def frida_detected_smaps():
    targets = ['libc.so', 'libselinux.so', 'libandroid_runtime.so']
    with open('/proc/self/smaps', 'r') as f:
        smaps = f.read()

    for lib in targets:
        # Find the library mapping
        pos = smaps.find(lib)
        if pos == -1:
            continue
        segment = smaps[pos:pos+800]
        # Private_Dirty > 0 → COW happened → Frida was here
        if 'Private_Dirty:' in segment:
            dirty = re.search(r'Private_Dirty:\s+(\d+)', segment)
            if dirty and int(dirty.group(1)) > 0:
                return True
    return False
Enter fullscreen mode Exit fullscreen mode

Method 2 – pagemap (nuclear option, bit 61)

Linux pagemap entry (64 bit):bit 63 → page present

bit 61 → soft-dirty (set on COW)

import struct, ctypes

def page_is_dirty(vaddr):
    with open('/proc/self/pagemap', 'rb') as f:
        f.seek((vaddr // 4096) * 8)
        entry = struct.unpack('Q', f.read(8))[0]
        present     = (entry >> 63) & 1
        soft_dirty  = (entry >> 61) & 1   # ← this is the killer bit 
        return present == 1 and soft_dirty == 1  

# Example: check fork()
libc = ctypes.CDLL('libc.so')
fork_addr = ctypes.cast(libc.fork, ctypes.c_void_p).value
print(page_is_dirty(fork_addr))   # True → Frida present
# you can add more function for detection, such as : vfork/opendir/abort/closedir/close/signal/sigaction/exit
Enter fullscreen mode Exit fullscreen mode

Bottom LineIn

2025, if you’re still checking /proc/self/maps or thread names, you’re fighting 2022 battles.
Dirty-page detection is the current meta.

I’m H.
Six years deep in Android reversing.
The real game has just started.
— H

Top comments (0)