mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-28 03:02:06 +01:00
148 lines
7.0 KiB
Python
148 lines
7.0 KiB
Python
"""frame.unreal_thumb_touch (GitHub #49, found by Klownicle in Vader Immortal: Episode I 1.1.1): UE4 OculusInput's
|
|
ThumbUp axis from the capacitive touches instead of near-touch, which the Frame never reports."""
|
|
import struct
|
|
import zipfile
|
|
|
|
from frameport.analysis import detect
|
|
from frameport.apk.workspace import ApkWorkspace
|
|
from frameport.core.events import Reporter
|
|
from frameport.core.models import Analysis
|
|
from frameport.patches import base
|
|
from frameport.patches.frame import unreal_thumb_touch as tt
|
|
|
|
SYM = detect.UNREAL_THUMB_TOUCH
|
|
TEXT = 0x1000
|
|
# Vader Immortal: Episode I 1.1.1's libUE4.so at 0x578f188 (the capacitive-axis loop of SendControllerEvents): the
|
|
# per-hand masks (csel), the jump table, Touches loads at [sp, #240] and NearTouches at [sp, #244] (IndexPointing, then
|
|
# ThumbUp: ldr w8, [sp, #244]; tst w8, w23; fcsel s8, s13, s14, ne)
|
|
VADER = [
|
|
0xAA1F03F4, 0xF100039F, 0x321C03E8, 0x321403E9, 0xF94037FA, 0x1A880135, 0x320003E8, 0x321E03E9, 0x1A890116,
|
|
0x321F03E8, 0x321D03E9, 0x1A890117, 0x1E2703E8, 0x7100169F, 0x54000348, 0x92407E88, 0xB8A87B28, 0x8B190108,
|
|
0xD61F0100, 0xB940F3E8, 0xB94087E9, 0x14000009, 0xB940F7E8, 0x6A16011F, 0x1400000F, 0xB940F3E8, 0xB94077E9,
|
|
0x14000003, 0xB940F3E8, 0xB9408BE9, 0x6A09011F, 0x1E2D1DC8, 0x14000008, 0xB940F3E8, 0x6A15011F, 0x1E2D1DC8,
|
|
0x14000004, 0xB940F7E8, 0x6A17011F, 0x1E2E1DA8, 0xBD400340, 0x1E202100, 0x54000120, 0xF9401260, 0xF85F8341,
|
|
0xB9400302, 0x4EA81D00, 0xF9400008, 0xF9404508, 0xD63F0100, 0xBD000348, 0x91000694, 0x9100435A, 0xF1001A9F,
|
|
0x54FFFAC1, 0xF94033E8, 0x9100079C,
|
|
]
|
|
MASK_LEFT, MASK_RIGHT, THUMB_LOAD = 9, 10, 37 # word indexes Klownicle changed (0x578f1ac, 0x578f1b0, 0x578f21c)
|
|
|
|
|
|
def make_engine(words=VADER) -> bytes:
|
|
"""A minimal ELF64 (one PT_LOAD at vaddr 0 = file offset, .dynsym/.dynstr/.text) exporting SendControllerEvents."""
|
|
text = struct.pack(f"<{len(words)}I", *words)
|
|
dynstr = b"\0" + SYM.encode() + b"\0"
|
|
dynsym = bytes(24) + struct.pack("<IBBHQQ", 1, 0x12, 0, 3, TEXT, len(text))
|
|
shstr = b"\0.dynsym\0.dynstr\0.text\0.shstrtab\0"
|
|
out = bytearray(TEXT) + text
|
|
dynstr_off = len(out)
|
|
out += dynstr
|
|
while len(out) % 8:
|
|
out += b"\0"
|
|
dynsym_off = len(out)
|
|
out += dynsym
|
|
shstr_off = len(out)
|
|
out += shstr
|
|
while len(out) % 8:
|
|
out += b"\0"
|
|
sh_off = len(out)
|
|
|
|
def sh(name, typ, off, size, link=0, entsize=0, flags=0, addr=0):
|
|
return struct.pack("<IIQQQQIIQQ", name, typ, flags, addr, off, size, link, 1 if typ == 11 else 0, 8, entsize)
|
|
|
|
out += bytes(64)
|
|
out += sh(1, 11, dynsym_off, len(dynsym), link=2, entsize=24, flags=2, addr=dynsym_off)
|
|
out += sh(9, 3, dynstr_off, len(dynstr), flags=2, addr=dynstr_off)
|
|
out += sh(17, 1, TEXT, len(text), flags=6, addr=TEXT)
|
|
out += sh(23, 3, shstr_off, len(shstr))
|
|
ident = b"\x7fELF\x02\x01\x01" + bytes(9)
|
|
struct.pack_into("<16sHHIQQQIHHHHHH", out, 0, ident, 3, 183, 1, 0, 64, sh_off, 0, 64, 56, 1, 64, 5, 4)
|
|
struct.pack_into("<IIQQQQQQ", out, 64, 1, 5, 0, 0, 0, sh_off + 5 * 64, sh_off + 5 * 64, 0x4000)
|
|
return bytes(out)
|
|
|
|
|
|
def _analysis(**extra):
|
|
return Analysis(package="com.example.ue4", version="1.0", label="UE4 Game", abis=["arm64-v8a"],
|
|
engine="Unreal", xr="VrApi", graphics="GLES or unknown (no Vulkan declaration)",
|
|
direct_vrapi=False, libs=["libOVRPlugin.so", "libUE4.so"], launcher_activity=None,
|
|
has_info_category=False, meta_permissions=[], uses_glad_gl=False, unity_msaa_levels=0,
|
|
oculus_os_classes=False, is_overport_output=True, debuggable=False, extra={"size": 1, **extra})
|
|
|
|
|
|
def _apk(tmp_path, manifest, engine):
|
|
p = tmp_path / "in.apk"
|
|
with zipfile.ZipFile(p, "w") as z:
|
|
z.writestr("AndroidManifest.xml", manifest)
|
|
z.writestr("classes.dex", b"dex\n035\0")
|
|
z.writestr("lib/arm64-v8a/libUE4.so", engine)
|
|
return p
|
|
|
|
|
|
def test_the_edits_are_klownicles():
|
|
site = tt.thumb_site(make_engine())
|
|
assert site and not site["done"]
|
|
at = {TEXT + 4 * i: i for i in (MASK_LEFT, MASK_RIGHT, THUMB_LOAD)}
|
|
assert [(at[o], old, new) for o, old, new in site["edits"]] == [
|
|
(MASK_LEFT, 0x321F03E8, 0x5281E008), # orr w8, wzr, #0x2 -> mov w8, #0xf00 (bytes 08 e0 81 52)
|
|
(MASK_RIGHT, 0x321D03E9, 0x528001E9), # orr w9, wzr, #0x8 -> mov w9, #0xf (bytes e9 01 80 52)
|
|
(THUMB_LOAD, 0xB940F7E8, 0xB940F3E8), # ldr w8, [sp, #0xf4] -> ldr w8, [sp, #0xf0]
|
|
]
|
|
assert detect.unreal_thumb_touch(make_engine())
|
|
|
|
|
|
def test_other_code_is_left_alone():
|
|
words = list(VADER)
|
|
words[MASK_RIGHT] = 0x321E03E9 # orr w9, wzr, #0x4: not the thumb masks
|
|
assert tt.thumb_site(make_engine(words)) is None
|
|
words = list(VADER)
|
|
words[THUMB_LOAD + 2] = 0x14000004 # no fcsel after the tst
|
|
assert tt.thumb_site(make_engine(words)) is None
|
|
words = list(VADER)
|
|
for i in (19, 25, 28, 33): # no other loads of the Touches slot: not OculusInput's state layout
|
|
words[i] = 0xB940EBE8
|
|
assert tt.thumb_site(make_engine(words)) is None
|
|
assert not detect.unreal_thumb_touch(b"\0" + SYM.encode() + b"\0")
|
|
|
|
|
|
def test_patch_rewrites_three_instructions_once(tmp_path, quest_manifest):
|
|
data = make_engine()
|
|
a = _analysis(unreal_thumb_touch=True)
|
|
patch = base.get("frame.unreal_thumb_touch")
|
|
assert patch.applies(a) and patch.detect(a).recommended
|
|
assert not patch.applies(_analysis(unreal_thumb_touch=False)) and not patch.detect(_analysis())
|
|
assert patch.applies(_analysis()) # analysed by an older FramePort: maybe
|
|
with ApkWorkspace(_apk(tmp_path, quest_manifest, data)) as ws:
|
|
ctx = base.ApkContext(ws, a, {}, Reporter(), {})
|
|
assert patch.apply(ctx)
|
|
out = ws.read_lib("libUE4.so")
|
|
diff = [i for i in range(0, len(data), 4) if data[i:i + 4] != out[i:i + 4]]
|
|
assert diff == [TEXT + 4 * i for i in (MASK_LEFT, MASK_RIGHT, THUMB_LOAD)]
|
|
assert tt.thumb_site(out)["done"]
|
|
assert not patch.apply(ctx) # already patched: no-op
|
|
assert patch.validate(ctx)[0][1]
|
|
assert any("ThumbUp" in n for n in ctx.notes)
|
|
|
|
|
|
def test_unexpected_code_is_reported_not_patched(tmp_path, quest_manifest):
|
|
words = list(VADER)
|
|
words[MASK_LEFT] = 0x321E03E8
|
|
data = make_engine(words)
|
|
with ApkWorkspace(_apk(tmp_path, quest_manifest, data)) as ws:
|
|
ctx = base.ApkContext(ws, _analysis(), {}, Reporter(), {})
|
|
assert not base.get("frame.unreal_thumb_touch").apply(ctx)
|
|
assert ws.read_lib("libUE4.so") == data
|
|
assert any("doesn't match" in n for n in ctx.notes)
|
|
|
|
|
|
def test_analysis_records_the_match(tmp_path, quest_manifest):
|
|
a = detect.analyze(_apk(tmp_path, quest_manifest, make_engine()))
|
|
assert a.extra["unreal_thumb_touch"] is True
|
|
assert a.extra["analysis_version"] == detect.ANALYSIS_VERSION >= 7
|
|
|
|
|
|
def test_vader_recipe_uses_the_engine_fix():
|
|
from frameport.recommend import catalog
|
|
|
|
entry = catalog.load()["com.ILMxLAB.VaderImmortal.ep1"]
|
|
assert "frame.unreal_thumb_touch" in entry.frame
|
|
assert "proximity_emul" not in entry.adapter # the binding didn't animate the thumbs (GitHub #49)
|