Files
2026-04-16 04:45:22 -07:00

196 lines
8.3 KiB
Markdown

# Paralogue MITM Toolkit
Man-in-the-middle proxy and protocol analysis toolkit for **Operation Lovecraft: Fallen Doll** (Paralogue). Intercepts, decodes, modifies, and injects game network traffic in real time via a desktop GUI.
## Features
- **Real-time packet inspector** — decoded protobuf messages with JSON syntax highlighting and hex dump
- **Bidirectional wire decryption** — ChaCha20 key extraction via Frida, proxy-side decrypt/re-encrypt
- **Packet modification** — frame-aware S->C rewriter with pluggable modifier callbacks
- **Packet injection** — send custom or replayed packets in both C->S and S->C directions
- **Full type mapping** — all 191 dispatch-table msg_ids resolved to protobuf type names via Frida GetTypeName()
- **ImGui GUI** with 6 tabs: Packets, Config, Session, Wire, Dispatch, Composer
## Requirements
- Python 3.12+ (tested on 3.13)
- Windows 10/11
- [Frida](https://frida.re/) (`pip install frida-tools`)
- [imgui-bundle](https://github.com/pthom/imgui_bundle) (`pip install imgui-bundle`)
- [cryptography](https://pypi.org/project/cryptography/) (`pip install cryptography`)
- [protobuf](https://pypi.org/project/protobuf/) (`pip install protobuf`)
```
pip install frida-tools imgui-bundle cryptography protobuf
```
## Quick Start
### GUI mode (recommended)
```bat
cd frida_hooks
run_gui.bat
```
1. GUI opens with proxy listening on ports 11001 (game) and 18801 (HTTP dispatch)
2. Click **"Launch Game (Steam)"** or launch via Steam manually
3. Frida auto-attaches, hooks installed, redirect active
4. Log in — packets stream into the **Packets** tab
5. Toggle modifiers in the **Config** tab
6. Click any packet → **Send** tab → **"Replay Original"** or **"Open in Composer"**
### Headless mode (CLI)
```bat
cd frida_hooks
run_mitm.bat :: normal passthrough
run_mitm.bat --inject-items :: with all modifiers active
run_mitm.bat --no-decoder :: proxy only, no Frida
```
### Re-extract type names (after game update)
```bat
:: Game must be running
cd frida_hooks
run_extract_types.bat
```
Updates `dispatch_type_names.json`, `known_msg_ids.json`, and `factory_to_type.json`.
## Architecture
```
┌────────────┐ ┌─────────────────────┐ ┌──────────────┐
│ Game │◄──►│ Proxy (port 11001) │◄──►│ Real Server │
│ Client │ │ ┌───────────────┐ │ │ 3.33.185.193│
│ │ │ │ WireRewriter │ │ │ │
│ Frida ────┼────┤ │ S->C + C->S │ │ │ │
│ hooks │ │ └───────────────┘ │ │ │
└────────────┘ │ ┌───────────────┐ │ └──────────────┘
│ │ Modifier Chain│ │
│ └───────────────┘ │
└──────────┬──────────┘
│ SharedState
┌──────────▼──────────┐
│ ImGui GUI │
│ (main thread) │
└─────────────────────┘
```
### Threading model
- **Main thread**: ImGui render loop at ~60fps
- **Background daemon thread**: asyncio event loop running the TCP proxy, Frida bridge, and injection consumer
- **SharedState**: thread-safe bridge (deque for packets, Lock for config, inject_queue for packet injection)
### Encryption
The game uses **ChaCha20** stream cipher with a shared key+nonce for both directions but independent byte counters:
| Offset | Field | Usage |
|--------|-------|-------|
| +0x68 | key ptr | 32B ChaCha20 key (shared) |
| +0x78 | nonce ptr | 12B nonce (shared) |
| +0x88 | EVP ctx | encrypt (C->S) |
| +0x90 | EVP ctx | decrypt (S->C) |
| +0x98 | counter | encrypt byte offset (C->S) |
| +0xA0 | counter | decrypt byte offset (S->C) |
The proxy uses `WireRewriter` per direction to decrypt→modify→re-encrypt. ChaCha20 XOR involution means identity transforms produce byte-identical output.
### Wire format
```
[2B big-endian prefix N][N-byte PacketHead protobuf][body_len bytes body]
```
PacketHead fields: `msg_id`, `body_len`, `packet_id` (request-response correlation), `seq_id`, `flag`, `total_pack_count`.
## GUI Tabs
### Packets
Real-time scrolling table of decoded messages. Filter by direction, source (Frida/wire), msg_id, or type name. Click a row to expand:
- **Tree** — recursive protobuf field viewer
- **JSON** — syntax-highlighted JSON via TextEditor
- **Hex** — formatted hex dump with ASCII sidebar
- **Send** — replay original or open in Composer
### Config
Toggle modifiers individually:
- **Card Bag Expand** — `CardBagOpenRsp.rets` → 0..2000 (unlock experiments)
- **Inject Blueprints** — add 11000..11999 to bag (lootbox items)
- **Role Attrs Saturate** — max affection, dilation, unlock all roles
- **Inject Bag Items** — dense fill across cloth/card/normal ranges
- **Unlock Functions** — populate `PlayerMainDataRsp.unlockFunctions`
Parameter sliders for card range, attr values. Auto-saves to `gui_config.json`.
### Session
Session key/nonce, game PID, Frida status (color-coded), proxy ports, dispatch summary, packet counts. Action buttons:
- **Restart Frida** — reattach after game relaunch
- **Launch Game (Steam)** — opens via `steam://rungameid/2701680`
### Wire
Raw encrypted traffic hex viewer with timestamps and direction arrows.
### Dispatch
All 191 msg_id → protobuf type mappings. Green = pinned (resolved), red = unknown. Searchable by type name.
### Composer
Compose and send custom protobuf packets:
1. Select type from dropdown (all 191 dispatch types)
2. Edit JSON in syntax-highlighted editor
3. Choose direction (C->S / S->C)
4. Click Send
## Files
| File | Purpose |
|------|---------|
| `frida_hooks/paralogue_gui.py` | ImGui GUI application |
| `frida_hooks/paralogue_mitm.py` | Core proxy + Frida bridge + WireRewriter |
| `frida_hooks/paralogue_decoder.py` | Protobuf framing, identification, parsing |
| `frida_hooks/protocol_decoder.js` | Frida hooks for decrypt/encrypt wrappers |
| `frida_hooks/redirect_connect.js` | Frida ws2_32!connect redirect hook |
| `frida_hooks/dispatch_dump.js` | Frida dispatch table walker |
| `frida_hooks/extract_type_names.js` | Frida GetTypeName() extraction |
| `frida_hooks/item_table_dump.js` | Frida bag item subsystem walker |
| `frida_hooks/paralogue_net_full.proto` | 239-message protobuf schema |
| `frida_hooks/paralogue_net_full_pb2.py` | Compiled protobuf module |
| `frida_hooks/dispatch_type_names.json` | msg_id → type name (ground truth, 191 entries) |
| `frida_hooks/factory_to_type.json` | vtable → type name (stable across runs) |
| `frida_hooks/known_msg_ids.json` | Merged msg_id → type cache |
| `frida_hooks/gui_config.json` | Persisted GUI modifier toggles |
| `frida_hooks/run_gui.bat` | GUI launcher |
| `frida_hooks/run_mitm.bat` | CLI launcher |
| `frida_hooks/run_extract_types.bat` | Type extraction launcher |
| `RESEARCH.md` | Detailed protocol research notes |
## Game Data Model
### Bag items (BagItemFullS2C, msg_id 1036)
| ID range | Category | Description |
|----------|----------|-------------|
| 0-999 | Experiments/Cards | Unlocked via CardBagOpen |
| 11000-11999 | Pack/Blueprints | Lootbox items (open → grant card) |
| 20000-28999 | Card/Normal | Collectibles |
| 31000-31999 | Cloth | Outfit pieces |
| 40000-40999 | Normal | Misc items |
| 99000-99999 | Money | Currencies (with counts) |
### Experiments
- Stored as client-side `learn_cards` collection (persisted on disk)
- Updated incrementally via `CardBagOpenRsp.rets[].card_id`
- UI filters by client DataTable (invalid IDs silently dropped)
- Unlock all: enable "Card Bag Expand" modifier, open one blueprint
### Roles (RoleListFullS2C, msg_id 1202)
Per-role data: `name`, `is_unlock`, `favor_lv`, `worn_clothes[]`, `preset_clothes[]`, `attrs{}`, `appearance`, `clothes_colors`.
## License
Research and educational purposes only. Not affiliated with the game developers.