Skip to content

Commit 174bcef

Browse files
authored
Merge 5a1d7a6 into 92271c6
2 parents 92271c6 + 5a1d7a6 commit 174bcef

5 files changed

Lines changed: 363 additions & 8 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
-- ============================================================================
2+
-- POINTER_CHAIN_CHECK (type 244 / 0xF4) example seeds for the `warden` table.
3+
--
4+
-- Wire format on the client side is identical to MEM_CHECK (243 / 0xF3); the
5+
-- server walks a multi-hop pointer dereference chain across consecutive Warden
6+
-- cycles and `memcmp`-validates the bytes at the final resolved address.
7+
--
8+
-- Schema reminder (see WardenCheckMgr::LoadWardenChecks):
9+
-- id uint16 -- check id
10+
-- build uint16 -- client build (vanilla 1.12.1 enUS = 5875)
11+
-- type uint8 -- 244 for POINTER_CHAIN_CHECK
12+
-- data string -- unused for this type (leave empty)
13+
-- result string -- hex of the EXPECTED final-hop bytes (length must
14+
-- match the `length` column)
15+
-- address uint32 -- chain base address (32-bit, x86)
16+
-- length uint8 -- bytes to read at the final hop (1..N)
17+
-- str string -- comma-separated hex offsets, e.g. '0x10,0x24,0x8'.
18+
-- Empty string = zero hops (degenerate single-read).
19+
-- Each offset is added to the pointer dereferenced at
20+
-- the previous hop to produce the next hop's address.
21+
-- comment string -- free text
22+
--
23+
-- Walk semantics for offsets `o1, o2, ..., oN` and base `B`:
24+
-- hop 0 (intermediate): read 4 bytes at B -> P0
25+
-- hop 1 (intermediate): read 4 bytes at P0 + o1 -> P1
26+
-- hop 2 (intermediate): read 4 bytes at P1 + o2 -> P2
27+
-- ...
28+
-- hop N (terminal): read `length` bytes at P_{N-1} + oN
29+
--
30+
-- IMPORTANT — addresses below are templates calibrated for the canonical
31+
-- vanilla 1.12.1 enUS WoW.exe (build 5875,
32+
-- MD5 5fea0d4eed95002f436200a16a4f4795). Image base 0x00400000. They are
33+
-- consistent with the function addresses already used in WardenWin.cpp's
34+
-- module-init block (SFileOpenFile = 0x002485F0 + image base, etc.) and with
35+
-- offsets widely documented in vanilla emulation/cheat communities (e.g.
36+
-- ownedcore vanilla reverse-engineering threads, public vanilla bot/cheat
37+
-- repos).
38+
--
39+
-- BEFORE GOING TO PRODUCTION:
40+
-- 1. Re-confirm every address against your actual binary disassembly. A
41+
-- different localisation (frFR, deDE, etc.) shifts addresses.
42+
-- 2. Capture the real expected bytes at the resolved address from a known
43+
-- clean client and paste them into the `result` column. Placeholder
44+
-- values are noted with `-- TODO` below.
45+
-- 3. Pick check ids that don't collide with your existing rows.
46+
-- ============================================================================
47+
48+
49+
-- ----------------------------------------------------------------------------
50+
-- Example 1 — Vtable hook detection on the Client Object Manager
51+
--
52+
-- Detects: cheats that hook a virtual function on the global Object Manager
53+
-- singleton (a common technique for "object dumper" cheats that swap
54+
-- a vtable slot for a thunk that filters returned objects, leaks
55+
-- GUIDs, or injects fake updates).
56+
--
57+
-- Chain (3 hops, terminal reads 5 bytes of the first virtual function's
58+
-- prologue):
59+
-- base = 0x00B41414 ; s_curMgr — pointer to ClntObjMgr instance
60+
-- hop 0: read [0x00B41414] -> objMgrInstance
61+
-- hop 1: read [objMgrInstance + 0x00] -> vtable
62+
-- hop 2: read [vtable + 0x00] -> first virtual function pointer
63+
-- hop 3 (terminal): read 5 bytes at that function -> expected prologue
64+
--
65+
-- A `jmp` detour is 5 bytes (E9 XX XX XX XX), which guarantees the prologue
66+
-- bytes change if the function is hooked.
67+
-- ----------------------------------------------------------------------------
68+
INSERT INTO `warden`
69+
(`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`)
70+
VALUES
71+
(10001, 5875, 244, '',
72+
'0000000000', -- TODO: replace with the real first 5 prologue bytes from clean WoW.exe
73+
0x00B41414, 5, '0x0,0x0,0x0',
74+
'Pointer chain: ClntObjMgr -> vtable -> vtable[0] -> prologue (detect vtable hook)');
75+
76+
77+
-- ----------------------------------------------------------------------------
78+
-- Example 2 — Hook detection on import-resolved GetTickCount
79+
--
80+
-- Detects: cheats that patch the WoW import for kernel32!GetTickCount to a
81+
-- thunk returning bogus timestamps, defeating the TIMING_CHECK
82+
-- (87/0x57). The IAT slot lives in WoW's `.idata`; following it
83+
-- lands inside kernel32.dll's loaded copy.
84+
--
85+
-- Chain (1 hop, terminal reads 5 bytes of the resolved function prologue):
86+
-- base = 0x00C2D154 ; IAT slot for kernel32!GetTickCount
87+
-- ; (TODO: confirm RVA on your binary)
88+
-- hop 0 (terminal): read 5 bytes at [0x00C2D154]
89+
--
90+
-- Caveat: kernel32.dll is part of the OS, so the prologue bytes vary across
91+
-- Windows versions. In practice you'd seed `result` per-OS or use a small
92+
-- whitelist via multiple check rows. Listed here as a textbook IAT-hook
93+
-- pattern; consider it a template, not a drop-in.
94+
-- ----------------------------------------------------------------------------
95+
INSERT INTO `warden`
96+
(`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`)
97+
VALUES
98+
(10002, 5875, 244, '',
99+
'0000000000', -- TODO: per-OS captured bytes of kernel32!GetTickCount prologue
100+
0x00C2D154, 5, '',
101+
'Pointer chain: WoW IAT[GetTickCount] -> kernel32 prologue (detect IAT detour)');
102+
103+
104+
-- ----------------------------------------------------------------------------
105+
-- Example 3 — Sanity check on Local Player object type field
106+
--
107+
-- Detects: object-replace cheats that swap the local player's object type at
108+
-- its UnitFields descriptor block to confuse server-side validation.
109+
-- Reads the object type byte, expected to be 4 (TYPEID_PLAYER) for
110+
-- the local player object.
111+
--
112+
-- Chain (3 hops, terminal reads 1 byte):
113+
-- base = 0x00B41414 ; s_curMgr
114+
-- hop 0: read [0x00B41414] -> objMgrInstance
115+
-- hop 1: read [objMgrInstance + 0xAC] -> first object in linked list
116+
-- (publicly documented offset)
117+
-- hop 2: read [object + 0x14] -> object type id field
118+
-- (TYPEID layout is documented
119+
-- in Object.h)
120+
--
121+
-- Note: the linked list head at +0xAC is not always the local player; it
122+
-- iterates by +0x3C until matching local GUID. For a simple template we use
123+
-- the head — adjust if you want strict local-player semantics. This is
124+
-- primarily useful as a presence/structural-integrity check.
125+
-- ----------------------------------------------------------------------------
126+
INSERT INTO `warden`
127+
(`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`)
128+
VALUES
129+
(10003, 5875, 244, '',
130+
'04', -- TYPEID_PLAYER
131+
0x00B41414, 1, '0x0,0xAC,0x14',
132+
'Pointer chain: ObjMgr -> first object -> typeId byte (detect object spoof)');
133+
134+
135+
-- ----------------------------------------------------------------------------
136+
-- Example 4 — Zero-hop sanity (degenerate chain)
137+
--
138+
-- Equivalent in semantics to a plain MEM_CHECK, but routed through the
139+
-- POINTER_CHAIN_CHECK code path. Useful as a smoke test when bringing the
140+
-- feature up: pick a known stable byte run in WoW.exe's `.text` (any
141+
-- function whose prologue you already trust) and seed the expected bytes.
142+
--
143+
-- Reads 5 bytes of the SFileOpenFile prologue at 0x006485F0
144+
-- (image base 0x00400000 + RVA 0x002485F0, taken straight from the
145+
-- WardenInitModuleRequest in src/game/Warden/WardenWin.cpp).
146+
-- ----------------------------------------------------------------------------
147+
INSERT INTO `warden`
148+
(`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`)
149+
VALUES
150+
(10004, 5875, 244, '',
151+
'0000000000', -- TODO: replace with real first 5 bytes of SFileOpenFile prologue
152+
0x006485F0, 5, '',
153+
'Zero-hop pointer-chain smoke test (SFileOpenFile prologue)');
154+
155+
156+
-- ----------------------------------------------------------------------------
157+
-- Optional: action override per check id (only if you want non-default
158+
-- penalty for these). Default action comes from
159+
-- CONFIG_UINT32_WARDEN_CLIENT_FAIL_ACTION (0=LOG, 1=KICK, 2=BAN). The
160+
-- override lives in the characters DB.
161+
-- ----------------------------------------------------------------------------
162+
-- INSERT INTO `warden_action` (`wardenId`, `action`) VALUES
163+
-- (10001, 1), -- kick on vtable-hook detection
164+
-- (10002, 0), -- log only on IAT check (more false-positive prone)
165+
-- (10003, 1), -- kick on object-type spoof
166+
-- (10004, 0); -- log only on smoke test

src/game/Warden/Warden.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ enum WardenCheckType
6565
DRIVER_CHECK = 0x71, // 113: uint Seed + byte[20] SHA1 + byte driverNameIndex (check to ensure driver isn't loaded)
6666
TIMING_CHECK = 0x57, // 87: empty (check to ensure GetTickCount() isn't detoured)
6767
PROC_CHECK = 0x7E, // 126: uint Seed + byte[20] SHA1 + byte moluleNameIndex + byte procNameIndex + uint Offset + byte Len (check to ensure proc isn't detoured)
68-
MODULE_CHECK = 0xD9 // 217: uint Seed + byte[20] SHA1 (check to ensure module isn't injected)
68+
MODULE_CHECK = 0xD9, // 217: uint Seed + byte[20] SHA1 (check to ensure module isn't injected)
69+
POINTER_CHAIN_CHECK = 0xF4 // 244: SERVER-SIDE ONLY. Wire format identical to MEM_CHECK (0xF3).
70+
// Walks a pointer-deref chain across multiple Warden cycles
71+
// and memcmp-validates the bytes at the final resolved address.
72+
// Never appears in any byte sent to or from the client module.
6973
};
7074

7175
#if defined(__GNUC__)

src/game/Warden/WardenCheckMgr.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,21 +106,21 @@ void WardenCheckMgr::LoadWardenChecks()
106106
}
107107
}
108108

109-
if (checkType == MEM_CHECK || checkType == PAGE_CHECK_A || checkType == PAGE_CHECK_B || checkType == PROC_CHECK)
109+
if (checkType == MEM_CHECK || checkType == PAGE_CHECK_A || checkType == PAGE_CHECK_B || checkType == PROC_CHECK || checkType == POINTER_CHAIN_CHECK)
110110
{
111111
wardenCheck->Address = address;
112112
wardenCheck->Length = length;
113113
}
114114

115115
// PROC_CHECK support missing
116-
if (checkType == MEM_CHECK || checkType == MPQ_CHECK || checkType == LUA_STR_CHECK || checkType == DRIVER_CHECK || checkType == MODULE_CHECK)
116+
if (checkType == MEM_CHECK || checkType == MPQ_CHECK || checkType == LUA_STR_CHECK || checkType == DRIVER_CHECK || checkType == MODULE_CHECK || checkType == POINTER_CHAIN_CHECK)
117117
{
118118
wardenCheck->Str = str;
119119
}
120120

121121
CheckStore.insert(std::pair<uint16, WardenCheck*>(build, wardenCheck));
122122

123-
if (checkType == MPQ_CHECK || checkType == MEM_CHECK)
123+
if (checkType == MPQ_CHECK || checkType == MEM_CHECK || checkType == POINTER_CHAIN_CHECK)
124124
{
125125
WardenCheckResult* wr = new WardenCheckResult();
126126
wr->Id = id;
@@ -251,7 +251,7 @@ void WardenCheckMgr::GetWardenCheckIds(bool isMemCheck, uint16 build, std::list<
251251
{
252252
if (isMemCheck)
253253
{
254-
if ((it->second->Type == MEM_CHECK) || (it->second->Type == MODULE_CHECK))
254+
if ((it->second->Type == MEM_CHECK) || (it->second->Type == MODULE_CHECK) || (it->second->Type == POINTER_CHAIN_CHECK))
255255
{
256256
idl.push_back(it->second->CheckId);
257257
}

0 commit comments

Comments
 (0)