mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-28 11:01:11 +01:00
fix(scripts): find VR offsets when Roblox moves the pointer fields
Build 3222 moved the hand selector, hand frame and camera HeadScale fields, so the fixed byte patterns found nothing. Match the code shape instead, read the three offsets from it and write them to the output.
This commit is contained in:
1 parent
228121157b
commit
4b67176970
1 file changed
+58
-41
@@ -95,10 +95,8 @@ CONSTRUCTOR_MEMSET_SIZE = 0x134
|
||||
READY_BYTE_OFFSETS = (0x94, 0x144, 0x147)
|
||||
FRAMEBUFFER_SLOT_OFFSET = 0x150
|
||||
TEXTURE_SLOT_OFFSET = 0x170
|
||||
USERCFRAME_SELECTOR_OFFSET = 0x118
|
||||
USERCFRAME_BASE_OFFSET = 0x174
|
||||
USERCFRAME_STRIDE = 0x30
|
||||
CAMERA_HEAD_SCALE_OFFSET = 0x140
|
||||
MIN_CAMERA_HEAD_SCALE_OFFSET = 0x100
|
||||
|
||||
EXPECTED_STATE_GETTER_SLOT = 0x28
|
||||
EXPECTED_EYE_GETTER_SLOT = 0x48
|
||||
@@ -106,9 +104,12 @@ EXPECTED_EYE_INITIALIZER_SLOT = 0xD0
|
||||
EXPECTED_HAPTIC_SLOT = 0x18
|
||||
MAX_VTABLE_SCAN_BYTES = 0x200
|
||||
|
||||
# movsxd rax, [rsi+0x118]; movups xmm0, [rsi+rax+0x174].
|
||||
POINTER_GETTER_SELECTOR_PATTERN = bytes.fromhex("48 63 86 18 01 00 00")
|
||||
POINTER_GETTER_MOVUPS_PATTERN = bytes.fromhex("0f 10 84 06 74 01 00 00")
|
||||
# movsxd rax, [rsi+selector]; lea rax, [rax+rax*2]; shl rax, 4;
|
||||
# movups xmm0, [rsi+rax+base]. The stride is fixed; the selector and base
|
||||
# displacements are captured because they differ between builds.
|
||||
POINTER_GETTER_PATTERN = re.compile(
|
||||
rb"\x48\x63\x86(.{4})\x48\x8d\x04\x40\x48\xc1\xe0\x04"
|
||||
rb"\x0f\x10\x84\x06(.{4})", re.DOTALL)
|
||||
|
||||
TEXT_REF_OPCODES = re.compile(
|
||||
rb"[\x8d\x8b\x89\x8a\x88\x3b\x39\x3a\x38\x03\x33\x2b\x85\x80\x81\x83"
|
||||
@@ -661,18 +662,17 @@ def find_text_refs(
|
||||
return results
|
||||
|
||||
|
||||
def scan_text_bytes(image: ElfImage, pattern: bytes) -> list[int]:
|
||||
def scan_pointer_getter_sites(image: ElfImage) -> list[tuple[int, int, int]]:
|
||||
"""Return (address, selector offset, UserCFrame base offset) per site."""
|
||||
text = image.text_section()
|
||||
data = image.section_bytes(text)
|
||||
base = text.address
|
||||
hits: list[int] = []
|
||||
position = 0
|
||||
while True:
|
||||
position = data.find(pattern, position)
|
||||
if position < 0:
|
||||
return hits
|
||||
hits.append(base + position)
|
||||
position += 1
|
||||
sites: list[tuple[int, int, int]] = []
|
||||
for match in POINTER_GETTER_PATTERN.finditer(data):
|
||||
selector = struct.unpack("<i", match.group(1))[0]
|
||||
base = struct.unpack("<i", match.group(2))[0]
|
||||
if selector > 0 and base > 0:
|
||||
sites.append((text.address + match.start(), selector, base))
|
||||
return sites
|
||||
|
||||
|
||||
def ends_with_ret(instructions: Sequence[Any]) -> bool:
|
||||
@@ -948,22 +948,28 @@ def extract_hand_pitch(instructions: Sequence[Any],
|
||||
return None
|
||||
|
||||
|
||||
def match_pointer_getter(instructions: Sequence[Any]) -> bool:
|
||||
"""World-space pointer getter: UserCFrame copy + workspace/camera path."""
|
||||
def match_pointer_getter(instructions: Sequence[Any], selector_offset: int,
|
||||
base_offset: int) -> int | None:
|
||||
"""World-space pointer getter: UserCFrame copy + workspace/camera path.
|
||||
|
||||
Returns the camera HeadScale offset, or None when the contract fails.
|
||||
"""
|
||||
has_selector = False
|
||||
has_scale = False
|
||||
has_stride = False
|
||||
usercframe_loads = 0
|
||||
has_workspace_handoff = False
|
||||
has_camera_slot_call = False
|
||||
has_head_scale = False
|
||||
head_scale_offsets: set[int] = set()
|
||||
frame_registers = (capstone_x86.X86_REG_RBP, capstone_x86.X86_REG_RSP,
|
||||
capstone_x86.X86_REG_RIP)
|
||||
for instruction in instructions:
|
||||
operands = instruction.operands
|
||||
if instruction.mnemonic == "movsxd" and len(operands) == 2:
|
||||
if (operands[1].type == capstone_x86.X86_OP_MEM and
|
||||
operands[1].mem.base == capstone_x86.X86_REG_RSI and
|
||||
operands[1].mem.index == capstone_x86.X86_REG_INVALID and
|
||||
operands[1].mem.disp == USERCFRAME_SELECTOR_OFFSET):
|
||||
operands[1].mem.disp == selector_offset):
|
||||
has_selector = True
|
||||
if instruction.mnemonic == "lea" and len(operands) == 2:
|
||||
memory = operands[1]
|
||||
@@ -987,17 +993,19 @@ def match_pointer_getter(instructions: Sequence[Any]) -> bool:
|
||||
memory.mem.base == capstone_x86.X86_REG_RSI and
|
||||
memory.mem.index == capstone_x86.X86_REG_RAX and
|
||||
memory.mem.disp in (
|
||||
USERCFRAME_BASE_OFFSET,
|
||||
USERCFRAME_BASE_OFFSET + 0x10,
|
||||
USERCFRAME_BASE_OFFSET + 0x20,
|
||||
base_offset,
|
||||
base_offset + 0x10,
|
||||
base_offset + 0x20,
|
||||
)):
|
||||
usercframe_loads += 1
|
||||
if instruction.mnemonic == "movss" and len(operands) == 2:
|
||||
memory = operands[1]
|
||||
# The only float the getter loads from an object is HeadScale.
|
||||
if (memory.type == capstone_x86.X86_OP_MEM and
|
||||
memory.mem.index == capstone_x86.X86_REG_INVALID and
|
||||
memory.mem.disp == CAMERA_HEAD_SCALE_OFFSET):
|
||||
has_head_scale = True
|
||||
memory.mem.base not in frame_registers and
|
||||
memory.mem.disp >= MIN_CAMERA_HEAD_SCALE_OFFSET):
|
||||
head_scale_offsets.add(memory.mem.disp)
|
||||
for index, instruction in enumerate(instructions):
|
||||
operands = instruction.operands
|
||||
if (instruction.mnemonic == "mov" and len(operands) == 2 and
|
||||
@@ -1033,9 +1041,11 @@ def match_pointer_getter(instructions: Sequence[Any]) -> bool:
|
||||
== capstone_x86.X86_REG_RCX and
|
||||
caller.operands[0].mem.disp > 0x100):
|
||||
has_camera_slot_call = True
|
||||
return (has_selector and has_scale and has_stride and
|
||||
if not (has_selector and has_scale and has_stride and
|
||||
usercframe_loads >= 3 and has_workspace_handoff and
|
||||
has_camera_slot_call and has_head_scale)
|
||||
has_camera_slot_call and len(head_scale_offsets) == 1):
|
||||
return None
|
||||
return next(iter(head_scale_offsets))
|
||||
|
||||
|
||||
def extract_pointer_getter_anchors(instructions: Sequence[Any],
|
||||
@@ -1358,34 +1368,35 @@ def find_vr_offsets(elf_path: pathlib.Path) -> dict[str, Any]:
|
||||
f"hand pitch: dword default {hand_pitch_default} degrees with name"
|
||||
f" pointer at storage+8 -> 0x{hand_pitch_storage:x}")
|
||||
|
||||
selector_hits = scan_text_bytes(image, POINTER_GETTER_SELECTOR_PATTERN)
|
||||
movups_hits = scan_text_bytes(image, POINTER_GETTER_MOVUPS_PATTERN)
|
||||
getter_candidates: set[int] = set()
|
||||
for hit in selector_hits + movups_hits:
|
||||
getter_candidates: dict[int, tuple[int, int]] = {}
|
||||
for hit, selector, base in scan_pointer_getter_sites(image):
|
||||
try:
|
||||
start, _end = table.bounds(hit)
|
||||
except AnalyzerError:
|
||||
continue
|
||||
getter_candidates.add(start)
|
||||
getter_matches: list[tuple[int, tuple[Any, ...]]] = []
|
||||
for start in sorted(getter_candidates):
|
||||
getter_candidates[start] = (selector, base)
|
||||
getter_matches: list[tuple[int, tuple[Any, ...], int, int, int]] = []
|
||||
for start, (selector, base) in sorted(getter_candidates.items()):
|
||||
instructions = disasm.function(*table.bounds(start))
|
||||
if match_pointer_getter(instructions):
|
||||
getter_matches.append((start, instructions))
|
||||
head_scale = match_pointer_getter(instructions, selector, base)
|
||||
if head_scale is not None:
|
||||
getter_matches.append(
|
||||
(start, instructions, selector, base, head_scale))
|
||||
if not getter_matches:
|
||||
raise AnalyzerError(
|
||||
"no function satisfies the world-space pointer getter contract")
|
||||
if len(getter_matches) > 1:
|
||||
listed = ", ".join(f"0x{start:x}" for start, _ in getter_matches)
|
||||
listed = ", ".join(f"0x{match[0]:x}" for match in getter_matches)
|
||||
raise AnalyzerError(f"ambiguous pointer getter matches: {listed}")
|
||||
pointer_getter, getter_instructions = getter_matches[0]
|
||||
(pointer_getter, getter_instructions, selector_offset, base_offset,
|
||||
head_scale_offset) = getter_matches[0]
|
||||
workspace_getter, camera_slot = extract_pointer_getter_anchors(
|
||||
getter_instructions, image)
|
||||
evidence.append("pointer getter: UserCFrame selector"
|
||||
f" +0x{USERCFRAME_SELECTOR_OFFSET:x},"
|
||||
f" base +0x{USERCFRAME_BASE_OFFSET:x}, stride"
|
||||
f" +0x{selector_offset:x},"
|
||||
f" base +0x{base_offset:x}, stride"
|
||||
f" 0x{USERCFRAME_STRIDE:x},"
|
||||
f" HeadScale +0x{CAMERA_HEAD_SCALE_OFFSET:x}"
|
||||
f" HeadScale +0x{head_scale_offset:x}"
|
||||
f" -> 0x{pointer_getter:x}")
|
||||
evidence.append(f"workspace getter 0x{workspace_getter:x}"
|
||||
" via mov rdi,rsi + direct call;"
|
||||
@@ -1433,6 +1444,12 @@ def find_vr_offsets(elf_path: pathlib.Path) -> dict[str, Any]:
|
||||
f"0x{workspace_getter:x}",
|
||||
"workspace_camera_vtable_offset":
|
||||
f"0x{camera_slot:x}",
|
||||
"usercframe_selector_offset":
|
||||
f"0x{selector_offset:x}",
|
||||
"usercframe_base_offset":
|
||||
f"0x{base_offset:x}",
|
||||
"camera_head_scale_offset":
|
||||
f"0x{head_scale_offset:x}",
|
||||
"new_vr_system_hand_pitch_storage_rva":
|
||||
f"0x{hand_pitch_storage:x}",
|
||||
"new_vr_system_hand_pitch_default_degrees":
|
||||
|
||||
Reference in new issue
Block a user