diff --git a/src/minimapicons/RESEARCH.md b/src/minimapicons/RESEARCH.md index 169bad0..5c11454 100644 --- a/src/minimapicons/RESEARCH.md +++ b/src/minimapicons/RESEARCH.md @@ -14,38 +14,52 @@ The addon's .lua code still runs (served via `loadFileDetour`), but it's loaded ``` HandleLogin (0x0046afb0) - -> SetupAddonProcessing (0x0051c740) - -> ProcessAddonDirectory (0x0051c760) - -> EnumerateDirectoryWithCallback("Interface\\AddOns\\", ...) -- filesystem scan + -> SetupAddonProcessing (0x0051c740) -- __fastcall(ECX=mgr_ptr), RET + -> ShutdownAddonSystem (0x0051fa40) -- clears old state + -> ProcessAddonDirectory (0x0051c760) -- filesystem scan + -> EnumerateDirectoryWithCallback("Interface\\AddOns\\", ...) -> ProcessAddonFilePath (0x0051c910) -- per .toc file found -> LoadAddonTOC (0x0051c9b0) -- parses TOC, registers addon in hash table -- reads TOC via LoadFileWithTextureResourceFallback (hookable!) + -> [our hook: callLoadAddonTOC per non-hidden embedded addon] InitializeGameInterface (0x0048fbf0) -> LoadAddonSavedVariables (0x0051ebe0) -- loads WTF/.../AddOns.txt (enabled/disabled state per addon) -> LoadAddonsRecursively (0x0051f600) -- iterates registered addon list -> LoadAddonRecursive (0x0051f242) -- per addon: 1. loadFileListWithIncludes() -- loads TOC file list (.lua/.xml) - 2. Loads Bindings.xml if exists + 2. Loads Bindings.xml via preloadFileWithFlags (does NOT go through our file hook) 3. Loads WTF/Account//SavedVariables/.lua (account-wide) 4. Loads WTF/Account////SavedVariables/.lua (per-character) 5. FireLuaEvent(0x1ad) -- ADDON_LOADED event - -> Config_LoadSavedVariables (0x0051f650) -- called from World_HandlePlayerLogin (0x00490bd0) on logout/exit + -> [our hook: for hidden addons, callLoadFileListWithIncludes + callLoadUIBindingsFromFile] + -> [our hook: for all addons with Bindings.xml, callLoadUIBindingsFromFile] + -> SaveAddonVariables (0x0051f650) -- called from World_HandlePlayerLogin (0x00490bd0) on logout ``` ### Key Functions | Address | Name | Convention | Notes | |------------|----------------------------|------------|-------| -| 0x0051c740 | SetupAddonProcessing | ? | Calls ProcessAddonDirectory | +| 0x0051c740 | SetupAddonProcessing | __fastcall(ECX=mgr_ptr), RET | Calls ShutdownAddonSystem + ProcessAddonDirectory | | 0x0051c760 | ProcessAddonDirectory | cdecl/void | Scans filesystem, builds addon list | | 0x0051c910 | ProcessAddonFilePath | __fastcall(ECX=path) | Per-.toc callback, strips prefix, calls LoadAddonTOC | -| 0x0051c9b0 | LoadAddonTOC | __fastcall(ECX=addonName) | Parses TOC, registers addon. RET (no stack cleanup) | +| 0x0051c9b0 | LoadAddonTOC | __fastcall(ECX=addonName), RET | Parses TOC, registers addon in hash table | +| 0x0051d410 | ProcessAddonInterface | __fastcall(ECX=addonData) | Reads .pub file for secure addons | +| 0x0051d4c0 | ProcessAddonURL | __fastcall(ECX=addonData) | Reads .url file | | 0x0051ebe0 | LoadAddonSavedVariables | __fastcall(ECX=addonName or NULL for all) | Loads AddOns.txt state | | 0x0051f242 | LoadAddonRecursive | __fastcall(ECX=addonName, EDX=loadFlag, stack=callback) | Loads one addon + saved vars | | 0x0051f600 | LoadAddonsRecursively | __fastcall(ECX=callback) | Iterates all registered addons | | 0x0051f650 | SaveAddonVariables | cdecl/void | Writes all dirty addons' saved vars to WTF/ | -| 0x0051fa40 | ShutdownAddonSystem | ? | Cleanup | +| 0x0051fa40 | ShutdownAddonSystem | ? | Cleanup | +| 0x0064b3f0 | String_ComputeHash | **__stdcall(string)**, RET 0x4 | NOT fastcall! Reads from [EBP+8]. Returns hash in EAX | +| 0x0051def0 | GetAddonCount | cdecl | Returns PTR_00be1b90 | +| 0x0051df00 | GetAddonByIndex | __fastcall(ECX=index) | Returns PTR_00be1b94[index] | +| 0x0051e3e0 | IsAddonSecure | __fastcall(ECX=name) | Returns addon[0x30] byte (from .pub parsing) | +| 0x0051e470 | ValidateCommandAccess | __fastcall(ECX=name, EDX=acctName, stack=flag) | Checks addon enabled state | +| 0x0051e780 | EnableAddonWithDependencies | __fastcall(ECX=name, ...) | Validates + enables addon | +| 0x0051e980 | GetAddonSecurityString | __fastcall(ECX=state) | Returns PTR_s_SECURE_0085367c[state] | +| 0x0051e990 | GetAddonState | __fastcall(ECX=name) | Returns addon[9] (+0x24) from hash table | ### Addon Data Structure (hash table entry) @@ -57,8 +71,9 @@ InitializeGameInterface (0x0048fbf0) +0x19: u8 loaded flag (entry[6] byte 1) +0x1c: u32 interface version (## Interface) +0x20: u32 revision (## Revision) ++0x24: u32 security state (0=SECURE, 1=INSECURE, 2=BANNED) +0x26-0x2f: hash table for Title/Notes/Author/Version metadata -+0x28: u8 secure flag (## Secure) ++0x28: u8 secure TOC flag (## Secure) -- triggers .pub validation +0x2b: u8 default state enabled/disabled (## DefaultState) +0x2c: u8 load on demand flag (## LoadOnDemand) +0x34: u32 line count in TOC @@ -70,55 +85,104 @@ Dynamic arrays (each: capacity/count/data_ptr/growth at 3 dword intervals): +0x68/6c/70/74: SavedVariables (## SavedVariables) -- account-wide +0x78/7c/80/84: SavedVariablesPerCharacter (## SavedVariablesPerCharacter) +0x8c-0x98: dependents array + ++0xC1: u8 .pub type byte (from ParseAddonInterfaceData) ++0xC2: [256]u8 .pub key data (256 bytes) ++0x1C2: [16]u8 MD5 hash for signature validation ``` ### Key Globals | Address | Name | Purpose | |------------|------|---------| -| 0x00be1b6c | PTR_00be1b6c | Addon linked list head | +| 0x00be1b60 | PTR_00be1b60 | Hash table control structure | | 0x00be1b64 | PTR_00be1b64 | Linked list base for traversal (node+4 offset) | +| 0x00be1b6c | PTR_00be1b6c | Addon linked list head | | 0x00be1b7c | PTR_00be1b7c | Hash table buckets array | | 0x00be1b84 | PTR_00be1b84 | Hash table mask (0xffffffff = uninitialized) | +| 0x00be1b90 | PTR_00be1b90 | Flat addon count (for GetAddonCount) | +| 0x00be1b94 | PTR_00be1b94 | Flat addon array (for GetAddonByIndex) | | 0x00be1bd8 | PTR_00be1bd8 | Saved variables state list head | -| 0x00be1b60 | PTR_00be1b60 | Hash table control structure | -### Fix: Hook SetupAddonProcessing +### Hash Table Structure +- Buckets at PTR_00be1b7c, mask at PTR_00be1b84 +- Each bucket is 12 bytes: `[+0] next_offset, [+4] ?, [+8] list_head_ptr` +- Node traversal: `next = *(node + bucket.next_offset + 4)` +- Lookup: compute hash via `String_ComputeHash`, bucket = `mask & hash`, walk chain comparing hash then name -`LoadAddonTOC` reads the TOC file via `LoadFileWithTextureResourceFallback`, which our `loadFileDetour` already intercepts. So calling `LoadAddonTOC("MinimapIcons")` will: -1. Hit our file hook, serve the embedded TOC content -2. Parse `## SavedVariablesPerCharacter: WeirdUtils_MinimapIconsSettings` -3. Register the addon in the hash table with the variable name at +0x78/7c/80 +## Addon Security System -Hook `SetupAddonProcessing` (0x0051c740). After calling the original (which runs `ProcessAddonDirectory` and initializes the addon system), call `LoadAddonTOC` for each DLL-embedded addon. This ensures: -- The addon memory pool is initialized -- Our addons appear in the list before `LoadAddonsRecursively` runs -- `LoadAddonRecursive` will find our addon, load its files (via our hook), load saved vars from WTF/, and fire ADDON_LOADED -- `SaveAddonVariables` will find our addon and write its variables to WTF/ on logout +### Security States (+0x24) +- **0 = SECURE** -- Blizzard addon, hidden from addon list, always loaded +- **1 = INSECURE** -- Normal third-party addon, shown in list +- **2 = BANNED** -- Failed signature validation, shown as "Banned" -**Note:** Bindings.xml loading via `preloadFileWithFlags` does NOT go through our file hook, so we still explicitly call `callLoadUIBindingsFromFile` in `loadAddonsDetour` for addons with Bindings.xml. +String table at `PTR_s_SECURE_0085367c` indexed by state value. -### Current Status (WORKING) +### Signature Validation Flow +When `## Secure: 1` is in the TOC: +1. `LoadAddonTOC` sets `+0x28` secure flag, then calls `ProcessAddonInterface` + `ProcessAddonURL` +2. `ProcessAddonInterface` (0x0051d410) reads `Interface\AddOns\\.pub` via `LoadFileWithTextureResourceFallback` +3. `.pub` file must be exactly **257 bytes**: 1 byte type + 256 bytes public key +4. Stored at addon struct `+0xC1` (type) and `+0xC2` (key data) +5. Later in `LoadAddonRecursive`, MD5 hash of addon files is compared against signature +6. If no valid `.pub` or signature mismatch: security state set to **2 (BANNED)** -- SavedVariablesPerCharacter persists to `WTF/Account/.../SavedVariables/WeirdUtils_MinimapIcons.lua` -- Addon names use `WeirdUtils_` namespace (6 addons) -- Game's `LoadAddonRecursive` handles .lua/.toc loading and saved variables -- Explicit bindings loading still needed (kept in `loadAddonsDetour`) +### Hiding Addons -- Approaches Tried -### Verification Plan +**Approach 1: `## Secure: 1` in TOC** -- FAILED +Sets secure flag but without a valid `.pub` file, `ProcessAddonInterface` runs and the addon +ends up as BANNED (state 2). Cannot forge a valid `.pub` without Blizzard's private key. -1. Hook `SetupAddonProcessing`, call `LoadAddonTOC("WeirdUtils_MinimapIcons")` after original -2. Check console for `[file] served embedded` messages for the .toc read during `LoadAddonTOC` -3. Toggle some NPC categories, log out -4. Check `WTF/Account////SavedVariables/WeirdUtils_MinimapIcons.lua` exists on disk -5. Re-login, verify toggles persisted +**Approach 2: Patch +0x24 after LoadAddonTOC** -- NOT VERIFIED +After calling `LoadAddonTOC`, look up addon in hash table and write 0 to `+0x24`. +Issue: `String_ComputeHash` is `__stdcall` NOT `__fastcall` -- initial crash was caused +by calling it via `hook.fastcall` which passed the string pointer in ECX instead of on +the stack. Fixed to use direct function pointer cast with `callconv(sc)`. +The write logic itself was not verified to work at runtime before being removed. -## Login Screen Addon List (WIP) +**Approach 3: Skip LoadAddonTOC for hidden addons** -- CURRENT +Hidden addons (`hidden = true` in EmbedModule) are NOT registered via `LoadAddonTOC` at all. +They don't appear in the addon hash table, so the game doesn't know about them. +Their files are loaded directly via `callLoadFileListWithIncludes` in `loadAddonsDetour`, +same as the original pre-SavedVariables approach. This means they are invisible in the +addon list but also cannot have SavedVariables. -### Goal -Make DLL-embedded addons appear in the character select screen's "AddOns" button list. -This allows users to enable/disable them before entering world, and the DLL could -check the enabled state to skip hooking for disabled addons. +### Current Implementation + +Two-tier addon loading controlled by `EmbedModule.hidden`: + +**Visible addons** (`hidden = false`): +- Registered via `LoadAddonTOC` in `setupAddonsDetour` (hook on SetupAddonProcessing) +- Appear in charselect addon list as INSECURE (normal) +- Game handles file loading, SavedVariables, ADDON_LOADED events +- Bindings.xml loaded explicitly (preloadFileWithFlags bypass) + +**Hidden addons** (`hidden = true`): +- NOT registered via `LoadAddonTOC` -- invisible in addon list +- Files loaded directly via `callLoadFileListWithIncludes` in `loadAddonsDetour` +- Bindings.xml loaded explicitly +- No SavedVariables support (no addon hash table entry) + +| Addon | Hidden | +|-------|--------| +| WeirdUtils_Screenshot | yes | +| WeirdUtils_Interact | no | +| WeirdUtils_Outline | no | +| WeirdUtils_WorldMarkers | yes | +| WeirdUtils_LogSessions | no | +| WeirdUtils_MinimapIcons | no | + +### Future: Proper Addon Hiding +To revisit Approach 2 (patching +0x24 to SECURE after registration), need to: +1. Verify `markAddonSecure` hash table traversal with debug logging +2. Confirm `hook.writeMem` actually writes to the addon struct +3. Check if `GetAddonState` is the only thing the UI reads, or if there are other + security-related fields that also need patching +4. Investigate whether generating a valid `.pub` + MD5 signature is feasible + (likely needs Blizzard's private key, but the validation algorithm is in-binary) + +## Login Screen Addon List ### Architecture @@ -126,13 +190,7 @@ Two separate Lua C function tables exist for addon management: - **Glue (login/charselect) table** at 0x008374a0 -- functions in 0x0046dxxx range - **In-game table** at 0x0083e488 -- functions in 0x0048exxx range -Both read from the **same** underlying addon data: -- `GetAddonCount` (0x0051def0): returns `PTR_00be1b90` (simple count) -- `GetAddonByIndex` (0x0051df00): returns `PTR_00be1b94[index]` (flat array of addon ptrs) - -These globals (`PTR_00be1b90` count, `PTR_00be1b94` array) are separate from the -hash table (PTR_00be1b7c) and linked list (PTR_00be1b6c) -- they're a flat indexed -view, likely populated after `ProcessAddonDirectory` finishes. +Both read from the **same** underlying addon data. #### Glue Lua Functions (charselect screen) @@ -150,39 +208,18 @@ view, likely populated after `ProcessAddonDirectory` finishes. | 0x0046d990 | SaveAddOns | | 0x0046d9a0 | ResetAddOns | -### Call Flow +### Data Path +- `GetAddonCount` (0x0051def0): returns `PTR_00be1b90` (simple count) +- `GetAddonByIndex` (0x0051df00): returns `PTR_00be1b94[index]` (flat array of addon ptrs) -``` -HandleLogin (0x0046afb0) -- triggered by login credentials submit - -> SetupAddonProcessing (0x0051c740) -- OUR HOOK IS HERE - -> ShutdownAddonSystem (0x0051fa40) -- clears old state - -> ProcessAddonDirectory (0x0051c760) -- filesystem scan - -> [our hook: callLoadAddonTOC per embedded addon] -``` - -After `HandleLogin` returns, the client transitions to the character select screen -where the "AddOns" button is available. The Lua `GetNumAddOns`/`GetAddOnInfo` calls -read from the flat array at PTR_00be1b90/PTR_00be1b94. +These are a flat indexed view, separate from the hash table (PTR_00be1b7c) and +linked list (PTR_00be1b6c). Registered addons appear in the list after `SetupAddonProcessing`. ### Open Questions -1. **When is PTR_00be1b90/PTR_00be1b94 populated?** These are a flat indexed array, - not the hash table. Possibly built from the linked list after `ProcessAddonDirectory` - finishes, or lazily on first `GetAddonCount` call. Need to find xrefs to PTR_00be1b90 - to determine who writes to it and when. +1. **When is PTR_00be1b90/PTR_00be1b94 populated?** Need to find xrefs to determine + who writes to it and when relative to our hook. -2. **Does our `LoadAddonTOC` call also update the flat array?** If it only adds to the - hash table + linked list but not the flat array, our addons won't appear in the UI. - Need to trace `LoadAddonTOC` -> `Container_MoveOrInsertElement` to see if the flat - array is updated as a side effect. - -3. **Timing**: Our hook fires after `SetupAddonProcessing` returns (which is after - `ProcessAddonDirectory`). If the flat array is built at the end of - `ProcessAddonDirectory` and never updated, we'd need to either: - - Hook ProcessAddonDirectory and inject our addons before the array is built - - Manually append to PTR_00be1b90/PTR_00be1b94 after calling LoadAddonTOC - - Find and call whatever rebuilds the flat array - -4. **Enable/Disable state**: `EnableAddOn`/`DisableAddOn` write to `AddOns.txt` via - `SaveAddOns`. The DLL could check this file or the addon struct's enabled field +2. **Enable/Disable persistence**: `EnableAddOn`/`DisableAddOn` write to `AddOns.txt` + via `SaveAddOns`. The DLL could check this file or the addon struct's enabled field at startup to decide whether to install hooks for a given module. diff --git a/src/minimapicons/addon/MinimapIcons.lua b/src/minimapicons/addon/MinimapIcons.lua index ab618b4..f7848bf 100644 --- a/src/minimapicons/addon/MinimapIcons.lua +++ b/src/minimapicons/addon/MinimapIcons.lua @@ -84,28 +84,28 @@ local AMMO_FILTERS = { -- Filtered entries take priority over unfiltered catch-all entries with the same flag. -- "dynamic" filter is resolved at runtime via getFilter(). local NPC_CATEGORIES = { - { name = "Auctioneer", trackingType = "auctioneer", icon = "Interface\\Minimap\\Tracking\\Auctioneer" }, { name = "Banker", trackingType = "banker", icon = "Interface\\Minimap\\Tracking\\Banker" }, - { name = "Battle Master", trackingType = "battlemaster", icon = "Interface\\Minimap\\Tracking\\BattleMaster" }, - { name = "Brainwasher", trackingType = "brainwasher", icon = "Interface\\Minimap\\Tracking\\Brainwasher", scale = 1.8, default = 1 }, - { name = "Class Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Class", - getFilter = function() return (UnitClass("player")) end }, - { name = "Flight Master", trackingType = "flightmaster", icon = "Interface\\Minimap\\Tracking\\FlightMaster" }, - { name = "Innkeeper", trackingType = "innkeeper", icon = "Interface\\Minimap\\Tracking\\Innkeeper", default = 1 }, + { name = "Auctioneer", trackingType = "auctioneer", icon = "Interface\\Minimap\\Tracking\\Auctioneer" }, + { name = "Repair", trackingType = "repair", icon = "Interface\\Minimap\\Tracking\\Repair", default = 1 }, { name = "Mailbox", trackingType = "mailbox", icon = "Interface\\Minimap\\Tracking\\Mailbox" }, - { name = "Poison Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Poison", - getFilter = function() return getLocaleFilter(POISON_FILTERS) end }, - { name = "Profession Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Profession", - getExclude = function() return UnitClass("player") end }, + { name = "Brainwasher", trackingType = "brainwasher", icon = "Interface\\Minimap\\Tracking\\Brainwasher", scale = 1.8, default = 1 }, + { name = "Innkeeper", trackingType = "innkeeper", icon = "Interface\\Minimap\\Tracking\\Innkeeper", default = 1 }, + { name = "Flight Master", trackingType = "flightmaster", icon = "Interface\\Minimap\\Tracking\\FlightMaster" }, { name = "Reagent Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Reagents", getFilter = function() return getLocaleFilter(REAGENT_FILTERS) end }, - { name = "Repair", trackingType = "repair", icon = "Interface\\Minimap\\Tracking\\Repair", default = 1 }, - { name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" }, + { name = "Poison Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Poison", + getFilter = function() return getLocaleFilter(POISON_FILTERS) end }, { name = "Trade Goods", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Profession", getFilter = function() return getLocaleFilter(TRADE_FILTERS) end }, { name = "Ammunition", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Ammunition", getFilter = function() return getLocaleFilter(AMMO_FILTERS) end }, - { name = "General Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Food" }, + { name = "General Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Food" }, + { name = "Class Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Class", + getFilter = function() return (UnitClass("player")) end }, + { name = "Profession Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Profession", + getExclude = function() return UnitClass("player") end }, + { name = "Battle Master", trackingType = "battlemaster", icon = "Interface\\Minimap\\Tracking\\BattleMaster" }, + { name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" }, } -- =============================================================================