The wall
I was trying to reproduce an Android app's backend requests from my own client. Normal stuff: read the traffic, rebuild the call, send it.
It didn't work, and not in a way I could easily explain.
I went deep on fidelity. I used a good TLS-fingerprint client (bogdanfinn's tls-client in Go), then rebuilt the request in Node with a hand-picked cipher order and all the low-level knobs. As far as I could measure, my request was identical to the app's: same certificate behavior, same body, same headers, same TLS. Every byte I could observe matched.
Still rejected.
That's the annoying kind of wall. When your request is visibly different, you have something to fix. When it's identical and still fails, something invisible is happening on the server side, in a dimension you aren't watching.
Ruling things out
I didn't want to guess, so I started eliminating.
First guess: proxy timing. Maybe the round trip through my proxy was off in some way the server cared about. That didn't hold up.
Second guess: the proxy was quietly changing the TLS as it re-routed. This one I actually tested instead of assuming. I stood up a server that logged every TLS property of the request as it arrived through the proxy, and compared. Everything came through unchanged. The proxy wasn't touching the handshake. Ruled out, with evidence.
So the request was clean, the transport was clean, and it still failed. Whatever the server was keying on, it wasn't in the request.
The part that made no sense
Then a thought: what if a connection opens at some point and authenticates everything after it? If the server trusts a connection instead of a request, then no amount of per-request fidelity would matter.
To check, I used Frida to hook OkHttp inside the app and fired a fresh request from within. Same OkHttp client, same device, same app, same process. A request coming from inside the real app, through its own networking, should obviously be accepted.
It got rejected too.
That's where it got interesting. Coming from inside the legitimate app wasn't enough. Same client, same process, same everything, and the server still said no. Whatever it trusted was narrower than "the app."
The realization
If a brand-new request from inside the app fails, then the thing being trusted isn't the app, or the process, or the networking stack. It's more specific than that.
It's the session itself. The exact OkHttp session the app had already established, the one with a history of requests sent and accepted. That connection had a relationship with the server. It had been seen behaving correctly over time. A new request, even from inside, opens or uses a connection without that history, and gets treated as a stranger.
This is channel binding in the wild (on the detection side you'll also see it called TLS session persistence): the server holds onto the signature of an established, validated connection and won't extend that trust to anything that shows up outside it, even a perfect replica. It's a genuinely strong defense against replay, because it doesn't care how convincing your request looks. It cares whether you're the connection it already trusts.
Fingerprint spoofing loses here by design. You can be a flawless copy of the client and still not be the client.
Session borrowing
So I stopped trying to look like the trusted session and used it directly.
Instead of building a new request or a new connection, I borrowed the OkHttp session the app had already established. The live one, with its accepted-request history and its existing relationship with the server. I ran a request through that, and it went through.
That reframed the whole thing. I don't need to reproduce anything. I just need to:
- Drive the real app until it establishes a valid, trusted session.
- Borrow that session, the live connection, not a copy of it.
- Expose it as a local proxy so my own client can send requests through it freely.
The heavy, fragile part (earning the server's trust) gets done by the legitimate client, once. After that my client rides the borrowed session. And since the exit can be layered, an optional tertiary proxy can sit on top to vary the outgoing IP without disturbing the borrowed session underneath.
I call this session borrowing: you don't reproduce the session, you borrow the one the real client already holds.
The underlying mechanic, using Frida to instrument the app and run through its own live OkHttp session, isn't new to anyone who does Android RE. What I want to name is the approach: using it specifically to sidestep channel binding by becoming the trusted connection instead of impersonating it.
What this says to the defensive side
Channel binding is a strong control, and it's worth saying plainly because it's the useful lesson: binding trust to an established, validated connection defeats replay in a way that fingerprint-based detection never can. A perfect forgery from the outside doesn't get in. That's a real property, and if you're defending an API, it's a good one to want.
But it has a blind spot, and the blind spot is structural. Channel binding protects against reproducing the session from the outside. It does not protect against borrowing the session from the inside. The defense assumes the adversary is trying to look like the trusted client. It has nothing to say about an adversary who lets the real client do the trust-building and then rides the result.
The takeaway isn't "channel binding is useless." It's the opposite. It's one of the stronger things you can do at the network layer. The takeaway is that binding to the connection moves the fight from "can you forge the client?" to "can you get code inside the client?", which is a much better place to move it, but not the end of the game. Anything that runs on a device you don't control can, in principle, borrow what that device is trusted to do.