Files
LuaHooks32/luahooks32.lua
2025-10-06 23:04:30 -05:00

525 lines
15 KiB
Lua

hk = require('luahooks32core')
-- Memory protection
-- Allow writing to code areas
local PAGE_EXECUTE_READWRITE = 0x40
function hk.protectRWX(addr, len)
return hk.protect(addr, len, PAGE_EXECUTE_READWRITE)
end
hk._openImages = hk._openImages or {}
-- open library memoization
function hk.open(name)
if name==nil then name = '_baseImage' end
local img = hk._openImages[name]
if img then return img end
img = hk._openRaw(name~='_baseImage' and name or nil)
hk._openImages[name] = img
return img
end
-- Scanning
-- Convert text-style scan pattern into code-style pattern
-- Input: '11 22 33 ? 44'
-- Output: '\x11\x22\x33\x00\x44', 'xxx?x'
local function patToCode(p)
if p:find('^str:') then -- raw string
local pat = p:sub(4, #p)
local mask = ('x'):rep(#pat)
return pat, mask
else -- hex pattern
if p:find('[^a-fA-F0-9 \r\n\t%?]') then
error('hk pattern: pattern contains invalid character', 3) end
local patT, maskT = {}, {}
for word in p:gmatch('[^ \r\n\t]+') do
if word:find('%?') then
table.insert(patT, string.char(0))
table.insert(maskT, '?')
else
local val = tonumber(word, 16)
assert(val and val>=0 and val<=255, 'invalid word in scan pattern: '..word, 3)
table.insert(patT, string.char(val))
table.insert(maskT, 'x')
end
end
return table.concat(patT), table.concat(maskT)
end
end
-- Scan
-- hk.scan('15 1 ? 1f') - return first match (or nil)
-- hk.scan('15 1 ? 1f', 1) - return nth match (or nil) (starting from 1)
-- hk.scan('15 1 ? 1f', true) - return list of all matches (or {})
local unhookAll, rehookAll
hk._scanResultCache = hk._scanResultCache or {}
function hk.scan(pat, opt, img)
if type(pat)~='string' then
error('hk.scan: argument #1: expected string', 2) end
local code, mask = patToCode(pat)
img = img or hk.open()
opt = opt or 1
local _
local cacheEntry = tostring(img)..':'..pat..':'..tostring(opt)
local res = hk._scanResultCache[cacheEntry]
if res then return res end
if opt==true then -- find all matches
res = {}
unhookAll()
while true do
local suc,a = pcall(hk._scanRaw, img, code, mask, #res)
if not a then break end
table.insert(res, a)
end
rehookAll()
elseif type(opt)=='number' and opt%1==0 and opt>0 then -- find nth match
unhookAll()
suc,res = pcall(hk._scanRaw, img, code, mask, opt-1)
rehookAll()
else
error('hk.scan: argument #2: expected true, positive integer, or nil', 2)
end
hk._scanResultCache[cacheEntry] = res
return res
end
-- Writing
local function hexToStr(h)
local t = {}
if h:find('[^a-zA-Z0-9 \r\n\t]') then
error('hk.write: invalid character in hex string', 3) end
for w in h:gmatch('[^ \r\n\t]+') do
local v = tonumber(w, 16)
if not (v and v>=0 and v<=255) then
error('hk.write: invalid hex number: '..w, 3) end
table.insert(t, string.char(v))
end
return table.concat(t)
end
local customWriters = {
hex = function(addr, str, len)
local data = hexToStr(str)
if len then return addr+#data end
return hk.writeStr(addr, data)
end,
str = function(addr, data, len)
if len then return addr+#data end
return hk.writeStr(addr, data)
end,
char = function(addr, val, len)
if len then return addr+1 end
return hk.writeChar(addr, val)
end,
short = function(addr, val, len)
if len then return addr+2 end
return hk.writeShort(addr, val)
end,
int = function(addr, val, len)
if len then return addr+4 end
return hk.writeInt(addr, val)
end,
rel = function(addr, val, len)
if len then return addr+4 end
return hk.writeInt(addr, val - (addr+4))
end,
float = function(addr, val, len)
if len then return addr+4 end
return hk.writeFloat(addr, val)
end,
double = function(addr, val, len)
if len then return addr+8 end
return hk.writeFloat(addr, val)
end,
}
local customWriterDefaults = {
number = 'int',
string = 'hex',
}
local function writeData(addr, data, typ, len)
if type(data)=='table' then
if typ then
error('hk.write: argument #3: expected nil when argument #2 is table', 2) end
if not table.islist(data) then
error('hk.write: argument #2: table must be a list', 2) end
local ntyp = nil
for i,v in ipairs(data) do
if type(v)=='string' and v:sub(#v,#v)==':' then
ntyp = v:sub(1,#v-1)
if not customWriters[ntyp] then
error('hk.write: argument #2: expected writer type at index '
..i..', got \''..ntyp..'\'', 2) end
else
addr = hk.write(addr, data[i], ntyp, len)
ntyp = nil
end
end
return addr
else
if not typ then
typ = customWriterDefaults[type(data)]
if not typ then
error('hk.write: argument #2: expected string, number, or table') end
end
if not customWriters[typ] then
error('hk.write: argument #3: expected writer type, got \''..typ..'\'') end
return customWriters[typ](addr, data, len)
end
end
function hk.write(addr, data, typ)
return writeData(addr, data, typ, false)
end
-- write to a write-protected area by turning off write protection first,
-- then re-enable write protection afterward
function hk.patch(addr, data, typ)
local len = writeData(addr, data, typ, true) - addr
local oldProt = hk.protectRWX(addr, len)
local addrW = writeData(addr, data, typ, false)
hk.protect(addr, len, oldProt)
return addrW
end
-- Hooking
local function writeTrampoline(trAddr, hkAddr, regsPtr)
return hk.write(trAddr, {
-- save registers and flags
'a3',regsPtr, -- mov [regsPtr],eax
'b8',regsPtr, -- mov eax,regsPtr
'89 58 04', -- mov [eax+0x04],ebx
'89 48 08', -- mov [eax+0x08],ecx
'89 50 0c', -- mov [eax+0x0c],edx
'89 70 10', -- mov [eax+0x10],esi
'89 78 14', -- mov [eax+0x14],edi
'89 60 18', -- mov [eax+0x18],esp
'89 68 1c', -- mov [eax+0x1c],ebp
'c7 40 20',hkAddr, -- mov [eax+0x20],hkAddr (res->eip)
'9c', -- pushfd
'5a', -- pop edx
'89 50 24', -- mov [eax+0x24],edx (regs->flags)
'c7 40 28',0, -- mov [eax+0x28],0 (regs->brk = 0)
-- load arguments into ecx+edx and call
'89 c1', -- mov ecx,eax
'ba',hk._getLuaStatePtr(), -- mov edx,L
'e8','rel:',hk._getCallbackPtr(), -- call hook function
-- restore registers
'b8',regsPtr, -- mov eax,regsPtr
'8b 58 04', -- mov ebx,[eax+0x04]
'8b 48 08', -- mov ecx,[eax+0x08]
'8b 50 0c', -- mov edx,[eax+0x0c]
'8b 70 10', -- mov esi,[eax+0x10]
'8b 78 14', -- mov edi,[eax+0x14]
'8b 60 18', -- mov esp,[eax+0x18]
'8b 68 1c', -- mov ebp,[eax+0x1c]
-- if regs.brk, restore eax and retn; otherwise continue
'83 78 28 00', -- cmp dword ptr [eax+0x28],0
'74 06', -- je (past eax restore and retn)
'a1',regsPtr, -- mov eax,[regsPtr]
'c3', -- retn
-- restore flags and eax
'8b 50 24', -- mov edx,[eax+0x24] (regs->flags)
'52', -- push edx
'9d', -- popfd
'a1',regsPtr, -- mov eax,[regsPtr]
-- after this, moved code will be written, followed by a jump back
})
end
local function writeTrampolineReturn(trAddr, retAddr)
return hk.write(trAddr, {
'e9','rel:',retAddr, -- jmp retAddr
})
end
local regsStructSize = 4*11
local regsStruct = {
eax= 0, ebx= 4, ecx= 8, edx=12,
esi=16, edi=20, esp=24, ebp=28,
eip=32, flags=36, brk=40,
}
local regsList = {'eax','ebx','ecx','edx','esi','edi','esp','ebp','eip','flags','brk'}
local function newRegs()
return hk.malloc(regsStructSize)
end
local function readRegsStruct(regsPtr)
local regs = {}
for _,regname in ipairs(regsList) do
regs[regname] = hk.readInt(regsPtr + regsStruct[regname])
end
return regs
end
-- Basic instruction length determination for automatic trampoline construction
-- Add to this table as needed to define instructions
-- a value of true indicates a position-dependent instruction that cannot be moved
local instrLen = {
['1b'] = {
['c9'] = 2, -- sbb ecx,ecx
},
['23'] = {
['c8'] = 2, -- and ecx,eax
},
['33'] = {
['c4'] = 2, -- xor eax,esp
},
['3b'] = {
['15'] = 6, -- cmp edx,i32
},
['50'] = 1, -- push eax
['51'] = 1, -- push ecx
['53'] = 1, -- push ebx
['55'] = 1, -- push ebp
['56'] = 1, -- push esi
['57'] = 1, -- push edi
['5d'] = 1, -- pop ebp
['5e'] = 1, -- pop esi
['5f'] = 1, -- pop edi
['68'] = 5, -- push i32
['6a'] = 2, -- push i8
['72'] = true, -- jb rel8
['74'] = true, -- jz rel8
['75'] = true, -- jnz rel8
['81'] = {
['ec'] = 6, -- sub esp,i32
['c2'] = 6, -- add edx,i32
},
['83'] = {
['c4'] = 3, -- add esp,i8
['e4'] = 3, -- and esp,i8
['ec'] = 3, -- sub esp,i8
},
['85'] = {
['c0'] = 2, -- test eax,eax
},
['89'] = {
['0d'] = 6, -- mov [i32],ecx
['15'] = 6, -- mov [i32],edx
},
['8b'] = {
['0d'] = 6, -- mov ecx,i32
['40'] = 3, -- mov eax,[eax+i8]
['44'] = {
['24'] = 4, -- mov eax,[esp+i8]
},
['45'] = 3, -- mov eax,[ebp+i8]
['6b'] = 3, -- mov ebp,[ebx+i8]
['c8'] = 2, -- mov ecx,eax
['dc'] = 2, -- mov ebx,esp
['e5'] = 2, -- mov esp,ebp
['ec'] = 2, -- mov ebp,esp
['f1'] = 2, -- mov esi,ecx
},
['8d'] = {
['34'] = {
['01'] = 3, -- lea esi,[ecx+eax]
},
['90'] = 6, -- lea edx,[eax+i32]
},
['a1'] = 5, -- mov eax,i32
['b8'] = 5, -- mov eax,i32
['c3'] = 1, -- retn
['d9'] = {
['47'] = 3, -- fld:32 [edi+i8]
['87'] = 6, -- fld:32 [edi+i32]
},
['dd'] = {
['5c'] = {
['24'] = 4, -- fstp:64 [esp+i8]
},
},
['e8'] = true, -- call rel32
['f7'] = {
['d9'] = 2, -- neg ecx
},
}
local function readByteHex(addr)
return ('%02x'):format(hk.readChar(addr)%256)
end
local jmpoutLen = 5 -- Length of the long jump instruction to be inserted as a hook
-- Determine the minimum code length >= jmpoutLen that can be
-- copied out into the trampoline.
-- Returns false if unrecognized instructions
-- Returns true if instructions are position-dependent and cannot be moved
local function determineOverwriteLen(addr)
local len = 0
while len < jmpoutLen do
local val = readByteHex(addr)
local il = instrLen[val]
local ofs = 0
while type(il)=='table' do
ofs = ofs+1
val = readByteHex(addr+ofs)
il = il[val]
end
if not il then return false end
if il==true then return true end
addr = addr + il
len = len + il
end
return len
end
-- Write into existing function to jump to trampoline
local function writeJumpout(rh)
local oldProt = hk.protectRWX(rh.hkAddr, rh.hkLen)
local hkAddrW = hk.write(rh.hkAddr, {
'e9','rel:',rh.trAddr, -- jmp trAddr
})
for i = 1, rh.hkLen-jmpoutLen do -- fill extra space with nops
hkAddrW = hk.write(hkAddrW, '90')
end
hk.protect(rh.hkAddr, rh.hkLen, oldProt) -- restore write protection on jumpout
return hkAddrW
end
-- Write code copied from hooked function into trampoline
local function writeOldCode(rh)
local oldProt = hk.protectRWX(rh.hkAddr, rh.hkLen)
hk.writeStr(rh.hkAddr, rh.movedCode)
hk.protect(rh.hkAddr, rh.hkLen, oldProt)
end
hk._registeredHooks = hk._registeredHooks or {} -- map of addr -> list of callbacks
-- Remove/replace all hooks, used when scanning
unhookAll = function()
for _,rh in pairs(hk._registeredHooks) do
writeOldCode(rh)
end
end
rehookAll = function()
for _,rh in pairs(hk._registeredHooks) do
writeJumpout(rh)
end
end
-- Called from C in trampoline
function _bllua_hk_callback(regsPtr)
local regs = readRegsStruct(regsPtr)
local rh = hk._registeredHooks[regs.eip]
if not rh then
error('_bllua_hk_callback: no callback registered at address') end
rh.callback(regs)
end
-- Main raw hooking function
function hk.hook(hkAddr, callback, hkLen)
if type(hkAddr)~='number' or hkAddr<0 or hkAddr%1~=0 then
error('hk.hook: argument #1: expected number >0 integer', 2) end
if type(callback)~='function' then
error('hk.hook: argument #2: expected function', 2) end
if hkLen~=nil and (type(hkLen)~='number' or hkLen<0 or hkLen%1~=0) then
error('hk.hook: argument #3: expected nil or number', 2) end
-- if a hook is already registered, overwrite it
-- todo: multiple hooks? package names?
if hk._registeredHooks[hkAddr] then
print('hk.hook: warning: a hook is already registered at address '..
('%08x'):format(hkAddr)..', will overwrite.')
hk._registeredHooks[hkAddr].callback = callback
return
end
if hkLen then
if hkLen<jmpoutLen then
error('hk.hook: argument #3: overwrite length must be >= '
..jmpoutLen, 2) end
else
hkLen = determineOverwriteLen(hkAddr)
if hkLen==false then
error('hk.hook: could not automatically determine instruction length. '
..'please specify a length >= '..jmpoutLen..' in argument #3', 2)
elseif hkLen==true then
error('hk.hook: the hook location contains position-dependent code! '
..'please move the hook or use a different hooking method', 2)
end
end
-- create register save struct
local regsPtr = newRegs()
-- create trampoline code
local movedCode = hk.readStr(hkAddr, hkLen)
local trLen = 256 + #movedCode -- eh, good enough
local trAddr = hk.malloc(trLen)
local trOldProt = hk.protectRWX(trAddr, trLen) -- allow execution
local trAddrW = trAddr
trAddrW = writeTrampoline(trAddrW, hkAddr, regsPtr)
trAddrW = hk.writeStr(trAddrW, movedCode)
trAddrW = writeTrampolineReturn(trAddrW, hkAddr + hkLen)
-- create info struct
local rh = {
hkAddr = hkAddr,
hkLen = hkLen,
regsPtr = regsPtr,
trAddr = trAddr,
trLen = trLen,
trOldProt = trOldProt,
movedCode = movedCode,
callback = callback,
}
-- save to hook registry
hk._registeredHooks[hkAddr] = rh
-- write jump out
writeJumpout(rh)
end
-- Remove a hook made by hk.hookRaw
function hk.unhook(hkAddr)
if type(hkAddr)~='number' or hkAddr<0 or hkAddr%1~=0 then
error('hk.unhook: argument #1: expected number >0 integer', 2) end
local rh = hk._registeredHooks[hkAddr]
assert(rh, 'hk.unhook: no hook registered at address '..
('%08x'):format(hkAddr))
-- remove jumpout
writeOldCode(rh)
-- delete allocated data
hk.protect(rh.trAddr, rh.trLen, rh.trOldProt)
hk.free(rh.trAddr)
hk.free(rh.regsPtr)
-- remove from hook registry
hk._registeredHooks[hkAddr] = nil
end
function hex(v) return v and ('%08x'):format(v):sub(1,8) end
-- called when blocklua unloads
function hk.unhookAll()
unhookAll()
for _,rh in pairs(hk._registeredHooks) do
hk.protect(rh.trAddr, rh.trLen, rh.trOldProt)
hk.free(rh.trAddr)
hk.free(rh.regsPtr)
end
hk._registeredHooks = {}
end
-- Utility to display addresses as hex
function hk.hex(v)
return ('%08x'):format(v)
end
-- todo: stack manipulation
_bllua_on_unload['libhooks'] = hk.unhookAll
return hk
-- testing
--[[
'dofile('modules/lualib/luahooks32.lua')
'f = hk.scan('55 8B EC 83 E4 C0 83 EC 38 56 57 8B 7D 08 8B 47 44 D1 E8 A8 01 74 5C 8B 47 28', 2)
'hk.hook(f, function(regs) print(regs) end)
not documented:
hk.open
hk.write / hk.patch formats other than hex
hk.protect
hk.protectRWX
]]