Module hlrun.patch

A Python-based system for patching and hooking of bytecode, similar to DCCM.

Classes

class Patch (name: str | None = None, author: str | None = None, sha256: str | None = None)
Expand source code
class Patch:
    """
    Main patching class that manages bytecode hooks and patches.
    """

    def __init__(
        self,
        name: Optional[str] = None,
        author: Optional[str] = None,
        sha256: Optional[str] = None,
    ):
        """
        Initialize a new patch.

        Note: sha256 is hash of input file
        """
        self.name = name
        self.author = author
        self.sha256 = sha256
        self.interceptions: Dict[str | int, Callable[[Args], Args]] = {}
        self.patches: Dict[str | int, Callable[[Bytecode, Function], None]] = {}
        self.needs_pyhl = False
        self.custom_fns: Dict[str, fIndex] = {}

    def intercept(self, fn: str | int) -> Callable[[Callable[[Args], Args]], Callable[[Args], Args]]:
        """
        Decorator to intercept and modify a function's arguments at call-time.
        """

        self.needs_pyhl = True

        def decorator(
            func: Callable[[Args], Args],
        ) -> Callable[[Args], Args]:
            self.interceptions[fn] = func
            return func

        return decorator

    def patch(
        self, fn: str | int
    ) -> Callable[[Callable[[Bytecode, Function], None]], Callable[[Bytecode, Function], None]]:
        """
        Decorator to patch a function's opcodes directly.
        """

        def decorator(
            func: Callable[[Bytecode, Function], None],
        ) -> Callable[[Bytecode, Function], None]:
            self.patches[fn] = func
            return func

        return decorator

    def _intercept(self, code: Bytecode, fn: Function, identifier: str | int) -> None:
        """
        Apply an interception.
        """
        arg_regs = fn.resolve_fun(code).args
        arg_virt = Virtual()
        arg_virt.fields.extend([Field(code.add_string(f"arg_{i}"), typ) for i, typ in enumerate(arg_regs)])
        arg_typ = Type()
        arg_typ.kind.value = Type.Kind.VIRTUAL.value
        arg_typ.definition = arg_virt
        arg_tid = code.add_type(arg_typ)

        types_str = ",".join([str(arg.resolve(code).kind.value) for arg in arg_regs])

        # fn.regs.append(code.find_prim_type(Type.Kind.VOID))
        # void_reg = Reg(len(fn.regs) - 1)
        bytes_type = code.find_prim_type(Type.Kind.BYTES)
        fn.regs.append(bytes_type)
        fn_name_reg = Reg(len(fn.regs) - 1)
        fn.regs.append(code.find_prim_type(Type.Kind.I32))
        nargs_reg = Reg(len(fn.regs) - 1)
        fn.regs.append(arg_tid)
        virt_reg = Reg(len(fn.regs) - 1)
        fn.regs.append(code.find_prim_type(Type.Kind.BOOL))
        ret_reg = Reg(len(fn.regs) - 1)
        fn.regs.append(code.find_prim_type(Type.Kind.BYTES))
        types_reg = Reg(len(fn.regs) - 1)

        # since we insert at the start, we place in the ops backwards.
        # therefore, we start by reading back from the virt and we end setting up the virt

        for i in reversed(range(len(arg_regs))):
            op = Opcode()
            op.op = "Field"
            op.df = {"dst": Reg(i), "obj": virt_reg, "field": fieldRef(i)}
            fn.push_op(code, op)

        op = Opcode()
        op.op = "Call4"
        op.df = {
            "dst": ret_reg,
            "fun": self.custom_fns["intercept"],
            "arg0": virt_reg,
            "arg1": nargs_reg,
            "arg2": fn_name_reg,
            "arg3": types_reg,
        }
        fn.push_op(code, op)

        op = Opcode()
        op.op = "String"
        op.df = {"dst": fn_name_reg, "ptr": code.add_string(str(identifier))}
        fn.push_op(code, op)

        op = Opcode()
        op.op = "String"
        op.df = {"dst": types_reg, "ptr": code.add_string(types_str)}
        fn.push_op(code, op)

        op = Opcode()
        op.op = "Int"
        op.df = {"dst": nargs_reg, "ptr": code.add_i32(len(arg_regs))}
        fn.push_op(code, op)

        for i in reversed(range(len(arg_regs))):
            op = Opcode()
            op.op = "SetField"
            op.df = {"obj": virt_reg, "field": fieldRef(i), "src": Reg(i)}
            fn.push_op(code, op)

        op = Opcode()
        op.op = "New"
        op.df = {"dst": virt_reg}
        fn.push_op(code, op)

    def _apply_pyhl(self, code: Bytecode) -> None:
        print("Installing pyhl native...")
        pyhl_funcs: Dict[str, Optional[tIndex]] = {
            "init": None,
            "deinit": None,
            "call": None,
            "intercept": None,
        }
        indices: Dict[str, Optional[fIndex]] = {
            "init": None,
            "deinit": None,
            "call": None,
            "intercept": None,
        }
        for func in pyhl_funcs.keys():
            print(f"Generating types for pyhl.{func}")
            voi = code.find_prim_type(Type.Kind.VOID)
            match func:
                case "init" | "deinit":
                    typ = Type()
                    typ.kind.value = Type.Kind.FUN.value
                    fun = Fun()
                    fun.args = []
                    fun.ret = voi
                    typ.definition = fun
                    pyhl_funcs[func] = code.add_type(typ)
                case "call":
                    typ = Type()
                    typ.kind.value = Type.Kind.FUN.value
                    fun = Fun()
                    byt = code.find_prim_type(Type.Kind.BYTES)
                    fun.args = [byt, byt]
                    fun.ret = code.find_prim_type(Type.Kind.BOOL)
                    typ.definition = fun
                    pyhl_funcs[func] = code.add_type(typ)
                case "intercept":
                    typ = Type()
                    typ.kind.value = Type.Kind.FUN.value
                    fun = Fun()
                    fun.args = [
                        code.find_prim_type(Type.Kind.DYN),
                        code.find_prim_type(Type.Kind.I32),
                        code.find_prim_type(Type.Kind.BYTES),
                        code.find_prim_type(Type.Kind.BYTES),
                    ]
                    fun.ret = code.find_prim_type(Type.Kind.BOOL)
                    typ.definition = fun
                    pyhl_funcs[func] = code.add_type(typ)
                case _:
                    raise NameError("No such pyhl function typedefs: " + func)

        for func, tid in pyhl_funcs.items():
            print(f"Injecting pyhl.{func}")
            native = Native()
            native.lib = code.add_string("pyhl")
            native.name = code.add_string(func)
            assert tid is not None, "Something goofed!"
            native.type = tid
            native.findex = code.next_free_findex()
            indices[func] = native.findex
            code.natives.append(native)

        assert all(tid is not None for tid in indices.values()), "Some indices are None!"
        for k, v in indices.items():
            self.custom_fns[k] = v  # type: ignore

    def apply(self, code: Bytecode) -> None:
        """
        Apply all registered hooks and patches.
        """
        assert code.is_ok()
        print(f"----- Applying patch:{' ' + self.name if self.name else ''} -----")

        if self.needs_pyhl:
            self._apply_pyhl(code)

            print("Applying entrypoint patches")
            entry = code.entrypoint.resolve(code)
            assert isinstance(entry, Function), "Entry can't be a native!"

            entry.regs.append(code.find_prim_type(Type.Kind.VOID))
            void_reg = Reg(len(entry.regs) - 1)
            op = Opcode()
            op.op = "Call0"
            assert self.custom_fns["init"] is not None, "Invalid fIndex!"
            op.df = {"dst": void_reg, "fun": self.custom_fns["init"]}
            entry.insert_op(code, 0, op)

        for identifier, interceptor in self.interceptions.items():
            if isinstance(identifier, int):
                fn = fIndex(identifier).resolve(code)
            else:
                mtch: Optional[Function] = None
                for fn in code.functions:
                    if code.full_func_name(fn) == identifier:
                        mtch = fn
                if not mtch:
                    raise NameError(f"No such function '{identifier}'")
                fn = mtch
            assert not isinstance(fn, Native), "Cannot intercept a native! (Yet...)"  # TODO: native intercept
            print(f"(Intercept) {func_header(code, fn)}")  # TODO: other handlers than pyhl
            self._intercept(code, fn, identifier)

        for identifier, patch in self.patches.items():
            if isinstance(identifier, int):
                fn = fIndex(identifier).resolve(code)
            else:
                mtch = None
                for fn in code.functions:
                    if code.full_func_name(fn) == identifier:
                        mtch = fn
                if not mtch:
                    raise NameError(f"No such function '{identifier}'")
                fn = mtch
            assert not isinstance(fn, Native), "Cannot patch a native!"
            print(f"(Patch) {func_header(code, fn)}")
            patch(code, fn)

        code.set_meta()  # just to be safe
        assert code.is_ok()

    def do_intercept(self, args: Args, identifier: str | int) -> Args:
        """
        Called at runtime by pyhl to intercept a call. Do not call manually!
        """
        return self.interceptions[identifier](args)

Main patching class that manages bytecode hooks and patches.

Initialize a new patch.

Note: sha256 is hash of input file

Methods

def apply(self, code: crashlink.core.Bytecode) ‑> None
Expand source code
def apply(self, code: Bytecode) -> None:
    """
    Apply all registered hooks and patches.
    """
    assert code.is_ok()
    print(f"----- Applying patch:{' ' + self.name if self.name else ''} -----")

    if self.needs_pyhl:
        self._apply_pyhl(code)

        print("Applying entrypoint patches")
        entry = code.entrypoint.resolve(code)
        assert isinstance(entry, Function), "Entry can't be a native!"

        entry.regs.append(code.find_prim_type(Type.Kind.VOID))
        void_reg = Reg(len(entry.regs) - 1)
        op = Opcode()
        op.op = "Call0"
        assert self.custom_fns["init"] is not None, "Invalid fIndex!"
        op.df = {"dst": void_reg, "fun": self.custom_fns["init"]}
        entry.insert_op(code, 0, op)

    for identifier, interceptor in self.interceptions.items():
        if isinstance(identifier, int):
            fn = fIndex(identifier).resolve(code)
        else:
            mtch: Optional[Function] = None
            for fn in code.functions:
                if code.full_func_name(fn) == identifier:
                    mtch = fn
            if not mtch:
                raise NameError(f"No such function '{identifier}'")
            fn = mtch
        assert not isinstance(fn, Native), "Cannot intercept a native! (Yet...)"  # TODO: native intercept
        print(f"(Intercept) {func_header(code, fn)}")  # TODO: other handlers than pyhl
        self._intercept(code, fn, identifier)

    for identifier, patch in self.patches.items():
        if isinstance(identifier, int):
            fn = fIndex(identifier).resolve(code)
        else:
            mtch = None
            for fn in code.functions:
                if code.full_func_name(fn) == identifier:
                    mtch = fn
            if not mtch:
                raise NameError(f"No such function '{identifier}'")
            fn = mtch
        assert not isinstance(fn, Native), "Cannot patch a native!"
        print(f"(Patch) {func_header(code, fn)}")
        patch(code, fn)

    code.set_meta()  # just to be safe
    assert code.is_ok()

Apply all registered hooks and patches.

def do_intercept(self,
args: Args,
identifier: str | int) ‑> Args
Expand source code
def do_intercept(self, args: Args, identifier: str | int) -> Args:
    """
    Called at runtime by pyhl to intercept a call. Do not call manually!
    """
    return self.interceptions[identifier](args)

Called at runtime by pyhl to intercept a call. Do not call manually!

def intercept(self, fn: str | int) ‑> Callable[[Callable[[Args], Args]], Callable[[Args], Args]]
Expand source code
def intercept(self, fn: str | int) -> Callable[[Callable[[Args], Args]], Callable[[Args], Args]]:
    """
    Decorator to intercept and modify a function's arguments at call-time.
    """

    self.needs_pyhl = True

    def decorator(
        func: Callable[[Args], Args],
    ) -> Callable[[Args], Args]:
        self.interceptions[fn] = func
        return func

    return decorator

Decorator to intercept and modify a function's arguments at call-time.

def patch(self, fn: str | int) ‑> Callable[[Callable[[crashlink.core.Bytecode, crashlink.core.Function], None]], Callable[[crashlink.core.Bytecode, crashlink.core.Function], None]]
Expand source code
def patch(
    self, fn: str | int
) -> Callable[[Callable[[Bytecode, Function], None]], Callable[[Bytecode, Function], None]]:
    """
    Decorator to patch a function's opcodes directly.
    """

    def decorator(
        func: Callable[[Bytecode, Function], None],
    ) -> Callable[[Bytecode, Function], None]:
        self.patches[fn] = func
        return func

    return decorator

Decorator to patch a function's opcodes directly.