de-slop pinning/dpop docs

This commit is contained in:
Divy Srivastava committed 2026-07-13 17:58:58 +05:30
1 parent 37ff46811d
commit 4889872b20
2 files changed
+107 -108

No files matched your search

+42 -41
View File
@@ -1,65 +1,66 @@
# DPoP / sender-constrained tokens # DPoP / sender-constrained tokens
Unlike [cert pinning](pinning.md), this one defeats mimic's core model, not just [Cert pinning](pinning.md) blocks capture; you can get past it and mimic works
capture. If a target uses DPoP, static header replay does not work — by design. as normal. DPoP is different: it breaks the replay itself, which is the thing
mimic does.
## Why replay fails ## Why replay fails
A plain app authenticates every request with the same reusable bundle (a bearer A plain app authenticates every request with the same reusable bundle: a bearer
token, cookies, device ids). Capture it once, replay it forever — that's mimic. token, cookies, device ids. Capture it once, replay it, done. That's mimic.
DPoP ([RFC 9449][rfc]) breaks that. Every request carries a fresh `DPoP:` header: DPoP ([RFC 9449][rfc]) doesn't work that way. Every request carries a fresh
a JWT signed by a private key the client holds, with claims that bind it to *that `DPoP:` header, a JWT signed by a private key the client holds, with claims that
one request*: tie it to that one request:
| Claim | Binds the proof to | Why a copy dies | | Claim | Ties the proof to | Why a copy fails |
|-------|--------------------|-----------------| |-------|-------------------|------------------|
| `htm` | HTTP method | reuse on another verb rejected | | `htm` | HTTP method | reuse on another verb is rejected |
| `htu` | request URL | reuse on another endpoint rejected | | `htu` | request URL | reuse on another endpoint is rejected |
| `iat` | issue time | server accepts only a few-second window | | `iat` | issue time | server accepts only a few-second window |
| `jti` | unique id | server caches it; single-use | | `jti` | unique id | server caches it; single use |
| `nonce` | server-issued value | unpredictable, short-lived (§8) | | `nonce` | server-issued value | unpredictable, short-lived (§8) |
| `ath` | the access token | proof tied to that token | | `ath` | the access token | proof is tied to that token |
The access token itself is bound to the key's thumbprint (`cnf.jkt`), so a stolen The access token is bound to the key's thumbprint (`cnf.jkt`), so a stolen token
token is useless without signing a matching proof *per request*. Copying the does nothing without a matching signature on every request. Copying the header
header replays nothing. replays nothing.
## The only path: a signing oracle ## The only option: a signing oracle
You can't extract the key in the common case, but you might *borrow the app's You usually can't extract the key, but you might borrow the app's ability to use
ability to use it*. Keep the app running on a device you control, hook its it. Keep the app running on a device you control, hook its DPoP-signing routine
DPoP-signing routine with Frida, and ask it to mint a fresh proof for each with Frida, and have it mint a fresh proof for each request you want to send. The
request you want to send. It never touches the key — it uses the app as an oracle never touches the key; it uses the app to sign. Whether that's even
oracle. Whether this is even possible comes down to one fact: possible depends on where the key lives:
| Key storage | Extractable? | Replay path | | Key storage | Extractable? | Replay path |
|-------------|--------------|-------------| |-------------|--------------|-------------|
| **Secure Enclave** (P-256, `kSecAttrTokenIDSecureEnclave`) — recommended, common | **No.** A jailbreak gives userland root, not silicon access. | Live Frida oracle only — tethered to the running app | | Secure Enclave (P-256, `kSecAttrTokenIDSecureEnclave`) — recommended, common | No. A jailbreak gives userland root, not enclave access. | Live Frida oracle only, tethered to the running app |
| **Keychain software key** (lazier apps) | **Yes**, on a jailbroken device (keychain-dumper, or a Frida `SecItemCopyMatching` hook) | Dump the key, sign proofs offline in Python | | Keychain software key (lazier apps) | Yes, on a jailbroken device (keychain-dumper, or a Frida `SecItemCopyMatching` hook) | Dump the key, sign proofs offline in Python |
So: fingerprint the target first. Dump the keychain, check whether the DPoP key Fingerprint the target first: dump the keychain and check whether the DPoP key is
is Secure-Enclave-backed or a plain software key. That single fact decides Secure-Enclave-backed or a plain software key. That answer decides the rest.
everything.
## The nonce problem ## The nonce problem
Even with a working oracle, RFC 9449 §8 nonces make offline replay impossible: A working oracle still can't do offline replay if the server uses nonces
they're server-issued, opaque, and short-lived. You can't pre-mint proofs. Each (RFC 9449 §8). They're server-issued, opaque, and short-lived, so you can't
request becomes a live dance — send, get `use_dpop_nonce` + a fresh nonce, sign pre-mint proofs. Each request turns into a round trip: send, get a
with it, resend — with the device online the whole time. Fine for poking an API `use_dpop_nonce` error with a fresh nonce, sign with it, resend, with the device
interactively; useless for detached or scaled replay. online the whole time. Workable for poking an API by hand, not for detached or
bulk replay.
## Bottom line for mimic ## Bottom line
- **Software key + jailbreak** → dump it, port the app's proof construction into - Software key plus a jailbreak: dump it, port the app's proof construction into
Python, sign offline. Clean win (subject to the nonce dance). Python, sign offline. Works, minus the nonce round trip.
- **Secure Enclave** → no extraction, ever. Best case is a live Frida oracle on a - Secure Enclave: no extraction. The best you get is a live Frida oracle on a
tethered jailbroken device. Not worth wiring into a general tool. tethered jailbroken device, not worth wiring into a general tool.
- **No device access** → infeasible. There is no network-only path. - No device access: nothing works. There's no network-only path.
There's no published DPoP-specific tooling (no Frida script, no mitmproxy addon); No DPoP-specific tooling exists (no Frida script, no mitmproxy addon); you'd build
you'd build the oracle from generic Frida hooking. Static header replay — the the oracle from generic Frida hooks. Static header replay, which is what mimic is,
thing mimic *is* — does not carry over to DPoP targets. doesn't carry over to DPoP targets.
[rfc]: https://www.rfc-editor.org/rfc/rfc9449 [rfc]: https://www.rfc-editor.org/rfc/rfc9449
+65 -67
View File
@@ -1,57 +1,58 @@
# Certificate pinning # Certificate pinning
By default mimic captures traffic with a proxy (mitmproxy). A pinned app rejects mimic captures traffic through a proxy (mitmproxy). A pinned app rejects the
the proxy's cert, so the proxy sees nothing and there's nothing to capture. This proxy's cert, so the proxy sees nothing and there's nothing to capture. This
page is about getting past that. page covers getting past that.
## What pinning actually does ## What pinning does
Worth being precise, because it changes the fix. Normal TLS authenticates the Normal TLS authenticates the server to the client: the app checks that the
*server* to the *client*: the app checks that the server's cert is signed by a server's cert is signed by a trusted CA. To MITM it, you add your own CA to the
trusted CA. To MITM, you add your own CA to the device's trust store, and the device trust store and the proxy presents a cert signed by that CA. Pinning
proxy presents a cert signed by it. Pinning breaks that by hardcoding the exact hardcodes the exact cert (or public-key hash) the app will accept, so a proxy
cert (or public-key hash) the app will accept, so a proxy cert signed by *any* cert signed by any other CA, including one you control, gets rejected.
CA — including one you control — is rejected.
Two things follow: So two things are true:
- **Pinning stops capture, not replay.** It only makes the traffic hard to Pinning stops capture, not replay. It only makes traffic hard to observe. Once
*observe*. Once you can see a request, the auth bundle in it replays exactly you can see a request, the auth bundle in it replays the same as it would for an
as it would for an unpinned app. Pinning adds zero replay resistance — that's unpinned app. Pinning adds no replay resistance; that's what DPoP is for (see
what DPoP is for (see [dpop.md](dpop.md)). [dpop.md](dpop.md)).
- **You can't beat it by feeding the app a cert.** The pinned value is a
*public* cert; presenting it requires the matching private key, which lives on
the server. The fix is app-side: make the app stop checking, by hooking its
TLS verification at runtime with [Frida](https://frida.re).
mimic doesn't reimplement those hooks (a moving target across iOS versions and You can't beat it by handing the app a cert. The pinned value is a public cert,
TLS stacks). `mimic unpin` orchestrates the maintained upstream, and presenting it in a handshake needs the matching private key, which sits on
[httptoolkit/frida-interception-and-unpinning][ht], and owns the part that's the server. The fix is on the app side: stop the app from checking, by hooking
genuinely mimic's job: baking *your* mitmproxy CA and proxy address into the its TLS verification at runtime with [Frida](https://frida.re).
scripts and emitting the exact command to run.
mimic doesn't reimplement those hooks; they change across iOS versions and TLS
stacks. `mimic unpin` drives the upstream that maintains them,
[httptoolkit/frida-interception-and-unpinning][ht], and handles the mimic-specific
part: baking your mitmproxy CA and proxy address into the scripts and printing
the command to run.
## Prerequisites ## Prerequisites
```bash ```bash
mimic doctor # the "optional — only for mimic unpin" section mimic doctor # see the "optional — only for mimic unpin" section
``` ```
- **git** — fetches the unpinning scripts. - git, to fetch the unpinning scripts.
- **frida** — runs the hooks (`pipx install frida-tools`). - frida, to run the hooks (`pipx install frida-tools`).
- **objection** — only for the no-jailbreak path (`pipx install objection`). - objection, for the no-jailbreak path only (`pipx install objection`).
- A **mitmproxy CA** at `~/.mitmproxy/mitmproxy-ca-cert.pem`. Running - A mitmproxy CA at `~/.mitmproxy/mitmproxy-ca-cert.pem`. Running `mimic record`
`mimic record` once generates it. once generates it.
## Path A — jailbroken device (simplest) ## Path A: jailbroken device
The phone runs `frida-server`; you attach over USB. No repackaging, no signing. The phone runs `frida-server` and you attach over USB. No repackaging, no
signing.
```bash ```bash
mimic record # in one terminal — starts the proxy mimic record # one terminal — starts the proxy
mimic unpin com.example.app # bundle id → prints a ready frida command mimic unpin com.example.app # bundle id → prints a frida command
``` ```
`unpin` fetches the scripts, bakes in your CA + this Mac's LAN IP:8080, and `unpin` fetches the scripts, bakes in your CA and this Mac's LAN IP:8080, and
prints: prints:
```bash ```bash
@@ -64,55 +65,52 @@ frida -U \
-f com.example.app -f com.example.app
``` ```
Run it, use the app, then the normal flow: `mimic hosts` → `mimic gen <host>`. Run it, use the app, then `mimic hosts` and `mimic gen <host>` as usual.
## Path B — stock device (Frida gadget) ## Path B: stock device (Frida gadget)
No jailbreak. Inject the Frida gadget into a **decrypted** IPA, re-sign, sideload. No jailbreak. You inject the Frida gadget into a decrypted IPA, re-sign, and
sideload.
```bash ```bash
mimic unpin ./MyApp.ipa --codesign <TEAM_ID> mimic unpin ./MyApp.ipa --codesign <TEAM_ID>
``` ```
`unpin` bakes the config as above, then runs `objection patchipa` to inject the `unpin` bakes the config, runs `objection patchipa` to inject the gadget, and
gadget and writes `mimic-unpin/MyApp-patched.ipa`. mimic hands off the signing + writes `mimic-unpin/MyApp-patched.ipa`. It stops there and prints the signing
install step, because that's the Apple-auth swamp tools like Sideloadly exist to and install step, since that's what tools like Sideloadly handle:
handle:
- **easiest** — drag the patched IPA into [Sideloadly](https://sideloadly.io) - drag the patched IPA into [Sideloadly](https://sideloadly.io), or
- **scripted** — `pymobiledevice3 apps install mimic-unpin/MyApp-patched.ipa` - `pymobiledevice3 apps install mimic-unpin/MyApp-patched.ipa`
Then attach the scripts to the gadget (`unpin` prints the exact `frida -U` Then attach the scripts to the gadget (`unpin` prints the `frida -U` command) and
command) and capture as usual. capture as usual.
### The catches ### The catches
- **Decrypted IPA required.** Your own app build is fine. An App Store app is - You need a decrypted IPA. Your own app build is fine. An App Store app is
FairPlay-encrypted; decrypting it needs a jailbroken device once (or a FairPlay-encrypted, and decrypting it takes a jailbroken device once (or a
pre-decrypted IPA). "No jailbreak" often quietly means "no jailbreak *if pre-decrypted IPA). "No jailbreak" often means "no jailbreak, if someone
someone already decrypted it*." already decrypted it."
- **7-day cert churn.** Free Apple certs expire weekly. - Free Apple certs expire after 7 days.
[TrollStore](https://github.com/opa334/TrollStore) (iOS ≤ 17.0.x) installs [TrollStore](https://github.com/opa334/TrollStore) (iOS 17.0.x and below)
with real entitlements permanently and avoids the re-sign dance. installs with real entitlements permanently and skips the weekly re-sign.
- **Anti-tamper.** Apps with binary-integrity checks detect the re-signed bundle - Apps with binary-integrity checks detect the re-signed bundle and refuse to
and refuse to run. run.
## Framework notes ## Framework notes
- **React Native** — uses `NSURLSession` and honors the system proxy. The - React Native uses `NSURLSession` and honors the system proxy. The standard
standard hooks work. hooks work.
- **Flutter** — ships its own BoringSSL and *ignores the system proxy*, so - Flutter ships its own BoringSSL and ignores the system proxy, so
`SecTrustEvaluate` hooks never fire and setting a device proxy captures `SecTrustEvaluate` hooks never fire and a device proxy captures nothing. It
nothing. Needs [reFlutter](https://github.com/Impact-I/reFlutter) to patch the needs [reFlutter](https://github.com/Impact-I/reFlutter) to patch the engine;
engine. `mimic unpin` won't handle Flutter apps as-is. `mimic unpin` won't handle Flutter apps as they are.
## What still won't work ## What still won't work
Pinning is beatable; some things past it aren't: - DPoP / sender-constrained tokens. The per-request signing key never leaves the
device, so captured requests don't replay. See [dpop.md](dpop.md).
- **DPoP / sender-constrained tokens** — the per-request signing key never - Hardware attestation (App Attest, DeviceCheck). Out of scope.
leaves the device, so captured requests don't replay. See [dpop.md](dpop.md).
- **Certificate transparency / attestation** backed by hardware
(App Attest / DeviceCheck) — out of scope.
[ht]: https://github.com/httptoolkit/frida-interception-and-unpinning [ht]: https://github.com/httptoolkit/frida-interception-and-unpinning