An empty proxy history is first a routing question. A connection that reaches the proxy but fails over HTTPS needs a separate TLS investigation. This 2020 iOS test application uses Dart HttpClient for both HTTP and HTTPS requests, allowing the two boundaries to be tested independently.
The analysis concerns the bundled ARM64 Flutter engine, not every Flutter version or plugin. Requests carry Dart/2.7 (dart:io), but that header alone does not identify the full engine commit or build configuration.
Establish an HTTP baseline first
The minimum evidence chain has three observation points: device, gateway, and proxy. An application success message establishes that a response arrived. To establish the intended route, correlate that operation with the inbound connection and proxy entry.
| Observation point | Required evidence | Check first if missing |
|---|---|---|
| Gateway ingress | A connection from the test device | Wi-Fi, VPN, interface, routing, and DNS |
| HTTP proxy | Host, path, and response status | Redirection, bind address, transparent parsing |
| TLS handshake | Connection arrival and handshake start | Destination recovery, SNI, server reachability |
| Application result | A response matching the proxy record | Certificate, hostname, and application checks |
The device can reach a Linux gateway through the analysis machine's hotspot or through OpenVPN. The goal is to place that gateway on the actual forwarding path, not merely fill in the system HTTP proxy again. Dart exposes its own proxy-selection interface, findProxy; its documentation specifies direct connections when that function is unset. Platform channels and other clients still need separate inspection. Dart HttpClient.findProxy
- 1
Prepare a dedicated profile
Transfer only the configuration needed for this test through a controlled channel. Do not expose a whole home directory through an HTTP file server, including shell history, key directories, or unrelated files.
- 2
Hand the file to the client
Open the configuration file with OpenVPN. The historical interface shows a file-import page and an ADD action to finish importing; exact labels vary with client versions.
- 3
Connect the tunnel
Confirm the profile was imported and reaches CONNECTED. Moving traffic counters establish tunnel activity, not that a particular application request reached the proxy.
- 4
Send HTTP first
Trigger HTTP Request and correlate that operation across device, gateway, and proxy before investigating HTTPS.
Redirection and transparent parsing are different jobs
The gateway setup sends TCP ports 80/443 from a dedicated ingress to a local listener on port 8080. Names such as tun0, wlan0, and eth0 are environment-specific. Confirm forwarding, NAT, firewall backend, and routing against the actual topology. Match only the test device or dedicated ingress to avoid redirecting the proxy's own outbound connections back into itself.
The important listener states are summarized below. 192.0.2.1 is a documentation address, not a deployment value.
| Setting | Illustrative value or state |
|---|---|
| Bind to port | 8080 |
| Bind to address | A specific gateway ingress address, such as 192.0.2.1 |
| Support invisible proxying | Enabled |
| Force use of TLS | Unchecked |
| Redirect to host / port | Blank in this setup |
An explicit HTTP proxy expects a full URL; HTTPS clients normally identify the destination through CONNECT first. A transparently redirected client may instead send an origin-form request or begin TLS immediately. Burp's invisible proxying handles these inputs: HTTP destination selection depends on Host, while certificate selection during the handshake can depend on SNI. Missing information calls for explicit destination handling, not an assumption that NAT has solved every protocol detail. PortSwigger: invisible proxying
Identify the IPA image and its address spaces
The framework in this sample is located at:
- Payload/
- Runner.app/
- Frameworks/
- Flutter.framework/
- Flutter
- Flutter.framework/
- Frameworks/
- Runner.app/
The sample contains ARMv7 and ARM64 slices; this analysis uses ARM64. Confirm the slice actually loaded and whether its code is suitable for analysis. A byte-pattern hit has little explanatory value if encrypted code has not first become credible instructions.
File offsets, image virtual addresses, and runtime addresses are separate coordinate systems. Within a file-backed segment, the relationship is:
image_va = segment_vmaddr + file_offset - segment_fileoff
runtime_va = image_va + slide
segment_fileoff <= file_offset < segment_fileoff + segment_filesizeApple's Mach-O definitions establish the roles of vmaddr, fileoff, and filesize. Zero-filled regions have no corresponding file bytes. Adding an arbitrary file offset directly to the module base is not a general translation rule. Apple loader.h
A constant search only produces candidates
Searching for scalar 0x186, decimal 390, yields two locations:
| Location | Instruction | Function label |
|---|---|---|
| 0x0007c320 | mov w3, #0x186 |
FUN_0007c178 |
| 0x00406a08 | mov w3, #0x186 |
FUN_004068c8 |
These addresses belong to the analysis database, not the running device. The second candidate's context includes ssl_x509.cc, ssl_server, ssl_client, and certificate-processing branches. The combination of constants, string references, and call relationships supports identifying an outer certificate-chain decision function.
The visible control flow returns uVar5, assigning 0 on failure and 1 on success. Other BoringSSL interfaces may return an enum instead: the first member of ssl_verify_result_t, ssl_verify_ok, corresponds to 0. "Set every verification result to one" is therefore not a valid cross-function rule. BoringSSL ssl.h
Bound the search and require an unambiguous match
The five-image comparison found four matches and one miss; some matching addresses remained stable, while others moved. Full versions and image hashes are absent, so this establishes only limited reuse across those builds, not reliable cross-version coverage.
Shortening a prefix may improve recall while increasing false matches. Each candidate still needs to fall inside the correct executable image, identify an entry point or understood call site, execute on the current HTTPS path, and have a confirmed return convention. Uniqueness is only one of those checks.
This offline test covers segment bounds, empty signatures, zero and multiple matches, and the 32-byte prefix length. Its address-translation values are illustrative. It is neither a complete Mach-O scanner nor a device test.
def fileoff_to_runtime(fileoff, segment_fileoff, segment_filesize,
segment_vmaddr, slide):
if not segment_fileoff <= fileoff < segment_fileoff + segment_filesize:
raise ValueError("offset outside file-backed segment")
return segment_vmaddr + fileoff - segment_fileoff + slide
def unique_signature_offset(blob, signature):
if not signature:
raise ValueError("empty signature")
hits = [i for i in range(len(blob) - len(signature) + 1)
if blob[i:i + len(signature)] == signature]
if len(hits) != 1:
raise ValueError(f"expected one match, got {len(hits)}")
return hits[0]
assert fileoff_to_runtime(0x2340, 0x2000, 0x1000,
0x100004000, 0x200000) == 0x100204340
for offset in (0x1fff, 0x3000):
try:
fileoff_to_runtime(offset, 0x2000, 0x1000, 0x100004000, 0)
except ValueError:
pass
else:
raise AssertionError("out-of-range offset accepted")
assert unique_signature_offset(b"ABCDWXYZ", b"WXYZ") == 4
for blob, sig in ((b"ABAB", b"AB"), (b"AB", b"X"), (b"AB", b"")):
try:
unique_signature_offset(blob, sig)
except ValueError:
pass
else:
raise AssertionError("invalid signature match accepted")
prefix = bytes.fromhex(
"ff 03 05 d1 fc 6f 0f a9 f8 5f 10 a9 f6 57 11 a9 "
"f4 4f 12 a9 fd 7b 13 a9 fd c3 04 91 08 0a 80 52")
assert len(prefix) == 32
assert 0x004068c8 + len(prefix) == 0x004068e8
print("PASS: segment bounds; unique/zero/multiple/empty matches; 32-byte prefix")$ python flutter_ios_models.py
PASS: segment bounds; unique/zero/multiple/empty matches; 32-byte prefixChanging a return value leaves the function body's side effects intact, unlike replacing the function entirely. An outward success result may therefore coexist with inconsistent session state, errors, or later checks. Dynamic validation must continue until the application receives a response, not stop when the hook fires.
Close the evidence chain at both ends
The proxy capture uses Burp Suite Community Edition 2020.2.1. Both entries are GET requests for / with status 200. Hosts below are documentation domains. The lengths preserve the proxy list's Length column and are not asserted to be response-body sizes.
| Protocol | Host | Method and path | Status | Length | MIME |
|---|---|---|---|---|---|
| HTTP | example.com |
GET / | 200 | 1833 | HTML |
| HTTPS | secure.example.com |
GET / | 200 | 43137 | HTML |
The visible HTTPS request headers are shown below with Host replaced. No missing response content has been filled in.
GET / HTTP/1.1
user-agent: Dart/2.7 (dart:io)
Accept-Encoding: gzip, deflate
content-length: 0
host: secure.example.com
Connection: close
Interface illustration: the HTTP baseline and HTTPS check are shown side by side, not as simultaneous operations.
The device displays success for HTTP and HTTPS, while the proxy contains readable requests. Together these support the two-boundary interpretation for this sample. They do not prove that the system trust store was configured successfully or that application-specific signatures, pinning, and response validation were also handled.
For another sample, record the engine version, architecture, image hash, candidate location, return semantics, and before/after request state again. The transferable result is the validation order, not this build's absolute address or prologue bytes.