Part 2 ended with the punchline: editing the plaintext expiry in a profile file does nothing, because the enforcement code literally cannot read a string field. This post goes into the crypto layer properly — the envelope, the parser, the integrity checks, and a runtime check I will call the “runtime gate”.
A note on detail. This post explains the design of the protection scheme — the layers, the invariants, and why the obvious attacks fail. It intentionally omits the concrete constants, algorithm parameters, and addresses that would turn it into a reproduction guide. The mechanisms are the interesting part; the exact numbers are the part I am deliberately not publishing.
1. The envelope, at a structural level
The profile outer layer is plaintext JSON with a fixed key set and a fixed key order. Three content classes:
| Class | What it is |
|---|---|
| Plaintext metadata | user agent, GPU strings, window/screen geometry, Client Hints, kernel/version/release tags, an expiry string |
| An encrypted key block | a fixed-size blob, unique per file |
| The fingerprint ciphertext | a large blob whose length is always a multiple of the block size -> a block cipher (CBC / padded) |
| The outer expiry | an ISO 8601 string — invisible to the numeric-only enforcement lookup |
Two structural facts matter from here on:
- The key block is a fixed size and unique per file, with a first-byte distribution that rules out the most naive key-wrapping schemes.
- The fingerprint ciphertext length is always block-aligned, which tells you it is a block cipher, not a stream cipher.
2. The decryption pipeline
At a high level, loading a profile is a chain:
Callers (several entry points: the main profile loader, a second parser,
and the automation-script environment)
|
v
Parser
| one-time init:
| decrypt an embedded configuration blob using a custom key-derivation step
| (the two string inputs to that step are runtime-initialized globals,
| so they are not readable statically)
|
+- Unwrap the key block -> key material
| (embedded, obfuscated key material; see below)
|
+- Decrypt the fingerprint block -> inner profile data
An embedded-material math test. I checked whether the embedded key blobs are plain RSA components (testing p*q==n and (n,e,d) self-consistency across plausible exponents). None held. The key material is not stored as plain components — it is obfuscated by a reordering step plus a derivation step.
Offline-ness (carried over from Part 2): decryption involves no network. So the key material needed to decrypt locally must be embedded in the binary — otherwise it could not decrypt at all. This is the foundation for the “analysis is possible” judgment in Part 5.
3. The runtime gate
After parsing, the loader runs a check that gates success. I fully unwrapped what it is, and it is the most elegant piece of the design.
It is not a cryptographic signature over the file bytes. It is an in-process state channel. Two thread-local variables are written at three points along the decryption path, always as a matched pair with a fixed relationship between the two values. At the end of the pipeline, a gate checks that the pair still satisfies that relationship, and that the primary value is non-zero.
Three consequences:
- It proves the pipeline actually ran. The gate only opens if decryption genuinely advanced far enough to write the pair. You cannot satisfy it by setting a constant somewhere.
- The paired write is anti-single-point-tampering design. Change one value, or change the relationship constant, and you break the invariant — the gate fails.
- It is no extra obstacle to copy or to analysis. A legitimately copied profile decrypts normally and the gate passes. The obstacle is the encryption itself, not the gate.
The design lesson: this is a proof of work, not a signature. It makes sure the code path really executed, without needing a secret. That is a neat way to defend against “patch out the check” attacks without shipping a verifier key.
4. Failure means refusal: there is no fallback load
After a decrypt/verify failure, the dispatcher inspects a status flag and a selector, then jumps through a table. Decoding the table targets, they are all destructor / cleanup functions — every path ends in exit.
Conclusion: there is no fallback path that loads an unencrypted profile when verification fails. Decryption failure = profile load failure. A forged profile cannot cheat by bypassing checks; it must genuinely decrypt.
5. The expiry decision, summarized
- The numeric lookup only recognizes numbers -> the outer ISO string is invisible.
- A two-pass loop brings the inner (decrypted) numeric expiry into the decision.
- The clock comes from local time.
- Two independent guards sit on the headless and headed paths.
That is the complete reason “changing the outer expiry does nothing”: the outer string is for humans; the effective expiry is inside the ciphertext — and without the key you cannot change it.
6. Direct answers
Can a profile be copied directly? — Yes
- It is self-contained (key, ciphertext, metadata all in one file); no machine ID / hardware binding / one-time write was found.
- The backup file is byte-identical to the original -> the program never rewrites or “activates” a profile.
- Copy it to another machine, use another profile directory, and it works.
Can a profile be generated? — Conditionally feasible in principle
In principle yes — if the decryption key material is embedded, the corresponding public material needed to encrypt a new file must be obtainable from the binary or derivable from the key material. But there are hard prerequisites, all of which are exactly the pieces I am not publishing:
- Recover the public key / key material despite the reordering and derivation.
- Confirm the key-wrapping scheme and its parameters.
- Confirm the block-cipher mode and IV convention.
- Match the inner data schema exactly.
Once those are in hand, generation is essentially: build inner data -> block-cipher encrypt -> wrap the key -> assemble the envelope. The runtime gate and the outer expiry are no extra obstacle to a correctly encrypted file.
Alternative if the goal is just “use an existing profile indefinitely”: the patch route is far cheaper than fully replicating the crypto. Full generation only pays off if you need to mass-produce new profiles.
7. Open questions at this stage
| # | Question | Status |
|---|---|---|
| 1 | The internal key-derivation step (inputs/outputs known, internals not fully disassembled) | open |
| 2 | Two runtime-initialized string constants feeding the derivation | open (suspected salt / label) |
| 3 | The reordering scheme | open |
| 4 | Plaintext location of the public key | ruled out as plain storage in the embedded blobs |
| 5 | Block-cipher mode / IV, inner data schema | needs the key, or dynamic analysis |
| 6 | Some failure-dispatcher selector entries | unaligned addresses, decoding uncertain (does not affect “no fallback”) |
| 7 | The role of a separate RSA public key in the binary | unrelated to profile verification; exact use undetermined |
Most of these get resolved in Part 5, where the key material is recovered and the whole chain is analyzed offline.
8. Method appendix: cross-reference scanning
To find all references to a global in a stripped binary, scan for the architecture’s page-relative address + immediate instruction pairs (on arm64, ADRP + ADD-immediate; on x86-64, RIP-relative lea/mov). Building a small, correct scanner for this one pattern is what makes the whole series possible: it turns “somewhere in a few hundred megabytes” into “these specific sites”. The subtlety is getting the immediate encoding exactly right — one wrong mask bit and you silently miss every reference.
Series navigation:
- Part 1: What an Anti-Detection Browser Actually Is
- Part 2: Static Recon Without Running It
- Part 3: The Crypto Layer and the Runtime Gate (this post)
- Part 4: Binary Patching as a Research Exercise
- Part 5: Offline Analysis of the Encrypted Profile
- Part 6: GeoIP, Timezone Mismatch, and Database Forensics