SReverseby Simpa Labs

SReverse · frameworks / flutter

Flutter apps still make plain HTTP requests.

A Flutter app calls its server the same way any app does. It opens a socket and sends bytes. What you cannot read is where those bytes are built, because the Dart code is compiled into the app binary. So we watch the requests leave the device and rebuild the logic from what we see.

SReverse · Flutter Field guide Runtime observation 8 min read
observe_net.js
// attached to a running Flutter app
// Dart is AOT in libapp.so, so we read the wire, not the Dart.
const engine = Process.getModuleByName("libflutter.so");
Interceptor.attach(
  Module.findExportByName(null, "SSL_write"),
  {
    onEnter(args) { this.buf = args[1]; this.n = args[2].toInt32(); },
    onLeave(ret)  {
      const n = ret.toInt32(); if (n <= 0) return;
      send({ dir: "out", n,
             data: Array.from(new Uint8Array(Memory.readByteArray(this.buf, n))) });
    }
  });

Fig. 01 · reading the app at runtime, not decompiling libapp.so

The request path

The request path is short.

Flutter networking is Dart. An app uses the http package, dio, or a gRPC client. Those libraries call into the Dart engine, and the engine talks to the socket. Your server sees a normal HTTPS request. Nothing about Flutter changes the wire format.

That matters. The hard part is not the protocol. It is finding what the app sends before it sends: the headers, the signature, the body shape.

Static analysis

The Dart code is compiled, and that is the problem.

When a Flutter app ships to the Play Store, the Dart code is not on disk as readable Dart. It is Ahead-of-Time compiled, so the classes and methods live in libapp.so as native machine code, or in a kernel snapshot blob.

Strings often survive. Endpoints are a different story. Apps build URLs from parts, load hostnames from a config endpoint, or keep the strings inside a Dart snapshot that is not plain text on two sides. Searching the binary text for a URL is a starting point. It is not a guarantee.

Runtime

Observe the network layer instead.

The reliable way to recover a Flutter API is to watch the app talk. Put the app behind a proxy, let it make its real calls, and read each request as it leaves. When a Flutter app refuses the proxy, it usually does so through the platform transport. In that case we attach with Frida at runtime so the app tolerates the capture.

Where the proxy is not enough, we hook the engine socket layer and read the bytes. We are observing the app, not translating its Dart. That is the honest boundary of the work, and it is enough to reproduce the call.

boring_ssl.js
# inbound path: capture request bodies as the app sends them
ssl = Module.findExportByName("libflutter.so", "SSL_write");
Interceptor.attach(ssl, {
  onEnter(args) { this.p = args[1]; this.len = args[2].toInt32(); },
  onLeave(ret)  { log(hexdump(this.p, { length: ret.toInt32() })); }
});

Fig. 02 · the request as the app wrote it, before the server saw it

Where the friction is

Signing, dio, and protobuf sit at the edges.

When a Flutter app signs requests, the logic lives in Dart or behind a platform channel that calls into native code. We find the value the app computes for each request and reproduce it, rather than copying a signature from a capture.

When the app uses dio or gRPC, the wire format is usually protobuf. The app ships the message schema inside the binary, not as a .proto file. We read the field layout from the bytes and rebuild the schema. The client then serializes the same message the app sends.

Output

Reproduce the flow, not a frame.

We take the flow we observed, put the signing and body back together, and return a program that makes the same call. If the endpoint changes or a value is regenerated, the client keeps working because it builds state the way the app does instead of replaying a captured request.

flutter_client.py
# rebuilds the signing the Dart layer performs per call
ts = str(int(time.time()))
body = proto.Encode(Feed(limit=24, cursor=cursor))
sig = hmac.new(key, f"{ts}:{len(body)}", sha256).hexdigest()

r = requests.post(
  host + "/v1/feed",
  data=body,
  headers={"X-Time": ts, "X-Signature": sig,
           "Content-Type": "application/x-protobuf"},
)

Fig. 03 · the observed Flutter flow, now callable from Python

Keep reading

All field guides

Send us the app

Your Flutter app has an API behind it.
We can make it callable.

Send the full APK. We return the complete callable API in Python, JavaScript/TypeScript and Postman, with full API documentation.

Projects start at $120. Most are delivered in 24 to 72 hours.

One full APK. One complete delivery.

Projects start at $120. Most are delivered in 24 to 72 hours.

Every format included

Python, JavaScript/TypeScript, Postman and complete API documentation cover the same full endpoint set. Your team runs the clients in its own server or system.

Ready in 24–72 hours

The delivery window starts after we receive the APK and any account access needed to run it. The fixed quote states the deadline. Most projects finish sooner.

Deployment checked before the quote

The package includes signing, encryption, decryption and session handling. We verify device-bound keys and server integrity checks during review and document runtime requirements before you commit.

30 days of fixes

Report a defect within 30 days of delivery. We fix any delivered call that does not match the tested APK at no extra cost.

Start your full APK reconstruction

Send the full APK

Projects start at $120. Choose WhatsApp or email, then attach the APK in the app that opens. We reply within one hour with the next step and send the fixed quote after review.

Want us to contact you?