2025, Dec 27 17:00

How to Send Android Bitmap byte[] from Frida to Python: Fixing send() 'expected a pointer' with Base64

Learn why Frida send() rejects Java byte[] and how to transfer Android Bitmap data to Python reliably using Base64 encoding, with JS and Python code.

Moving raw image data from Frida's JavaScript runtime back into Python often looks straightforward until you hit a wall with Java byte arrays. A common scenario is capturing a Bitmap inside an Android process and attempting to forward its bytes through Frida's messaging bridge. The snag appears when passing a Java byte[] as the second argument to send(), which expects something entirely different.

Reproducing the issue

The following minimal example compresses a Bitmap to PNG using Android APIs, obtains the byte[], and tries to send it to Python via send(). The Python handler writes whatever arrives as binary to disk.

import frida
import sys
import os

AGENT_JS = '''
setTimeout(function () {
  Java.perform(function () {
    const File = Java.use("java.io.File");
    const Bitmap = Java.use("android.graphics.Bitmap");
    const BitmapCompressFormat = Java.use("android.graphics.Bitmap$CompressFormat");
    const BitmapConfig = Java.use("android.graphics.Bitmap$Config");
    const ByteArrayOutputStream = Java.use("java.io.ByteArrayOutputStream");

    const bmp = Bitmap.createBitmap(100, 100, BitmapConfig.ARGB_8888.value);

    const out = ByteArrayOutputStream.$new();
    const ok = bmp.compress(BitmapCompressFormat.PNG.value, 100, out);
    console.log("[*] Compressed as PNG:", ok);

    const buf = out.toByteArray();
    console.log("[*] Byte array length:", buf.length);

    // This triggers: Error: expected a pointer
    send({ kind: "bitmap" }, buf);
    out.close();
  });
}, 1000);
'''

OUT_FILE = "image.png"

def handle_event(msg, bin_data):
    if msg["type"] == "send" and msg["payload"].get("kind") == "bitmap":
        with open(OUT_FILE, "wb") as fp:
            fp.write(bin_data)
        print(f"[+] Wrote {OUT_FILE}")
    else:
        print(f"[?] Message: {msg}")

def run():
    usb = frida.get_usb_device(timeout=5)
    sess = usb.attach(target_pid)
    agent = sess.create_script(AGENT_JS)
    agent.on("message", handle_event)
    agent.load()
    usb.resume(target_pid)

if __name__ == "__main__":
    run()

Why it fails

Frida's messaging bridge supports raw binary only when it is backed by native memory, i.e., an ArrayBuffer or a NativePointer. A Java byte[] is not either of those, so using it as the second argument to send() results in Error: expected a pointer. In other words, the second argument of send() is for native-backed blobs; Java arrays must be serialized into a form that can live in the first argument.

Fix: base64 over the Frida bridge

For Android targets, the simplest and most robust approach is to convert the Java byte[] to a base64 String and send that as the primary payload. Android provides android.util.Base64 out of the box, and the NO_WRAP flag avoids line breaks in the encoded output.

setTimeout(function () {
  Java.perform(function () {
    const Bitmap = Java.use("android.graphics.Bitmap");
    const Format = Java.use("android.graphics.Bitmap$CompressFormat");
    const Cfg = Java.use("android.graphics.Bitmap$Config");
    const BAOS = Java.use("java.io.ByteArrayOutputStream");
    const B64 = Java.use("android.util.Base64");

    const bmp = Bitmap.createBitmap(100, 100, Cfg.ARGB_8888.value);
    const out = BAOS.$new();
    bmp.compress(Format.PNG.value, 100, out);

    const raw = out.toByteArray();
    const pageNum = 1; // example identifier if you need to tag the payload
    const encoded = B64.encodeToString(raw, 2); // 2 == Base64.NO_WRAP
    send("BITMAP#" + pageNum + "#" + encoded);

    out.close();
  });
}, 1000);

On the Python side, parse the received string, extract the base64 segment, decode it to bytes, and persist to disk.

import base64

OUT_FILE = "image.png"

def handle_event(msg, bin_data):
    if msg["type"] == "send" and isinstance(msg["payload"], str) and msg["payload"].startswith("BITMAP#"):
        marker, page_id, b64_blob = msg["payload"].split("#", 2)
        binary = base64.b64decode(b64_blob)
        with open(OUT_FILE, "wb") as fp:
            fp.write(binary)
        print(f"[+] Saved page {page_id} as {OUT_FILE}")
    else:
        print(f"[?] Message: {msg}")

Memory considerations

This technique is straightforward but duplicates the data in the target process because the byte[] is converted to a base64 String. For large buffers that may be sensitive to memory pressure, consider using the variant of encodeToString(byte[] input, int offset, int len, int flags) to process the array in multiple blocks. This way, the conversion happens in chunks and avoids holding an additional full-size copy of the array in memory.

Why this matters

Frida bridges are powerful, but they draw a clear line between native memory and managed runtime objects. When you know that the second argument to send() is strictly for native-backed blobs like ArrayBuffer or NativePointer, it becomes obvious why a Java byte[] does not fit. Understanding that boundary saves time and prevents brittle workarounds, especially when moving high-volume binary data such as images.

Takeaways

If you need to transfer Java byte[] buffers from an Android app to Python through Frida, serialize them to a String first. Android's Base64 API offers a convenient and reliable encoder; use the NO_WRAP flag to keep the payload compact and easy to parse. For large buffers, process in slices with the offset/len encodeToString overload to reduce peak memory usage. With this pattern, you avoid pointer-related errors and keep your data path predictable end-to-end.