-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHookSecureFunc.cpp
More file actions
191 lines (167 loc) · 7.82 KB
/
HookSecureFunc.cpp
File metadata and controls
191 lines (167 loc) · 7.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#include <cstring>
namespace HookSecureFunc {
namespace {
// Names that must not be hooked when targeting `_G`. Hooking any of these
// would either break taint propagation (the "secure" functions) or replace
// core Lua language primitives whose behavior the engine depends on
// internally — e.g. `pairs` is called from C in the iterator path; if Lua
// `pairs` is replaced with a wrapper closure, the engine still gets the
// original via its own table-method dispatch, but Lua-side iteration goes
// through our wrapper, and the two diverging is a footgun. Modern WoW
// rejects the hook outright; we mirror that. Sorted alphabetically.
const char *const kUnhookableNames[] = {
"getfenv", "getmetatable", "hooksecurefunc", "ipairs", "issecurevalue",
"issecurevariable", "next", "pairs", "pcall", "pcallwithenv", "rawget",
"rawset", "scrub", "securecall", "securecallfunction",
"secureexecuterange", "select", "setfenv", "setmetatable", "type",
"unpack", "wipe", "xpcall",
};
bool IsUnhookable(const char *name) {
if (name == nullptr)
return false;
for (const char *blocked : kUnhookableNames) {
if (std::strcmp(name, blocked) == 0)
return true;
}
return false;
}
// Wrapper closure body — called when the hooked function is invoked.
// Stack contains the wrapper's args (`[arg1..argN]`); the closure's
// upvalues hold `(orig, callback)`.
//
// Modern `hooksecurefunc` semantics: original runs first, callback
// runs after with the same args (returns ignored), original's
// results propagate to the caller. No cap on return count. A buggy
// callback must NOT prevent the original's results from reaching
// the caller — that's why step (3) uses pcall.
//
// Stack-shuffling mirrors 3.3.5's `FUN_00817050` so the args sit at
// the top for both calls without copying them twice; results sink
// to the bottom and end up as the wrapper's natural return frame.
int __fastcall HookWrapper(void *L) {
const int nargs = Game::Lua::GetTop(L);
// (1) orig(...) — pops orig + nargs args (copies), pushes results.
// Stack after: `[arg1..argN, res1..resM]`.
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1));
for (int i = 1; i <= nargs; ++i)
Game::Lua::PushValue(L, i);
Game::Lua::Call(L, nargs, Game::Lua::MULTRET);
const int nresults = Game::Lua::GetTop(L) - nargs;
// (2) Shuffle results below the saved args. Each `Insert(L, 1)`
// moves the current top down to position 1; running it
// nresults times rotates the results block beneath the args.
// Stack after: `[res1..resM, arg1..argN]`.
for (int i = 0; i < nresults; ++i)
Game::Lua::Insert(L, 1);
// (3) callback(...) via pcall — errors are caught and dropped so
// the original's return values always reach the caller. This
// is the load-bearing safety property of hooksecurefunc on
// 3.x; on 1.12 it just means a buggy hook can't break the
// hooked path. errfunc=0 → no traceback (3.3.5 uses
// `registry["_TRACEBACK"]`, not a 1.12 convention; we'd be
// discarding the formatted message anyway).
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(2));
Game::Lua::Insert(L, nresults + 1);
if (Game::Lua::PCall(L, nargs, 0, 0) != 0) {
// pcall failure leaves the error message on top; drop it.
Game::Lua::Remove(L, -1);
}
// Stack: `[res1..resM]` — return the original's results verbatim.
return nresults;
}
// `hooksecurefunc(name, callback)` — hooks a global by name, target
// table is `_G`.
// `hooksecurefunc(table, name, callback)` — hooks `table[name]`.
//
// In both cases the original function runs first and its results
// propagate; `callback` runs after with the same args (returns
// discarded). The "secure" label refers to taint propagation in
// 3.x+; on 1.12 it's just a post-hook with return preservation.
int __fastcall Script_HookSecureFunc(void *L) {
int targetIdx, nameIdx, callbackIdx;
const int t1 = Game::Lua::Type(L, 1);
if (t1 == Game::Lua::TYPE_TABLE) {
// Three-arg form: (table, name, callback)
targetIdx = 1;
nameIdx = 2;
callbackIdx = 3;
} else if (t1 == Game::Lua::TYPE_STRING) {
// Two-arg form: (name, callback) — target = _G via pseudo-
// index. Don't push _G with `lua_pushvalue(GLOBALS_INDEX)`:
// it crashes (the pushvalue → setobj2s chain dereferences a
// bad TValue pointer for that pseudo-index). `lua_gettable` /
// `lua_settable` accept GLOBALS_INDEX as the table arg
// directly, so we use it that way without pushing.
targetIdx = Game::Lua::GLOBALS_INDEX;
nameIdx = 1;
callbackIdx = 2;
} else {
Game::Lua::Error(L,
"hooksecurefunc(table, name, callback) or hooksecurefunc(name, callback)");
return 0;
}
if (Game::Lua::Type(L, nameIdx) != Game::Lua::TYPE_STRING) {
Game::Lua::Error(L, "hooksecurefunc: name must be a string");
return 0;
}
if (Game::Lua::Type(L, callbackIdx) != Game::Lua::TYPE_FUNCTION) {
Game::Lua::Error(L, "hooksecurefunc: callback must be a function");
return 0;
}
// Block the hook when targeting `_G` and the name is in the unhookable
// set. We don't apply this to the three-arg form (table targets):
// hooking `MyLib.pairs` is fine even if Lua's global `pairs` is not —
// the blacklist is specifically about `_G[name]`. A user who passes
// `_G` explicitly as the table target is opting in deliberately and we
// don't second-guess it.
if (targetIdx == Game::Lua::GLOBALS_INDEX) {
const char *name = Game::Lua::ToString(L, nameIdx);
if (IsUnhookable(name)) {
Game::Lua::Error(L,
"hooksecurefunc: function is unhookable");
return 0;
}
}
// Fetch target[name] — the original function. `gettable` uses
// metamethods, so frame methods (resolved via the frame
// metatable's __index) come back as callable closures, exactly
// what we want to wrap.
Game::Lua::PushValue(L, nameIdx);
Game::Lua::GetTable(L, targetIdx);
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) {
Game::Lua::Error(L, "hooksecurefunc: target field is not a function");
return 0;
}
// Push callback as the second upvalue; orig is already on top
// from the GetTable above.
Game::Lua::PushValue(L, callbackIdx);
// Build the wrapper as a C closure with (orig, callback) as the
// two upvalues. PushCClosure pops the upvalues from the stack
// and pushes the closure.
Game::Lua::PushCClosure(L, &HookWrapper, 2);
// Install: target[name] = wrapper. SetTable expects the value
// on top and the key just below; push them in that order.
Game::Lua::PushValue(L, nameIdx);
Game::Lua::PushValue(L, -2); // duplicate the closure
Game::Lua::SetTable(L, targetIdx);
return 0;
}
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("hooksecurefunc", &Script_HookSecureFunc);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace HookSecureFunc