Skip to content

Commit

Permalink
Switch to PyBroma and prepare for function signature importer...
Browse files Browse the repository at this point in the history
  • Loading branch information
SpaghettDev committed May 4, 2024
1 parent 64fe38e commit 4d362ea
Show file tree
Hide file tree
Showing 18 changed files with 25,397 additions and 339 deletions.
183 changes: 33 additions & 150 deletions BromaIDA.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
VERSION = "2.0.0"
VERSION = "3.0.0"
__AUTHOR__ = "SpaghettDev"

PLUGIN_NAME = "BromaIDA"
PLUGIN_HOTKEY = "Ctrl+Shift+B"

import idaapi
import ida_kernwin
import ida_funcs
import idc
import idautils

from typing import cast
from re import sub
from io import TextIOWrapper

from broma_ida.binding_type import Binding
from broma_ida.utils import (
popup, stop, rename_func, get_short_info, get_platform,
get_platform_printable
from idaapi import (
msg as ida_msg, register_action, unregister_action,
plugin_t as ida_plugin_t, action_desc_t as ida_action_desc_t,
PLUGIN_PROC, PLUGIN_HIDE, PLUGIN_KEEP
)
from broma_ida.broma.parser import BromaParser
from ida_kernwin import ask_file, ASKBTN_BTN1, ASKBTN_BTN2
from idautils import Names

from broma_ida.utils import popup, stop, get_platform, get_platform_printable
from broma_ida.broma.importer import BromaImporter
from broma_ida.broma.exporter import BromaExporter
from broma_ida.ida_ctx_entry import IDACtxEntry

Expand All @@ -31,169 +26,57 @@ def bida_main():
"Import or Export Broma file?"
)

if import_export_prompt == ida_kernwin.ASKBTN_BTN1:
filePath = ida_kernwin.ask_file(False, "GeometryDash.bro", "bro")
if import_export_prompt == ASKBTN_BTN1:
filePath = ask_file(False, "GeometryDash.bro", "bro")

if filePath is None or (filePath and not filePath.endswith(".bro")):
popup("Ok", None, None, "Please select a valid file!")
stop()

platform = get_platform()
broma_parser = BromaParser(platform)
broma_importer = BromaImporter(platform)

try:
with open(filePath, "r") as f:
broma_parser.parse_file_stream(cast(TextIOWrapper, f))
broma_importer.parse_file_stream(f)
except FileNotFoundError:
popup("Ok", None, None, "File doesn't exist? Please try again.")
popup("Ok", "Ok", None, "File doesn't exist? Please try again.")
stop()

print(
f"\n\n[+] Read {len(broma_parser.bindings)} "
f"{get_platform_printable(platform)} bindings from {filePath}"
)
print(
f"[+] Read {len(broma_parser.duplicates)} "
f"duplicate {get_platform_printable(platform)} "
f"bindings from {filePath}\n"
)

# first, handle non-duplicates
for binding in broma_parser.bindings:
ida_ea = idaapi.get_imagebase() + binding["address"]
ida_name = idc.get_name(ida_ea)
ida_func_flags = idc.get_func_flags(ida_ea)

if ida_name.startswith("loc_"):
ida_funcs.add_func(ida_ea)

if ida_func_flags & idc.FUNC_LIB:
print(
f"[!] Tried to rename a library function! "
f"({get_short_info(binding)})"
)
continue

if ida_funcs.get_func(ida_ea).start_ea != ida_ea:
print(
f"[!] Function is in the middle of another one! "
f"({get_short_info(binding)})"
)
continue

if ida_name.startswith("sub_"):
rename_func(
ida_ea,
binding["idaQualifiedName"] # type: ignore
)
elif sub("_[0-9]+", "", ida_name) != binding["idaQualifiedName"]:
mismatch_popup = popup(
"Overwrite", "Keep", "",
f"""Mismatch in Broma ({binding["qualifiedName"]}) """
f"and idb ({ida_name})!\n"
"Overwrite from Broma or keep current name?"
)

if mismatch_popup == ida_kernwin.ASKBTN_BTN1:
rename_func(
ida_ea,
binding["idaQualifiedName"] # type: ignore
)
elif mismatch_popup == ida_kernwin.ASKBTN_CANCEL:
stop()

# and now for what took me 3 hours :D
for addr, bindings in broma_parser.duplicates.items():
ida_ea = idaapi.get_imagebase() + addr

func_cmt = idc.get_func_cmt(ida_ea, True)
func_names = ", ".join(
[binding["qualifiedName"]
for binding in bindings] # type: ignore
)

if func_cmt == "":
# use the first occurrence as the name (very good imo)
rename_func(
ida_ea,
bindings[0]["idaQualifiedName"] # type: ignore
)

idc.set_func_cmt(ida_ea, f"Merged with: {func_names}", True)
elif func_cmt.startswith("Merged with: "):
cmt_func_names = func_cmt.lstrip("Merged with: ")

if func_names != cmt_func_names:
print(
"[!] Mismatch in merged function list "
f"(Current: {cmt_func_names} | Correct: {func_names})! "
"Correcting..."
)
idc.set_func_cmt(
ida_ea, f"Merged with: {func_names}", True
)
else:
if popup(
"Overwrite", "Keep", None,
f"{hex(addr)} already has a comment! Would you like to "
"overwrite it with merge information or keep the current "
"comment?\n"
"(You will be prompted with this again if you rerun the "
"script and there are merged functions!)"
) == ida_kernwin.ASKBTN_BTN1:
idc.set_func_cmt(
ida_ea, f"Merged with: {func_names}", True
)
broma_importer.import_into_idb()

print("[+] Finished importing bindings from Broma file")
popup(
"Ok", "Ok", None,
f"Finished importing {get_platform_printable(platform)} "
"Finished importing "
f"{get_platform_printable(platform)} "
"bindings from Broma file."
)
broma_parser.reset()

elif import_export_prompt == ida_kernwin.ASKBTN_BTN2:
elif import_export_prompt == ASKBTN_BTN2:
platform = get_platform()

filePath = ida_kernwin.ask_file(True, "GeometryDash.bro", "bro")
# for_saving is not True because we need to read the file first
# which may not even exist if the saving prompt is used
# (since you can select files that don't exist within said prompt)
filePath = ask_file(False, "GeometryDash.bro", "bro")

if filePath is None or (filePath and not filePath.endswith(".bro")):
popup("Ok", None, None, "Please select a valid file!")
popup("Ok", "Ok", None, "Please select a valid file!")
stop()

broma_exporter = BromaExporter(platform, filePath)

for ea, name in idautils.Names():
if "::" not in name:
continue

split_name = name.split("::")

# ["GJUINode", "dGJUINode"] -> "GJUINode" == "GJUINode"
if split_name[0] == split_name[1][1:]:
split_name = [
split_name[0],
split_name[1].replace("::d", "::~")
]

broma_exporter.push_binding(Binding({
"name": split_name[1],
"className": split_name[0],
"inheritedClasses": [],
"address": ea - idaapi.get_imagebase()
}))

broma_exporter.import_from_idb(Names())
broma_exporter.export()

print(f"[+] Finished exporting {broma_exporter.num_exports} bindings.")
popup("Ok", "Ok", None, "Finished exporting bindings to Broma file.")
broma_exporter.reset()


class BromaIDAPlugin(idaapi.plugin_t):
class BromaIDAPlugin(ida_plugin_t):
"""BromaIDA Plugin"""
flags = idaapi.PLUGIN_PROC | idaapi.PLUGIN_HIDE
flags = PLUGIN_PROC | PLUGIN_HIDE
comment = "Broma support for IDA."
help = "Ctrl-Shift-I to start the importing/exporting."
wanted_name = PLUGIN_NAME
Expand All @@ -206,35 +89,35 @@ def init(self):
"""Ran on plugin load"""
self._register_action()

idaapi.msg(f"{self.wanted_name} v{VERSION} initialized\n")
ida_msg(f"{self.wanted_name} v{VERSION} initialized\n")

return idaapi.PLUGIN_KEEP
return PLUGIN_KEEP

def term(self):
"""Ran on plugin unload"""
self._unregister_action()

idaapi.msg(f"{self.wanted_name} v{VERSION} unloaded\n")
ida_msg(f"{self.wanted_name} v{VERSION} unloaded\n")

def run(self, arg):
"""Ran on "File -> Script File" (shocker) (broken for me :D)"""
bida_main()

def _register_action(self):
"""Registers BromaIDA's hotkey"""
hotkey = idaapi.action_desc_t(
hotkey = ida_action_desc_t(
self.ACTION_BTIDA,
"BromaIDA",
IDACtxEntry(bida_main),
PLUGIN_HOTKEY,
self.ACTION_DESC
)

idaapi.register_action(hotkey)
register_action(hotkey)

def _unregister_action(self):
"""Unregisters BromaIDA's hotkey"""
idaapi.unregister_action(self.ACTION_BTIDA)
unregister_action(self.ACTION_BTIDA)


def PLUGIN_ENTRY():
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# BromaIDA

Broma IDA support (now real).
IDA Broma support (now real).

Parses a broma file and exports/imports the bindings into the current IDA project.
Parses a Broma file and exports the bindings into a Broma file/imports the bindings into the current IDA project.

[![Broma-To-IDA](assets/btida.gif)](https://github.com/SpaghettDev/Broma-To-IDA/releases)
[![BromaIDA](assets/bida.gif)](https://github.com/SpaghettDev/BromaIDA/releases)

## Requirements

Expand All @@ -20,4 +20,8 @@ Parses a broma file and exports/imports the bindings into the current IDA projec

1. `Ctrl-Shift-B` to start importing/exporting
2. Browse and select the broma file (not tested with anything but `GeometryDash.bro`)
3. Let the script handle the rest and enjoy free bindings
3. Let the script handle the rest and enjoy free/exported bindings

## Thanks

Special thanks to [CallocGD](https://github.com/CallocGD)'s [PyBroma](https://github.com/CallocGD/PyBroma) which is used to import bindings from Broma.
File renamed without changes
51 changes: 37 additions & 14 deletions broma_ida/binding_type.py → broma_ida/broma/binding.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
from typing import Union, TypedDict, Literal, cast

from broma_ida.pybroma import Type


class BaseBindingType(TypedDict):
"""Base binding type"""
name: str
className: str
qualifiedName: str
idaQualifiedName: str
inheritedClasses: list[str]
address: int

return_type: Type
parameters: dict[str, Type]


class BaseShortBindingType(TypedDict):
"""Base binding type (but shorter)"""
name: str
className: str
inheritedClasses: list[str]
address: int


class BaseShortBindingTypeWithMD(BaseShortBindingType):
"""Base binding type (shorter, with metadata about the function)"""
return_type: Type
parameters: dict[str, Type]


class Binding:
"""Actual binding type. Implements __eq__ because
TypedDict can't be instantiated, and as such, can't
Expand All @@ -27,16 +36,23 @@ class Binding:
to stop putting type: ignore everywhere
"""
binding: BaseBindingType
is_overload: bool

def __init__(
self,
binding: Union[BaseShortBindingType, BaseBindingType]
binding: Union[
BaseShortBindingType, BaseShortBindingTypeWithMD, BaseBindingType
],
is_overload: bool = False
) -> None:
if binding.get("qualifiedName"):
if binding.get("qualifiedName") is not None:
self.binding = cast(BaseBindingType, binding)
else:
binding = cast(BaseShortBindingType, binding)

if binding.get("return_type"):
binding = cast(BaseShortBindingTypeWithMD, binding)

self.binding = BaseBindingType({
"name": binding["name"],
"className": binding["className"],
Expand All @@ -46,10 +62,13 @@ def __init__(
f"""{binding["className"]}::{binding["name"]}""".replace(
"::~", "::d"
),
"inheritedClasses": binding["inheritedClasses"],
"address": binding["address"]
"address": binding["address"],
"return_type": binding.get("return_type") or Type(),
"parameters": binding.get("parameters") or {}
})

self.is_overload = is_overload

def __eq__(self, key: object) -> bool:
if isinstance(key, int):
return self.binding["address"] == key
Expand All @@ -62,18 +81,22 @@ def __getitem__(
self,
key: Literal[
"name", "className", "qualifiedName",
"idaQualifiedName", "inheritedClasses", "address"
"idaQualifiedName", "address", "return_type",
"parameters"
]
) -> Union[str, list[str], int]:
) -> Union[str, list[str], int, bool]:
return self.binding.__getitem__(key) # type: ignore

def __str__(self) -> str:
return f"""name="{self.binding["name"]}" """ \
f"""className="{self.binding["className"]}" """ \
f"""qualifiedName="{self.binding["qualifiedName"]}" """ \
f"""idaQualifiedName="{self.binding["idaQualifiedName"]}" """ \
f"""inheritedClasses="{self.binding["inheritedClasses"]}" """ \
f"""address="{self.binding["address"]}\""""
return f"""name={self.binding["name"]}""" \
f"""className={self.binding["className"]}""" \
f"""qualifiedName={self.binding["qualifiedName"]}""" \
f"""idaQualifiedName={self.binding["idaQualifiedName"]}""" \
f"""address={hex(self.binding["address"])} """ \
f"""is_overload={self.is_overload} """ \
f"""{self.binding["return_type"]}({
', '.join(self.binding["parameters"])
})"""

def __hash__(self) -> int:
return hash((
Expand Down
Loading

0 comments on commit 4d362ea

Please sign in to comment.