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, 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 ]]