Skip to content

Commit 8af4386

Browse files
authored
Merge 4d14198 into 92271c6
2 parents 92271c6 + 4d14198 commit 8af4386

5 files changed

Lines changed: 434 additions & 8 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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+
-- A leading '!' flips the terminal compare: instead of
22+
-- "fail on mismatch" (verify expected bytes), the row
23+
-- becomes "fail on match" (detect a forbidden cheat
24+
-- signature, e.g. PQR landing in a dynamically
25+
-- resolved memory region). The '!' is consumed before
26+
-- the offsets are parsed, so '!0x4,0x8' is a 2-hop
27+
-- chain in signature-detect mode.
28+
-- comment string -- free text
29+
--
30+
-- Walk semantics for offsets `o1, o2, ..., oN` and base `B`:
31+
-- hop 0 (intermediate): read 4 bytes at B -> P0
32+
-- hop 1 (intermediate): read 4 bytes at P0 + o1 -> P1
33+
-- hop 2 (intermediate): read 4 bytes at P1 + o2 -> P2
34+
-- ...
35+
-- hop N (terminal): read `length` bytes at P_{N-1} + oN
36+
--
37+
-- IMPORTANT — addresses below are templates calibrated for the canonical
38+
-- vanilla 1.12.1 enUS WoW.exe (build 5875,
39+
-- MD5 5fea0d4eed95002f436200a16a4f4795). Image base 0x00400000. They are
40+
-- consistent with the function addresses already used in WardenWin.cpp's
41+
-- module-init block (SFileOpenFile = 0x002485F0 + image base, etc.) and with
42+
-- offsets widely documented in vanilla emulation/cheat communities (e.g.
43+
-- ownedcore vanilla reverse-engineering threads, public vanilla bot/cheat
44+
-- repos).
45+
--
46+
-- BEFORE GOING TO PRODUCTION:
47+
-- 1. Re-confirm every address against your actual binary disassembly. A
48+
-- different localisation (frFR, deDE, etc.) shifts addresses.
49+
-- 2. Capture the real expected bytes at the resolved address from a known
50+
-- clean client and paste them into the `result` column. Placeholder
51+
-- values are noted with `-- TODO` below.
52+
-- 3. Pick check ids that don't collide with your existing rows.
53+
-- ============================================================================
54+
55+
56+
-- ----------------------------------------------------------------------------
57+
-- Example 1 — Vtable hook detection on the Client Object Manager
58+
--
59+
-- Detects: cheats that hook a virtual function on the global Object Manager
60+
-- singleton (a common technique for "object dumper" cheats that swap
61+
-- a vtable slot for a thunk that filters returned objects, leaks
62+
-- GUIDs, or injects fake updates).
63+
--
64+
-- Chain (3 hops, terminal reads 5 bytes of the first virtual function's
65+
-- prologue):
66+
-- base = 0x00B41414 ; s_curMgr — pointer to ClntObjMgr instance
67+
-- hop 0: read [0x00B41414] -> objMgrInstance
68+
-- hop 1: read [objMgrInstance + 0x00] -> vtable
69+
-- hop 2: read [vtable + 0x00] -> first virtual function pointer
70+
-- hop 3 (terminal): read 5 bytes at that function -> expected prologue
71+
--
72+
-- A `jmp` detour is 5 bytes (E9 XX XX XX XX), which guarantees the prologue
73+
-- bytes change if the function is hooked.
74+
-- ----------------------------------------------------------------------------
75+
INSERT INTO `warden`
76+
(`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`)
77+
VALUES
78+
(10001, 5875, 244, '',
79+
'0000000000', -- TODO: replace with the real first 5 prologue bytes from clean WoW.exe
80+
0x00B41414, 5, '0x0,0x0,0x0',
81+
'Pointer chain: ClntObjMgr -> vtable -> vtable[0] -> prologue (detect vtable hook)');
82+
83+
84+
-- ----------------------------------------------------------------------------
85+
-- Example 2 — Hook detection on import-resolved GetTickCount
86+
--
87+
-- Detects: cheats that patch the WoW import for kernel32!GetTickCount to a
88+
-- thunk returning bogus timestamps, defeating the TIMING_CHECK
89+
-- (87/0x57). The IAT slot lives in WoW's `.idata`; following it
90+
-- lands inside kernel32.dll's loaded copy.
91+
--
92+
-- Chain (1 hop, terminal reads 5 bytes of the resolved function prologue):
93+
-- base = 0x00C2D154 ; IAT slot for kernel32!GetTickCount
94+
-- ; (TODO: confirm RVA on your binary)
95+
-- hop 0 (terminal): read 5 bytes at [0x00C2D154]
96+
--
97+
-- Caveat: kernel32.dll is part of the OS, so the prologue bytes vary across
98+
-- Windows versions. In practice you'd seed `result` per-OS or use a small
99+
-- whitelist via multiple check rows. Listed here as a textbook IAT-hook
100+
-- pattern; consider it a template, not a drop-in.
101+
-- ----------------------------------------------------------------------------
102+
INSERT INTO `warden`
103+
(`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`)
104+
VALUES
105+
(10002, 5875, 244, '',
106+
'0000000000', -- TODO: per-OS captured bytes of kernel32!GetTickCount prologue
107+
0x00C2D154, 5, '',
108+
'Pointer chain: WoW IAT[GetTickCount] -> kernel32 prologue (detect IAT detour)');
109+
110+
111+
-- ----------------------------------------------------------------------------
112+
-- Example 3 — Sanity check on Local Player object type field
113+
--
114+
-- Detects: object-replace cheats that swap the local player's object type at
115+
-- its UnitFields descriptor block to confuse server-side validation.
116+
-- Reads the object type byte, expected to be 4 (TYPEID_PLAYER) for
117+
-- the local player object.
118+
--
119+
-- Chain (3 hops, terminal reads 1 byte):
120+
-- base = 0x00B41414 ; s_curMgr
121+
-- hop 0: read [0x00B41414] -> objMgrInstance
122+
-- hop 1: read [objMgrInstance + 0xAC] -> first object in linked list
123+
-- (publicly documented offset)
124+
-- hop 2: read [object + 0x14] -> object type id field
125+
-- (TYPEID layout is documented
126+
-- in Object.h)
127+
--
128+
-- Note: the linked list head at +0xAC is not always the local player; it
129+
-- iterates by +0x3C until matching local GUID. For a simple template we use
130+
-- the head — adjust if you want strict local-player semantics. This is
131+
-- primarily useful as a presence/structural-integrity check.
132+
-- ----------------------------------------------------------------------------
133+
INSERT INTO `warden`
134+
(`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`)
135+
VALUES
136+
(10003, 5875, 244, '',
137+
'04', -- TYPEID_PLAYER
138+
0x00B41414, 1, '0x0,0xAC,0x14',
139+
'Pointer chain: ObjMgr -> first object -> typeId byte (detect object spoof)');
140+
141+
142+
-- ----------------------------------------------------------------------------
143+
-- Example 4 — Zero-hop sanity (degenerate chain)
144+
--
145+
-- Equivalent in semantics to a plain MEM_CHECK, but routed through the
146+
-- POINTER_CHAIN_CHECK code path. Useful as a smoke test when bringing the
147+
-- feature up: pick a known stable byte run in WoW.exe's `.text` (any
148+
-- function whose prologue you already trust) and seed the expected bytes.
149+
--
150+
-- Reads 5 bytes of the SFileOpenFile prologue at 0x006485F0
151+
-- (image base 0x00400000 + RVA 0x002485F0, taken straight from the
152+
-- WardenInitModuleRequest in src/game/Warden/WardenWin.cpp).
153+
-- ----------------------------------------------------------------------------
154+
INSERT INTO `warden`
155+
(`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`)
156+
VALUES
157+
(10004, 5875, 244, '',
158+
'0000000000', -- TODO: replace with real first 5 bytes of SFileOpenFile prologue
159+
0x006485F0, 5, '',
160+
'Zero-hop pointer-chain smoke test (SFileOpenFile prologue)');
161+
162+
163+
-- ----------------------------------------------------------------------------
164+
-- Example 5 — Signature-detect mode (third-party allocation scan)
165+
--
166+
-- Inspired by Krilliac/AdvancedWarden's MEM2_CHECK / GAGARIN pair pattern,
167+
-- which targets cheats that allocate executable memory in well-known
168+
-- dynamic regions and place their payload at a fixed offset within that
169+
-- region. Approach there: one MEM_CHECK reads the dynamic base address and
170+
-- caches it on the session; a paired MEM_CHECK then scans `base + small
171+
-- offset` and fails when bytes != 0 (i.e. the region is not empty as it
172+
-- should be on a clean client).
173+
--
174+
-- Our generalised equivalent: a single POINTER_CHAIN_CHECK row that walks
175+
-- to the suspect address and uses signature-detect mode (leading `!`) to
176+
-- fail when a known cheat-signature pattern appears.
177+
--
178+
-- Chain (1 hop, terminal reads 4 bytes; fails if pattern found):
179+
-- base = 0x009F348 ; static slot that holds the dynamic
180+
-- ; allocation pointer for this cheat
181+
-- ; family. (TODO: confirm against your
182+
-- ; binary; the AdvancedWarden seed used
183+
-- ; address=652040=0x9F348, length=4.)
184+
-- hop 0 (terminal): read 4 bytes at *base + 2 (or other small offset)
185+
-- expected = the cheat's 4-byte signature
186+
-- fail when read == expected (signature present)
187+
--
188+
-- The leading '!' on the offset string flips the terminal compare into
189+
-- signature-detect mode. Pick the offset value (here 0x2) to match the
190+
-- byte-window where the cheat is known to land. Length must equal the
191+
-- length of the signature in `result`.
192+
-- ----------------------------------------------------------------------------
193+
INSERT INTO `warden`
194+
(`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`)
195+
VALUES
196+
(10005, 5875, 244, '',
197+
'00003000', -- TODO: replace with the real cheat signature bytes
198+
0x0009F348, 4, '!0x2',
199+
'Signature detect: dynamic 3rd-party allocation scan (PQR-class)');
200+
201+
202+
-- ----------------------------------------------------------------------------
203+
-- Optional: action override per check id (only if you want non-default
204+
-- penalty for these). Default action comes from
205+
-- CONFIG_UINT32_WARDEN_CLIENT_FAIL_ACTION (0=LOG, 1=KICK, 2=BAN). The
206+
-- override lives in the characters DB.
207+
-- ----------------------------------------------------------------------------
208+
-- INSERT INTO `warden_action` (`wardenId`, `action`) VALUES
209+
-- (10001, 1), -- kick on vtable-hook detection
210+
-- (10002, 0), -- log only on IAT check (more false-positive prone)
211+
-- (10003, 1), -- kick on object-type spoof
212+
-- (10004, 0), -- log only on smoke test
213+
-- (10005, 2); -- ban on confirmed signature match

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)