Skip to content

EXE findings

Catalog of interesting functions and data we've identified inside ImagineClient.exe v1.666. This page is a living document — it grows as we pin more opcodes, file formats, and internal systems.

Every address here is given both as an absolute VA (which matches what Frida reports at runtime because Wine honors the PE preferred base of 0x00400000) and an RVA (useful when bitmap-scanning or rebasing in another tool).

Module base

Key Value
Module ImagineClient.exe
Runtime base 0x00400000
Module size 0x8513C00
Frida platform Windows (32-bit)
Host Wine prefix, launched via a Python/Frida harness

Packet pipeline

The star of the show: the client's outbound encrypted-packet send path. Understanding this chain is what unlocked the packet dump Frida hook, which streams every plaintext outbound packet into the envision-client session log.

FUN_00d04660EncryptedConnection::SendPacket

Address Value
VA 0x00d04660
RVA 0x00904660

The outbound encrypt-and-send pump. Each call sends one Blowfish-encrypted frame out to the game server. Function shape, reconstructed from the decompile:

dequeue plaintext:    FUN_00d128b0(this+0x4784, &buf, &len)
cap at 0x7fc7:        if (len > 0x7fc7) len = 0x7fc8
optional gzip:        if (this+0x48d0 != 0) FUN_00d19800(...)
cap at 0x7fd8:        if (len > 0x7fd8) len = 0x7fd8
round up to 8:        padded = ((len + 7) / 8) * 8 + 8
copy + zero-pad:      memcpy(stackbuf, buf, len); zero the padding
build outer header:   FUN_00cf7db0(&hdr, paddedSize)
                      FUN_00cf7db0(&hdr+4, realSize)
initialize Blowfish:  if (first time) FUN_00d19d70(ctx, this+0x8c, keylen)
                      else            key[0]++   // sequence counter
encrypt in place:     for each 8-byte block:
                          FUN_00d19b70(ctx, &lo, &hi)
push to socket queue: FUN_00d126a0(this+0x47ac, hdr+stackbuf, padded+8)

Notable details:

  • The plaintext lives at a stack local in this function. There's no convenient register to grab it from at the function prologue, but the first call to FUN_00d128b0 (the dequeue helper) writes the plaintext pointer + length out to stack slots. That call site is at RVA 0x00d04698; the instruction after it is at 0x0090469d. Our hook intercepts FUN_00d128b0 and filters this.returnAddress == 0x0090469d to isolate this single call site out of the ~27 other places the dequeue helper is invoked.
  • The key[0]++ branch is unusual — most Blowfish usages initialize the key schedule once and then use it forever. This looks like an anti-replay counter baked directly into the key bytes: each outbound packet uses a slightly different key schedule. The server side implicitly mirrors it because its own key is derived the same way.
  • The outer frame header is the standard two-u32 big-endian [paddedSize][realSize] pair documented in the protocol reference.

Plaintext inner format

After the dequeue, the buffer holds one or more inner packets concatenated back-to-back, each in the shape:

[sizeBE u16]  ── redundant, big-endian copy of sizeLE
[sizeLE u16]  ── counts sizeLE (2) + code (2) + body (N), so body = sizeLE − 4
[code  u16le] ── opcode
[body  N]

This is exactly the shape Envision's server-side InnerPacketCodec parses after decrypting the outer frame. The Frida hook walks the plaintext buffer with this format and emits one structured log line per inner packet, with the decoded opcode and the hex body.

Blowfish family

Identified via an xref walk starting from the standard pi-derived P-array init table.

P-array init constants

Address Value
VA 0x016f9dd0
RVA 0x012f9dd0

Found by pattern-scanning .rdata for the first 8 u32s of the standard Blowfish P-array:

88 6a 3f 24  d3 08 a3 85  2e 8a 19 13  44 73 70 03
22 38 09 a4  d0 31 9f 29  98 fa 2e 08  89 6c 4e ec

Exactly one function in the binary references this address: FUN_00d19d70, which is the Blowfish key setup. The init tables for the four S-boxes live immediately after, at VA 0x016f9e18 (RVA 0x012f9e18).

FUN_00d19d70Blowfish_SetKey

Address Value
VA 0x00d19d70
RVA 0x00919d70

Standard Blowfish key expansion:

  1. Copy the S-box init tables from DAT_016f9e18 into the passed context. The context struct is 0x1048 bytes — 18 u32s for the P-array (0x48) plus four 256-u32 S-boxes (4 × 0x400).
  2. XOR the init P-array with the user's key bytes, ring-indexed over keylen bytes (the classic "reuse the key if it's shorter than 72 bytes" trick).
  3. Scramble the whole context by encrypting all-zero blocks in place, 18 times for the P-array and 256 times for each S-box. Uses FUN_00d19b70 internally.

FUN_00d19b70Blowfish_EncryptBlock

Address Value
VA 0x00d19b70
RVA 0x00919b70

Standard 16-round Feistel core. Signature is void Blowfish_EncryptBlock(ctx, &lo, &hi) — takes the two u32 halves of an 8-byte block by pointer and overwrites them with the ciphertext halves.

Callers:

Call site Context
FUN_00d19d70 Internal — key schedule scrambling
FUN_00d19f30 Self-test (below)
FUN_00d04660 The outbound packet encrypt loop

FUN_00d19cc0Blowfish_DecryptBlock

Address Value
VA 0x00d19cc0
RVA 0x00919cc0

Symmetric counterpart — identified via its use in the self-test function for round-trip verification.

FUN_00d19f30Blowfish_SelfTest

Address Value
VA 0x00d19f30
RVA 0x00919f30

Present in the shipped EXE, runs at library init. Its decompile is a tiny gem — it runs the canonical Blowfish test vector:

  1. Call SetKey with the ASCII string "TESTKEY" (7 bytes).
  2. Encrypt {lo=1, hi=2}.
  3. Check the result against the known-answer vector {lo=-0x20ccc02e, hi=0x30a71bb4}.
  4. Decrypt and verify the round-trip returns {1, 2}.
  5. Return 0 on success, -1 on failure.

This is the same known-answer vector published with the original 1993 Blowfish reference implementation, and it confirms beyond ambiguity that the game ships a standard, unmodified Blowfish library. Our server side uses BouncyCastle's BlowfishEngine, which produces byte-for-byte identical output against the same test vector.

FUN_00d128b0 — outbound packet queue dequeue

Address Value
VA 0x00d128b0
RVA 0x009128b0

A generic "read next entry" helper used across the networking layer with ~27 call sites. Signature: int FUN_00d128b0(queue, void **out_buf, uint *out_len).

We hook this for the packet dump — but filter aggressively by return address so we only dump the single call site inside FUN_00d04660 (RVA 0x00904698, the first call at function entry).

User-interface patches

FUN_00f26cc0 — character-name SJIS lead-byte validator

Address Value
VA 0x00f26cc0
RVA 0x00b26cc0

Parameterless function used by the character creator. Walks the current input string (fetched internally through FUN_00f26be0) and checks every byte against the Shift-JIS lead-byte ranges 0x81..0x9F and 0xE0..0xFC. Returns 1 if every byte is a valid SJIS lead and the string is non-empty, 0 otherwise.

The Japanese 1.666 client uses this to enforce the zenkaku-only name rule with the popup:

キャラクター名に半角は使用できません。全角で再入力して下さい。

(The error string itself lives at .rwdata VA 0x0889bab8, and the call site that raises the popup is at 0x00f28be1 inside MiUI_Window_CharaMake::SetButtonStatus at VA 0x00f287f0.)

What we did with it: our Frida harness replaces this function entirely with a stub that unconditionally returns 1, letting the character creator accept ASCII / half-width names for dev testing. No .text bytes are patched — it's a pure Interceptor.replace swap.

MiUI_* RTTI type descriptors

Detail Value
MiUI_Window_CharaMake RTTI descriptor 0x0190ff48

The retail client ships with unstripped MSVC RTTI for every MiUI_* UI class, which means a simple symbol search for MiUI_ yields the entire UI hierarchy. This is the primary anchor for finding UI-related functions: look up the RTTI descriptor, cross-reference it to find the vtable, walk the vtable to enumerate the methods. It's how we found the character-name validator's call site in the first place.

File-system anchors

0x009eedae — reads ImagineClient.dat (CLI-arg config)

Address Value
VA 0x009eedae
RVA 0x005eedae

Opens ImagineClient.dat, a plain-text file in the client's working directory containing -ip <host> and -port <port> on separate lines. This is the function that parses the runtime target server address during startup.

Not a hook target — rewriting the file itself works just as well, and that's what Envision's client launcher does.

0x08bf3172 — client opens its own EXE

Address Value
VA 0x08bf3172
RVA 0x087f3172

The client opens C:\Imagine\ImagineClient.exe during startup. The call lives very late in the PE's own address space, in a region consistent with resource-section access — most likely the embedded-resource loader pulling strings or dialog templates out of .rsrc. Could also be a CRC self-check, but so far Envision doesn't modify the EXE so it's informational only.

Sticky negative findings

A few things we went looking for and didn't find, documented so nobody retraces the same dead ends:

webaccess.sdat is not loaded at boot

The title-screen boot sequence never touches webaccess.sdat. The retail client's web-auth URL (https://secure.cave-online.jp/authsv/) is hardcoded in the EXE's string table, not loaded from the sidecar data file. Our Frida harness patches it at runtime by hooking wininet!InternetConnectW and rewriting the host during connection, rather than trying to intercept the file load.

This means the login flow becomes observable purely through network hooks, not file hooks, which simplifies the instrumentation considerably.