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_00d04660 — EncryptedConnection::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 RVA0x00d04698; the instruction after it is at0x0090469d. Our hook interceptsFUN_00d128b0and filtersthis.returnAddress == 0x0090469dto 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:
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_00d19d70 — Blowfish_SetKey¶
| Address | Value |
|---|---|
| VA | 0x00d19d70 |
| RVA | 0x00919d70 |
Standard Blowfish key expansion:
- Copy the S-box init tables from
DAT_016f9e18into the passed context. The context struct is0x1048bytes — 18 u32s for the P-array (0x48) plus four 256-u32 S-boxes (4 × 0x400). - XOR the init P-array with the user's key bytes,
ring-indexed over
keylenbytes (the classic "reuse the key if it's shorter than 72 bytes" trick). - 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_00d19b70internally.
FUN_00d19b70 — Blowfish_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_00d19cc0 — Blowfish_DecryptBlock¶
| Address | Value |
|---|---|
| VA | 0x00d19cc0 |
| RVA | 0x00919cc0 |
Symmetric counterpart — identified via its use in the self-test function for round-trip verification.
FUN_00d19f30 — Blowfish_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:
- Call
SetKeywith the ASCII string"TESTKEY"(7 bytes). - Encrypt
{lo=1, hi=2}. - Check the result against the known-answer vector
{lo=-0x20ccc02e, hi=0x30a71bb4}. - Decrypt and verify the round-trip returns
{1, 2}. - 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.