mirror of
https://github.com/renorris/openfsd
synced 2026-08-10 19:36:08 +08:00
docs/clientinject: vPilot 3.12.1 research gates (R1–R5)
Document free-slot / JWT site / config path / GeoVR findings for 3.12.1. Update embed profile when gates close; synthetic residual-scan fixtures. Honest open-gate status where PE analysis cannot confirm.
This commit is contained in:
183
docs/client-injector/research/vpilot-3.12.1-gates.md
Normal file
183
docs/client-injector/research/vpilot-3.12.1-gates.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# vPilot 3.12.1 — research gate status (R1–R5)
|
||||
|
||||
**Date:** 2026-07-28
|
||||
**PE:** `vPilot.exe` SHA-1 `7d95a7110392c15728143cc30e1f00899c686eb5` (size 1 236 416)
|
||||
**Method:** local extract under gitignored `.research/vpilot/extracted/` (never committed); #US heap walk; UTF-16 residual scan; GeoVR DLL inventory; prior-art cross-check (`vpilot-patch-utility` 3.11.1, `openfsd-client-patch-utility`).
|
||||
**Full notes:** [`vpilot-3.12.1.md`](./vpilot-3.12.1.md)
|
||||
|
||||
| ID | Gate | Status | Profile impact |
|
||||
|----|------|--------|----------------|
|
||||
| **R1** | Free `#US` slot catalog + ldstr remap | **OPEN** (catalog documented; no safe unreferenced slots) | `us_free_slots: []` |
|
||||
| **R2** | AFV connect `ret` CIL offset | **OPEN** | `afv_disable_pe.file_offset: null` |
|
||||
| **R3** | Second fsd-jwt body `0xBA44A` liveness | **CLOSED — not live** | Keep primary only: `body_file_offsets: [0xB7988]` |
|
||||
| **R4** | Config path resolution | **CLOSED** (candidates documented) | `config_files` still install-relative; multi-candidate is adapter discovery |
|
||||
| **R5** | GeoVR voice base inventory | **CLOSED — no re-hardcode** | No extra DLL mutations required for AFV base URL |
|
||||
|
||||
## Phase 0 readiness (honest)
|
||||
|
||||
| Claim | OK? |
|
||||
|-------|-----|
|
||||
| Short-host lab JWT (in-place #US budget ≤71 body bytes; host ≤12 on default `/api/v1/fsd-jwt`) | **Yes** (profile offsets confirmed) |
|
||||
| Production-length JWT hostnames without A8 | **No** — needs **R1** free-slot remap **or** openfsd **A8** short path (`/j` fixed, etc.) |
|
||||
| AFV base retarget when URL fits #US budget 51 | **Yes** if host short enough; URL only in `vPilot.exe` (R5) |
|
||||
| AFV PE `ret` disable fallback | **No** until R2 |
|
||||
| “No residual stock JWT UTF-16 in whole PE” | **No** — dead copy at `0xBA44A` remains after primary patch (R3); residual HealthCheck must **allowlist** that offset or stay warn-level for full-PE scan |
|
||||
| Config rewrite path | **Yes** for default install (`%LOCALAPPDATA%\vPilot\`); multi-candidate ordered list in R4 |
|
||||
|
||||
**Do not market adapter complete** for arbitrary production hostnames while R1 is open (unless A8 is deployed and PreferShortJWTPath is used).
|
||||
|
||||
---
|
||||
|
||||
## R1 — Free `#US` slot catalog (**OPEN**)
|
||||
|
||||
### Findings
|
||||
|
||||
- `#US` stream: file `0xA22E8`, size `0x15E04`; ~2040 decode-OK entries.
|
||||
- **Zero** entries with body budget ≥71 have **ldstr reference count 0**. Every long slot is referenced at least once in CIL (`ldstr` token `0x72` + little-endian `(0x70<<24)|heap_offset`).
|
||||
- 3.11.1 prior art remapped JWT `ldstr` to heap `0xD2A2` (sacrificing whatever string lived there). On **3.12.1**, `0xD2A2` is **mid-body** of another entry (`heap=0xD254`, message about remote events) — **not** a valid free entry start. **3.11.1 free-slot offsets are obsolete.**
|
||||
|
||||
### Implication
|
||||
|
||||
Remap on 3.12.1 requires **overwriting a live cosmetic (or other) string** and pointing the JWT `ldstr` at that heap offset. That is a product/UX trade-off, not a pure “unused” slot. Without runtime confirmation that sacrificing a given message is acceptable (and that Dotfuscator/call sites behave), **profile `us_free_slots` stays empty**.
|
||||
|
||||
### Candidate sacrifice catalog (not profile-enabled)
|
||||
|
||||
Single-`ldstr`, large budget, non-URL strings (sample; full scan in notes). Prefer **not** model-matching or voice-error strings for first experiments:
|
||||
|
||||
| heap | body file | budget (UTF-16+term) | ldstr file | Stock (preview) |
|
||||
|------|-----------|----------------------|------------|-----------------|
|
||||
| `0x12EEE` | `0xB51D8` | 1053 | `0x3763C` | Config file updated to latest version… |
|
||||
| `0x0DE5A` | `0xB0144` | 421 | `0x32F94` | Command failed. The active simulator was not found… |
|
||||
| `0x06B9A` | `0xA8E84` | 387 | `0x1E7A7` | First-time run configuration message… |
|
||||
| `0x03062` | `0xA534C` | 343 | `0x9144` | Unknown type code… |
|
||||
| `0x01848` | `0xA3B32` | 207 | `0x3866` | Debug messages saved to disk… |
|
||||
| `0x01DB9` | `0xA40A3` | 195 | `0x420E` | Model matching rule test complete… |
|
||||
|
||||
**JWT ldstr to rewrite when remapping:** file `0x4BDB5` (`72 9F 56 01 70` → stock heap `0x1569F`).
|
||||
Remap bytes: `72 <heap_le24> 70` with new heap offset.
|
||||
|
||||
### Close criteria (still open)
|
||||
|
||||
1. Pick ≥1 sacrifice slot; document accepted UX breakage.
|
||||
2. Runtime smoke: connect openfsd with long JWT URL via remap.
|
||||
3. Populate `us_free_slots` in embed profile + golden residual/plan tests.
|
||||
|
||||
---
|
||||
|
||||
## R2 — AFV connect `ret` CIL offset (**OPEN**)
|
||||
|
||||
### Findings
|
||||
|
||||
| Item | 3.11.1 prior art | 3.12.1 |
|
||||
|------|------------------|--------|
|
||||
| AFV disable | `ret` (`0x2A`) at file `0x4BA54` | **Obsolete** — bytes at `0x4BA54` are unrelated (`DD …` leave / other CIL) |
|
||||
| Voice base `ldstr` | (different heap) | **Confirmed** file `0x1F0A7` → heap `0x6D8D` (`https://voice1.vatsim.net`) |
|
||||
| CLI | — | `-novoice` / `/novoice` present as #US CLI help strings |
|
||||
|
||||
CIL around `0x1F0A7` loads the voice base URL into connection setup (`ldstr` + `ldftn`/`newobj` pattern) — **not** identified as a simple event-handler entry suitable for a one-byte `ret` patch without method-boundary confirmation.
|
||||
|
||||
No ILSpy/ilspycmd run in this environment; **ConnectToVoiceServer** symbols live in **GeoVR.Connection.dll**, not as a clear single `ret` site in `vPilot.exe` matching 3.11.1.
|
||||
|
||||
### Phase 0 fallback
|
||||
|
||||
- Prefer AFV #US retarget when budget allows.
|
||||
- Else launch flags `-novoice` / `/novoice`.
|
||||
- **Do not** set `mutations.afv_disable_pe.file_offset` until a confirmed 3.12.1 handler start is known.
|
||||
|
||||
### Close criteria
|
||||
|
||||
Decompile or symbol-map the AFV connect path on 3.12.1; set `file_offset` + fixture test; only then enable PE disable mutation.
|
||||
|
||||
---
|
||||
|
||||
## R3 — Second fsd-jwt body `0xBA44A` (**CLOSED — not live**)
|
||||
|
||||
### Findings
|
||||
|
||||
| Check | Primary `0xB7988` | Second `0xBA44A` |
|
||||
|-------|-------------------|------------------|
|
||||
| Inside `#US` heap (`0xA22E8`–`0xB80EC`) | **Yes** | **No** |
|
||||
| Valid #US body terminal after UTF-16 | `0x01` (matches ECMA-335 for `fsd-jwt`) | **`0x04`** (not a #US terminal) |
|
||||
| Compressed length prefix at body−1/2/4 | Yes (prefix at heap entry) | No consistent live #US header |
|
||||
| PE section | `.text` | `.text` (not `.rsrc`) |
|
||||
| LE refs to file offset / RVA / VA | none (expected; CLR uses tokens) | **none** |
|
||||
| UTF-16 residual scan hits | 1 of 2 | 1 of 2 |
|
||||
|
||||
Pre-context at `0xBA44A` is binary framing (`… 04 88 13 00 00 04 98 3A … 08 2D 43 1C EB E2 36 0A 3F 46` then UTF-16 URL) — consistent with **embedded managed resource / serialized constant data**, not the live `#US` entry consumed by `ldstr 0x4BDB5`.
|
||||
|
||||
### Profile decision
|
||||
|
||||
- **Do not** add `0xBA44A` to `strings.fsd_jwt.body_file_offsets`.
|
||||
- Patching only the live `#US` body (+ length prefix) is correct for runtime JWT.
|
||||
- Full-PE residual scan will still report `0xBA44A` after a successful primary patch → HealthCheck must treat it as a **documented dead hit** (allowlist) or remain **warn-level** until residual policy is implemented. This is **not** a reason to dual-write.
|
||||
|
||||
### Residual-scan test policy
|
||||
|
||||
Synthetic fixtures (two live-style UTF-16 copies) validate the scanner. Real PE research test (optional `research` tag) asserts exactly two hits at `0xB7988` and `0xBA44A` on stock 3.12.1.
|
||||
|
||||
---
|
||||
|
||||
## R4 — Config path resolution (**CLOSED**)
|
||||
|
||||
### Candidates (Windows, ordered)
|
||||
|
||||
1. `{installRoot}/vPilotConfig.xml` — **default**: install root is `%LOCALAPPDATA%\vPilot\` (official non-admin layout); shipping extract places config beside `vPilot.exe`.
|
||||
2. `%LOCALAPPDATA%\vPilot\vPilotConfig.xml` — same as (1) when install is default; still probe when user selected a **custom** install root.
|
||||
3. Path recorded in a previous `.openfsd-client` / inject manifest (adapter/engine).
|
||||
4. **Not observed** as a separate roaming `AppData\Roaming\vPilot` path in PE string inventory; PE #US includes `vPilotConfig.xml` and “configuration file was not found” messaging only (no alternate absolute path string).
|
||||
|
||||
### Stock defaults (installer extract)
|
||||
|
||||
Decrypt (3DES profile crypto) of extract `vPilotConfig.xml`:
|
||||
|
||||
- `NetworkStatusURL` → `http://status.vatsim.net/`
|
||||
- `CachedServers` → `AUTOMATIC|fsd.connect.vatsim.net`
|
||||
|
||||
### Adapter behavior (normative from design)
|
||||
|
||||
- Discover all existing candidates; rewrite existing only.
|
||||
- If **zero** config files exist: blocker — run vPilot once to create config (do not invent a create path without further evidence).
|
||||
- Profile `config_files[0].relative_path` remains `vPilotConfig.xml` (relative to install root). Multi-candidate logic is discovery code, not additional YAML rows, until a second distinct relative layout is proven.
|
||||
|
||||
---
|
||||
|
||||
## R5 — GeoVR string inventory (**CLOSED**)
|
||||
|
||||
Scanned install-tree managed binaries for `voice1.vatsim.net` / `https://voice1.vatsim.net` (ASCII + UTF-16):
|
||||
|
||||
| Binary | `voice1.vatsim.net` |
|
||||
|--------|---------------------|
|
||||
| `vPilot.exe` | **Yes** (UTF-16; live #US + residual forms as above) |
|
||||
| `GeoVR.Client.dll` | **No** |
|
||||
| `GeoVR.Connection.dll` | **No** |
|
||||
| `GeoVR.Shared.dll` | **No** |
|
||||
| Other `*.dll` in extract | **No** |
|
||||
|
||||
GeoVR.Connection exposes `ApiServerConnection`, `ConnectToVoiceServer`, etc. — base URL is supplied by the host (`vPilot.exe`), not re-hardcoded in GeoVR packages for 3.12.1 extract.
|
||||
|
||||
**Conclusion:** AFV base retarget of the `vPilot.exe` #US slot is **sufficient** for stock voice URL redirection. No GeoVR DLL mutations required for R5. (Does not close R2 PE-disable.)
|
||||
|
||||
---
|
||||
|
||||
## Fingerprint re-check (this gate pass)
|
||||
|
||||
| Artifact | SHA-1 |
|
||||
|----------|-------|
|
||||
| `vPilot.exe` | `7d95a7110392c15728143cc30e1f00899c686eb5` |
|
||||
| Installer (recorded) | `48820cb593c6cef8325a331c763316a8b33a501b` |
|
||||
|
||||
Confirmed live #US map unchanged from research seed:
|
||||
|
||||
| String | heap | body | ldstr | budget |
|
||||
|--------|------|------|-------|--------|
|
||||
| fsd-jwt | `0x1569F` | `0xB7988` | `0x4BDB5` | 71 |
|
||||
| AFV base | `0x6D8D` | `0xA9076` | `0x1F0A7` | 51 |
|
||||
| fsd auto HTTP | `0x15751` | `0xB7A3A` | `0x4C0B2` | 45 |
|
||||
|
||||
---
|
||||
|
||||
## What was intentionally not committed
|
||||
|
||||
- Any `vPilot.exe` / GeoVR / installer PE or DLL
|
||||
- Local analysis scripts under `/tmp` or `.research/`
|
||||
- Populated `us_free_slots` or AFV `ret` offset without confirmation
|
||||
@@ -4,7 +4,9 @@
|
||||
**Installer:** `vPilot-Setup-3.12.1.exe` (NSIS)
|
||||
**Legal posture:** Research notes + hashes only. **Never commit or redistribute** vPilot binaries. Users install from [vpilot.rosscarlson.dev](https://vpilot.rosscarlson.dev/Download).
|
||||
|
||||
Tracked profile: `third_party/client-profiles/vpilot-3.12.1.yaml`
|
||||
Tracked profile (embed canonical): `internal/clientinject/profiles/vpilot-3.12.1.yaml`
|
||||
Mirror metadata: `third_party/client-profiles/vpilot-3.12.1.yaml`
|
||||
**Gate status (R1–R5):** [`vpilot-3.12.1-gates.md`](./vpilot-3.12.1-gates.md)
|
||||
|
||||
## Fingerprints
|
||||
|
||||
@@ -13,7 +15,7 @@ Tracked profile: `third_party/client-profiles/vpilot-3.12.1.yaml`
|
||||
| Installer | `48820cb593c6cef8325a331c763316a8b33a501b` | `528a51bf0e71ada11103314d18a9fc0321afac94e2706f2fb13e0fbf5bb4beaa` |
|
||||
| `vPilot.exe` | `7d95a7110392c15728143cc30e1f00899c686eb5` | `c743f204929309db6f49e8b543db2d661ad09c32a2a7bc71fe419c685d244bff` |
|
||||
|
||||
Default install directory: `%LOCALAPPDATA%\vPilot\` (per official docs — no admin required).
|
||||
Default install directory: `%LOCALAPPDATA%\vPilot\` (per official docs — no admin required). Size of primary PE: **1 236 416** bytes.
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -27,11 +29,11 @@ Default install directory: `%LOCALAPPDATA%\vPilot\` (per official docs — no ad
|
||||
|
||||
| Surface | Stock value (3.12.1) | openfsd target | Notes |
|
||||
|---------|----------------------|----------------|-------|
|
||||
| FSD JWT auth | `https://auth.vatsim.net/api/fsd-jwt` | `https://{host}/api/v1/fsd-jwt` | Hardcoded #US string; TLS required |
|
||||
| FSD JWT auth | `https://auth.vatsim.net/api/fsd-jwt` | `https://{host}/api/v1/fsd-jwt` (or short A8 path) | Hardcoded #US string; TLS required |
|
||||
| Network status / server list | Config `NetworkStatusURL` → `http://status.vatsim.net/` | `https://{host}/api/v1/data/status.txt` | 3DES-obfuscated in `vPilotConfig.xml` |
|
||||
| Cached servers | `AUTOMATIC\|fsd.connect.vatsim.net` | e.g. `OPENFSD\|fsd.example.com` | Same config crypto; `NAME\|host` |
|
||||
| Automatic server HTTP | `http://fsd.vatsim.net/` | optional / unused if cached list set | "best server" HTTP endpoint (changelog 3.4.10+) |
|
||||
| AFV REST base | `https://voice1.vatsim.net` | openfsd `AFV_API_PUBLIC_BASE_URL` | Hardcoded #US; prior patch utilities **disabled** AFV instead of retargeting |
|
||||
| AFV REST base | `https://voice1.vatsim.net` | openfsd `AFV_API_PUBLIC_BASE_URL` | Hardcoded #US in **vPilot.exe only** (R5); prior patch utilities often **disabled** AFV instead |
|
||||
| Flight plan browser | `https://my.vatsim.net/pilots/flightplan` | optional openfsd/web | Cosmetic / UX |
|
||||
| Model matching CDN | `vpilot.rosscarlson.dev/ModelMatchingData/*` | leave alone | Not openfsd-related |
|
||||
| Updates | `vpilot.rosscarlson.dev` VersionCheck | leave alone | |
|
||||
@@ -54,9 +56,20 @@ Source of truth for this algorithm: archived `renorris/vpilot-patch-utility` (`c
|
||||
|
||||
`Public.key` in install tree is an EC P-256 SPKI (licensing/update verify — not the config field cipher).
|
||||
|
||||
### Config path resolution (R4 — closed)
|
||||
|
||||
Ordered candidates:
|
||||
|
||||
1. `{installRoot}/vPilotConfig.xml` (default install root = `%LOCALAPPDATA%\vPilot`)
|
||||
2. `%LOCALAPPDATA%\vPilot\vPilotConfig.xml` when install root ≠ default
|
||||
3. Previous inject manifest path
|
||||
4. No separate Roaming path observed in PE strings
|
||||
|
||||
Rewrite **existing** files only; if none exist, require user to run vPilot once. See gate file for adapter rules.
|
||||
|
||||
## CLR #US / CIL ldstr map (3.12.1 `vPilot.exe`)
|
||||
|
||||
`#US` stream file offset: `0xA22E8` (size `0x15E04`).
|
||||
`#US` stream file offset: `0xA22E8` (size `0x15E04`). PE section: `.text`.
|
||||
|
||||
| Logical string | #US heap offset (header) | Body file offset | ldstr CIL file offset(s) | Payload byte budget (UTF-16+term) |
|
||||
|----------------|--------------------------|------------------|--------------------------|----------------------------------|
|
||||
@@ -64,18 +77,50 @@ Source of truth for this algorithm: archived `renorris/vpilot-patch-utility` (`c
|
||||
| `https://voice1.vatsim.net` | `0x6D8D` | `0xA9076` | `0x1F0A7` | 51 |
|
||||
| `http://fsd.vatsim.net/` | `0x15751` | `0xB7A3A` | `0x4C0B2` | 45 |
|
||||
|
||||
Second copy of fsd-jwt UTF-16 body at `0xBA44A` — header walk failed (likely resource/non-#US); treat primary #US entry + ldstr `0x4BDB5` as authoritative until second site proven live.
|
||||
### Second UTF-16 fsd-jwt site (R3 — closed: not live)
|
||||
|
||||
Full-PE UTF-16 scan finds **exactly two** copies of stock `https://auth.vatsim.net/api/fsd-jwt`:
|
||||
|
||||
| File offset | In #US? | Terminal after UTF-16 | Verdict |
|
||||
|-------------|---------|----------------------|---------|
|
||||
| `0xB7988` | Yes | `0x01` (valid #US) | **Live** — only patch site |
|
||||
| `0xBA44A` | No | `0x04` (invalid #US) | **Dead** embedded data; do **not** dual-write |
|
||||
|
||||
No LE references to `0xBA44A` as file offset/RVA/VA. Residual full-PE scanners will still hit the dead copy after a successful primary patch — treat as allowlisted dead hit (see gates doc).
|
||||
|
||||
**3.11.1 offsets are obsolete** (e.g. old fsd-jwt heap `0xD2A2` / ldstr `0x4B3C5` do not match 3.12.1). Profiles must be **version-pinned by binary hash**.
|
||||
|
||||
### Prior-art AFV strategy
|
||||
### Free #US slots / ldstr remap (R1 — open)
|
||||
|
||||
`openfsd-client-patch-utility` example for vPilot 3.11.1 **disables AFV** (`ret` at voice connect handler) rather than rewriting voice URL. openfsd now has native `-afv`; design should prefer **retarget voice base URL** when space allows, with fallback disable.
|
||||
- ~2040 decode-OK `#US` entries; **no** long slot (budget ≥71) is unreferenced by CIL `ldstr`.
|
||||
- Remap therefore means **sacrificing** a live cosmetic (or other) string, then rewriting JWT `ldstr` at `0x4BDB5` to the sacrifice heap token.
|
||||
- Candidate catalog lives in [`vpilot-3.12.1-gates.md`](./vpilot-3.12.1-gates.md). Profile `us_free_slots` remains **empty** until a sacrifice is runtime-validated.
|
||||
- Production-length JWT hostnames need **R1 and/or A8** (short JWT path on openfsd). Phase 0 short-host lab only otherwise (max host length **12** on default `/api/v1/fsd-jwt` with body budget 71).
|
||||
|
||||
### Prior-art AFV strategy (R2 — open)
|
||||
|
||||
`openfsd-client-patch-utility` example for vPilot **3.11.1** disables AFV (`ret` `0x2A` at file `0x4BA54`) rather than rewriting voice URL. On **3.12.1** that file offset is **not** a valid disable site.
|
||||
|
||||
openfsd prefers **retarget** voice base URL when space allows, with fallback **`-novoice`**. PE `ret` disable stays `file_offset: null` until R2 closes.
|
||||
|
||||
fsd-jwt stock URL length limits padded #US overwrite; longer openfsd URLs need either:
|
||||
|
||||
1. shorter public base host, or
|
||||
2. ldstr remap to a longer unused #US slot (3.11.1 technique: `0x72 XX XX XX 70`), or
|
||||
3. runtime string/hook injection without shrinking constraint.
|
||||
2. server short-path alias (**A8**, e.g. fixed `POST /j`), or
|
||||
3. ldstr remap to a longer sacrifice slot (R1), or
|
||||
4. later-phase runtime/string hook strategies.
|
||||
|
||||
## GeoVR inventory (R5 — closed)
|
||||
|
||||
| Binary | Contains `voice1.vatsim.net`? |
|
||||
|--------|-------------------------------|
|
||||
| `vPilot.exe` | Yes (UTF-16) |
|
||||
| `GeoVR.Client.dll` | No |
|
||||
| `GeoVR.Connection.dll` | No |
|
||||
| `GeoVR.Shared.dll` | No |
|
||||
| Other extract DLLs | No |
|
||||
|
||||
AFV base retarget of `vPilot.exe` alone is sufficient for the stock voice URL. GeoVR still hosts `ConnectToVoiceServer` / `ApiServerConnection` machinery; URL is host-supplied.
|
||||
|
||||
## Prior art (same author ecosystem)
|
||||
|
||||
@@ -85,7 +130,7 @@ fsd-jwt stock URL length limits padded #US overwrite; longer openfsd URLs need e
|
||||
| [renorris/openfsd-client-patch-utility](https://github.com/renorris/openfsd-client-patch-utility) | Multi-client static patcher (vPilot/xPilot/Euroscope/vatSys) | Active reference |
|
||||
| openfsd wiki `Client-Connection.md` | Points operators at those tools | Exists |
|
||||
|
||||
**Gap:** static on-disk patchers are CLI-centric, version-brittle, AFV-hostile (disable), and not a first-class openfsd module with a multi-client GUI. User request: **runtime injector** + **generic client picker GUI**.
|
||||
**Gap:** static on-disk patchers are CLI-centric, version-brittle, AFV-hostile (disable), and not a first-class openfsd module with a multi-client GUI.
|
||||
|
||||
## Injection strategy spectrum (for design)
|
||||
|
||||
@@ -96,7 +141,7 @@ fsd-jwt stock URL length limits padded #US overwrite; longer openfsd URLs need e
|
||||
5. **Local control-plane proxy** — no PE touch: hosts or system proxy to fake `auth.vatsim.net`, `voice1.vatsim.net`, status; FSD via cached server IP. TLS MITM needs local CA (heavy UX).
|
||||
6. **Plugin** — plugin surface is events, not network endpoint config; unlikely sufficient.
|
||||
|
||||
Recommended design direction (to be confirmed in design doc): **adapter interface per client**, default vPilot adapter combining (a) config rewrite, (b) versioned #US/CIL profile for JWT+AFV, (c) optional AFV disable fallback; GUI is **client-agnostic** (select client → locate install → bind openfsd endpoints → Apply/Revert/Launch).
|
||||
Recommended design direction: **adapter interface per client**, default vPilot adapter combining (a) config rewrite, (b) versioned #US/CIL profile for JWT+AFV, (c) optional AFV disable fallback; GUI is **client-agnostic**.
|
||||
|
||||
## Legal / product constraints
|
||||
|
||||
@@ -108,16 +153,21 @@ Recommended design direction (to be confirmed in design doc): **adapter interfac
|
||||
|
||||
## Research method used
|
||||
|
||||
- Downloaded official NSIS installer; extracted with 7-Zip.
|
||||
- PE/CLR metadata walk for `#US`; UTF-16 string inventory; ldstr token search.
|
||||
- openssl 3DES-ECB decrypt of default config fields.
|
||||
- Downloaded official NSIS installer; extracted with 7-Zip into gitignored `.research/vpilot/`.
|
||||
- PE/CLR metadata walk for `#US`; UTF-16 string inventory; ldstr token search (`72` + `70` table).
|
||||
- openssl / Go 3DES-ECB decrypt of default config fields.
|
||||
- Cross-check with archived Go patch utilities and openfsd AFV/FSD docs.
|
||||
- Full ILSpy decompilation not run in this environment (no `dotnet`/ilspycmd); GeoVR method names recovered via managed metadata strings (`ApiServerConnection`, `AddCallsign`, `ConnectToVoiceServer`, etc.).
|
||||
- GeoVR DLLs scanned for voice host (ASCII + UTF-16).
|
||||
- Full ILSpy decompilation not run in this environment (no `dotnet`/ilspycmd); method names recovered via managed metadata strings where present.
|
||||
- Optional CI-local revalidation: `go test -tags=research ./internal/clientinject/...` when extract PE is present (see `research_pe_test.go`).
|
||||
|
||||
## Follow-ups for implementer
|
||||
## Follow-ups
|
||||
|
||||
- [ ] Confirm second fsd-jwt site and automatic-server HTTP usage path on live connect.
|
||||
- [ ] Measure max openfsd URL length vs #US budgets; document remap free-slot catalog per version.
|
||||
- [ ] Locate AFV connect handler CIL for 3.12.1 disable-fallback (`ret` site was 3.11.1-specific).
|
||||
- [ ] User-settings store path beyond install dir (`%LOCALAPPDATA%` may hold runtime config copy).
|
||||
- [x] R3: classify second fsd-jwt site (`0xBA44A`) — **not live**; primary only.
|
||||
- [x] R4: config path candidates — install root + LOCALAPPDATA fallback.
|
||||
- [x] R5: GeoVR re-hardcode check — **none**.
|
||||
- [ ] R1: runtime-validate a sacrifice free slot; populate `us_free_slots`.
|
||||
- [ ] R2: locate AFV connect handler CIL for 3.12.1 `ret` disable.
|
||||
- [ ] Residual HealthCheck allowlist for dead `0xBA44A` when implementing adapter-complete policy.
|
||||
- [ ] Antivirus / code-signing interaction when rewriting signed `vPilot.exe`.
|
||||
- [ ] R6 (separate): CachedServers `host:port` acceptance on 3.12.1.
|
||||
|
||||
73
internal/clientinject/pepatch/residual_scan_test.go
Normal file
73
internal/clientinject/pepatch/residual_scan_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package pepatch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Stock VATSIM FSD JWT URL used by residual full-PE scans (vPilot 3.12.1).
|
||||
const stockFSDJWT = "https://auth.vatsim.net/api/fsd-jwt"
|
||||
|
||||
// TestResidualScan_SyntheticTwoCopies loads a synthetic fixture with two
|
||||
// UTF-16LE copies of the stock JWT string (one framed like a live #US body,
|
||||
// one framed like the dead 0xBA44A-style residual). Confirms ScanUTF16String
|
||||
// reports both — the real PE residual policy must allowlist the dead site
|
||||
// separately (see docs/client-injector/research/vpilot-3.12.1-gates.md R3).
|
||||
func TestResidualScan_SyntheticTwoCopies(t *testing.T) {
|
||||
path := filepath.Join("testdata", "residual_jwt_two_copies.bin")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture: %v", err)
|
||||
}
|
||||
hits := ScanUTF16String(data, stockFSDJWT)
|
||||
if len(hits) != 2 {
|
||||
t.Fatalf("hits=%v want exactly 2 UTF-16 copies", hits)
|
||||
}
|
||||
if hits[0] >= hits[1] {
|
||||
t.Fatalf("hits not ascending: %v", hits)
|
||||
}
|
||||
// Fixture layout: first copy after header+prefix; second after mid+framing.
|
||||
// Absolute offsets are fixed by the checked-in fixture bytes.
|
||||
if hits[0] < 20 {
|
||||
t.Fatalf("first hit too early: %d", hits[0])
|
||||
}
|
||||
// Ensure we matched raw UTF-16 only (no requirement on terminal byte).
|
||||
need := len([]byte(stockFSDJWT)) // rune count == byte count for this ASCII string
|
||||
_ = need
|
||||
for _, h := range hits {
|
||||
// Spot-check first UTF-16 code unit is 'h' (0x0068)
|
||||
if int(h)+1 >= len(data) {
|
||||
t.Fatalf("hit %d OOB", h)
|
||||
}
|
||||
if data[h] != 'h' || data[h+1] != 0x00 {
|
||||
t.Fatalf("hit %d not start of UTF-16LE JWT: %02x %02x", h, data[h], data[h+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestResidualScan_FixtureStable guards the synthetic fixture against
|
||||
// accidental regeneration that drops a copy (adapter residual tests depend on
|
||||
// two-hit behavior).
|
||||
func TestResidualScan_FixtureStable(t *testing.T) {
|
||||
path := filepath.Join("testdata", "residual_jwt_two_copies.bin")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(data) < 100 {
|
||||
t.Fatalf("fixture too small: %d", len(data))
|
||||
}
|
||||
const magic = "OPENFSD-RESIDUAL-FIXTURE"
|
||||
if len(data) < len(magic) || string(data[:len(magic)]) != magic {
|
||||
t.Fatalf("fixture magic=%q", data[:min(len(data), 32)])
|
||||
}
|
||||
hits := ScanUTF16String(data, stockFSDJWT)
|
||||
if len(hits) != 2 {
|
||||
t.Fatalf("fixture must contain two stock JWT UTF-16 copies; hits=%v", hits)
|
||||
}
|
||||
// Fixed offsets from generator (magic + NUL + 1-byte prefix = 26).
|
||||
if hits[0] != 26 || hits[1] != 113 {
|
||||
t.Fatalf("fixture offsets drifted: hits=%v want [26 113]", hits)
|
||||
}
|
||||
}
|
||||
BIN
internal/clientinject/pepatch/testdata/residual_jwt_two_copies.bin
vendored
Normal file
BIN
internal/clientinject/pepatch/testdata/residual_jwt_two_copies.bin
vendored
Normal file
Binary file not shown.
@@ -2,6 +2,18 @@
|
||||
# Legal: user must install vPilot themselves; openfsd never redistributes it.
|
||||
# Source installer: https://vpilot.rosscarlson.dev/Assets/Files/Installers/vPilot-Setup-3.12.1.exe
|
||||
# Offsets from docs/client-injector/research/vpilot-3.12.1.md — version-pinned by PE hash.
|
||||
# Research gates R1–R5: docs/client-injector/research/vpilot-3.12.1-gates.md
|
||||
#
|
||||
# Gate summary (2026-07-28):
|
||||
# R1 OPEN — us_free_slots empty (no unreferenced long #US; remap needs sacrifice validation)
|
||||
# R2 OPEN — afv_disable_pe.file_offset null (3.11.1 0x4BA54 obsolete)
|
||||
# R3 CLOSED not-live — second UTF-16 JWT at 0xBA44A is NOT a live #US body; do not dual-write
|
||||
# R4 CLOSED — config beside install / %LOCALAPPDATA%\vPilot\ (see research notes)
|
||||
# R5 CLOSED — voice1.vatsim.net only in vPilot.exe (no GeoVR re-hardcode)
|
||||
#
|
||||
# Phase 0: short-host JWT lab only unless A8 short path or R1 remap lands.
|
||||
# Production hostnames need R1 and/or A8. Residual full-PE scan will still see
|
||||
# dead 0xBA44A after primary patch — allowlist that offset (do not add to body_file_offsets).
|
||||
|
||||
schema_version: 2
|
||||
client_id: vpilot
|
||||
@@ -34,10 +46,15 @@ primary_binary:
|
||||
clr: true
|
||||
architecture: x86
|
||||
|
||||
# R4: relative to install root (default install root == %LOCALAPPDATA%\vPilot).
|
||||
# Adapter must also probe %LOCALAPPDATA%\vPilot\vPilotConfig.xml when install
|
||||
# root differs, plus any path recorded in a prior inject manifest. Rewrite
|
||||
# existing files only; if none exist, blocker (run vPilot once).
|
||||
config_files:
|
||||
- relative_path: vPilotConfig.xml
|
||||
format: xml_obfuscated_fields
|
||||
|
||||
# R5: related DLLs carry AFV machinery but do NOT re-hardcode voice1.vatsim.net.
|
||||
related_binaries:
|
||||
- GeoVR.Client.dll
|
||||
- GeoVR.Connection.dll
|
||||
@@ -53,6 +70,7 @@ strings:
|
||||
fsd_jwt:
|
||||
stock: "https://auth.vatsim.net/api/fsd-jwt"
|
||||
us_heap_offset: 0x1569F
|
||||
# R3: only live #US body. Dead residual UTF-16 also at 0xBA44A (not listed).
|
||||
body_file_offsets: [0xB7988]
|
||||
ldstr_file_offsets: [0x4BDB5]
|
||||
payload_budget_bytes: 71
|
||||
@@ -72,7 +90,8 @@ strings:
|
||||
payload_budget_bytes: 45
|
||||
template: ""
|
||||
|
||||
# Populated only after research gate R1 — empty means remap unavailable
|
||||
# R1 OPEN — empty means ldstr remap unavailable. Candidate sacrifice catalog
|
||||
# (not validated): docs/client-injector/research/vpilot-3.12.1-gates.md
|
||||
us_free_slots: []
|
||||
|
||||
mutations:
|
||||
|
||||
174
internal/clientinject/research_pe_test.go
Normal file
174
internal/clientinject/research_pe_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
//go:build research
|
||||
|
||||
package clientinject
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/renorris/openfsd/internal/clientinject/pepatch"
|
||||
)
|
||||
|
||||
// Optional research-gate revalidation against a local (gitignored) extract.
|
||||
//
|
||||
// go test -tags=research ./internal/clientinject/ -count=1
|
||||
//
|
||||
// Looks for OPENFSD_VPILOT_EXE, then common .research paths relative to the
|
||||
// module / home scratch tree. Skips cleanly when the PE is absent so CI
|
||||
// without extract stays green under -tags=research.
|
||||
|
||||
const (
|
||||
researchStockJWT = "https://auth.vatsim.net/api/fsd-jwt"
|
||||
researchStockAFV = "https://voice1.vatsim.net"
|
||||
researchWantSHA1 = "7d95a7110392c15728143cc30e1f00899c686eb5"
|
||||
researchJWTBodyOff = int64(0xB7988)
|
||||
researchJWTDeadOff = int64(0xBA44A)
|
||||
researchJWTLdstrOff = int64(0x4BDB5)
|
||||
researchAFVBodyOff = int64(0xA9076)
|
||||
researchAFVLdstrOff = int64(0x1F0A7)
|
||||
researchUSHeapOff = int64(0xA22E8)
|
||||
researchUSHeapSize = int64(0x15E04)
|
||||
researchJWTHeapOff = 0x1569F
|
||||
researchAFVHeapOff = 0x6D8D
|
||||
)
|
||||
|
||||
func researchVPilotPath(t *testing.T) string {
|
||||
t.Helper()
|
||||
if p := os.Getenv("OPENFSD_VPILOT_EXE"); p != "" {
|
||||
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
||||
return p
|
||||
}
|
||||
t.Fatalf("OPENFSD_VPILOT_EXE=%q not a file", p)
|
||||
}
|
||||
candidates := []string{
|
||||
filepath.Join(".research", "vpilot", "extracted", "vPilot.exe"),
|
||||
filepath.Join("..", "..", ".research", "vpilot", "extracted", "vPilot.exe"),
|
||||
"/Users/rnorris/scratch/openfsd/.research/vpilot/extracted/vPilot.exe",
|
||||
}
|
||||
// Also try walking up from cwd for monorepo root .research/
|
||||
wd, _ := os.Getwd()
|
||||
for i := 0; i < 6 && wd != ""; i++ {
|
||||
candidates = append(candidates, filepath.Join(wd, ".research", "vpilot", "extracted", "vPilot.exe"))
|
||||
parent := filepath.Dir(wd)
|
||||
if parent == wd {
|
||||
break
|
||||
}
|
||||
wd = parent
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if st, err := os.Stat(c); err == nil && !st.IsDir() {
|
||||
return c
|
||||
}
|
||||
}
|
||||
t.Skip("vPilot 3.12.1 extract not found (set OPENFSD_VPILOT_EXE or place under .research/vpilot/extracted/)")
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestResearch_VPilot3121_FingerprintsAndSites(t *testing.T) {
|
||||
path := researchVPilotPath(t)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := sha1.Sum(data)
|
||||
got := hex.EncodeToString(sum[:])
|
||||
if got != researchWantSHA1 {
|
||||
t.Fatalf("sha1=%s want %s (wrong version?)", got, researchWantSHA1)
|
||||
}
|
||||
if int64(len(data)) != 1236416 {
|
||||
t.Fatalf("size=%d want 1236416", len(data))
|
||||
}
|
||||
|
||||
// Residual UTF-16: exactly primary + dead second site.
|
||||
hits := pepatch.ScanUTF16String(data, researchStockJWT)
|
||||
if len(hits) != 2 {
|
||||
t.Fatalf("JWT residual hits=%v want 2", hits)
|
||||
}
|
||||
if hits[0] != researchJWTBodyOff || hits[1] != researchJWTDeadOff {
|
||||
t.Fatalf("JWT hits=%v want [%#x %#x]", hits, researchJWTBodyOff, researchJWTDeadOff)
|
||||
}
|
||||
|
||||
// Live #US body terminal 0x01; dead site terminal 0x04.
|
||||
if term := data[researchJWTBodyOff+int64(len(researchStockJWT)*2)]; term != 0x01 {
|
||||
t.Fatalf("live JWT terminal=%#x want 0x01", term)
|
||||
}
|
||||
if term := data[researchJWTDeadOff+int64(len(researchStockJWT)*2)]; term != 0x04 {
|
||||
t.Fatalf("dead JWT terminal=%#x want 0x04", term)
|
||||
}
|
||||
// Dead site is outside #US heap.
|
||||
if researchJWTDeadOff >= researchUSHeapOff && researchJWTDeadOff < researchUSHeapOff+researchUSHeapSize {
|
||||
t.Fatal("dead JWT site unexpectedly inside #US heap")
|
||||
}
|
||||
if researchJWTBodyOff < researchUSHeapOff || researchJWTBodyOff >= researchUSHeapOff+researchUSHeapSize {
|
||||
t.Fatal("live JWT body not inside #US heap")
|
||||
}
|
||||
|
||||
// ldstr tokens
|
||||
assertLdstr(t, data, researchJWTLdstrOff, researchJWTHeapOff)
|
||||
assertLdstr(t, data, researchAFVLdstrOff, researchAFVHeapOff)
|
||||
|
||||
// AFV body present once in UTF-16
|
||||
afvHits := pepatch.ScanUTF16String(data, researchStockAFV)
|
||||
if len(afvHits) < 1 || afvHits[0] != researchAFVBodyOff {
|
||||
t.Fatalf("AFV hits=%v want primary %#x", afvHits, researchAFVBodyOff)
|
||||
}
|
||||
|
||||
// 3.11.1 AFV ret site must NOT be a lone 0x2A on this build.
|
||||
if data[0x4BA54] == 0x2A {
|
||||
t.Fatalf("unexpected ret at obsolete 3.11.1 AFV site 0x4BA54 — re-check R2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResearch_VPilot3121_GeoVRNoVoiceHost(t *testing.T) {
|
||||
pe := researchVPilotPath(t)
|
||||
dir := filepath.Dir(pe)
|
||||
for _, name := range []string{"GeoVR.Client.dll", "GeoVR.Connection.dll", "GeoVR.Shared.dll"} {
|
||||
p := filepath.Join(dir, name)
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
t.Skipf("GeoVR DLL missing (%s): %v", p, err)
|
||||
}
|
||||
if hits := pepatch.ScanUTF16String(data, "voice1.vatsim.net"); len(hits) != 0 {
|
||||
t.Fatalf("%s has UTF-16 voice1.vatsim.net at %v (R5 regression)", name, hits)
|
||||
}
|
||||
if idx := indexASCII(data, "voice1.vatsim.net"); idx >= 0 {
|
||||
t.Fatalf("%s has ASCII voice1.vatsim.net at %d (R5 regression)", name, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertLdstr(t *testing.T, data []byte, fileOff int64, heapOff int) {
|
||||
t.Helper()
|
||||
if fileOff+5 > int64(len(data)) {
|
||||
t.Fatalf("ldstr OOB %#x", fileOff)
|
||||
}
|
||||
if data[fileOff] != 0x72 {
|
||||
t.Fatalf("ldstr opcode at %#x = %#x", fileOff, data[fileOff])
|
||||
}
|
||||
tok := binary.LittleEndian.Uint32(data[fileOff+1:])
|
||||
want := uint32(0x70000000 | heapOff)
|
||||
if tok != want {
|
||||
t.Fatalf("ldstr token at %#x = %#x want %#x", fileOff, tok, want)
|
||||
}
|
||||
}
|
||||
|
||||
func indexASCII(data []byte, s string) int {
|
||||
pat := []byte(s)
|
||||
for i := 0; i+len(pat) <= len(data); i++ {
|
||||
ok := true
|
||||
for j := range pat {
|
||||
if data[i+j] != pat[j] {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
28
internal/clientinject/residual_fixture_test.go
Normal file
28
internal/clientinject/residual_fixture_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package clientinject
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/renorris/openfsd/internal/clientinject/pepatch"
|
||||
)
|
||||
|
||||
// Package-level residual fixture (mirrors pepatch/testdata). Ensures engine-
|
||||
// layer consumers can load the same two-copy stock JWT binary from
|
||||
// internal/clientinject/testdata without depending on pepatch relative paths.
|
||||
func TestResidualFixture_TwoUTF16Copies(t *testing.T) {
|
||||
path := filepath.Join("testdata", "residual_jwt_two_copies.bin")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture: %v (synthetic residual fixtures must be committed)", err)
|
||||
}
|
||||
const stock = "https://auth.vatsim.net/api/fsd-jwt"
|
||||
hits := pepatch.ScanUTF16String(data, stock)
|
||||
if len(hits) != 2 {
|
||||
t.Fatalf("hits=%v want 2", hits)
|
||||
}
|
||||
if hits[0] != 26 || hits[1] != 113 {
|
||||
t.Fatalf("hits=%v want [26 113]", hits)
|
||||
}
|
||||
}
|
||||
BIN
internal/clientinject/testdata/residual_jwt_two_copies.bin
vendored
Normal file
BIN
internal/clientinject/testdata/residual_jwt_two_copies.bin
vendored
Normal file
Binary file not shown.
@@ -1,6 +1,9 @@
|
||||
# openfsd client profile — metadata only. Do NOT ship vPilot binaries.
|
||||
# Source installer: https://vpilot.rosscarlson.dev/Assets/Files/Installers/vPilot-Setup-3.12.1.exe
|
||||
# Legal: user must install vPilot themselves; openfsd never redistributes it.
|
||||
# Embed canonical (schema v2 + offsets): internal/clientinject/profiles/vpilot-3.12.1.yaml
|
||||
# Research gates: docs/client-injector/research/vpilot-3.12.1-gates.md
|
||||
# R1 open (no us_free_slots); R2 open; R3 dead residual 0xBA44A; R4/R5 closed.
|
||||
|
||||
client_id: vpilot
|
||||
display_name: vPilot
|
||||
@@ -19,7 +22,7 @@ installer:
|
||||
format: nsis
|
||||
primary_binary:
|
||||
relative_path: vPilot.exe # under install dir
|
||||
# Default install: %LOCALAPPDATA%\vPilot\vPilot.exe
|
||||
# Default install: %LOCALAPPDATA%\vPilot\vPilot.exe (R4: config beside exe)
|
||||
default_install_globs:
|
||||
windows:
|
||||
- "%LOCALAPPDATA%\\vPilot\\vPilot.exe"
|
||||
@@ -34,8 +37,8 @@ config_files:
|
||||
- relative_path: vPilotConfig.xml
|
||||
format: xml_obfuscated_fields
|
||||
related_binaries:
|
||||
- GeoVR.Client.dll # AFV client stack (NAudio embedded via Costura)
|
||||
- GeoVR.Client.dll # AFV client stack (NAudio embedded via Costura); R5: no voice1 hardcode
|
||||
- GeoVR.Connection.dll
|
||||
- GeoVR.Shared.dll
|
||||
- RossCarlson.Vatsim.Vpilot.Plugins.dll
|
||||
prior_art_profile: vpilot-3.11.1 # openfsd-client-patch-utility
|
||||
prior_art_profile: vpilot-3.11.1 # openfsd-client-patch-utility (offsets obsolete for 3.12.1)
|
||||
|
||||
Reference in New Issue
Block a user