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
|
# 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
@@ -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
|
||||||
Reference in new issue
Block a user