Apple Silicon Mac の Whisky で KANADE.exe を起動し、Frida で PackinOne.dll の鍵派生関数 (fn 0x10010570 / fn 0x1000bb80) と KANADE.exe の TVPCreateStream を hook して、復号鍵 + 復号後の 生ストリームを両取りする手順を整理。 併せて packinone の静的解析ノートも記録に残す。 スクリプト本体 (packages/frida/hook_kanade_packinone.js) は packages/frida/*.js が gitignore 対象のため commit せず、 リポジトリ内ローカル運用とする。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28 KiB
PackinOne.dll Static Analysis
Target: /home/vscode/app/targets/Kanade/plugin/PackinOne.dll
Format: PE32 (MSVC x86), 708608 bytes, ImageBase=0x10000000
Analysis method: static only (lief, capstone, radare2, objdump)
Section Layout
| Section | RVA | File Offset | VSize | RawSize |
|---|---|---|---|---|
| .text | 0x001000 | 0x000400 | 0x72a25 | 0x72c00 |
| .rdata | 0x074000 | 0x073000 | 0x23484 | 0x23600 |
| .data | 0x098000 | 0x096600 | 0x0ac98 | 0x007c00 |
| .reloc | 0x0a3000 | 0x09e200 | - | - |
VA-to-file-offset: file = section.rawOffset + (rva - section.virtualAddress)
1. 登録エントリテーブル
プラグインは DllMain から3つのリンクリストを走査してエントリを登録する。
リストヘッドアドレス: [0x1009fb20] (List0), [0x1009fb24] (List1), [0x1009fb28] (List2)
スタブパターン (x86):
A1 [head_va] ; mov eax, [list_head]
A3 [entry_va+4] ; mov [entry.next], eax
C7 05 [head_va] [entry_va] ; mov [list_head], entry_ptr
C3 ; ret
List 0 — ncbCallbackAutoRegister (4 entries)
| stub VA | entry VA | vtable VA | vtable[0] (fn0) | entry[8] callback | 備考 |
|---|---|---|---|---|---|
| 0x10073310 | 0x1009a3a8 | 0x1007770c | 0x100215d0 | 0x00000000 | 起動時コールバック未設定 |
| 0x100734b0 | 0x1009bba8 | 0x1007770c | 0x100215d0 | 0x00000000 | 起動時コールバック未設定 |
| 0x100734d0 | 0x1009bc88 | 0x1007770c | 0x100215d0 | 0x10032ec0 | XP3 登録+TJS初期化コールバック |
| 0x100734f0 | 0x1009bc50 | 0x1007770c | 0x100215d0 | 0x10040590 | System.commandExecute 登録 |
ncbCallbackAutoRegister vtable 0x1007770c:
- vtable[0]
0x100215d0: thunk —mov eax,[ecx+8]; test eax,eax; jmp eax / ret - vtable[1]
0x10021e90: DoRegister thunk - COL → type
.?AUncbCallbackAutoRegister@@
List0[2] callback 0x10032ec0: TVPGetScriptDispatch() を取得後、TVPFunctionExporter で複数 TJS API を解決し、XP3 ストレージプロバイダ登録 (fn 0x10031a40 → TVPRegisterStorageMedia) を実行する。
List0[3] callback 0x10040590: tTJSString::tTJSString(const tjs_nchar *) + TVPAddImportantLog を使用して System_commandExecute を登録。
List 1 — ncbNativeClassAutoRegister / ncbAttachTJS2ClassAutoRegister / ncbNativeFunctionAutoRegisterTempl (25 entries)
| stub VA | entry VA | vtable VA | fn0 VA | 登録内容 |
|---|---|---|---|---|
| 0x100732d0 | 0x1009a37c | 0x10077a38 | 0x10021000 | ncbAttachTJS2Class: VStoragesFstat |
| 0x10073330 | 0x1009a38c | 0x10077dc8 | 0x10021110 | ncbNativeClass: VTemporaryFiles |
| 0x10073370 | 0x1009bb68 | 0x10079234 | 0x10034a70 | ncbAttachTJS2Class: VArrayAdd |
| 0x10073390 | 0x1009bb78 | 0x10079278 | 0x10034b80 | ncbAttachTJS2Class: VDictAdd |
| 0x100733b0 | 0x1009bbd0 | 0x10079d5c | 0x10034810 | ncbAttachTJS2Class: UFontEx |
| 0x100733d0 | 0x1009bbb8 | 0x100795bc | 0x10034c90 | ncbAttachTJS2Class: VScriptsAdd |
| 0x100733f0 | 0x1009bb88 | 0x10079294 | 0x10034da0 | ncbAttachTJS2Class: VScriptsAddForSaveStruct |
| 0x10073410 | 0x1009bc40 | 0x1007a1f4 | 0x10034ee0 | ncbAttachTJS2Class: VlayerExImage |
| 0x10073430 | 0x1009bc60 | 0x1007a2b0 | 0x10034950 | ncbAttachTJS2Class: UlayerExRaster |
| 0x10073510 | 0x1009bc20 | 0x1007a084 | 0x10034ff0 | ncbNativeFunction: Layer_clipAlphaRect |
| 0x10073530 | 0x1009bc18 | 0x1007a050 | 0x10035040 | ncbNativeFunction: Layer_copyAlphaToProvince |
| 0x10073550 | 0x1009bc08 | 0x10079ff0 | 0x10035090 | ncbNativeFunction: Layer_copyBottomBlueToTopAlpha |
| 0x10073570 | 0x1009bc00 | 0x10079fb0 | 0x100350e0 | ncbNativeFunction: Layer_copyRightBlueToLeftAlpha |
| 0x10073590 | 0x1009bc10 | 0x1007a030 | 0x10035130 | ncbNativeFunction: Layer_fillAlpha |
| 0x100735b0 | 0x1009bc30 | 0x1007a0d4 | 0x10035180 | ncbNativeFunction: Layer_fillByProvince |
| 0x100735d0 | 0x1009bc38 | 0x1007a100 | 0x100351d0 | ncbNativeFunction: Layer_fillToProvince |
| 0x100735f0 | 0x1009bc28 | 0x1007a0ac | 0x10035220 | ncbNativeFunction: Layer_overwrapRect |
| 0x10073610 | 0x1009bbf0 | 0x10079d80 | 0x10035270 | ncbNativeFunction: Layer_shrinkCopy |
| 0x10073630 | 0x1009bbf8 | 0x10079da4 | 0x100352c0 | ncbNativeFunction: Layer_shrinkCopyFast |
| 0x10073650 | 0x1009bc70 | 0x1007a7b4 | 0x10035310 | ncbNativeFunction: Plugins_link |
| 0x10073670 | 0x1009bc78 | 0x1007a7dc | 0x10035360 | ncbNativeFunction: Plugins_unlink |
| 0x10073690 | 0x1009bbc8 | 0x100797cc | 0x100353b0 | ncbNativeFunction: Scripts_rehash |
| 0x100736b0 | 0x1009bc80 | 0x1007a7f8 | 0x10035440 | ncbNativeFunction: Storages_setCurrentDirectory |
| 0x10073700 | 0x1009d978 | 0x1007b3d8 | 0x10047960 | ncbNativeFunction: System_commandExecute |
| 0x10073720 | 0x1009d96c | 0x1007b278 | 0x100478c0 | ncbNativeClass: VProcess |
List 2 — ncbCallbackAutoRegister (5 entries)
| stub VA | entry VA | vtable VA | vtable[0] | entry[8] callback | 備考 |
|---|---|---|---|---|---|
| 0x100732f0 | 0x1009a398 | 0x1007770c | 0x100215d0 | 0x10020bd0 | TVPExecuteExpression でスクリプト実行 |
| 0x10073450 | 0x1009bc98 | 0x1007770c | 0x100215d0 | 0x00000000 | 未設定 |
| 0x10073470 | 0x1009bbe0 | 0x1007770c | 0x100215d0 | 0x00000000 | 未設定 |
| 0x10073490 | 0x1009bb98 | 0x1007770c | 0x100215d0 | 0x10033620 | TVPGetScriptDispatch + TJS呼び出し |
| 0x100736e0 | 0x1009d95c | 0x1007770c | 0x100215d0 | 0x00000000 | 未設定 |
List2[0] callback 0x10020bd0: TVPExecuteExpression と tTJSString::tTJSString(const tjs_char *) を使用。TJS スクリプトを実行してプラグイン設定を行う (推定)。
List2[3] callback 0x10033620: TVPGetScriptDispatch() + tTJSVariant::tTJSVariant(tjs_int32) を使用して TJS ディスパッチ経由で API を呼び出す。
2. TVPFunctionExporter 参照 (API ルックアップ)
TVPFunctionExporter テーブルは [0x100a0010] に格納される。ルックアップ関数は 0x10001b30 (引数: API 文字列 VA → 戻り値: 関数ポインタ)。
主要 API ルックアップ一覧:
| API 文字列 (VA) | 用途 |
|---|---|
tTJSVariant::tTJSVariant() (0x10098008) |
TJS バリアント初期化 |
tTJSString::tTJSString(const tjs_char *) (0x100983cc) |
ワイド文字列構築 |
tTJSString::tTJSString(const tTJSString &) (0x10098074) |
文字列コピー |
tTJSString::~ tTJSString() (0x100980a0) |
文字列解放 |
iTJSDispatch2 * ::TVPGetScriptDispatch() (0x10098a98) |
TJS スクリプトディスパッチ取得 |
void ::TVPExecuteExpression(...) (0x1009a8a4) |
TJS 式実行 |
void ::TVPRegisterStorageMedia(iTVPStorageMedia *) (0x1009c198) |
XP3 ストレージプロバイダ登録 |
void ::TVPUnregisterStorageMedia(iTVPStorageMedia *) (0x1009c1cc) |
ストレージプロバイダ解除 |
IStream * ::TVPCreateIStream(const ttstr &, tjs_uint32) (0x10099114) |
ファイルストリーム開く |
void ::TVPAddImportantLog(const ttstr &) (0x1009916c) |
ログ出力 |
3. XP3 アーカイブオープナーフック
登録経路
List0[2] callback (0x10032ec0)
-> TVPFunctionExporter lookup: TVPRegisterStorageMedia
-> fn 0x10031a40 ; alloc 0x14 bytes, call ctor 0x1002c630, call TVPRegisterStorageMedia
-> ctor 0x1002c630 : ProxyStorage ctor
sets vtable = 0x1007a5ec
COL @ 0x10090234 -> TypeDescriptor: .?AVProxyStorage@@
ProxyStorage クラス
- vtable VA:
0x1007a5ec - RTTI 型名:
.?AVProxyStorage@@ - 逆登録関数:
0x10028dd0(ポインタが0x1009b908に格納、TVPUnregisterStorageMedia を呼ぶ) - vtable[6]
0x10032980: ストリームオープン (iTVPStorageMedia::OpenStream相当)- 内部で
fn 0x10040080(URL フィルタ) を呼び出して XP3 エントリ番号を解決 - 次いで
fn 0x10014590(推定: 暗号ストリームラッパー生成) を呼び出す
- 内部で
4. 暗号クラス階層 (RTTI 復元)
RTTI 型名と vtable RVA
| 型名 (TypeDescriptor) | vtable VA | vtable RVA |
|---|---|---|
.?AUStreamCryptFilter@@ |
不明 | — |
.?AUBufferedStreamCryptFilter@@ |
不明 | — |
.?AUStreamCryptReaderWriter@@ |
0x10075590 | 0x00075590 |
.?AV?$BasicCryptFilter@U?$BlockStreamCipher@UChaCha@DJBernsteinStreamCipher@@@DJBernsteinStreamCipher@@@@ |
0x10075cb0 | 0x00075cb0 |
BasicCryptFilter 完全型名
BasicCryptFilter<
BlockStreamCipher<
ChaCha,
DJBernsteinStreamCipher
>,
DJBernsteinStreamCipher
>
RTTI COL (@ 0x1008e4e4): sig=0, offset=0, pTD=0x10099e50, pCH=0x1008e4f8
基底クラス (3): BasicCryptFilter → BufferedStreamCryptFilter → StreamCryptFilter
vtable 0x10075cb0 エントリ
| idx | VA | 備考 |
|---|---|---|
| [-1] | 0x1008e4e4 | CompleteObjectLocator (COL) |
| [0] | 0x10016f90 | dtor |
| [1] | 0x100154e0 | Read/Write dispatch |
| [2] | 0x1008e3a0 | 埋め込み COL (推定) |
| [17] | 0x10015610 | — |
| [18] | 0x10018830 | — |
| [19] | 0x10018830 | — |
5. ChaCha 暗号パラメータ
5-1. Sigma 定数 (expand 32-byte k)
.rdata VA 0x10075c54 (file offset 0x00074c54) に ビット反転 (NOT) で格納:
Raw: 9a 87 8f 9e 91 9b df cc cd d2 9d 86 8b 9a df 94
~Raw: 65 78 70 61 6e 64 20 33 32 2d 62 79 74 65 20 6b
ASCII: e x p a n d 3 2 - b y t e k
= 標準 ChaCha sigma "expand 32-byte k"。ChaCha 実装全体でステートワード (sigma, key, nonce) はメモリ上で常に反転して保持される。
5-2. 鍵設定関数
fn 0x10010570 (key setup):
[ebp+0xc]= key 配列先頭アドレス、[ebp+0x10]= nonce 配列先頭アドレス- key の各 4 バイトをビッグエンディアンで dword に組み立て
NOTを適用 → state+0x10..+0x2c (8 dwords = 32 bytes) - nonce の各バイトを同様に処理 → state+0x00..+0x0c
- 追加ワードを state+0x30..+0x3c に格納
- Sigma 定数
0x10075c54を別途NOTして state に展開
ChaCha ブロック関数 0x10011ed0 は SSE pandn (= NOT AND) を用いて state から実際の値を復元し、ROL 16/12/8/7 の標準 ChaCha ダブルラウンドを実行する。
5-3. ラウンド数の根拠
ブロック関数のループ変数 [ebp-0x84] = ceil(nonce_bytes / 2):
- 後退ジャンプ:
0x100121c8: jne 0x10011fc0 - 各ループ反復で QR を 8 回実行 (カラム 4 + 対角 4) = 1 ダブルラウンド = 2 ラウンド
- よってトータルラウンド数 =
loop_iters * 2 = nonce_bytes
5-4. ファクトリ関数とラウンド数の対応
fn 0x1000eb90 (cipher factory, 唯一の呼び出し元: 0x1000c140)
ジャンプテーブル: 0x1000ed3c (6 エントリ)
| cipher type | nonce_bytes | key_bytes | ラウンド数 | 種別 |
|---|---|---|---|---|
| 1 | 8 | 16 | 8 | ChaCha8 |
| 2 | 12 | 8 | 12 | ChaCha12 |
| 3 | 20 | 4 | 20 | ChaCha20 |
| 4 | 8 | 1 | 8 | ChaCha8 (key_bytes=1 → 推定: 1バイトキーの繰り返しまたは別方式) |
| 5 | 12 | 1 | 12 | ChaCha12 (同上) |
| 6 | 20 | 1 | 20 | ChaCha20 (同上) |
type 1/2/3 (key_bytes=16/8/4) と type 4/5/6 (key_bytes=1) の 2 グループが存在する。key_bytes=1 の場合の鍵展開方法は 不明 (推定: 1バイトから 32 バイトへの繰り返し埋め、または別の KDF)。
5-5. 鍵・Nonce の出処 (完全解析)
経路の分離確認
2 つの経路は完全に独立している:
| 経路 | 鍵設定関数 | 起動契機 |
|---|---|---|
| loadDataPack | fn 0x10010570 ← fn 0x1000bb80 ← fn 0x1000eb90 ← fn 0x1000c100 |
TJS Scripts.loadDataPack() |
| XP3 openStream | ProxyStorage::vtable[6] = fn 0x10032980 |
KANADE.exe が XP3 エントリを開くとき |
fn 0x10010570 の唯一の呼び出し元は fn 0x1000bb80 のみ (xref 1件: 0x1000bd3b)。
fn 0x10032980 は fn 0x1000bb80 も fn 0x10014590 も fn 0x1000c300 も呼ばない。
loadDataPack 経路の鍵導出 (完全追跡)
呼び出し連鎖:
TJS: Scripts.loadDataPack(path)
-> fn 0x10010a90 (loadDataPack コールバック; 0x1000f124 で登録)
-> fn 0x1000c300 (StorageReader ctor)
-> IStream::Read(buf, 0x44) // ヘッダ 68 バイト読み取り
-> fn 0x10014590 (stream wrapper factory)
-> fn 0x1000c570 (config struct 初期化)
-> fn 0x1001c860 (IStream ヘッダ解析 + 追加バイト読み取り)
-> IStream::Read([ebp-0x14], 0x10) // nonce 候補 16 バイト読み取り
-> IStream::Read([esi+0x14], [esi+0x24]) // key_string バイト読み取り
-> fn 0x1000c100 -> fn 0x1000eb90(type=1) -> fn 0x1000bb80 (BasicCryptFilter ctor)
fn 0x1001c860 内で IStream からの読み取り確定:
0x1001c978: call [eax+8]=IStream::Read([ebp-0x14], 0x10)→ 16 バイト nonce 候補0x1001c9ad: call [eax+8]=IStream::Read([esi+0x14], [esi+0x24])→ key_string バイト[esi+0x24]= key_string のバイト長 (config から)- 読んだ key_string は StorageReader の
this+0x28に格納される
fn 0x1000bb80 における ChaCha 鍵導出アルゴリズム
fn 0x1000bb80 (BasicCryptFilter ctor) の鍵導出手順 (命令レベルで確定):
Step 1: SHA-256 ベース KDF
SHA256_state = [ebp-0xec]
key_string = arg_14 (StorageReader+0x28 = IStream から読んだバイト列)
key_len = arg_18 (key_string の長さ)
// 0x1000bc96: mov [ebp-0x70], 0x01010420 (4 バイト整数、LE: 20 04 01 01)
init_block[0..3] = 0x20040101 // 定数
init_block[4..31] = 0x00 * 28 // ゼロパディング
// 0x1000bc9d: call 0x10017be0(ecx=SHA256_state, arg=[ebp-0x70])
fn 0x10017be0: SHA256_init(state) then XOR state IV with init_block[0..31]
// -> HMAC-SHA256 的な初期化 (初期 IV を定数で XOR してから開始)
// 0x1000bcc8: call 0x1001be20(ecx=SHA256_state, data=key_string, len=key_len)
SHA256_update(state, key_string, key_len)
// 0x1000bd00: call 0x100174f0(ecx=SHA256_state, out=[ebp-0x118], len=0x20)
SHA256_finalize(state) → 32 バイトダイジェスト at [ebp-0x118]
// これが ChaCha KEY (32 バイト)
Step 2: XXH32 ハッシュ (extra フィールド用)
// 0x1000bd16: call 0x10009700(data=key_string_ptr, len=key_len, seed=cipher_type)
fn 0x10009700: XXH32(key_string, key_len, seed=cipher_type=1)
// 返値 eax = 32-bit hash = extra_lo フィールド
Step 3: ChaCha state 初期化呼び出し
// 0x1000bd3b: call 0x10010570 の引数 (push 順):
push ebx+0x2c // state_ptr (arg+0x08)
push [ebp-0x118] // key_ptr (arg+0x0c) = SHA256(key_string) の 32 バイト
push 0x10075c54 // nonce_ptr (arg+0x10) = sigma 定数 "~(expand 32-byte k)"
push xxh32_result // extra_lo (arg+0x14) = XXH32(key_string, cipher_type)
push 0 // extra_hi (arg+0x18)
push 0 // extra_a (arg+0x1c)
push cipher_type(=1) // extra_b (arg+0x20)
nonce_ptr = 0x10075c54 (sigma 定数の NOT 格納アドレス = ~"expand 32-byte k")
つまり nonce は ChaCha sigma 定数で固定 (全 type 共通)。
fn 0x10010570 引数マッピング (確定版)
fn 0x10010570(state_ptr, key_ptr, nonce_ptr, extra_lo, extra_hi, extra_a, extra_b):
[ebp+0x08] = state base ptr
[ebp+0x0c] = key_ptr → SHA256(key_string) 32 バイト
[ebp+0x10] = nonce_ptr = 0x10075c54 (sigma 定数 NOT 格納)
[ebp+0x14] = extra_lo = XXH32(key_string, cipher_type) → ~extra_lo → state+0x38
[ebp+0x18] = extra_hi = 0 → ~0 → state+0x3c
[ebp+0x1c] = extra_a = 0 → ~0 → state+0x30
[ebp+0x20] = extra_b = cipher_type (=1) → ~1 → state+0x34
key 32 バイト → state+0x10..0x2c (各 4 バイト LE→BE + NOT)
nonce 16 バイト → state+0x00..0x0c (NOT なし)
XP3 openStream 経路 (fn 0x10032980) の鍵供給
fn 0x10032980 は fn 0x10010570/fn 0x1000bb80 を一切呼ばない。
鍵は TJS Dispatch オブジェクト経由で供給される:
fn 0x10032980 (ProxyStorage::OpenStream):
1. fn 0x10040080: URL → ProxyStorage[+4] dispatch に FuncCall(filepath) → TJSVariant result
2. VariantType 分岐:
- Type 1 (Object): fn 0x1002dea0 -> fn 0x1002b370 でストリームラップ
-> fn 0x10030860: QueryInterface('HSmM') at vtable+0x64
ProxyStorage vtable[25] = fn 0x1002e780 が担当
-> fn 0x100443a0: cipher attach (ecx+0x0c が cipher ptr)
- Type 2 (String): TVPCreateIStream -> QueryInterface('HSmM') on engine stream
engine (KANADE.exe) 側が鍵を供給 — PackinOne は関与しない
- Type 3 (Octet): fn 0x1003bfa0 -> fn 0x10044460 -> fn 0x10043c40
ProxyStorage dispatch FuncCall でパラメータ取得
3. fn 0x1003bfa0 -> fn 0x100443a0 -> vtable+0x0c に cipher ptr がセットされる
XP3 経路の鍵素材は TJS スクリプトが Plugins.PackinOneList グローバルオブジェクトに
事前に設定した dispatch 関数から得られる。 PackinOne.dll のバイナリに鍵定数はない。
TVPRegisterGlobalObject("PackinOneList", ...) (fn 0x10032ec0、0x100330c5) で
TJS グローバル Plugins.PackinOneList に ProxyStorage インスタンスが登録される。
鍵素材の出処まとめ
| 経路 | key 素材 | nonce | cipher_type | KDF |
|---|---|---|---|---|
| loadDataPack | IStream から読んだ可変長バイト列 (StorageReader+0x28) |
0x10075c54 = sigma定数固定 |
1 (ハードコード) | HMAC-SHA256(constant, key_string) + state extra にXXH32 |
| XP3 openStream (Type 1/3) | TJS Dispatch FuncCall の返値 (スクリプト制御) | スクリプト制御 | スクリプト制御 | スクリプト側で完結 |
| XP3 openStream (Type 2) | KANADE.exe engine 側 | engine 側 | engine 側 | 不明 (engine 内部) |
結論: 3 XP3 (data/patch/steam) が異なる keystream を使うのは、各 XP3 エントリに対応する
TJS Dispatch FuncCall が返すオブジェクト (key material) がアーカイブ別に異なるためと推定。
PackinOne.dll 内に XP3 名から鍵を派生させるコードは存在しない — 鍵はすべてスクリプト側から注入される。
Q1. 何のファイルから 0x44 バイトを読んでいるか
fn 0x1000c300 は TJS スクリプトから Scripts.loadDataPack(path, ...) を呼んだときに起動するコールバック (fn 0x10010a90) 経由で呼ばれる。
ファイルパスは TJS バリアント (実行時スクリプト変数) からの文字列であり、バイナリに定数として埋め込まれていない。ゲーム固有の TJS スクリプト (startup.tjs / config.tjs 等) で決まる。
Q2. 0x44 バイトのヘッダレイアウト (部分確定)
| header offset | サイズ | StorageReader フィールド | 用途 |
|---|---|---|---|
| 0x00..0x03 | 4 | this+0x10 |
fn 0x1000c100 → key/nonce 選択 |
| 0x04..0x07 | 4 | this+0x14 |
詳細不明 |
| 0x08..0x3f | 56 | 不使用 (fn 0x1000c300 内では未代入) | 不明 |
this+0x08 (cipher_type) は 0x1000c33a: mov byte [esi+8], 1 でハードコード=1 に固定。
Q3. cipher_type (1-6) の出処
loadDataPack 経路: fn 0x100145e8: push 1 でハードコード type=1 (ChaCha8, nonce=8 bytes)。
XP3 openStream 経路: TJS スクリプト制御。静的解析では確定不能。
5-6. 暗号ストリームラッパー
fn 0x10014590 (stream wrapper factory; loadDataPack 経路):
arg1 = IStream ptr
arg2 = config struct ptr (edi)
edi+0x08 = cipher type (常に 1)
edi+0x0c = nonce ptr
edi+0x10 or edi+0x28 = key area (選択は edi+0x38 > 0 か否か)
1. fn 0x1000c570 で config struct を初期化
2. fn 0x1001c860 で ストリーム方向 (read "TJS/" / write "TJS\") とチェックを実施
3. fn 0x1000c100 で BasicCryptFilter を生成
-> factory fn 0x1000eb90 を type=1 (ハードコード) で呼び出し
-> StreamCryptReaderWriter (vtable 0x10075590) でラップして返す
XP3 openStream 経路 (ProxyStorage::vtable[6] = fn 0x10032980):
fn 0x10032980 (ProxyStorage::OpenStream):
1. fn 0x10040080 で URL フィルタ / XP3 エントリ解決
2. TJS バリアント型でブランチ:
- type=0 (TJSVariantType_Object): fn 0x1002dea0 経由 → fn 0x1002b370
- type=1 (TJSVariantType_String): TVPCreateIStream でファイルオープン
- type=2 (TJSVariantType_Octet): fn 0x1003bfa0 → fn 0x10044460 (GlobalAlloc)
3. fn 0x10030860 (QueryInterface 'HSmM') で暗号 extension を問い合わせ
4. fn 0x10043c40 で dispatch vtable[6] 呼び出し → ストリーム付与
fn 0x1002b370/0x1002b430: 0x20/0x30 バイト struct を alloc し vtable (0x1007a774 / 0x1007a6fc) をセット。
内部で fn 0x10041940 (TJSVariantOctet からデータポインタ/長さ取得) と fn 0x10041aa0 (TJS dict から "file"/"offset"/"size" キーを取得してストリーム組み立て) を呼ぶ。
Q4. LZ4 と ChaCha の呼び出し順序
fn 0x10041aa0 のフローから:
fn 0x100419d0: ファイル名文字列でTVPCreateIStreamを呼んで raw IStream を開くfn 0x10042c60: IStream を offset に seekfn 0x100408f0: size を設定 (range-limit ラッパー)fn 0x10030860/fn 0x10043c40: 暗号ストリームをラップ (ChaCha)- LZ4DecompressStream (vtable
0x10099e0c) は別経路
LZ4DecompressStream の vtable 参照は .rdata の COL 0x1008e048 / 0x1008e074 にのみ存在し、XP3 openStream の直接経路では呼ばれていない。LZ4 解凍は ChaCha 復号の後に 別レイヤーで行われると推定されるが、静的解析でその具体的な呼び出し位置は未確認。
Q5. Hxv4 blob の flag (2 bytes) の意味
TLG 画像フォーマットのエラーメッセージ "Data flag must be 0 (any flags are not yet supported)" (VA 0x1007d7f2) はあるが、これは TLG ピクセルフォーマットのフラグであり Hxv4 index の flag フィールドとは別物。
Hxv4 blob (14 byte: offset[8] + size[4] + flag[2]) の flag フィールドが使われているコードを静的に特定できなかった。バイナリ内に "flag" テーブルキーや flag 分岐の有力な候補が見当たらない。
推定: Hxv4 index は PackinOne 側ではなく KirikiriZ エンジン本体 (KANADE.exe) で解析されている可能性が高く、PackinOne.dll の静的解析では flag の意味は不明。
6. XP3 に使用される cipher type の特定
cipher type は fn 0x1000c300 内のハードコード mov byte [esi+8], 1 によって 常に 1 (ChaCha8, nonce=8 bytes, key=16 bytes) に固定されている。
header (0x44 バイト) の [4..7] フィールドが this+0x10 に格納されるが、これは cipher type ではなく key selection フラグまたは key 素材の一部と推定される。
data.xp3(226269 bytes)、patch.xp3(11240 bytes)、steam.xp3(198 bytes) が対象アーカイブとして存在することは確認済み。- cipher type は設定ファイルのヘッダから取得されることはなく、type 4/5/6 (ChaCha12/20, key_bytes=1 グループ) が XP3 読み取りに使われることは静的解析では確認されていない。
7. その他の暗号関連実装
| 関数 VA | 型 / 役割 |
|---|---|
| 0x10017a40 | SHA-256 (IV 定数 0x6a09e667 確認済み) |
| 0x1009a0ac | .?AUXXH32Hasher@@ (XXH32 ハッシャ) |
| 0x1009a0c8 | .?AUBlake2sHasher@@ (Blake2s ハッシャ) |
| 0x1009a090 | .?AUVariantHasher@@ |
| 0x10099df0 | .?AVLZ4StreamBase@@ (LZ4 圧縮) |
| 0x10099e0c | .?AVLZ4DecompressStream@@ |
8. TJS メソッド登録 (makeDataPackDigest 等)
fn 0x1000ee30 が以下の TJS メソッドを登録する:
| type | fn VA | TJS メソッド名 |
|---|---|---|
| 1 | 0x100110d0 | makeDataPackDigest |
| 3 | 0x10012220 | makeDataPackThumb |
| 7 | 0x10010a90 | saveDataPack |
| 0xf | (不明) | loadDataPack |
TJS スクリプト側からの登録確認 (fn 0x1000ee30 の呼び出し元 0x1000f110 周辺):
| 登録 VA | コールバック fn VA | TJS メソッド / スコープ |
|---|---|---|
| 0x1000f124 | 0x10010a90 | Scripts.loadDataPack (index=7) |
| 0x1000f32d | 0x10010eb0 | Scripts.loadOctet (index=0x11) |
fn 0x10010a90 が fn 0x1000c300 を呼ぶ経路:
Scripts.loadDataPack(path) → fn 0x10010a90 → fn 0x1000c300(StorageReader, ttstr_from_variant, flag=1)
9. 不明・推定の整理
| 項目 | 状態 | 理由・根拠 |
|---|---|---|
| Q1: 読み込むファイルパス | 不確定 | TJS Scripts.loadDataPack(path) の path は実行時スクリプト変数。バイナリに定数なし |
| Q2: 0x44 ヘッダの全フィールドレイアウト | 部分確定 | [0..3]→this+0x10, [4..7]→this+0x14 のみ追跡。[0x08..0x43] の用途は未確認 |
| Q3: cipher_type の出処 | 確定 | mov byte [esi+8], 1 でハードコード type=1。header からは読まれない |
| Q4: LZ4 と ChaCha の順序 | 推定 | ChaCha 後に LZ4 と推定。LZ4DecompressStream のコール経路が XP3 openStream 上で未特定 |
| Q5: Hxv4 flag (2 bytes) の意味 | 不明 | PackinOne.dll 内で flag 値を参照する分岐コードが静的に特定できず。エンジン本体依存の可能性 |
| key_bytes=1 の場合の鍵展開方式 | 不明 | type 4/5/6 の内部実装を詳細追跡していない |
| BasicCryptFilter vtable[2..16] の意味 | 推定 | vtable[2]=0x1008e3a0 は埋め込み COL と推定、詳細未確認 |
| key_bytes=4 (type=3, ChaCha20) の意味 | 推定 | 4バイト鍵の用途は不明。XOR マスクや IV のみとして使う可能性あり |