fridaforge 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fridaforge/__init__.py +5 -0
- fridaforge/_version.py +1 -0
- fridaforge/backend.py +74 -0
- fridaforge/commands/__init__.py +24 -0
- fridaforge/commands/base.py +31 -0
- fridaforge/commands/hook_dlopen_functions.py +22 -0
- fridaforge/commands/hook_function.py +26 -0
- fridaforge/commands/modify_section_protection.py +39 -0
- fridaforge/commands/negate_cond_branch_arm64.py +42 -0
- fridaforge/commands/negate_cond_branch_x86.py +44 -0
- fridaforge/core/__init__.py +63 -0
- fridaforge/core/hook_dlopen_functions.py +14 -0
- fridaforge/core/hook_function.py +21 -0
- fridaforge/core/modify_section_protection.py +15 -0
- fridaforge/core/negate_cond_branch_arm64.py +15 -0
- fridaforge/core/negate_cond_branch_x86.py +15 -0
- fridaforge/core/utils.py +33 -0
- fridaforge/templates/hook_dlopen_functions.js +32 -0
- fridaforge/templates/hook_function.js +27 -0
- fridaforge/templates/modify_section_protection.js +36 -0
- fridaforge/templates/negate_cond_branch_arm64.js +30 -0
- fridaforge/templates/negate_cond_branch_x86.js +35 -0
- fridaforge-0.3.0.dist-info/METADATA +166 -0
- fridaforge-0.3.0.dist-info/RECORD +27 -0
- fridaforge-0.3.0.dist-info/WHEEL +5 -0
- fridaforge-0.3.0.dist-info/licenses/LICENSE +21 -0
- fridaforge-0.3.0.dist-info/top_level.txt +1 -0
fridaforge/__init__.py
ADDED
fridaforge/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
fridaforge/backend.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Host-agnostic backend interface for FridaForge commands."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ContextKind(Enum):
|
|
9
|
+
"""The host context a command needs to run."""
|
|
10
|
+
|
|
11
|
+
GLOBAL = auto() # only the module is needed
|
|
12
|
+
ADDRESS = auto() # the current address is needed
|
|
13
|
+
FUNCTION = auto() # the current function is needed
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class FunctionRef:
|
|
18
|
+
"""A resolved reference to a function in the loaded binary."""
|
|
19
|
+
|
|
20
|
+
start: int
|
|
21
|
+
name: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Backend(ABC):
|
|
25
|
+
"""The port each host (Binary Ninja, IDA) must implement."""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def get_module_name(self) -> str:
|
|
29
|
+
"""Return the loaded module's base file name."""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def get_image_base(self) -> int:
|
|
33
|
+
"""Return the image base address of the loaded binary."""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def get_current_address(self) -> int | None:
|
|
37
|
+
"""Return the currently selected address, or ``None``."""
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def get_current_function(self) -> FunctionRef | None:
|
|
41
|
+
"""Return the currently selected function, or ``None``."""
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def get_section_name_at(self, addr: int) -> str | None:
|
|
45
|
+
"""Return the name of the section/segment containing *addr*."""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def get_instruction_mnemonic(self, addr: int) -> str | None:
|
|
49
|
+
"""Return the mnemonic of the instruction at *addr*, or ``None``."""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def copy_to_clipboard(self, text: str) -> bool:
|
|
53
|
+
"""Copy *text* to the system clipboard. Return success."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def log_info(self, msg: str) -> None:
|
|
57
|
+
"""Emit an informational message in the host's idiom."""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def log_warn(self, msg: str) -> None:
|
|
61
|
+
"""Emit a warning message in the host's idiom."""
|
|
62
|
+
|
|
63
|
+
# --- concrete helpers shared by all hosts ---
|
|
64
|
+
|
|
65
|
+
def get_relative_address(self, addr: int) -> str:
|
|
66
|
+
"""Return *addr*'s offset from the image base as a hex string."""
|
|
67
|
+
return hex(addr - self.get_image_base())
|
|
68
|
+
|
|
69
|
+
def _resolve_function_name(self, start: int, raw_name: str) -> str:
|
|
70
|
+
"""Return *raw_name*, or a ``sub_<offset>`` fallback when empty."""
|
|
71
|
+
if raw_name:
|
|
72
|
+
return raw_name
|
|
73
|
+
relative_address = self.get_relative_address(start)
|
|
74
|
+
return f"sub_{relative_address.removeprefix('0x')}"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""FridaForge command registry."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.commands.base import Command
|
|
4
|
+
from fridaforge.commands.hook_dlopen_functions import DlopenHooksCommand
|
|
5
|
+
from fridaforge.commands.hook_function import FunctionHookCommand
|
|
6
|
+
from fridaforge.commands.modify_section_protection import (
|
|
7
|
+
ModifySectionProtectionCommand,
|
|
8
|
+
)
|
|
9
|
+
from fridaforge.commands.negate_cond_branch_arm64 import (
|
|
10
|
+
NegateArm64CondBranchCommand,
|
|
11
|
+
)
|
|
12
|
+
from fridaforge.commands.negate_cond_branch_x86 import (
|
|
13
|
+
NegateX86CondBranchCommand,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
ALL_COMMANDS: list[Command] = [
|
|
17
|
+
FunctionHookCommand(),
|
|
18
|
+
DlopenHooksCommand(),
|
|
19
|
+
ModifySectionProtectionCommand(),
|
|
20
|
+
NegateArm64CondBranchCommand(),
|
|
21
|
+
NegateX86CondBranchCommand(),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
__all__ = ["Command", "ALL_COMMANDS"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Base class for host-agnostic FridaForge commands."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from fridaforge.backend import Backend, ContextKind
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Command(ABC):
|
|
9
|
+
"""A single user-facing feature, independent of the host.
|
|
10
|
+
|
|
11
|
+
Subclasses set the metadata class attributes and implement
|
|
12
|
+
:meth:`run`, which pulls data from the *backend* and copies the
|
|
13
|
+
generated snippet to the clipboard.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
id: str
|
|
17
|
+
description: str
|
|
18
|
+
category: str
|
|
19
|
+
hotkey: str | None = None
|
|
20
|
+
context: ContextKind
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def run(self, backend: Backend) -> None:
|
|
24
|
+
"""Execute the command against the given backend."""
|
|
25
|
+
|
|
26
|
+
def _deliver(self, backend: Backend, snippet: str) -> None:
|
|
27
|
+
"""Copy the snippet to the clipboard and report the outcome."""
|
|
28
|
+
if backend.copy_to_clipboard(snippet):
|
|
29
|
+
backend.log_info("Frida snippet copied to clipboard.")
|
|
30
|
+
else:
|
|
31
|
+
backend.log_warn("Failed to copy snippet to clipboard.")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Command: generate Frida dlopen-family hooks."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.backend import Backend, ContextKind
|
|
4
|
+
from fridaforge.commands.base import Command
|
|
5
|
+
from fridaforge.core import DlopenHookGenerator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DlopenHooksCommand(Command):
|
|
9
|
+
id = "hook_dlopen_functions"
|
|
10
|
+
description = "Generate dlopen hooks (Basic for Android native libs)"
|
|
11
|
+
category = "Hooks"
|
|
12
|
+
hotkey = "Shift+D"
|
|
13
|
+
context = ContextKind.GLOBAL
|
|
14
|
+
|
|
15
|
+
def run(self, backend: Backend) -> None:
|
|
16
|
+
module_name = backend.get_module_name()
|
|
17
|
+
if not module_name:
|
|
18
|
+
backend.log_warn("Could not determine the module name.")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
snippet = DlopenHookGenerator().generate(module_name)
|
|
22
|
+
self._deliver(backend, snippet)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Command: generate a Frida function hook."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.backend import Backend, ContextKind
|
|
4
|
+
from fridaforge.commands.base import Command
|
|
5
|
+
from fridaforge.core import FunctionHookGenerator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FunctionHookCommand(Command):
|
|
9
|
+
id = "hook_function"
|
|
10
|
+
description = "Generate function hook"
|
|
11
|
+
category = "Hooks"
|
|
12
|
+
hotkey = "Shift+F"
|
|
13
|
+
context = ContextKind.FUNCTION
|
|
14
|
+
|
|
15
|
+
def run(self, backend: Backend) -> None:
|
|
16
|
+
func = backend.get_current_function()
|
|
17
|
+
if func is None:
|
|
18
|
+
backend.log_warn("No function at the current location.")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
module_name = backend.get_module_name()
|
|
22
|
+
relative_address = backend.get_relative_address(func.start)
|
|
23
|
+
snippet = FunctionHookGenerator().generate(
|
|
24
|
+
module_name, relative_address, func.name
|
|
25
|
+
)
|
|
26
|
+
self._deliver(backend, snippet)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Command: generate a Frida section protection modification."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.backend import Backend, ContextKind
|
|
4
|
+
from fridaforge.commands.base import Command
|
|
5
|
+
from fridaforge.core import ModifySectionProtectionGenerator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModifySectionProtectionCommand(Command):
|
|
9
|
+
id = "modify_section_protection"
|
|
10
|
+
description = "Modify current section permissions"
|
|
11
|
+
category = "Memory"
|
|
12
|
+
hotkey = "Shift+M"
|
|
13
|
+
context = ContextKind.ADDRESS
|
|
14
|
+
|
|
15
|
+
def run(self, backend: Backend) -> None:
|
|
16
|
+
addr = backend.get_current_address()
|
|
17
|
+
if addr is None:
|
|
18
|
+
backend.log_warn("Could not determine the current address.")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
section_name = backend.get_section_name_at(addr)
|
|
22
|
+
if not section_name:
|
|
23
|
+
backend.log_warn(
|
|
24
|
+
f"Could not determine the containing section for {hex(addr)}."
|
|
25
|
+
)
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if not section_name.startswith("."):
|
|
29
|
+
backend.log_warn(
|
|
30
|
+
f"Address is not in a standard section. "
|
|
31
|
+
f"(Section name: {section_name})"
|
|
32
|
+
)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
module_name = backend.get_module_name()
|
|
36
|
+
snippet = ModifySectionProtectionGenerator().generate(
|
|
37
|
+
module_name, section_name
|
|
38
|
+
)
|
|
39
|
+
self._deliver(backend, snippet)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Command: negate an ARM64 conditional branch."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.backend import Backend, ContextKind
|
|
4
|
+
from fridaforge.commands.base import Command
|
|
5
|
+
from fridaforge.core import NegateArm64CondBranchGenerator
|
|
6
|
+
|
|
7
|
+
ARM64_COND_BRANCHES = {
|
|
8
|
+
"b.eq", "b.ne", "b.hs", "b.lo", "b.cs",
|
|
9
|
+
"b.mi", "b.pl", "b.vs", "b.vc",
|
|
10
|
+
"b.hi", "b.ls", "b.ge", "b.lt",
|
|
11
|
+
"b.gt", "b.le", "tbz", "tbnz",
|
|
12
|
+
"cbz", "cbnz",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NegateArm64CondBranchCommand(Command):
|
|
17
|
+
id = "negate_cond_branch_arm64"
|
|
18
|
+
description = "Negate cond branch instruction (ARM64)"
|
|
19
|
+
category = "Patching"
|
|
20
|
+
hotkey = "Shift+A"
|
|
21
|
+
context = ContextKind.ADDRESS
|
|
22
|
+
|
|
23
|
+
def run(self, backend: Backend) -> None:
|
|
24
|
+
addr = backend.get_current_address()
|
|
25
|
+
if addr is None:
|
|
26
|
+
backend.log_warn("Could not determine the current address.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
mnemonic = (backend.get_instruction_mnemonic(addr) or "").lower()
|
|
30
|
+
if mnemonic not in ARM64_COND_BRANCHES:
|
|
31
|
+
backend.log_warn(
|
|
32
|
+
f"Instruction at {hex(addr)} ('{mnemonic}') "
|
|
33
|
+
f"is not a recognized ARM64 conditional branch."
|
|
34
|
+
)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
module_name = backend.get_module_name()
|
|
38
|
+
relative_address = backend.get_relative_address(addr)
|
|
39
|
+
snippet = NegateArm64CondBranchGenerator().generate(
|
|
40
|
+
module_name, relative_address
|
|
41
|
+
)
|
|
42
|
+
self._deliver(backend, snippet)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Command: negate an x86 conditional branch."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.backend import Backend, ContextKind
|
|
4
|
+
from fridaforge.commands.base import Command
|
|
5
|
+
from fridaforge.core import NegateX86CondBranchGenerator
|
|
6
|
+
|
|
7
|
+
X86_COND_BRANCHES = {
|
|
8
|
+
"je", "jne", "jz", "jnz",
|
|
9
|
+
"ja", "jnbe", "jae", "jnb", "jnc",
|
|
10
|
+
"jb", "jnae", "jc", "jbe", "jna",
|
|
11
|
+
"jg", "jnle", "jge", "jnl",
|
|
12
|
+
"jl", "jnge", "jle", "jng",
|
|
13
|
+
"jo", "jno", "js", "jns",
|
|
14
|
+
"jp", "jpe", "jnp", "jpo",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NegateX86CondBranchCommand(Command):
|
|
19
|
+
id = "negate_cond_branch_x86"
|
|
20
|
+
description = "Negate cond branch instruction (x86)"
|
|
21
|
+
category = "Patching"
|
|
22
|
+
hotkey = "Shift+X"
|
|
23
|
+
context = ContextKind.ADDRESS
|
|
24
|
+
|
|
25
|
+
def run(self, backend: Backend) -> None:
|
|
26
|
+
addr = backend.get_current_address()
|
|
27
|
+
if addr is None:
|
|
28
|
+
backend.log_warn("Could not determine the current address.")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
mnemonic = (backend.get_instruction_mnemonic(addr) or "").lower()
|
|
32
|
+
if mnemonic not in X86_COND_BRANCHES:
|
|
33
|
+
backend.log_warn(
|
|
34
|
+
f"Instruction at {hex(addr)} ('{mnemonic}') "
|
|
35
|
+
f"is not a recognized x86 conditional branch."
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
module_name = backend.get_module_name()
|
|
40
|
+
relative_address = backend.get_relative_address(addr)
|
|
41
|
+
snippet = NegateX86CondBranchGenerator().generate(
|
|
42
|
+
module_name, relative_address
|
|
43
|
+
)
|
|
44
|
+
self._deliver(backend, snippet)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Base class for Frida snippet generators.
|
|
2
|
+
|
|
3
|
+
Every generator **must** subclass :class:`SnippetGenerator` and:
|
|
4
|
+
|
|
5
|
+
1. Set the ``template_filename`` class attribute to the name of a
|
|
6
|
+
JavaScript template file located in the ``templates/`` directory.
|
|
7
|
+
2. Implement a ``generate()`` method with explicit, typed parameters
|
|
8
|
+
that builds a placeholder mapping and delegates to :meth:`_render`.
|
|
9
|
+
|
|
10
|
+
Both constraints are validated at class-creation time; forgetting
|
|
11
|
+
either one raises :class:`TypeError` immediately.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
|
|
16
|
+
from fridaforge.core.utils import fill_template, read_template
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SnippetGenerator:
|
|
20
|
+
"""Abstract base for all Frida snippet generators."""
|
|
21
|
+
|
|
22
|
+
template_filename: str
|
|
23
|
+
|
|
24
|
+
def __init_subclass__(cls, **kwargs: object) -> None:
|
|
25
|
+
super().__init_subclass__(**kwargs)
|
|
26
|
+
|
|
27
|
+
if not getattr(cls, "template_filename", None):
|
|
28
|
+
raise TypeError(
|
|
29
|
+
f"{cls.__name__} must define 'template_filename' "
|
|
30
|
+
f"as a non-empty class attribute."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if "generate" not in cls.__dict__:
|
|
34
|
+
raise TypeError(
|
|
35
|
+
f"{cls.__name__} must implement a 'generate()' method."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def _render(self, placeholders: Mapping[str, str | int]) -> str:
|
|
39
|
+
"""Read the template, substitute placeholders, and return the result."""
|
|
40
|
+
template = read_template(self.template_filename)
|
|
41
|
+
return fill_template(template, placeholders)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
from fridaforge.core.hook_dlopen_functions import DlopenHookGenerator
|
|
45
|
+
from fridaforge.core.hook_function import FunctionHookGenerator
|
|
46
|
+
from fridaforge.core.modify_section_protection import (
|
|
47
|
+
ModifySectionProtectionGenerator,
|
|
48
|
+
)
|
|
49
|
+
from fridaforge.core.negate_cond_branch_arm64 import (
|
|
50
|
+
NegateArm64CondBranchGenerator,
|
|
51
|
+
)
|
|
52
|
+
from fridaforge.core.negate_cond_branch_x86 import (
|
|
53
|
+
NegateX86CondBranchGenerator,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"SnippetGenerator",
|
|
58
|
+
"DlopenHookGenerator",
|
|
59
|
+
"FunctionHookGenerator",
|
|
60
|
+
"ModifySectionProtectionGenerator",
|
|
61
|
+
"NegateArm64CondBranchGenerator",
|
|
62
|
+
"NegateX86CondBranchGenerator",
|
|
63
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Generator for Frida dlopen hook snippets."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.core import SnippetGenerator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DlopenHookGenerator(SnippetGenerator):
|
|
7
|
+
"""Generate a Frida script that hooks dlopen-family functions."""
|
|
8
|
+
|
|
9
|
+
template_filename = "hook_dlopen_functions.js"
|
|
10
|
+
|
|
11
|
+
def generate(self, module_name: str) -> str:
|
|
12
|
+
return self._render({
|
|
13
|
+
"MODULE_NAME_PLACEHOLDER": module_name,
|
|
14
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Generator for Frida function hook snippets."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.core import SnippetGenerator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FunctionHookGenerator(SnippetGenerator):
|
|
7
|
+
"""Generate a Frida ``Interceptor.attach`` script for a function."""
|
|
8
|
+
|
|
9
|
+
template_filename = "hook_function.js"
|
|
10
|
+
|
|
11
|
+
def generate(
|
|
12
|
+
self,
|
|
13
|
+
module_name: str,
|
|
14
|
+
function_relative_address: str,
|
|
15
|
+
function_name: str,
|
|
16
|
+
) -> str:
|
|
17
|
+
return self._render({
|
|
18
|
+
"MODULE_NAME_PLACEHOLDER": module_name,
|
|
19
|
+
"FUNCTION_RELATIVE_ADDRESS_PLACEHOLDER": function_relative_address,
|
|
20
|
+
"FUNCTION_NAME_PLACEHOLDER": function_name,
|
|
21
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Generator for Frida section protection modification snippets."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.core import SnippetGenerator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModifySectionProtectionGenerator(SnippetGenerator):
|
|
7
|
+
"""Generate a Frida script that sets a section's protection to rwx."""
|
|
8
|
+
|
|
9
|
+
template_filename = "modify_section_protection.js"
|
|
10
|
+
|
|
11
|
+
def generate(self, module_name: str, section_name: str) -> str:
|
|
12
|
+
return self._render({
|
|
13
|
+
"MODULE_NAME_PLACEHOLDER": module_name,
|
|
14
|
+
"SECTION_NAME_PLACEHOLDER": section_name,
|
|
15
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Generator for Frida ARM64 conditional branch negation snippets."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.core import SnippetGenerator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NegateArm64CondBranchGenerator(SnippetGenerator):
|
|
7
|
+
"""Generate a Frida script that negates an ARM64 conditional branch."""
|
|
8
|
+
|
|
9
|
+
template_filename = "negate_cond_branch_arm64.js"
|
|
10
|
+
|
|
11
|
+
def generate(self, module_name: str, relative_address: str) -> str:
|
|
12
|
+
return self._render({
|
|
13
|
+
"MODULE_NAME_PLACEHOLDER": module_name,
|
|
14
|
+
"RELATIVE_ADDRESS_PLACEHOLDER": relative_address,
|
|
15
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Generator for Frida x86 conditional branch negation snippets."""
|
|
2
|
+
|
|
3
|
+
from fridaforge.core import SnippetGenerator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NegateX86CondBranchGenerator(SnippetGenerator):
|
|
7
|
+
"""Generate a Frida script that negates an x86 conditional branch."""
|
|
8
|
+
|
|
9
|
+
template_filename = "negate_cond_branch_x86.js"
|
|
10
|
+
|
|
11
|
+
def generate(self, module_name: str, relative_address: str) -> str:
|
|
12
|
+
return self._render({
|
|
13
|
+
"MODULE_NAME_PLACEHOLDER": module_name,
|
|
14
|
+
"RELATIVE_ADDRESS_PLACEHOLDER": relative_address,
|
|
15
|
+
})
|
fridaforge/core/utils.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Template loading and placeholder substitution utilities."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
|
|
6
|
+
TEMPLATES_DIR = os.path.join(
|
|
7
|
+
os.path.dirname(os.path.abspath(__file__)), "..", "templates"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_template(template_filename: str) -> str:
|
|
12
|
+
"""Read a JavaScript template file from the templates directory.
|
|
13
|
+
|
|
14
|
+
:param template_filename: The file name of the template
|
|
15
|
+
(e.g. ``hook_function.js``).
|
|
16
|
+
:return: The raw template content as a string.
|
|
17
|
+
"""
|
|
18
|
+
filepath = os.path.join(TEMPLATES_DIR, template_filename)
|
|
19
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
20
|
+
return f.read()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fill_template(template: str, data_to_replace: Mapping[str, str | int]) -> str:
|
|
24
|
+
"""Replace ``[PLACEHOLDER]`` tokens in a template with actual values.
|
|
25
|
+
|
|
26
|
+
:param template: The template string containing placeholders.
|
|
27
|
+
:param data_to_replace: A mapping of placeholder names (without
|
|
28
|
+
brackets) to their replacement values.
|
|
29
|
+
:return: The filled template string.
|
|
30
|
+
"""
|
|
31
|
+
for placeholder, value in data_to_replace.items():
|
|
32
|
+
template = template.replace(f"[{placeholder}]", str(value))
|
|
33
|
+
return template
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const moduleName = "[MODULE_NAME_PLACEHOLDER]";
|
|
3
|
+
|
|
4
|
+
function hookOnLibraryLoad(dlopenFunctionAddr) {
|
|
5
|
+
Interceptor.attach(dlopenFunctionAddr, {
|
|
6
|
+
onEnter: function (args) {
|
|
7
|
+
const loadPath = args[0].readCString();
|
|
8
|
+
if (loadPath != null && loadPath.includes(moduleName)) {
|
|
9
|
+
this.isTarget = true;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
onLeave: function () {
|
|
13
|
+
if (this.isTarget) {
|
|
14
|
+
console.log(`[+] ${moduleName} loaded!`);
|
|
15
|
+
|
|
16
|
+
// ** Your code goes in here... **
|
|
17
|
+
|
|
18
|
+
this.isTarget = false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dlopenFunctions = ["dlopen", "android_dlopen_ext"];
|
|
25
|
+
for (let i = 0; i < dlopenFunctions.length; i++) {
|
|
26
|
+
const address = Module.getGlobalExportByName(dlopenFunctions[i]);
|
|
27
|
+
if (address != null) {
|
|
28
|
+
hookOnLibraryLoad(address);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
})();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const moduleName = "[MODULE_NAME_PLACEHOLDER]";
|
|
3
|
+
const functionRelativeAddress = "[FUNCTION_RELATIVE_ADDRESS_PLACEHOLDER]";
|
|
4
|
+
const functionName = "[FUNCTION_NAME_PLACEHOLDER]";
|
|
5
|
+
|
|
6
|
+
const module = Process.findModuleByName(moduleName);
|
|
7
|
+
if (module != null) {
|
|
8
|
+
const targetAddr = module.base.add(functionRelativeAddress);
|
|
9
|
+
console.log(`[+] Hook installed for [${functionName}]`);
|
|
10
|
+
Interceptor.attach(targetAddr, {
|
|
11
|
+
onEnter: function (args) {
|
|
12
|
+
console.log(`-> enetered [${functionName}] at [${targetAddr}]`);
|
|
13
|
+
|
|
14
|
+
// Your code goes here...
|
|
15
|
+
|
|
16
|
+
},
|
|
17
|
+
onLeave: function (retval) {
|
|
18
|
+
console.log(`<- leaving [${functionName}] at [${this.context.pc}]`);
|
|
19
|
+
|
|
20
|
+
// Your code goes here...
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
} else {
|
|
25
|
+
console.log("[!] Failed to find module");
|
|
26
|
+
}
|
|
27
|
+
})();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const moduleName = "[MODULE_NAME_PLACEHOLDER]";
|
|
3
|
+
const sectionName = "[SECTION_NAME_PLACEHOLDER]";
|
|
4
|
+
|
|
5
|
+
const module = Process.findModuleByName(moduleName);
|
|
6
|
+
if (module != null) {
|
|
7
|
+
let target;
|
|
8
|
+
|
|
9
|
+
const sections = module.enumerateSections()
|
|
10
|
+
for (let i = 0; i < sections.length; i++) {
|
|
11
|
+
const section = sections[i];
|
|
12
|
+
if (section.name == sectionName) {
|
|
13
|
+
target = section;
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (target) {
|
|
19
|
+
const newPrermissions = 'rwx' // <-------- ** CHANGE THIS ACCORDING TO YOUR NEEDS **
|
|
20
|
+
|
|
21
|
+
console.log(`[+] Found section [${target.name}] at ${target.address}. size: ${target.size} (bytes)`);
|
|
22
|
+
console.log(`\tAttempting to change permissions from [${Memory.queryProtection(target.address)}] to [${newPrermissions}]`);
|
|
23
|
+
const protectionResult = Memory.protect(target.address, target.size, newPrermissions);
|
|
24
|
+
if (protectionResult) {
|
|
25
|
+
console.log(`\tSuccess! [${Memory.queryProtection(target.address)}]`);
|
|
26
|
+
} else {
|
|
27
|
+
console.log(`[!] Failed to change memory protection`);
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
console.log(`[!] Failed to find section ${sectionName}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
} else {
|
|
34
|
+
console.log("[!] Failed to find module");
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const moduleName = "[MODULE_NAME_PLACEHOLDER]";
|
|
3
|
+
const condBranchRelAddr = "[RELATIVE_ADDRESS_PLACEHOLDER]";
|
|
4
|
+
|
|
5
|
+
const module = Process.findModuleByName(moduleName);
|
|
6
|
+
if (module != null) {
|
|
7
|
+
const targetAddr = module.base.add(condBranchRelAddr);
|
|
8
|
+
|
|
9
|
+
console.log(`[+] Negating conditional branch at ${targetAddr}`);
|
|
10
|
+
console.log(`\tbefore: ${Instruction.parse(targetAddr)}`);
|
|
11
|
+
Memory.patchCode(targetAddr, 4, code => {
|
|
12
|
+
const raw = targetAddr.readU32();
|
|
13
|
+
let xorMask;
|
|
14
|
+
if ((raw & 0xFF000010) === 0x54000000) {
|
|
15
|
+
xorMask = 1; // b.cond: condition in bit 0
|
|
16
|
+
} else if ((raw & 0x7E000000) === 0x36000000) {
|
|
17
|
+
xorMask = 1 << 24; // tbz/tbnz: op in bit 24
|
|
18
|
+
} else if ((raw & 0x7E000000) === 0x34000000) {
|
|
19
|
+
xorMask = 1 << 24; // cbz/cbnz: op in bit 24
|
|
20
|
+
} else {
|
|
21
|
+
console.log("[!] Unrecognized conditional branch encoding");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
code.writeU32(raw ^ xorMask);
|
|
25
|
+
});
|
|
26
|
+
console.log(`\tafter: ${Instruction.parse(targetAddr)}`);
|
|
27
|
+
} else {
|
|
28
|
+
console.log("[!] Failed to find module");
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const moduleName = "[MODULE_NAME_PLACEHOLDER]";
|
|
3
|
+
const condBranchRelAddr = "[RELATIVE_ADDRESS_PLACEHOLDER]";
|
|
4
|
+
|
|
5
|
+
const module = Process.findModuleByName(moduleName);
|
|
6
|
+
if (module != null) {
|
|
7
|
+
const targetAddr = module.base.add(condBranchRelAddr);
|
|
8
|
+
|
|
9
|
+
console.log(`[+] Negating conditional branch at ${targetAddr}`);
|
|
10
|
+
console.log(`\tbefore: ${Instruction.parse(targetAddr)}`);
|
|
11
|
+
|
|
12
|
+
let patchPtr;
|
|
13
|
+
|
|
14
|
+
const firstByte = targetAddr.readU8();
|
|
15
|
+
if (firstByte === 0x0F) {
|
|
16
|
+
const secondByte = targetAddr.add(1).readU8();
|
|
17
|
+
if ((secondByte & 0xF0) === 0x80) {
|
|
18
|
+
patchPtr = targetAddr.add(1);
|
|
19
|
+
}
|
|
20
|
+
} else if ((firstByte & 0xF0) === 0x70) {
|
|
21
|
+
patchPtr = targetAddr;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (patchPtr) {
|
|
25
|
+
Memory.patchCode(patchPtr, 1, code => {
|
|
26
|
+
const raw = patchPtr.readU8();
|
|
27
|
+
code.writeU8(raw ^ 1);
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`\tafter: ${Instruction.parse(targetAddr)}`);
|
|
32
|
+
} else {
|
|
33
|
+
console.log("[!] Failed to find module");
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fridaforge
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Generate Frida scripts from IDA Pro, Binary Ninja, and Ghidra
|
|
5
|
+
Author: noobexon1
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/noobexon1/FridaForge
|
|
8
|
+
Project-URL: Issues, https://github.com/noobexon1/FridaForge/issues
|
|
9
|
+
Keywords: frida,binary-ninja,ida-pro,ghidra,reverse-engineering
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Security
|
|
13
|
+
Classifier: Topic :: Software Development :: Disassemblers
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
<div align="center">
|
|
20
|
+
|
|
21
|
+
<img src="https://raw.githubusercontent.com/noobexon1/FridaForge/main/assets/logo.jpg" alt="FridaForge logo"/>
|
|
22
|
+
|
|
23
|
+
**Generate Frida scripts directly from IDA Pro, Binary Ninja, and Ghidra.** β‘
|
|
24
|
+
|
|
25
|
+
[](https://github.com/noobexon1/FridaForge/releases)
|
|
26
|
+
[](https://github.com/noobexon1/FridaForge/stargazers)
|
|
27
|
+
[](https://github.com/noobexon1/FridaForge/commits)
|
|
28
|
+
[](https://github.com/noobexon1/FridaForge/issues)
|
|
29
|
+
[](https://github.com/noobexon1/FridaForge/releases)
|
|
30
|
+
[](https://github.com/noobexon1/FridaForge/actions/workflows/release.yml)
|
|
31
|
+

|
|
32
|
+

|
|
33
|
+

|
|
34
|
+

|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
## πΈ Examples
|
|
40
|
+
|
|
41
|
+
**Generate a function hook** β right-click in the disassembly/pseudocode view and pick an action:
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
**Get a ready-to-use Frida script** β copied straight to your clipboard:
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
## β¨ Features
|
|
50
|
+
|
|
51
|
+
Right-click (or hit a hotkey) in your disassembler to generate ready-to-use Frida snippets β the result is instantly copied to your clipboard.
|
|
52
|
+
|
|
53
|
+
- π§© **Composable scripts** β snippets are independent but can be nested to build complex instrumentation with a few clicks
|
|
54
|
+
- π― **Context-aware** β scripts auto-fill module name, function address, section, and any other detail available from your current context
|
|
55
|
+
- π§° **Three hosts, one experience** β identical features, hotkeys, and menus in IDA Pro, Binary Ninja, and Ghidra
|
|
56
|
+
- π **Clipboard integration** β generated scripts are copied the moment you run an action
|
|
57
|
+
|
|
58
|
+
### Supported Actions
|
|
59
|
+
|
|
60
|
+
#### πͺ Hooks
|
|
61
|
+
|
|
62
|
+
| Action | Description | Hotkey |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Generate function hook | Intercept calls to the selected function, log entry/exit | `Shift+F` |
|
|
65
|
+
| Generate dlopen hooks | Monitor `dlopen` (including Android variants) to detect module loading | `Shift+D` |
|
|
66
|
+
|
|
67
|
+
#### π§ Memory
|
|
68
|
+
|
|
69
|
+
| Action | Description | Hotkey |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Modify section protection | Change memory protection of the section at the current address | `Shift+M` |
|
|
72
|
+
|
|
73
|
+
#### π©Ή Patching
|
|
74
|
+
|
|
75
|
+
| Action | Description | Hotkey |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Negate cond branch (ARM64) | Flip the condition of an ARM64 conditional branch | `Shift+A` |
|
|
78
|
+
| Negate cond branch (x86/x64) | Flip the condition of an x86/x64 conditional branch | `Shift+X` |
|
|
79
|
+
|
|
80
|
+
## π§ How it Works
|
|
81
|
+
|
|
82
|
+
Each feature follows a simple pipeline: a **Frida template** (`fridaforge/templates/`) provides the JavaScript skeleton with placeholders, a **command** (`fridaforge/commands/`) gathers context through a host-agnostic **Backend**, and a **generator** (`fridaforge/core/`) fills the template to produce the final script.
|
|
83
|
+
|
|
84
|
+
The shared core knows nothing about IDA, Binary Ninja, or Ghidra. Each host lives in `hosts/` and provides a thin **adapter** that implements the `Backend` interface (data extraction, clipboard, menu + hotkey registration). Add a feature once and it works in all three.
|
|
85
|
+
|
|
86
|
+
## π¦ Installation
|
|
87
|
+
|
|
88
|
+
Binary Ninja users can install straight from the Extension Manager. For everything else, grab the archive for your tool from the [latest release](https://github.com/noobexon1/FridaForge/releases). There's one per host β `fridaforge-binja-<version>.zip`, `fridaforge-ida-<version>.zip`, and `fridaforge-ghidra-<version>.zip`.
|
|
89
|
+
|
|
90
|
+
### π Binary Ninja
|
|
91
|
+
|
|
92
|
+
**Extension Manager (recommended):** open the Extension Manager (**Plugins β Manage Plugins**), search for **FridaForge**, and click **Install**. The shared core (`fridaforge` on PyPI) is installed automatically. Restart Binary Ninja if prompted.
|
|
93
|
+
|
|
94
|
+
**Manual install:**
|
|
95
|
+
|
|
96
|
+
1. Find your user plugins directory by running this in Binary Ninja's Python console:
|
|
97
|
+
```python
|
|
98
|
+
binaryninja.user_plugin_path()
|
|
99
|
+
```
|
|
100
|
+
2. Extract `fridaforge-binja-<version>.zip` into that directory so that `<binja user plugins>/fridaforge/` exists.
|
|
101
|
+
3. Restart Binary Ninja.
|
|
102
|
+
|
|
103
|
+
> Switching from a manual install to the Extension Manager? Delete `<binja user plugins>/fridaforge/` first. Otherwise both copies load and every command appears twice.
|
|
104
|
+
|
|
105
|
+
### π΅ IDA Pro
|
|
106
|
+
|
|
107
|
+
1. **Prerequisites:**
|
|
108
|
+
- IDA Pro (tested with 9.0, but any version with Python 3 and `idaapi` should work).
|
|
109
|
+
- PyQt5 for clipboard functionality (usually bundled with IDA's Python).
|
|
110
|
+
2. Locate your IDA Pro user directory by running this in IDA's Python console:
|
|
111
|
+
```python
|
|
112
|
+
import ida_diskio; print(ida_diskio.get_user_idadir())
|
|
113
|
+
```
|
|
114
|
+
If not already there, create a `plugins` directory in the user directory. This is where the plugin will be installed.
|
|
115
|
+
3. Extract `fridaforge-ida-<version>.zip` into the `plugins` directory.
|
|
116
|
+
4. Restart IDA Pro. The plugin loads automatically. You should see FridaForge as an option after right-clicking in the disassembly view.
|
|
117
|
+
|
|
118
|
+
### π Ghidra
|
|
119
|
+
|
|
120
|
+
1. **Prerequisites:** Ghidra **11.3 or later** (PyGhidra ships bundled; no extra install needed).
|
|
121
|
+
2. Unzip `fridaforge-ghidra-<version>.zip` into a folder of your choice.
|
|
122
|
+
3. In Ghidra's CodeBrowser, open **Window β Script Manager**, click **Manage Script Directories** (the toolbar icon), and add the unzipped folder.
|
|
123
|
+
4. The five actions appear under **Tools β FridaForge** (categorized into Hooks / Memory / Patching) and respond to the hotkeys `Shift+F/D/M/A/X`. The generated Frida script is copied to your clipboard; `[FridaForge]` messages print to the Ghidra console.
|
|
124
|
+
|
|
125
|
+
> Ghidra actions are delivered as menu items + hotkeys (not a nested right-click popup), which is the supported integration path for PyGhidra scripts.
|
|
126
|
+
|
|
127
|
+
### π§± Script Composition
|
|
128
|
+
|
|
129
|
+
Snippets are designed to be independent building blocks. You can nest scripts inside each other β for example, hook a function, negate a conditional branch on entry, and restore it on exit. Complex instrumentation from a few clicks.
|
|
130
|
+
|
|
131
|
+
## π οΈ Building from Source
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip install pytest build
|
|
135
|
+
pytest -v # run the host-agnostic test suite
|
|
136
|
+
python build/build.py # writes dist/fridaforge-binja-*.zip, dist/fridaforge-ida-*.zip and dist/fridaforge-ghidra-*.zip
|
|
137
|
+
python -m build # builds the fridaforge core package (sdist + wheel) published to PyPI
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Builds are deterministic β the same source produces byte-identical archives.
|
|
141
|
+
|
|
142
|
+
To release, run `python build/bump_version.py X.Y.Z`, commit, and push tag `vX.Y.Z`. CI publishes the core to PyPI, then attaches the three archives to a GitHub release.
|
|
143
|
+
|
|
144
|
+
## π€ Contributing
|
|
145
|
+
|
|
146
|
+
Contributions welcome! Got a useful Frida snippet? Turn it into a template so everyone can use it.
|
|
147
|
+
|
|
148
|
+
1. Fork the repository.
|
|
149
|
+
2. Create a new branch for your feature or fix.
|
|
150
|
+
3. Make your changes (and add a test β the core is fully unit-tested without needing a host).
|
|
151
|
+
4. Submit a pull request.
|
|
152
|
+
|
|
153
|
+
| Adding... | Where |
|
|
154
|
+
|---|---|
|
|
155
|
+
| New Frida snippet | `fridaforge/templates/` (JS template) + `fridaforge/core/` (generator) + `fridaforge/commands/` (command) |
|
|
156
|
+
| Host-specific behavior | `hosts/binja/` or `hosts/ida/` |
|
|
157
|
+
|
|
158
|
+
Each command and generator lives in its own file β easy to contribute without merge conflicts.
|
|
159
|
+
|
|
160
|
+
## πΊοΈ Roadmap
|
|
161
|
+
|
|
162
|
+
See [open issues](https://github.com/noobexon1/FridaForge/issues) for planned features and known bugs.
|
|
163
|
+
|
|
164
|
+
## π License
|
|
165
|
+
|
|
166
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
fridaforge/__init__.py,sha256=4ta_Zh_8JSEfZq6VYqfMPXSiK_wG8Z3rFe_0BwBhbmk,144
|
|
2
|
+
fridaforge/_version.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
|
|
3
|
+
fridaforge/backend.py,sha256=wBMlXB2SUkgZm34ZVcXDwG5PocHwHUW1J2d_XNyQA9M,2407
|
|
4
|
+
fridaforge/commands/__init__.py,sha256=NdJuxFdKQxTgl7_CL8MVLIx1Is3A_g6KlmVu-pX3FAY,739
|
|
5
|
+
fridaforge/commands/base.py,sha256=pzWnW4Mq6l1FMi83k3feMqmipHmAJeqP-RSdPplV8wc,978
|
|
6
|
+
fridaforge/commands/hook_dlopen_functions.py,sha256=yCzxtCFcW2qsp_88HZ0tlg2u2NLcMEoc5jv-yU3eBAs,733
|
|
7
|
+
fridaforge/commands/hook_function.py,sha256=FNcf4lh6YGNi4q1oZw6xfEez_mIacgI3ChJuuz2xwRA,859
|
|
8
|
+
fridaforge/commands/modify_section_protection.py,sha256=3oTmdS9xyqJwN3goHS7ou7OdzxHxc4JN7Obk6kDDQII,1300
|
|
9
|
+
fridaforge/commands/negate_cond_branch_arm64.py,sha256=j8Tn1pBQC8BeY5hAHykjK-rK3wsBQ2AGFFY4LbEeE1Q,1415
|
|
10
|
+
fridaforge/commands/negate_cond_branch_x86.py,sha256=YvDraHVxzQaA8KZZiFsLucfP9nqOMAGRl9obNKQiqPU,1459
|
|
11
|
+
fridaforge/core/__init__.py,sha256=ko96ZbhAuM6ZQvDbip09jQRVevjVA4h1bTSFGSyNq04,2084
|
|
12
|
+
fridaforge/core/hook_dlopen_functions.py,sha256=jQ2v6v89lnlxLwmyYcETtkg-aD2tsA1uYXv81e2l2vU,406
|
|
13
|
+
fridaforge/core/hook_function.py,sha256=k3ocxuxWNBsur80NXnTQAaTS-X7UEKL1lKyqYGOSBgw,632
|
|
14
|
+
fridaforge/core/modify_section_protection.py,sha256=JXhd3aI8iHrOV6LOW4bpyOshxnhu347i_grsEEc2BDA,521
|
|
15
|
+
fridaforge/core/negate_cond_branch_arm64.py,sha256=d60ZI5URwwN9xnODopCbs1lCx34LJ8qglnTdoLeSm9o,533
|
|
16
|
+
fridaforge/core/negate_cond_branch_x86.py,sha256=AWMn3TL5XCH2GJoq4Uj7I2mSm8rj3m7YiLkE3rTDx1g,525
|
|
17
|
+
fridaforge/core/utils.py,sha256=HrIAufxYmkcT9fahSnyDSO1dBoYIkiuhvNhj3n9H-ao,1159
|
|
18
|
+
fridaforge/templates/hook_dlopen_functions.js,sha256=CR2thp5PaD7kQXfTnZHARWivy2ZYt2mEFAoNUdMXpBg,989
|
|
19
|
+
fridaforge/templates/hook_function.js,sha256=qRflfuljNEsak2AKRDPTEfnqchi1hdVK8pgD4qM4fJQ,933
|
|
20
|
+
fridaforge/templates/modify_section_protection.js,sha256=82T9gnbkIPdZBc3pYmKOGJGfJMEGMxRsS-8CytGelbI,1372
|
|
21
|
+
fridaforge/templates/negate_cond_branch_arm64.js,sha256=hDDIqMqe8krjNju2g7FeenPpr9S6s-a8L9kPhZC7qUc,1245
|
|
22
|
+
fridaforge/templates/negate_cond_branch_x86.js,sha256=z9GY73mmXP3UO_lNaNhVbtCHxvqDCxcBL-0VTUgwMIY,1117
|
|
23
|
+
fridaforge-0.3.0.dist-info/licenses/LICENSE,sha256=ZSX9KeL8nzVwiFiEpXKkZi6NNBKnVpv7zNbS1aJ3Pt0,1066
|
|
24
|
+
fridaforge-0.3.0.dist-info/METADATA,sha256=UF_fUDhGd8rB4Fww0g-7BmnCpqlIT_JZNtOmlkPZMnU,8757
|
|
25
|
+
fridaforge-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
26
|
+
fridaforge-0.3.0.dist-info/top_level.txt,sha256=6hd6j8PoEAtXqJFEJDyZi3obFDJqTr5MZOh5BUorw2c,11
|
|
27
|
+
fridaforge-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 noobexon1
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fridaforge
|