8.5 KiB
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(RVA0x49BD0F0). It reads[2B prefix][header][body]from the socket and callssub_147102D30on each blob to decrypt/deserialise. sub_147102D30(RVA0x7102D30) invokes(*(a1->vtbl + 72))(a1, buffer, ...)— a virtual dispatch that almost certainly routes into the decrypt cipher. The vtable pointer at*a1is the decrypt context we want.- Alternative trail:
sub_1449CEA90(send path) callssub_1449A52C0for encryption. Look for a sibling decrypt helper nearsub_1449A52C0in the same translation unit — it likely uses cipher-context fields at+0x88/+0x98instead 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.
- In
GameProxy.pipe(), whendirection == 'C->S', decrypt using the C->S context's key + per-direction byte counter. - When
direction == 'S->C', decrypt using the S->C context's key + its own counter. - Feed decrypted bytes into a per-direction
Stream, drain, emit tomitm_<ts>_decoded.jsonl. - 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 classmapping 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
VerifyLoginTokenRspbecause it containschacha20_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.jsonlfor messages withbody_len > 500andmsg_type.endswith("FullS2C"). Those are the initial-state pushes. - Grep for specific content: role names (
Alet,Celia),is_unlockfield values,ClothesCellrepeated 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
thispointer, 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 value1. 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