Trampolines for Unity Games?

Memory scanning, code injection, debugger internals and other gamemodding related discussion
Post Reply
gideon25
Table Makers
Table Makers
Posts: 1131
Joined: Mon Mar 20, 2017 1:42 am
Reputation: 939

Trampolines for Unity Games?

Post by gideon25 »

So upon occasion I see with 64 bit Unity games cheat engine is forced to use a 14 byte jump. Usually I will just manually create a 14 byte jump and use readmem, etc. BUT, from what Im seeing, thats not really the best option as even at the very beginning of a function the bytes can differ for some people. Using 14 bytes just makes it more likely that the bytes may differ. So using a 5 byte jump would be the most compatible. So, Im wanting to learn how to do trampolines for unity games. What I would like is to have a small area that I can use that will only ever be a 5 byte jump away from all the functions I want to use... I saw Sunbeams Script over here:
viewtopic.php?t=14340

Where he sets up a place in memory to use trampolines:
Spoiler
local t = 0

local gameModule = getAddress( process )
t = gameModule + 0x550
fullAccess( t, 0x1000 ) -- as long as size for fullAccess is > than required size, should be fine
unregisterSymbol( "Trampolines" )
registerSymbol( "Trampolines", t, true )
He uses gameModule but I tried it on a unity game and 0x550 from the gameModule (which is just the exe) was actually a 14 byte jump away from the functions/methods. So, I have not seen a whole lot of these types of scripts. Any help? Thanks!

User avatar
TheyCallMeTim13
Administration
Administration
Posts: 1713
Joined: Fri Mar 03, 2017 12:31 am
Reputation: 900

Re: Trampolines for Unity Games?

Post by TheyCallMeTim13 »

Are you using the "allocateNear" parameter when allocating?
i.e.: alloc(mySymbol, 0x400, injectionlocation)

gideon25
Table Makers
Table Makers
Posts: 1131
Joined: Mon Mar 20, 2017 1:42 am
Reputation: 939

Re: Trampolines for Unity Games?

Post by gideon25 »

TheyCallMeTim13 wrote:
Sat Jul 31, 2021 12:05 pm
Are you using the "allocateNear" parameter when allocating?
i.e.: alloc(mySymbol, 0x400, injectionlocation)
Sure.. The issue is when a 14 byte jump is needed and there is a difference in those 14 bytes (whether it be 16 bytes that need to be readmem or 18 bytes) between different PCs due to the way Unity can change up and add in LEA and MOV instead of just MOV for example. That was why I was thinking it would be nice to just have all the jumps just be 5 bytes to some place that I can then just use 14 byte jumps after that.

panraven
Table Makers
Table Makers
Posts: 66
Joined: Fri Mar 03, 2017 12:03 am
Reputation: 49

Re: Trampolines for Unity Games?

Post by panraven »

May look for series of [cc ] x14 near the hack point if near alloc fail.
There should be safe to place long jump.
But it may need 2 stage of scanning with vanilla AOBScan command.
eg.

Code: Select all

1st stage:
aobscan(hackpoint,...)
registersymbol(hackpoint)
2nd stage:
alloc(inject,...)
aobscanregion(cc_hackpoint, hackpoint-1ffffff,hackpoint+1ffffff,cc cc cc cc cc cc cc ... )

inject:
... your code ...
jmp return_hackpoint

hackpoint:
jmp cc_hackpoint
... nops if need ...
return_hackpoint:


cc_hackpoint:
jmp inject

And an alternative using custom assembler,

The script provide a jmp format

jmp!near <address>
call!near <address>

where the nearby allocation (using lua) to place long jump is persisted and allocated only once, so to minimize fail.

It may still fail tho.


Just come up with method to due with random debug codes.

In above 2nd stage scanning, it also scan the return_hackpoint.
eg.
aobscan(return_hackpoint, hackpoint+1, hackpoint+50, return_aob_pattern)

So now the random debug codes in between is not relevant.

If the size between hackpoint and return_hackpoint is large enough for a long jump (14 bytes + possible overlapped instruction ),
then the hackpoint jmp can be near or long. The cc_hackpoint is not need.

The burden is now on making the longer original codes with [reassemble] and/or reading the varying offset etc.

gideon25
Table Makers
Table Makers
Posts: 1131
Joined: Mon Mar 20, 2017 1:42 am
Reputation: 939

Re: Trampolines for Unity Games?

Post by gideon25 »

panraven wrote:
Sat Jul 31, 2021 7:39 pm

And an alternative using custom assembler,

The script provide a jmp format

jmp!near <address>
call!near <address>

where the nearby allocation (using lua) to place long jump is persisted and allocated only once, so to minimize fail.
I tested it with a game that I know ALWAYS has a 14 byte jump (At a specific location).
So with that Lua script above I placed the Lua script in the very top first script to be activated and then the other scripts I use I can replace like:

band_money:
jmp newmem
nop
return:
registersymbol(band_money)

with:

band_money:
jmp!near newmem
nop
return:
registersymbol(band_money)

and it will try to create a trampoline? Because the test place I used it did NOT create a trampoline. It actually did not create a jump at all.. I tried testing with:
[Link]

I used the jmp!near and it failed to create a jump. Using jump! or jump!short would create a 14 byte jump.. It would be VERY nice to have an auto-trampoline ability for such cases though..

gideon25
Table Makers
Table Makers
Posts: 1131
Joined: Mon Mar 20, 2017 1:42 am
Reputation: 939

Re: Trampolines for Unity Games?

Post by gideon25 »

So the game I was having problems with was with two scripts so what I did was:

Code: Select all


2705C608A7C: 58           - pop rax
2705C608A7D: C0 71 02 00  - rol byte ptr [rcx+02],00
2705C608A81: 00 00        - add [rax],al    <--- Since there were 14 bytes here to make a 14 byte jump
2705C608A83: 00 00        - add [rax],al
2705C608A85: 00 00        - add [rax],al
2705C608A87: 00 00        - add [rax],al
2705C608A89: 00 00        - add [rax],al
2705C608A8B: 00 00        - add [rax],al
2705C608A8D: 00 00        - add [rax],al
2705C608A8F: 00           - db 00
// ---------- INJECTING HERE ----------
RulesetCharacter:GetRemainingUsesOfPower: 55           - push rbp        <--Jumped up 14 bytes
// ---------- DONE INJECTING  ----------
RulesetCharacter:GetRemainingUsesOfPower+1: 48 8B EC     - mov rbp,rsp
RulesetCharacter:GetRemainingUsesOfPower+4: 48 83 EC 40  - sub rsp,40
It worked..but seems to be a workaround for something that could be done a better way..

panraven
Table Makers
Table Makers
Posts: 66
Joined: Fri Mar 03, 2017 12:03 am
Reputation: 49

Re: Trampolines for Unity Games?

Post by panraven »

gideon25 wrote:
Sun Aug 01, 2021 3:08 pm
...
It worked..but seems to be a workaround for something that could be done a better way..
Hi, I will try to add an extension of the command format so that it scan for near by cc cc cc .. 55 or c3 00 00 00 BEFORE trampoline allocation, like
jmp!near <longaddr_in_hex> : cc*55 48 8b ec ; 5d c3|00*
where cc* mean 14x cc bytes for short hand and | for trampoline jmp point, 0 pos of the aob if missing;
these give the command aob pattern to scan for near by possible trampoline;
different game/exe may has different bytes pattern fill between function so it is better make the pattern specified in place.

What do you think?


UPDATE:
here the updated script

gideon25
Table Makers
Table Makers
Posts: 1131
Joined: Mon Mar 20, 2017 1:42 am
Reputation: 939

Re: Trampolines for Unity Games?

Post by gideon25 »

panraven wrote:
Sun Aug 01, 2021 11:23 pm
gideon25 wrote:
Sun Aug 01, 2021 3:08 pm
...
It worked..but seems to be a workaround for something that could be done a better way..
Hi, I will try to add an extension of the command format so that it scan for near by cc cc cc .. 55 or c3 00 00 00 BEFORE trampoline allocation, like
jmp!near <longaddr_in_hex> : cc*55 48 8b ec ; 5d c3|00*
where cc* mean 14x cc bytes for short hand and | for trampoline jmp point, 0 pos of the aob if missing;
these give the command aob pattern to scan for near by possible trampoline;
different game/exe may has different bytes pattern fill between function so it is better make the pattern specified in place.

What do you think?


UPDATE:
here the updated script
YES! I used:

band_money:
jmp!near newmem: c3 | 00*
nop
return:
registersymbol(band_money)

and it created a 5 byte jump to a trampoline at the nearest 14 byte empty area after a RET. Very nice! Thanks!

panraven
Table Makers
Table Makers
Posts: 66
Joined: Fri Mar 03, 2017 12:03 am
Reputation: 49

Re: Trampolines for Unity Games?

Post by panraven »

gideon25 wrote:
Mon Aug 02, 2021 9:18 am
...
YES! I used:

band_money:
jmp!near newmem: c3 | 00*
nop
return:
registersymbol(band_money)

and it created a 5 byte jump to a trampoline at the nearest 14 byte empty area after a RET. Very nice! Thanks!
You are welcome.

btw, since the colon ':' look like a label marker, it is updated to use also semi-colon ';'.

User avatar
Rhark
Fearless Donors
Fearless Donors
Posts: 1115
Joined: Tue Apr 16, 2019 1:27 am
Reputation: 500

Re: Trampolines for Unity Games?

Post by Rhark »

panraven wrote:
Sun Aug 01, 2021 11:23 pm
Hi, I will try to add an extension of the command format so that it scan for near by cc cc cc .. 55 or c3 00 00 00 BEFORE trampoline allocation, like
jmp!near <longaddr_in_hex> : cc*55 48 8b ec ; 5d c3|00*
where cc* mean 14x cc bytes for short hand and | for trampoline jmp point, 0 pos of the aob if missing;
these give the command aob pattern to scan for near by possible trampoline;
different game/exe may has different bytes pattern fill between function so it is better make the pattern specified in place.

What do you think?


UPDATE:
here the updated script
Forgive me for my incompetence, but how does one use this? Having issues with 14byte jumps on unity games myself recently.

User avatar
notpikachu
Table Makers
Table Makers
Posts: 256
Joined: Wed Apr 01, 2020 10:32 am
Reputation: 210

Re: Trampolines for Unity Games?

Post by notpikachu »

Rhark wrote:
Fri Aug 06, 2021 5:12 pm
...
I don't have a unity game that does a 14-bytes jmp, but I experienced such case on a emulator game in one of it function. If you don't mind, you can probably look at my full script.
original script

Code: Select all

[ENABLE]
aobscan(datt,42 ?? ?? ?? ?? ?? 89 ?? 89 ?? 42 ?? ?? ?? ?? 48 ?? ?? ?? ?? 4C ?? ?? ?? ?? 42 ?? ?? ?? ?? 89 ?? 89 ?? 41 ?? ?? 41 ?? ?? ?? 40 ?? ?? ?? 40 ?? ?? ?? 45 ?? ?? 0F) // should be unique
alloc(newmem,$1000,datt)
alloc(_datt,8)
alloc(dattcopy,15)
registersymbol(datt)
registersymbol(_datt)
registersymbol(dattcopy)
label(code)
label(return)

dattcopy:
readmem(datt,15)

newmem:
mov [_datt],rcx
add [_datt],r8
add [_datt],20
code:
readmem(datt,15)
jmp return

datt:
  jmp newmem
  nop
return:

[DISABLE]
datt:
readmem(dattcopy,15)
dealloc(newmem)
dealloc(datt)
dealloc(_datt)
dealloc(dattcopy)
unregistersymbol(datt)
unregistersymbol(_datt)
unregistersymbol(dattcopy)
modified script -delete some of the early comment

Code: Select all

[ENABLE]
{$lua}
if syntaxcheck then return end
if not _customJumpCall then
_pidContext = _pidContext or {now=os.clock()+3; reset=os.clock()+60}

local function pf(...) if indebug then print(string.format(...))end end

--- modify to default +X and allow one result
local function AOBScanEx(aob,s,e,bAllResult,reuseMS,p,a,n,pb)
--  pf('scan '..aob)
  local p,a,n,s,e = p or '+X*W',a or fsmNotAligned,n or '0',s or 0x0,e or 0xffffffffffffffff
  reuseMS = type(reuseMS)=='userdata'and reuseMS.ClassName:lower():find'scan' and reuseMS
  local ms, result = reuseMS or pb and createMemScan(pb) or createMemScan()
  ms.OnlyOneResult = not bAllResult
  if ms.OnlyOneResult then
    ms.firstScan(soExactValue,vtByteArray,nil,aob,nil,s,e,p,a,n,true,false,false,false)
    ms.waitTillDone()
    local r = ms.Result
    if type(r)=='number' and r~=0 then result = r end
  else
    local fl = createFoundList(ms)
    ms.firstScan(soExactValue,vtByteArray,nil,aob,nil,s,e,p,a,n,true,false,false,false)
    ms.waitTillDone()
    fl.initialize()
    if fl.getCount() > 0 then
      result = {}
      for i=1,fl.getCount() do result[i]=tonumber(fl.getAddress(i-1),16) end
    end
    fl.destroy()
  end
  if not reuseMS then ms.destroy() end
  return result
end


if not getPidContext then
function getPidContext()
  local p, pid, now = _pidContext, getOpenedProcessID(), os.clock()
  if now > p.reset then
    p.reset = now + 60
    local ps = getProcessList()
    for k,v in pairs(p)do
      if type(k)=='number' and (not ps[k] or ps[k]~=v[1]) then p[k] = nil end
    end
  end
  if now > p.now and not readInteger(process)then
    return {}
  elseif p[pid] and now<p.now and p.pid == pid and p.process==process then
    return p[pid][2]
  elseif not readInteger(process) then
    return {}
  end
  p.now, p.pid, p.process = now+3, pid, process
  if not p[pid] or p[pid][1]~=process then
    local px = enumModules()[1]
    p[pid] = {process,{PID=pid,PROCESS=process,ExeBase=px.Address, ExeFile=px.PathToFile,ExeSize=getModuleSize(process)}}
  end
  return p[pid][2]
end
end
getPidContext()
local EMPTY = -1


local function s2aob(s)
--  print('s2aob:',type(s))
  return s:gsub('.',function(c)return string.format(' %02X',c:byte())end):sub(2)
end
local function toInt(n)return type(n)=='number'and math.tointeger(n)end
local function makeKey(target,hint)
  return string.format('%x->%x',hint,target)
end
local function makeTrampoline(from,target,hint,t)
  local  diff = from - hint
--   pf('try make: %X %X %X %X',from,target,hint, diff)
  if diff>-0x7ffffffb and diff<0x80000005 then

    local bs, r = string.pack('I6I8',0x25ff ,target),{}
    for c in bs:gmatch'.'do r[1+#r]=c:byte()end
    fullAccess(from,64)
    local x = writeBytes(from, r)
    local rs = readBytes(from,14,true)
--    pf('written %X', x or -1)
    if rs and byteTableToString(rs)==bs then
  --    pf('ok write: %X %X %X // %s  || %s',from,target,hint,s2aob(bs),s2aob(byteTableToString(rs)))
      t[makeKey(target,hint)] = {from,target}
      return from
    else
      return nil,'fail write bytes'
--      pf('fail write: %X %X %X',from,target,hint)
    end
  end
end
function GetTrampoline(target, hint, noNewAlloc, nearCavePatterns)

  if not toInt(target)or not toInt(hint) then return nil end
  local p = getPidContext()
  p.Trampoline = p.Trampoline or {}
  local key = makeKey(target,hint)
  local t,tcnt, diff = p.Trampoline,0
  if t[key] then return t[key][1]  end

  if not noNewAlloc and nearCavePatterns then
    local cs,ms = nearCavePatterns, createMemScan()
--    print('AOB scan --',#cs)
    for i=1,#cs do
      local aob,ofs = unpack(cs[i])
      local found = AOBScanEx(aob,hint-0x7fff0000,hint+0x7fff0000,false,ms)
--      pf('aob found: %02d %02X %X %s ',i,ofs,found or -1,aob)
      if found and makeTrampoline(found+ofs,target,hint,t) then
        t[found+ofs] = target
--        pf('ok %X', found+ofs)
          ms.Destroy()
        return found+ofs
      else
--        print'fail'
      end
    end
    ms.Destroy()
  end
------
  for from, to in pairs(t) do
    if to==EMPTY and makeTrampoline(from,target,hint,t) then
--      pf('new alloc: %X %X',from,target)
      t[from] = target
      return from
    end
  end
  -- no previous allocation, make new one
  if not noNewAlloc then
    local addr = allocateMemory(0x1000,hint)
    diff = addr and addr - hint
    if not diff or diff<=-0x7ffffffb or diff>=0x7ffff005 then
      if addr then deAlloc(addr)end
      return nil,'fail allocate trampoline'
    end
    p.TrmpAllocCnt = not p.TrmpAllocCnt and 1 or p.TrmpAllocCnt + 1
    for i=0,255 do t[addr+i*16]=EMPTY end
    return GetTrampoline(target, hint, true)
  end
end

if _customJumpCall then
  _customJumpCall = nil,unregisterAssembler(_customJumpCall)
end
indebug = true
_customJumpCall = registerAssembler(function (addr, inst)
--  pf('enter: %X - %s',addr, inst)
  local force, target, nearCave, isJmp, forceShort, forceNear, forceLong =
    inst:match'^%s*[jJ][mM][pP]!(%a*)%s+([^:;]+)%s*(.-)%s*$'
  if target then isJmp = true else
    force, target, nearCave = inst:match'^%s*[cC][aA][lL][lL]!(%a*)%s+([^:;]+)%s*(.-)%s*$'
  end
--  pf('fmt: force[%s] target[%s] nearCave[%s]',force or '?', target or '?',nearCave or '?')

  if not target then return end

  local nearCavePatterns
  if nearCave:len()>0 then -- near cave specificed
    local nc, cs, i, tag, _ = nearCave:sub(2), {},0
--    pf('nearCave:',nc)


    local pat = '^%s*([%x%?][%x%?]?)(%s*|?[|%*]?|?)'
    for aob in nc:gmatch'%s*([^;]+)%s*'do
      i = i+1
--      print(i,'aob:',aob)
      local as,nxt,ofs,tmp,foundOfs = {},0,1,aob
      if aob:sub(1,1)=='|' then foundOfs,aob = 0,aob:sub(2)end
      while aob:find(pat,nxt+1)do
        _, nxt,as[1+#as],tag = aob:find(pat,nxt+1)
--        pf('---->%d : %s, %s, %s, <<%s>>',i,_, nxt,as[#as],tag)
        if tag:find'^%s*%*' then
          as[#as] = (as[#as]..' '):rep(14)
          ofs = ofs + 13
          if tag:find'|' then
            if foundOfs then foundOfs = true break else foundOfs = ofs end
          end
        end
        if tag:find'^%s*|' then
          if foundOfs then foundOfs = true break else foundOfs = ofs end
          if tag:find'%*' then foundOfs = false break end
        end
        ofs = ofs + 1
      end
--      print(i,'>'..table.concat(as,' ')..'<','/',tostring(foundOfs),'/',aob)

      if foundOfs == true then
--        pf('err: %s', 'multiple "|" jmp ofs on aob #'..i..': '..tmp)
        return nil,'multiple "|" jmp ofs on aob #'..i..': '..tmp
      elseif foundOfs == false then
--        pf('err: %s', '"|*" pattern not allowed, aob #'..i..': '..tmp)
        return nil,'"|*" pattern not allowed, aob #'..i..': '..tmp
      end
--      pf('aob%d- %d , %s %s',i,ofs,aob,foundOfs)

      ofs = foundOfs or 0
      if #as>0 then
        cs[1+#cs] = {table.concat(as,' '):gsub('%s+',' '),ofs}
      end
--      print('-->',unpack(cs[#cs]))
    end
    nearCavePatterns = cs
  end

  target = target and target:len()>1 and GetAddressSafe(target)

  if target and force:len()>1 then
    force=force:lower()
    if force=='short' then forceShort = true
    elseif force=='near' then forceNear = true
    elseif force=='long' then forceLong = true
    else target = nil end
  end

  if not target then
    return --nil,'invalid :'..inst
  else
    local cmd = isJmp and {0xeb,0xe9,0x25ff} or {0xe8,0xe8,0x08eb0000000215ff}
    local diff, r, bs = target - addr, {}
    if isJmp and (forceShort or not forceNear and not forceLong) and diff>-0x7e and diff <0x82 then
      bs = string.pack('Bb',cmd[1], diff-2)
    elseif not targetIs64Bit() or not forceLong and diff>-0x7ffffffb and diff<0x80000005 then
      bs = string.pack('Bi4',cmd[2],diff-5)
    elseif not forceNear or addr<0x1000 then
      if isJmp then
        bs = string.pack('I6I8',cmd[3],target)
      else
        bs = string.pack('I8I8',cmd[3],target)
      end
    elseif diff<=-0x7ffffffb or diff>=0x80000005 then
      local trmp, errmsg = GetTrampoline(target, addr, nil,nearCavePatterns)
      if not trmp then return nil,(errmsg or '!')..', no trampoline:'..inst end
      bs = string.pack('Bi4',cmd[2], trmp - addr -5)
    end
    if bs then
      for c in bs:gmatch'.' do r[1+#r]=c:byte()end
      return r
    end
  end
end)
end
{$asm}
aobscan(datt,42 ?? ?? ?? ?? ?? 89 ?? 89 ?? 42 ?? ?? ?? ?? 48 ?? ?? ?? ?? 4C ?? ?? ?? ?? 42 ?? ?? ?? ?? 89 ?? 89 ?? 41 ?? ?? 41 ?? ?? ?? 40 ?? ?? ?? 40 ?? ?? ?? 45 ?? ?? 0F) // should be unique
alloc(newmem,$1000,datt)
alloc(_datt,8)
alloc(dattcopy,6)
registersymbol(datt)
registersymbol(_datt)
registersymbol(dattcopy)
label(code)
label(return)

dattcopy:
readmem(datt,6)

newmem:
mov [_datt],rcx
add [_datt],r8
add [_datt],20
code:
readmem(datt,6)
jmp return

datt:
  jmp!near newmem; c3 | 00*
  nop
return:

[DISABLE]
datt:
readmem(dattcopy,6)
dealloc(newmem)
dealloc(datt)
dealloc(_datt)
dealloc(dattcopy)
unregistersymbol(datt)
unregistersymbol(_datt)
unregistersymbol(dattcopy)
Here's a picture if I use the original script.
Original Function using original script
Image
Image
Here's what happen if I use the modified script.
Modified Script
Image
Image
Image
tldr: I added script panraven on top and modified the jmp newmem into jmp!near newmem; c3 | 00*. Also modify some of the readmem bcus that's how I handle 14-bytes before, back into their normal 5, or in this case 6 bytes.

tbh I'm still not sure if this is the right way to use it but it look like fine on my testing as I follow the code back to the original function. The only thing that worry me, emulator keep using dynamic allocation and sometimes this will messed up the trampoline, but I think it be fine on a normal game.

Hopefully that help~

Best regards,
notpikachu

User avatar
Rhark
Fearless Donors
Fearless Donors
Posts: 1115
Joined: Tue Apr 16, 2019 1:27 am
Reputation: 500

Re: Trampolines for Unity Games?

Post by Rhark »

notpikachu wrote:
Fri Aug 06, 2021 6:13 pm
...
tldr: I added script panraven on top and modified the jmp newmem into jmp!near newmem; c3 | 00*. Also modify some of the readmem bcus that's how I handle 14-bytes before, back into their normal 5, or in this case 6 bytes.

tbh I'm still not sure if this is the right way to use it but it look like fine on my testing as I follow the code back to the original function. The only thing that worry me, emulator keep using dynamic allocation and sometimes this will messed up the trampoline, but I think it be fine on a normal game.

Hopefully that help~

Best regards,
notpikachu
This helped, thank you :)

User avatar
notpikachu
Table Makers
Table Makers
Posts: 256
Joined: Wed Apr 01, 2020 10:32 am
Reputation: 210

Re: Trampolines for Unity Games?

Post by notpikachu »

Rhark wrote:
Fri Aug 06, 2021 7:05 pm
This helped, thank you :)
Glad to help, good luck on your cheat/trainer making ;) .

Post Reply

Who is online

Users browsing this forum: No registered users