Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ option(ACTIONBAR_LUA "" OFF)
option(DEVHELPER_LUA "" OFF)
option(GLUEMGREXTENSION "Glue-stage Lua function registration" ON)

# --- Ascension native logon (our reconstruction; not upstream) ------------------------------------
# Installs the original DLL's three logon hooks (client hello builder 0x8CCE00, key-material copy
# 0x9A83E0, 20-byte proof check 0x9A88C0) plus the four exe length-gate patches, replacing the
# client's native SRP6 handshake with Ascension's X25519/ChaCha20/HMAC-SHA256 scheme. OFF by
# default: only a server that speaks the same protocol (the collaborator's fork) can answer it.
# See src/Ascension/AscNativeLogon.cpp for the protocol, provenance and self-test.
option(ASCENSION_NATIVE_LOGON "Install the reconstructed Ascension native logon hooks (server must speak the protocol)" OFF)
set(ASCENSION_NATIVE_LOGON_SERVER_KEY "1a03d4c71f289f39046fad519f8af3e92b793709a8a340e492b4ff5ff55e1537" CACHE STRING "Server X25519 public key (hex, 64 chars) used by ASCENSION_NATIVE_LOGON")

set(UP ${CMAKE_CURRENT_SOURCE_DIR}/WotLKExtensions)
configure_file(${UP}/cmake/PatchConfig.hpp.in ${CMAKE_BINARY_DIR}/gen/PatchConfig.hpp)

Expand Down
60 changes: 50 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ packets and fires events.
## Deliberate differences from the genuine DLL

- **Logon protocol.** The genuine DLL hooks `0x8CCE00`, `0x9A83E0` and `0x9A88C0` to replace SRP6 with
Ascension's own logon protocol. These are **not installed**, so the client authenticates with stock SRP6
against an ordinary AzerothCore authserver. See `src/Ascension/AscMain.cpp`.
Ascension's own logon protocol. **Not installed by default** -- the client authenticates with stock SRP6
against an AzerothCore authserver. A native reconstruction of the protocol (X25519 / ChaCha20 /
HMAC-SHA256, plus the four handshake length gates) now ships in `src/Ascension/AscNativeLogon.cpp`,
compiled in only when the build enables `-DASCENSION_NATIVE_LOGON=ON` (see below); it exists so the
client can be tested against a server that speaks the original protocol. See `src/Ascension/AscMain.cpp`.
- **Character advancement initialization.** The native `0x726` known-entries handler synchronizes the
build's unit parameters before notifying listeners and cloning its pending build. Waiting for the next
manager tick leaves a new build at class and level zero during the first native update. This is an
Expand All @@ -40,15 +43,20 @@ The portable regression in `tests/native_ca_packets.py` compiles the production
against client API doubles. It covers initial state, listener timing, subsequent updates, new preset slots
and missing local units. It does not launch the game or replace the 32-bit Windows DLL build.

`tests/native_callback_order.py` exercises the production cast and aura-removal registrars and detours
against the original DLL's measured linked-list traversal, including registration permutations and vetoes.
`tests/native_callback_order.py` exercises the production cast, aura-removal and aura-application
registrars and detours, and the effect-filter and visual-hide registrars and consumer, against the
original DLL's measured linked-list traversal, including registration permutations and vetoes.
The original uses MSVC `unordered_set` callbacks: cast checks run aura requirements, stack requirements,
then class requirements; aura removal runs `AscAura137` before `AscSpellMods`. These two snapshots include
all corresponding callbacks in this source. Their native hash-bucket relationships remain identical
at every allocation-aligned 32-bit image base, so relocating the DLL preserves these orders.
Aura application, effect filtering and visual hiding still
use call-site ordering: their startup snapshots do not yet cover every reconstructed callback. Future
unknown callbacks in the two measured lists follow the captured entries in call-site order.
then class requirements; aura removal runs `AscAura137` before `AscSpellMods`; aura application runs
`AscAura137`, `AscSpellVisuals`, `AscSpellMods`, `AscAppearance`, then `AscSpawnVisibility`; effect
filtering and visual hiding run `AscSpellVisuals` before `AscQuestSpellHide`. These snapshots include
all corresponding callbacks in this source; the two emulated lists' native hash-bucket relationships
remain identical at every allocation-aligned 32-bit image base, so relocating the DLL preserves these
orders.
All five captured lists insert by the measured order (`InsertByCapturedOrder`): call-site ordering did
not reproduce them because registration execution order does not follow the call sites. The
aura-application list also carries one original-only callback that is not reconstructed - it runs after
the five captured entries, matching the rule that unknown callbacks follow the captured entries.

## Building

Expand All @@ -62,6 +70,35 @@ cmake --build build
The output is `build\Extensions.dll`. Place it next to `Ascension.exe`, which loads it itself (there is no
injector).

### Optional: native logon build (test against an Ascension-protocol server)

```bat
cmake -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DASCENSION_NATIVE_LOGON=ON -S . -B build\nl
cmake --build build\nl
```

This compiles in `src/Ascension/AscNativeLogon.cpp`: it installs the original DLL's three logon hooks
(`0x8CCE00`, `0x9A83E0`, `0x9A88C0`) plus the four handshake length gates, replacing the client's stock SRP6
with Ascension's own handshake (X25519 + ChaCha20 + HMAC-SHA256). It only works against a server that
answers that protocol. The server's X25519 public key is a build setting,
`-DASCENSION_NATIVE_LOGON_SERVER_KEY=<64 hex chars>` (default: the key live-tested during the 2026-10
campaign; the server prints its key at startup). Run the client with `-extlog 1` and check
`Logs\Extensions.log` for the `native-logon:` lines.

Live-validated end to end on 2026-10-10 against a `feature/ascension-native-login` server
(`CoA.AscensionAuth = 1`, matching key pair): the client logged `native-logon: proof check PASSED
(HMAC-SHA256(key_derived, "OK"))`, the server answered `Ascension custom auth: proof replied, session
authorised`, and the client went on through `COP_AUTHENTICATE ... AUTH_OK` to the character list
(`COP_GET_CHARACTERS ... result=TRUE`). No client-side patches were involved: this build installs its own
trampolines and length gates.

One server requirement found during that test: after the custom proof the authserver must persist the
handshake's 40-byte `key_session` as `account.session_key`. The worldserver validates the client's
`CMSG_AUTH_SESSION` digest and initialises its header cipher from that value - without it every world
connection fails its digest check immediately after the auth completes. The partner fork
(`feature/ascension-native-login`) handles this in `AuthSession.cpp` (`HandleLogonProof`, Ascension
client branch).

The DLL's own developer log (`Logs\Extensions.log`) is off by default; start the client with `-extlog 1` to
turn it on.

Expand All @@ -84,6 +121,9 @@ at "Authenticating" and then disconnects before character select.
`CoA.Enable = 1` is also required. For clients that do not connect from loopback, set
`CoA.AllowRemoteClients = 1` too, because both settings apply only to loopback otherwise.

The authserver side of the custom handshake additionally requires the `key_session` persistence described
in the native-logon section above (`AuthSession.cpp`, Ascension client branch).

## Client: required changes to `patch-B.MPQ`

The stock Ascension client's `Data\patch-B.MPQ` is built for the Ascension Live servers. To connect to any
Expand Down
4 changes: 4 additions & 0 deletions WotLKExtensions/cmake/PatchConfig.hpp.in
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@
#cmakedefine01 ACTIONBAR_LUA
#cmakedefine01 DEVHELPER_LUA
#cmakedefine01 GLUEMGREXTENSION

// Ascension native logon (see src/Ascension/AscNativeLogon.cpp)
#cmakedefine01 ASCENSION_NATIVE_LOGON
#cmakedefine ASCENSION_NATIVE_LOGON_SERVER_KEY "@ASCENSION_NATIVE_LOGON_SERVER_KEY@"
80 changes: 80 additions & 0 deletions docs/CALLBACK-ORDER-MEASURED.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Measured callback-list order — aura application, effect filtering, visual hiding

The three lists that used to insert by call-site order were measured from the original's live list
images. Call-site order cannot reproduce them: registration execution order does not follow the call
sites — and one original-only callback on the aura-application list has no call site at all.

## Captured order (call order of the original's detours and consumers)

| list (native object) | order |
| --- | --- |
| `0x10BE2B6C` aura application (before `0x724820`) | `AscAura137::BeforeApply`, `AscSpellVisuals::BeforeAuraApply`, `AscSpellMods::AuraFlagOn`, `AscAppearance::BeforeAuraApply`, `AscSpawnVisibility::BeforeAuraApply`, *+ one original-only callback (below)* |
| `0x10BE2D78` effect filtering (`0x744AC0` replacement) | `AscSpellVisuals::RemoveEffect`, `AscQuestSpellHide::RemoveEffect` |
| `0x10BE2D98` visual hiding | `AscSpellVisuals::HideVisual`, `AscQuestSpellHide::HideVisual` |

Sites (reference image) are in `tests/fixtures/original_callback_order.json`; the registrars rank by them.
The two filter lists are exactly reversed vs ascending site order. The aura list is not any site sort:
`AscSpellMods` registered before `AscSpellVisuals`, and `AscSpawnVisibility` before `AscAppearance`,
although their sites are the other way round.

## The original-only sixth callback (aura application)

- Runs **last of six** (`AscSpawnVisibility`'s entry precedes it). The list count for `0x10BE2B6C` is 6.
- Registered through the original's runtime layer: no static registration site exists anywhere in the
image; its address is only materialized at runtime (a VM lane cell, `.vm_sec` + `0xD70088`). It is not
reconstructed; `InsertByCapturedOrder`'s *unknown callbacks follow the captured entries* rule
reproduces its position.
- Behaviour — a spell veto called as `(unit, a, rec)`; `false` suppresses the aura application
(the detour returns without calling the original `0x724820`):

```c
bool AuraVeto(void* unit, uint32_t, uint32_t rec)
{
if (*(uint32_t*)((char*)unit + 0x14) != 4) return true; // interposes only on players
uint32_t spell = *(uint32_t*)rec;
uint32_t flags = *(uint32_t*)(*(uint32_t*)((char*)unit + 8) + 0x258); // PLAYER_FLAGS
if (spell == 0x79BB0 || spell == 0x79C3F)
return unit != ActivePlayer() && (flags & 0x8000) == 0
&& ((flags & 8) != 0 || AccountInfo()[0x0C] != 0) && AccountInfo()[0x0E] == 0;
if (spell == 0x79BC5)
return (flags & 0x8000) == 0;
if (spell == 0x79C40 || spell == 0x978960)
return unit != ActivePlayer() && (flags & 0x8000) == 0
&& ((flags & 8) != 0 || AccountInfo()[0x0D] != 0) && AccountInfo()[0x0E] == 0;
return true;
}
```

(Flag `8` = GM, `0x8000` = developer; `AccountInfo()` is the TLS account object accessor —
our `FUN_1008c870` class. All five spell ids are Ascension custom spells, absent from the shipped
`Spell.dbc`.)

### Spell identification — the client's Mentor / Newcomer / Returning-Player / overhead-icon handling

The five ids are CoA's community and staff marker spells (community-archive identification, not server
data — AscensionDB preservation archive, "Jeff-Fro CoA in-game spell tooltip capture", 2026-09-02,
snapshot `2aa98857`):

| id | name | captured tooltip |
| --- | --- | --- |
| `0x79BB0` (498608) | **Mentor** | `Mentor` |
| `0x79C3F` (498751) | **Mentor** | `Mentor \| I am a Mentor and I am here to help! New players can ask me questions about the game!` |
| `0x79BC5` (498629) | **Overhead Icon - Ascension Logo** | `Overhead Icon - Ascension Logo \| Instant` |
| `0x79C40` (498752) | **New Player Help - Newcomer** | `New Player Help - Newcomer \| Instant` |
| `0x978960` (9931104) | **Returning Player** | — |

The condition split mirrors the names: the two Mentor spells share the account byte `0x0C` branch, the
Newcomer / Returning-Player pair shares the `0x0D` branch, and the Ascension-logo overhead icon has its
own rule. Note the GM flag never suppresses here — it is only ever an *allow* path; the one suppressing
flag is `0x8000` (developer), and the tag pairs additionally never apply to your own active player.

## Measurement and validation

- Live walk of the original's list links in two independent game sessions of a sibling build
(`md5 8b3dd390…`), identical results; direction follows the hooks: `node = *(head)` (the list
head pointer), callback at `node+8`, next at `node+0`, stop when back at `head`.
- The same walk reproduces the two emulated fixtures (`Before71E930`, `After80B5D0`) byte-exactly in this
reference image; addresses were translated with per-band deltas validated on all entries.
- `tests/native_callback_order.py` locks the orders in: the registrars run under all registration
permutations, with vetoes, duplicate registrations, an off-array ("unknown") site, and — for visual
hiding — the production consumer's first-true-wins short-circuit.
2 changes: 1 addition & 1 deletion docs/DLL_REFERENCE.intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ who does want the original can go straight to it in the decompile.
## Deliberate differences from the genuine DLL

- **Logon protocol.** The genuine DLL hooks `0x8CCE00`, `0x9A83E0` and `0x9A88C0` to replace SRP6 with Ascension's
own logon protocol. They are not installed, so the client authenticates with stock SRP6 against an AzerothCore
own logon protocol. They are not installed by default, so the client authenticates with stock SRP6 against an AzerothCore
authserver (see `AscMain.cpp`, and `CLAUDE.md` G0-02).
- **Anti-cheat.** It is loaded exactly as the original does (`DivxTac.dll` after world entry, plus the account
kill switch). Nothing is added or removed.
Expand Down
2 changes: 1 addition & 1 deletion docs/DLL_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ who does want the original can go straight to it in the decompile.
## Deliberate differences from the genuine DLL

- **Logon protocol.** The genuine DLL hooks `0x8CCE00`, `0x9A83E0` and `0x9A88C0` to replace SRP6 with Ascension's
own logon protocol. They are not installed, so the client authenticates with stock SRP6 against an AzerothCore
own logon protocol. They are not installed by default, so the client authenticates with stock SRP6 against an AzerothCore
authserver (see `AscMain.cpp`, and `CLAUDE.md` G0-02).
- **Anti-cheat.** It is loaded exactly as the original does (`DivxTac.dll` after world entry, plus the account
kill switch). Nothing is added or removed.
Expand Down
22 changes: 16 additions & 6 deletions src/Ascension/AscMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <PatchConfig.hpp>
#include <Main.hpp>
#include <Ascension/AscLog.hpp>
#include <Ascension/AscNativeLogon.hpp>
#include <Ascension/AscRegistry.hpp>
#include <Ascension/AscRuntime.hpp>
#include <Ascension/AscBindings.hpp>
Expand All @@ -27,14 +28,17 @@ extern "C" __declspec(dllexport) int ClientExtensionsDummy() { return 1; }
// makes): the upstream WotLK-Extensions feature patches WoWTime (+ the year-offset setting only it reads),
// CMap, Misc, CGPlayer (char-create race fix, combo-point fix, LFD class roles), CGTooltip, CMissile,
// MacroConditions, plus Spell / LuaUnlock (already no-ops here).
// Deliberately NOT installed (the one remaining hook gap vs the original, 2026-09-27 live audit): the three
// hooks of Ascension's own logon protocol --
// Native logon is now reconstructed (src/Ascension/AscNativeLogon.cpp): the three hooks of Ascension's
// own logon protocol --
// 0x8CCE00 -> FUN_100e3dc0 (into the encrypted/VM region), 0x9A83E0 -> FUN_100e5d70 (copies the session key
// material into the client's auth object), 0x9A88C0 -> FUN_100e5dd0 (the 20-byte server proof,
// HMAC(K, "OK")).
// Installing them replaces the client's native SRP6 with Ascension's logon, which our AzerothCore authserver
// cannot answer until the translation proxy (BUILD_PLAN P3) exists. The JS client re-routes exactly these three
// for the same reason. Project decision: SRP6-restore (CLAUDE.md G0-02).
// HMAC(K, "OK"))
// -- plus the four exe length-gate patches, are installed behind the ASCENSION_NATIVE_LOGON build option
// (default OFF). With it OFF this DLL keeps the original audit behaviour: the client's native SRP6 stays
// in place, matching our AzerothCore authserver until the translation proxy (BUILD_PLAN P3) exists.
// With it ON the client speaks Ascension's protocol (X25519 / ChaCha20 / HMAC-SHA256) and needs a server
// that answers it (the collaborator's fork does). Project decision G0-02 (SRP6-restore) is unaffected:
// the option defaults to OFF.
static void ApplyUpstreamPatches()
{
#if CUSTOMPACKETS_PATCH
Expand Down Expand Up @@ -135,6 +139,12 @@ BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID)
{
ApplyUpstreamPatches();
AscLog::Printf("upstream patches applied");
#if ASCENSION_NATIVE_LOGON
// Ascension's own logon protocol, reconstructed natively (see AscNativeLogon.hpp).
// Switches the client from stock SRP6 to the original's X25519/ChaCha20/HMAC
// handshake; only meaningful against a server that speaks it.
AscNativeLogon::Install();
#endif
// The original DLL's module runtime: its custom event names, its world-entry / glue-screen
// lifecycle hooks, then each transcribed subsystem's init (which registers into both).
AscRuntime::RegisterEventNames();
Expand Down
Loading