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