Files
Operation-Lovecraft-Fallen-…/NEXT_STEPS_PARALOGUE_NET.md
2026-04-15 22:24:44 -07:00

8.5 KiB
Raw Permalink Blame History

Next steps — Paralogue / Fallen Doll protocol research

Short, actionable TODO list, ordered by "what to try next". Each item explains why, where to look, and roughly how much work.

1. Find the S->C decrypt cipher context

Why. We can decrypt C->S wire traffic purely from the captured ChaCha20 key (via cryptography library), but S->C fails — the S->C cipher context's key is stored at different this-struct offsets or in a different object entirely. Finding it unlocks full Frida-less wire decryption.

Where to look.

  • Start from the packet reader sub_1449BD0F0 (RVA 0x49BD0F0). It reads [2B prefix][header][body] from the socket and calls sub_147102D30 on each blob to decrypt/deserialise.
  • sub_147102D30 (RVA 0x7102D30) invokes (*(a1->vtbl + 72))(a1, buffer, ...) — a virtual dispatch that almost certainly routes into the decrypt cipher. The vtable pointer at *a1 is the decrypt context we want.
  • Alternative trail: sub_1449CEA90 (send path) calls sub_1449A52C0 for encryption. Look for a sibling decrypt helper near sub_1449A52C0 in the same translation unit — it likely uses cipher-context fields at +0x88/+0x98 instead of +0x90/+0xA0.

Hook idea. Once found, attach with Frida onEnter, read this+0x68 and this+0x78 (key and nonce), emit via send({t:'session_key', flag:0, ...}). Python is already prepared to accept multiple contexts keyed by (this_id, key).

Effort. 30–60 min of IDA work + one JS hook + one test.

2. Make the proxy do full wire decryption once both keys are captured

Why. After step 1 we'll have keys for both directions. The proxy can then decrypt wire traffic locally (no Frida runtime dependency for the plaintext output) and run it through the existing drain_stream + identify_body pipeline.

Concrete wiring.

  1. In GameProxy.pipe(), when direction == 'C->S', decrypt using the C->S context's key + per-direction byte counter.
  2. When direction == 'S->C', decrypt using the S->C context's key + its own counter.
  3. Feed decrypted bytes into a per-direction Stream, drain, emit to mitm_<ts>_decoded.jsonl.
  4. Disable the Frida chunk path (or keep it as a sanity-check against the proxy's own decryption).

Effort. ~40 lines in paralogue_mitm.py, most of it plumbing. Blocked on step 1.

3. Fix msg_id identification ambiguity

Why. Many S2C types (AchievementFullS2C, ClothesPresetUpdateS2C, AchievementStepUpdateS2C, BagItemFullS2C, ...) have wire-compatible field layouts and all score ~104 in the identifier. The current top-3 is frequent; the "winner" is often wrong.

Approach.

  • Hook the protobuf dispatch table inside the game. On the plaintext-parse side the game has a msg_id -> protobuf class mapping somewhere — probably a static array of function pointers. Finding that gives us ground-truth for every single msg_id at once.
  • Alternatively, log every message's top candidate to JSONL, then hand-label the highly-ambiguous ones against expected content (login response must be VerifyLoginTokenRsp because it contains chacha20_key; role list must contain "Alet", "Celia", etc.).
  • Save confirmed mappings into known_msg_ids.json. The runtime auto-loads and prefers them.

Effort. Dispatch-table hunt: 60–90 min IDA. Hand-labeling: ongoing.

4. Decode FullS2C payloads (clothes, research, experiments)

Why. The user's original question: "I can't see info about experiments / clothes unlocked". Those live in large initial-state push messages (RoleListFullS2C, BagItemFullS2C, ClothesPresetFullS2C if it exists, etc.) which the current captures either mis-identify as smaller types or never reach because of framing breakage mid-session.

Checklist.

  • After step 3, re-run the decoder through a fresh login sequence.
  • Inspect mitm_<ts>_decoded.jsonl for messages with body_len > 500 and msg_type.endswith("FullS2C"). Those are the initial-state pushes.
  • Grep for specific content: role names (Alet, Celia), is_unlock field values, ClothesCell repeated entries under role appearance.

5. RSA key-replacement MITM (future)

Why. Full Frida-independent proxy — including the RSA handshake. Would let the proxy run against an unmodified game binary without any in-process instrumentation for key extraction.

Plan.

  • Locate the server's RSA public key in Paralogue-Win64-Shipping.exe (grep for the modulus; standard RSA-2048 pubkey = 256-byte blob).
  • Patch the binary to use our own pubkey instead.
  • In the proxy: decrypt the client's KEY_A payload using our private key (which gives the client-chosen chacha20 key material), then re-encrypt with the real server's pubkey before forwarding. Do the mirror on KEY_B.
  • We then know the session ChaCha20 key without any Frida hook.

Effort. 2–4 hours. Prerequisites: binary patcher (or Frida hook to rewrite the pubkey at load time), RSA crypto code, confidence in the KEY_A/ KEY_B protobuf envelope format.

6. Write a protobuf-schema verifier

Why. The current 239-message schema was recovered from in-memory FileDescriptorProto blobs. Some fields may have been misidentified (wire types can ambiguously map string vs bytes, packed vs unpacked). A verifier script that round-trips every captured body through its identified type would surface mismatches.

Script sketch. Iterate through mitm_*_decoded.jsonl; for every record with a non-"" type, re-encode fields via json_format.ParseDict() + SerializeToString() and compare length and byte-equality against the original body in the chunks file. Log mismatches.

7. Modification experiments

Why. Once proxy decryption is bidirectional and the schema is trusted, we can try modifying outbound protobuf messages at the proxy.

Minimal test: intercept ClothesEquipReq, log it, forward unmodified. Confirm the proxy can reconstruct the wire bytes byte-for-byte by re-encoding the parsed protobuf and re-encrypting — proving the round-trip works.

Next: modify RoleListFullS2C inbound to flip RoleCell.is_unlock = true for all characters.

Depends on: steps 1, 2, 3.

8. Handle the HTTP dispatch side too

Why. The HTTP proxy on port 18801 currently logs the first line of each request/response. /get_notice returns JSON with patch notes. Might also return server-state flags worth inspecting (maintenance mode, server-open status, etc.).

Effort. Tiny. Decode JSON responses, pretty-print to the same mitm_<ts>_decoded.jsonl under a separate source: "http" tag.

Open questions / things to verify

  • Is the decrypt cipher context a separate this pointer, or the same pointer with different struct offsets? Step 1 answers this.
  • Do C->S and S->C share the ChaCha20 key with different nonces, or entirely different keys? All keys we've captured so far have been the C->S encrypt context; we have no S->C sample to compare.
  • When the user rejoins mid-session, does the game re-key (new session key via a fresh VerifyLoginTokenRsp) or re-use the previous key? Our captures show key rotation on full reconnect — worth confirming what triggers it.
  • [this+0x60] flag semantics — we only ever observe value 1. What does the flag actually select inside the wrapper? Read the disassembly of 0x49A2EB0 to find out.

Files that currently exist

File Role
frida_hooks/paralogue_mitm.py merged proxy + decoder, single process
frida_hooks/paralogue_decoder.py shared framing + identification
frida_hooks/protocol_decoder.js Frida recv+send hooks, key emit
frida_hooks/redirect_connect.js ws2_32!connect port-11001 redirect
frida_hooks/run_decoder.py standalone decoder (no proxy)
frida_hooks/tcp_proxy_decrypt.py old proxy (still has keystream-XOR path)
frida_hooks/known_msg_ids.json learned msg_id → type cache
frida_hooks/paralogue_net_full.proto 239-message schema
frida_hooks/paralogue_net_full_pb2.py compiled schema
frida_hooks/captures/mitm_<ts>_*.jsonl per-session output
frida_hooks/captures/current_session_key.json live cipher-context keys

Quick-start, once you pick this back up

PY=X:/Users/user/AppData/Local/Programs/Python/Python313/python.exe

# 1. Game must be closed first.
# 2. Start merged tool:
$PY -u frida_hooks/paralogue_mitm.py

# 3. Launch game via Steam. The tool auto-attaches to the process.
# 4. Log in / reconnect. Messages stream live; decoded.jsonl grows.
# 5. Ctrl-C when done. Summary prints.

To run decoder-only (no proxy, no redirect):

$PY -u frida_hooks/paralogue_mitm.py --no-proxy