"""kernel_jb_base.py — JB base class with infrastructure methods.""" import struct from collections import Counter from .kernel_asm import _PACIBSP_U32 from capstone.arm64_const import ( ARM64_OP_REG, ARM64_OP_IMM, ARM64_OP_MEM, ARM64_REG_X0, ARM64_REG_X1, ARM64_REG_W0, ARM64_REG_X8, ) from .kernel import ( KernelPatcher, NOP, MOV_X0_0, MOV_X0_1, MOV_W0_0, MOV_W0_1, CMP_W0_W0, CMP_X0_X0, RET, asm, _rd32, _rd64, ) CBZ_X2_8 = asm("cbz x2, #8") STR_X0_X2 = asm("str x0, [x2]") CMP_XZR_XZR = asm("cmp xzr, xzr") MOV_X8_XZR = asm("mov x8, xzr") class KernelJBPatcherBase(KernelPatcher): def __init__(self, data, verbose=False): super().__init__(data, verbose) self._jb_scan_cache = {} self._proc_info_anchor_scanned = False self._proc_info_anchor = (-1, -1) self._build_symbol_table() # ── Symbol table (best-effort, may find 0 on stripped kernels) ── def _build_symbol_table(self): """Parse nlist entries from LC_SYMTAB to build symbol→foff map.""" self.symbols = {} # Parse top-level LC_SYMTAB ncmds = struct.unpack_from(" self.size: break cmd, cmdsize = struct.unpack_from(" self.size: break cmd, cmdsize = struct.unpack_from(" self.size: return magic = _rd32(self.raw, mh_off) if magic != 0xFEEDFACF: return ncmds = struct.unpack_from(" self.size: break cmd, cmdsize = struct.unpack_from(" self.size: break n_strx, n_type, n_sect, n_desc, n_value = struct.unpack_from( "= self.size: continue name_end = self.raw.find(b"\x00", name_off) if name_end < 0 or name_end - name_off > 512: continue name = self.raw[name_off:name_end].decode("ascii", errors="replace") foff = n_value - self.base_va if 0 <= foff < self.size: self.symbols[name] = foff def _resolve_symbol(self, name): """Look up a function symbol, return file offset or -1.""" return self.symbols.get(name, -1) # ── Shared kernel anchor finders ────────────────────────────── def _find_proc_info_anchor(self): """Find `_proc_info` switch anchor as (func_start, switch_off). Shared by B6/B7 patches. Cached because searching this anchor in `kern_text` is expensive on stripped kernels. """ if self._proc_info_anchor_scanned: return self._proc_info_anchor def _scan_range(start, end): """Fast raw matcher for: sub wN, wM, #1 cmp wN, #0x21 """ key = ("proc_info_switch", start, end) cached = self._jb_scan_cache.get(key) if cached is not None: return cached scan_start = max(start, 0) limit = min(end - 8, self.size - 8) for off in range(scan_start, limit, 4): i0 = _rd32(self.raw, off) # SUB (immediate), 32-bit if (i0 & 0xFF000000) != 0x51000000: continue if ((i0 >> 22) & 1) != 0: # sh must be 0 continue if ((i0 >> 10) & 0xFFF) != 1: continue sub_rd = i0 & 0x1F i1 = _rd32(self.raw, off + 4) # CMP wN,#imm == SUBS wzr,wN,#imm alias (rd must be wzr) if (i1 & 0xFF00001F) != 0x7100001F: continue if ((i1 >> 22) & 1) != 0: # sh must be 0 continue if ((i1 >> 10) & 0xFFF) != 0x21: continue cmp_rn = (i1 >> 5) & 0x1F if sub_rd != cmp_rn: continue self._jb_scan_cache[key] = off return off self._jb_scan_cache[key] = -1 return -1 # Prefer direct symbol when present. proc_info_func = self._resolve_symbol("_proc_info") if proc_info_func >= 0: search_end = min(proc_info_func + 0x800, self.size) switch_off = _scan_range(proc_info_func, search_end) if switch_off < 0: switch_off = proc_info_func self._proc_info_anchor = (proc_info_func, switch_off) self._proc_info_anchor_scanned = True return self._proc_info_anchor ks, ke = self.kern_text switch_off = _scan_range(ks, ke) if switch_off >= 0: proc_info_func = self.find_function_start(switch_off) self._proc_info_anchor = (proc_info_func, switch_off) else: self._proc_info_anchor = (-1, -1) self._proc_info_anchor_scanned = True return self._proc_info_anchor # ── Code cave finder ────────────────────────────────────────── def _find_code_cave(self, size, align=4): """Find a region of zeros/0xFF/UDF in executable memory for shellcode. Returns file offset of the cave start, or -1 if not found. Reads from self.data (mutable) so previously allocated caves are skipped. Only searches __TEXT_EXEC and __TEXT_BOOT_EXEC segments. __PRELINK_TEXT is excluded because KTRR makes it non-executable at runtime on ARM64e, even though the Mach-O marks it R-X. """ EXEC_SEGS = ("__TEXT_EXEC", "__TEXT_BOOT_EXEC") exec_ranges = [ (foff, foff + fsz) for name, _, foff, fsz, _ in self.all_segments if name in EXEC_SEGS and fsz > 0 ] exec_ranges.sort() needed = (size + align - 1) // align * align for rng_start, rng_end in exec_ranges: run_start = -1 run_len = 0 for off in range(rng_start, rng_end, 4): val = _rd32(self.data, off) if val == 0x00000000 or val == 0xFFFFFFFF or val == 0xD4200000: if run_start < 0: run_start = off run_len = 4 else: run_len += 4 if run_len >= needed: return run_start else: run_start = -1 run_len = 0 return -1 # ── Branch encoding helpers ─────────────────────────────────── def _encode_b(self, from_off, to_off): """Encode an unconditional B instruction.""" delta = (to_off - from_off) // 4 if delta < -(1 << 25) or delta >= (1 << 25): return None return struct.pack("= (1 << 25): return None return struct.pack("