SReverseby Simpa Labs

Request reconstruction · signing

Why a captured Android request fails in Python

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.

What the copy actually holds

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.

Where the replay breaks

Arrange the copy next to what the app actually emits and the missing state is usually one of four things.

  • A signature over changing fields - the app signs the method, path, body, a timestamp and a nonce. The copy carries the signature for the moment you captured it; your replay has a fresh timestamp and body, so the signature no longer matches.
  • A value re-derived per call - a nonce, a request id, or an obfuscated identifier is generated for each request. Copying a past value makes the server treat the call as a replay.
  • A date-bearing header - X-Timestamp, Date, or an HMAC computed over a window. A stale value is rejected out of hand.
  • Session or token state - the request is bound to a token that was valid then and has since changed, or that is device-scoped.

How to confirm which one it is

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.

Isolating the missing state
replay.py
# 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.

Reproducing the generation

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.

The working reproduction
simpa_request.py
# 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.

The sign of a real replay problem

Status / behaviorWhat it points toWhere to look
403 with a generic bodyThe 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.
401Token or session expired / device-scopedThe auth and token flow
409 or "duplicate"Nonce or id replayedThe per-call value generator
Works in the app, 403 in PythonThe app computes a value you are notTrace the request build in the app

The part that actually matters

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.

Related research

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

Send the app you need to understand.

We rebuild the complete application flow and return the full API in Python, JavaScript/TypeScript, Postman and complete API documentation.

Projects start at $120. Most are delivered in 24 to 72 hours.

Start your full APK reconstruction

Send the full APK

Projects start at $120. Choose WhatsApp or email, then attach the APK in the app that opens. We reply within one hour with the next step and send the fixed quote after review.

Want us to contact you?