Reconstructing token generation from an APK
The login is the entry point, but the token is the state the app leaves behind and later proves.
Request reconstruction · signing
You copy a request out of Burp, reproduce it in Python, and the server rejects it. The path and the headers look identical. The difference is that the app rebuilt state at the moment it sent the request, and your reproduction is a different moment. A captured request is evidence of what was sent. The missing problem is how the application generated it.
A copied request is a snapshot: a method, a path, a set of headers, a body, each fixed at the instant you captured them. The server validated that instant. Your reproduction is a different instant, and the app is built so that a different instant needs a different request.
The values that change between the capture and your replay are usually a small set. The timestamp. A nonce. A session or a token. A device-derived value. The signature that covers some combination of the rest.
Arrange the copy next to what the app actually emits and the missing state is usually one of four things.
X-Timestamp, Date, or an HMAC computed over a window. A stale value is rejected out of hand.Change one input at a time. Replay the capture byte-for-byte and record the status as the baseline. Then regenerate one input and re-sign over the new canonical string, because if the signature covers the field you changed, a stale signature makes the test fail for the wrong reason. The step at which the server starts accepting the request tells you which state the app recomputed.
# the canonical string is what the app signs; re-sign after every change ts = str(int(time.time())) nonce = secrets.token_hex(8) payload = json.dumps(body, sort_keys=True).encode() def sig(): canonical = f"{method}\n{path}\n{ts}\n{nonce}\n".encode() + payload return hmac.new(key, canonical, sha256).hexdigest() # baseline: the exact capture r = requests.get(url, headers=frozen_headers); print(r.status_code) # 403 # fresh timestamp, re-signed headers["X-Timestamp"] = ts headers["X-Signature"] = sig() r = requests.get(url, headers=headers); print(r.status_code) # fresh nonce, re-signed nonce = secrets.token_hex(8) headers["X-Nonce"] = nonce headers["X-Signature"] = sig() r = requests.get(url, headers=headers); print(r.status_code)
Fig. 01 · the step at which the status changes identifies the state the app recomputed.
Once you know which fields regenerate, find where the app computes them. The signing logic is usually in an interceptor (OkHttp, Retrofit), in a helper class, or in a native library. The nonce and timestamp generation is often right next to it. Read what the app signs, in what order, with what encoding, and rebuild that exact operation.
# reproduce the generation, not the capture ts = str(int(time.time())) nonce = secrets.token_hex(8) payload = json.dumps(body, sort_keys=True).encode() canonical = f"{method}\n{path}\n{ts}\n{nonce}\n".encode() + payload sig = hmac.new(key, canonical, sha256).hexdigest() r = requests.post(base + path, data=payload, headers={"X-Timestamp": ts, "X-Nonce": nonce, "X-Signature": sig})
Fig. 02 · once each request recomputes its own state, it stops being a replay.
| Status / behavior | What it points to | Where to look |
|---|---|---|
| 403 with a generic body | The server understood and refused the request. The status alone does not name the fault. | Compare the response body, then re-sign and test before attributing it to signing. |
| 401 | Token or session expired / device-scoped | The auth and token flow |
| 409 or "duplicate" | Nonce or id replayed | The per-call value generator |
| Works in the app, 403 in Python | The app computes a value you are not | Trace the request build in the app |
The failing reproduction is the symptom. The fix is never "copy harder". It is finding the one or two values the app regenerates and rebuilding them the way the app does. Once the request recomputes its own state, it behaves the way the app behaves.
A captured request is a frozen frame. A working reproduction recomputes the frame.
Each page below takes one reason a replayed request stops working and shows how to confirm it and rebuild the value the app computes. Start with the symptom you have.
The login is the entry point, but the token is the state the app leaves behind and later proves.
An empty Java method is not empty once the logic lives in a native library.
Base URLs hide in the dex, in resources, or behind native code.
Relevant capability
When a replayed request fails, the deliverable is the working request layer, not a longer list of captured requests. Request signing
Start a project
We rebuild the complete application flow and return the full API in Python, JavaScript/TypeScript, Postman and complete API documentation.