Find where the Dart logic lives
Release Flutter compiles Dart ahead of time into a snapshot inside libapp.so. The Android wrapper is mostly startup, manifests and platform channels; the request code is in the snapshot. Confirm the AOT build and locate the library, then pull strings from it. The strings give you the endpoints and the client library, and they do not give you the code that fills the headers or signs the body.
unzip -l app.apk | grep -E 'lib/.*/lib(app|flutter).so' strings lib/arm64-v8a/libapp.so | grep -iE 'https?://|/api/v[0-9]' | sort -u strings lib/arm64-v8a/libapp.so | grep -iE 'dio|http|graphql|protobuf' | sort -u
Use the client name to narrow the search. If the strings show dio, the request goes through a Dio client and its interceptors. If they show a GraphQL package, the request is a query or a mutation, and the app may use persisted-query hashes instead of full query text. If they show protobuf, the body is structured binary, which can look encrypted when it is not.
Reconstruct the snapshot, or trace the call
Two routes. To read Dart-level structure, reconstruct the snapshot from libapp.so with a Dart snapshot tool such as Blutter, which recovers classes, methods and fields from the AOT image. Names may be reduced if the app was built with code obfuscation, so verify by the value rather than the label. To observe the request directly, hook the runtime client or capture the traffic.
The snapshot route gives you the shape of the code. The hook and capture route gives you the actual bytes. You usually want both, because the shape tells you where the value is created and the bytes tell you what it is.
The interception gotcha
Flutter's built-in dart:io HttpClient uses its own certificate store and does not automatically trust a system proxy CA. Adding a system CA is often not enough, so a proxy that works for a native app can silently fail for a Flutter one. You either add your CA inside the app, hook the client to trust it, or patch the transport. A proxy set at the OS also does not always reach the Dart layer, so confirm the request actually leaves through the proxy before concluding the app is not calling the endpoint.
Platform channels move the hard step
A Dart call can cross a platform channel into Java, Kotlin, C or C++ for device checks, key access or signing. Follow any channel method used before the request. That is often where the value you cannot reproduce in Dart is created, and it is the reason a captured body replayed from Python fails.
# list the channel names referenced in the snapshot strings lib/arm64-v8a/libapp.so | grep -iE 'channel|methodchannel|platform' | sort -u
Common reasons a replay fails
| Observation | Likely cause | Where to look |
|---|---|---|
| Headers differ from the code you see | A Dio interceptor adds headers after Dart builds the request. | The Dio interceptor queue. |
| A call fails after a token error | A refresh interceptor retries with a new access token. | The refresh interceptor and the refresh response. |
| A value is not in Dart | A native plugin supplies a device or crypto value. | The platform channel and the native method. |
| The proxy sees no traffic | Certificate pinning or a non-standard trust store. | The transport or the client's certificate handling. |
| The body looks encrypted | Protobuf or another binary serializer. | Field boundaries, not cipher. |
Confirm what the app actually sent
Capture at the network boundary with your CA injected inside the app, then compare the request to the strings and the reconstructed snapshot. The endpoint and the client library are in the snapshot; the headers and body are what the app produced at send time. When the two disagree, the interceptor or the platform channel is where the difference is.
A Dart method is not a Java method. Java.use-style hooks reach the wrapper and the platform channel, not the Dart code, which runs in the AOT snapshot. For the Dart layer you either reconstruct the snapshot or instrument the transport, and the two give different views of the same call.
Then rebuild the call in your target language from the captured bytes and the reconstructed generation rules. The server does not care that the app was written in Dart; it cares that the request is valid.
Reviewed 30 August 2026 · SReverse research desk