Why the usual entry points are empty
In a Flutter release build, Dart is compiled ahead of time into machine code that ships as a shared library, usually libapp.so on Android. The classes that build a request, sign it and send it exist as compiled Dart, not as Java bytecode. A decompiler pointed at classes.dex finds the embedding layer: the activity, the engine bootstrap, plugin registration and the platform channels that let Dart call Android APIs. The request logic is not there.
Traffic interception often works anyway. A Flutter app that uses the standard networking library opens real TLS connections, and an instrumented device with a trusted certificate authority can see them. What tends to break is the certificate check, because verification happens inside the Flutter engine rather than through the platform trust manager.
Read the compiled library as a program
libapp.so holds the application code and its data. Two properties make it useful before any decompilation.
- String literals survive in the compiled image. Base URLs, path segments, header names, query keys and JSON field names usually appear in the binary as literal text, and they cluster near the code that uses them.
- The networking code is compiled machine code with recognisable structure. A disassembler shows the call shape even when the Dart symbol names are unreadable.
Searching for a hostname or a path fragment often locates a small region of the image. Working outward from that region tells you which functions assemble the request and which values they take from configuration or from the device.
Watch the flow on a running device
Observation answers questions that static reading leaves open: which values are derived at send time, which calls precede the interesting one, and how a token refresh changes the request.
- Capture at the TLS layer when pinning permits it. You see the plaintext a compatible client would send and the response the server returns.
- Capture at the socket layer when you need the bytes as the app wrote them.
- Log the sequence of calls rather than only the request. A signature that looks wrong on the first call is often correct once an earlier call has established session state.
Both views matter because the app builds some values from code and others from stored state. A capture on its own does not tell you which is which.
Work around pinning without fighting it
Flutter apps frequently pin through the engine, so the familiar platform-level bypass does nothing. The practical routes are to neutralise the verification call inside the compiled library, or to keep the connection intercepted in a way the app cannot distinguish.
Pinning is also a signal about the build. An app that pins usually protects other parts of the flow as well, which changes the order of work: establish whether integrity checks compare the library against a signature before you modify it, because a modified library can fail before any request is made.
Separate app values from platform values
A captured request mixes sources. Some fields come from the application, some from the networking library, and some from the operating system or the transport. Sorting them decides what your client has to compute and what it can let its own HTTP stack handle.
- Values that stay constant across every call are configuration: an API key, an application identifier, a fixed accept header. Record them once.
- Values that change with the payload are derived in application code, which is where the interesting work sits.
- Values that a browser or a standard client would also produce, such as a content length, a host header or a user agent, usually need no special handling. Check whether the server validates the user agent, since a mismatch there is a common cause of a rejected request.
The same request sent from a different network, or after a fresh install, separates device state from code. A field that changes between installs is stored or generated rather than computed, and the client needs a plan for it.
Turn captures into a specification
The deliverable is a description of the protocol rather than of the compiled code. For each endpoint, record the method, the exact payload shape, the headers the app produces, which of those are static, and the algorithm behind any value derived at send time. Capture enough examples to cover an error response, a token refresh and one paginated call, since these paths usually carry fields the happy path never shows.
Once the specification matches the server for those cases, the Dart compilation stops being an obstacle. The client you build sends requests the server accepts, and you own a readable implementation.
Related work
Reviewed 28 September 2026 · SReverse research desk