mirror of
https://github.com/littledivy/mimic.git
synced 2026-10-26 19:15:10 +00:00
de-slop pinning/dpop docs
This commit is contained in:
1 parent
37ff46811d
commit
4889872b20
2 files changed
+107
-108
No files matched your search
+42
-41
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user