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 ADDED
@@ -0,0 +1,5 @@
1
+ """FridaForge: generate Frida scripts from IDA Pro and Binary Ninja."""
2
+
3
+ from fridaforge._version import __version__
4
+
5
+ __all__ = ["__version__"]
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
+ })
@@ -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
+ [![GitHub release](https://img.shields.io/github/v/release/noobexon1/FridaForge)](https://github.com/noobexon1/FridaForge/releases)
26
+ [![GitHub stars](https://img.shields.io/github/stars/noobexon1/FridaForge)](https://github.com/noobexon1/FridaForge/stargazers)
27
+ [![GitHub last commit](https://img.shields.io/github/last-commit/noobexon1/FridaForge)](https://github.com/noobexon1/FridaForge/commits)
28
+ [![GitHub issues](https://img.shields.io/github/issues/noobexon1/FridaForge)](https://github.com/noobexon1/FridaForge/issues)
29
+ [![GitHub downloads](https://img.shields.io/github/downloads/noobexon1/FridaForge/total)](https://github.com/noobexon1/FridaForge/releases)
30
+ [![Release](https://img.shields.io/github/actions/workflow/status/noobexon1/FridaForge/release.yml?label=release)](https://github.com/noobexon1/FridaForge/actions/workflows/release.yml)
31
+ ![IDA Pro](https://img.shields.io/badge/IDA%20Pro-Plugin-blue)
32
+ ![Binary Ninja](https://img.shields.io/badge/Binary%20Ninja-Plugin-orange)
33
+ ![Ghidra](https://img.shields.io/badge/Ghidra-Plugin-green)
34
+ ![Frida](https://img.shields.io/badge/Frida-Instrumentation-red)
35
+ [![GitHub License](https://img.shields.io/github/license/noobexon1/FridaForge)](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
+ ![Generating a function hook from the FridaForge context menu](https://raw.githubusercontent.com/noobexon1/FridaForge/main/assets/Screenshot_1.jpg)
44
+
45
+ **Get a ready-to-use Frida script** β€” copied straight to your clipboard:
46
+
47
+ ![The generated Frida hook script](https://raw.githubusercontent.com/noobexon1/FridaForge/main/assets/Screenshot_2.jpg)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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