diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cede15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/.DS_Store +*.py[cod] +**/__pycache__/ +**/.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a9e565 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# flutter-re-demo diff --git a/flure/__init__.py b/flure/__init__.py new file mode 100644 index 0000000..dc94963 --- /dev/null +++ b/flure/__init__.py @@ -0,0 +1,70 @@ +from flure.code_info import CodeInfo, ClassInfo, FunctionInfo + +LIBRARY_TOKEN = b"Library:'" +CLASS_TOKEN = b" Class: " +FUNCTION_TOKEN = b" Function " +KNOWN_PREFIXES_IGNORED = [b"Random ", b"Map dynamic + func_info = func_lines[0].strip()[:-1] + func_name = func_info.split(b"'")[1].decode("ascii") + func_signature = func_info.split(b"'")[2][1:].decode("ascii") + # Code Offset: _kDartIsolateSnapshotInstructions + 0x00000000002ebfb0 + func_offset = func_lines[2].strip().split(b"+")[1].strip() + func_relative_base = func_lines[2].strip().split(b"+")[0].split(b":")[1].strip().decode("ascii") + return FunctionInfo(func_name, func_signature, int(func_offset, 16), func_relative_base) \ No newline at end of file diff --git a/flure/code_info.py b/flure/code_info.py new file mode 100644 index 0000000..90f1150 --- /dev/null +++ b/flure/code_info.py @@ -0,0 +1,66 @@ + +class FunctionInfo(object): + def __init__(self, name, signature, offset, relative_base): + self.name = name + self.signature = signature + self.offset = offset + self.relative_base = relative_base + + @staticmethod + def load(func_info_dict): + return FunctionInfo(func_info_dict["name"], func_info_dict["signature"], + func_info_dict["offset"], func_info_dict["relative_base"]) + + def dump(self): + return { + "name": self.name, + "signature": self.signature, + "offset": self.offset, + "relative_base": self.relative_base, + } + + +class ClassInfo(object): + def __init__(self, module_path, name, full_declaration): + self.module_path = module_path + self.name = name + self.full_declaration = full_declaration + self.functions = [] + + def add_function(self, func_info: FunctionInfo): + self.functions.append(func_info) + + @staticmethod + def load(class_info_dict): + class_info = ClassInfo(class_info_dict["module"], class_info_dict["name"], class_info_dict["full_declaration"]) + for func_info_dict in class_info_dict["functions"]: + class_info.add_function(FunctionInfo.load(func_info_dict)) + return class_info + + def dump(self): + return { + "module": self.module_path, + "name": self.name, + "full_declaration": self.full_declaration, + "functions": [func_info.dump() for func_info in self.functions] + } + + +class CodeInfo(object): + def __init__(self): + self.classes = [] + + def add_classes(self, func_info: ClassInfo): + self.classes.append(func_info) + + @staticmethod + def load(code_info_dict): + code_info = CodeInfo() + for class_info_dict in code_info_dict["classes"]: + code_info.add_classes(ClassInfo.load(class_info_dict)) + return code_info + + def dump(self): + return { + "classes": [class_info.dump() for class_info in self.classes] + } diff --git a/flure/ida/__init__.py b/flure/ida/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flure/ida/patch_names.py b/flure/ida/patch_names.py new file mode 100644 index 0000000..b403147 --- /dev/null +++ b/flure/ida/patch_names.py @@ -0,0 +1,57 @@ +import idautils +import ida_dirtree +import idc +import ida_name, ida_funcs + +from flure.code_info import CodeInfo +from flure.ida.utils import safe_rename + +func_dir: ida_dirtree.dirtree_t = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS) + +SMALL_FUNC_MAPPING = { + "==": "__equals__", + ">>": "__rshift__", + "<<": "__lshift__", + "~/": "__truncdiv__", + "[]=": "__list_set__", + "unary-": "__neg__", + "<=": "__inf_eq__", + ">=": "__sup_eq__", + "!=": "__neq__", + "|": "__or__", + "&": "__and__", + "^": "__xor__", + "+": "__add__", + "*": "__mul__", + "-": "__sub__", + "<": "__inf__", + ">": "__sup__", + "%": "__mod__", + "/": "__fiv__", + "~": "__bnot__", +} + + +def create_ida_folders(code_info: CodeInfo): + exported_entries = {} + for entry in idautils.Entries(): + exported_entries[entry[3]] = entry[2] + for class_info in code_info.classes: + dir_path = class_info.module_path.replace(":", "/") + func_dir.mkdir(dir_path) + class_name = class_info.name + for func_info in class_info.functions: + func_name = func_info.name + if func_info.relative_base != 0: + func_offset = func_info.offset + exported_entries[func_info.relative_base] + else: + func_offset = func_info.offset + for k, v in SMALL_FUNC_MAPPING.items(): + if func_name == k: + func_name = v + func_name = func_name.replace(":", "::") + full_func_name = f"{class_name}::{func_name}" if class_info.name is not None else func_name + + ida_funcs.add_func(func_offset, idc.BADADDR) + given_name = safe_rename(func_offset, full_func_name) + func_dir.rename(given_name, f"{dir_path}/{given_name}") diff --git a/flure/ida/utils.py b/flure/ida/utils.py new file mode 100644 index 0000000..c74f038 --- /dev/null +++ b/flure/ida/utils.py @@ -0,0 +1,9 @@ +import idc +import ida_name + + +def safe_rename(offset, wanted_name, option=ida_name.SN_FORCE | ida_name.SN_NOCHECK): + idc.set_name(offset, wanted_name, option) + # Because ida_name.SN_FORCE and ida_name.SN_NOCHECK, the actual name can be different + # from wanted_name, thus we read it from DB after it has been set + return idc.get_name(offset) diff --git a/flure/parser/__init__.py b/flure/parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flure/parser/dwarf.py b/flure/parser/dwarf.py new file mode 100644 index 0000000..1ac3ef1 --- /dev/null +++ b/flure/parser/dwarf.py @@ -0,0 +1,76 @@ +from elftools.elf.elffile import ELFFile, SymbolTableSection + +from flure.code_info import CodeInfo, ClassInfo, FunctionInfo + + +class DwarfParser(object): + def __init__(self, filename): + self.filename = filename + self.code_info = CodeInfo() + self.parse() + + @staticmethod + def get_file_entries(dwarf_info, cu): + line_program = dwarf_info.line_program_for_CU(cu) + if line_program is None: + print('Warning: DWARF info is missing a line program for this CU') + return [] + return line_program.header["file_entry"] + + def parse(self): + known_symbol_address = self.parse_dwarf() + self.parse_symbols_table(known_symbol_address) + + def parse_dwarf(self): + known_symbol_address = [] + with open(self.filename, 'rb') as f: + elffile = ELFFile(f) + if not elffile.has_dwarf_info(): + raise Exception(f"File {self.filename} has no DWARF info") + dwarf_info = elffile.get_dwarf_info() + classes_by_name = {} + for CU in dwarf_info.iter_CUs(): + file_entries = self.get_file_entries(dwarf_info, CU) + for DIE in CU.iter_DIEs(): + if DIE.tag == 'DW_TAG_subprogram': + if 'DW_AT_low_pc' not in DIE.attributes: + continue + low_pc = DIE.attributes['DW_AT_low_pc'].value + if 'DW_AT_abstract_origin' not in DIE.attributes: + raise Exception(f"Unknown DIE: {DIE}") + abstract_origin = DIE.get_DIE_from_attribute('DW_AT_abstract_origin') + full_func_name = abstract_origin.attributes['DW_AT_name'].value.decode("ascii") + decl_file = file_entries[abstract_origin.attributes['DW_AT_decl_file'].value - 1].name.decode("ascii") + if "." in full_func_name: + class_name = full_func_name.split(".")[0] + func_name = ".".join(full_func_name.split(".")[1:]) + else: + class_name = "" + func_name = full_func_name + if " " in class_name: + if (class_name.split(" ")[0] != "new") or (len(class_name.split(" ")) != 2): + raise Exception(f"Not-handled space in '{full_func_name}'") + class_name = class_name.split(" ")[1] + if class_name == "": + class_name = "__null__" + full_class_name = ".".join([decl_file, class_name]) + if full_class_name not in classes_by_name: + classes_by_name[full_class_name] = ClassInfo(decl_file, class_name, "") + classes_by_name[full_class_name].add_function(FunctionInfo(func_name, "", low_pc, 0)) + known_symbol_address.append(low_pc) + for full_class_name, class_info in classes_by_name.items(): + self.code_info.add_classes(class_info) + return known_symbol_address + + def parse_symbols_table(self, known_symbol_address): + known_symbol_address = known_symbol_address if known_symbol_address is not None else [] + with open(self.filename, 'rb') as f: + elffile = ELFFile(f) + precompiled_code = ClassInfo("precompiled", None, None) + for section in elffile.iter_sections(): + if isinstance(section, SymbolTableSection): + for symbol in section.iter_symbols(): + symbol_address = symbol.entry['st_value'] + if symbol_address not in known_symbol_address: + precompiled_code.add_function(FunctionInfo(symbol.name, "", symbol.entry['st_value'], 0)) + self.code_info.add_classes(precompiled_code) diff --git a/flure/parser/reflutter.py b/flure/parser/reflutter.py new file mode 100644 index 0000000..5f0c103 --- /dev/null +++ b/flure/parser/reflutter.py @@ -0,0 +1,70 @@ +from flure.code_info import CodeInfo, ClassInfo, FunctionInfo + +LIBRARY_TOKEN = b"Library:'" +CLASS_TOKEN = b"Class: " +FUNCTION_TOKEN = b" Function " +KNOWN_PREFIXES_IGNORED = [b"Random ", b"Map dynamic + func_info = func_lines[0].strip()[:-1] + func_name = func_info.split(b"'")[1].decode("ascii") + func_signature = func_info.split(b"'")[2][1:].decode("ascii") + # Code Offset: _kDartIsolateSnapshotInstructions + 0x00000000002ebfb0 + func_offset = func_lines[2].strip().split(b"+")[1].strip() + func_relative_base = func_lines[2].strip().split(b"+")[0].split(b":")[1].strip().decode("ascii") + return FunctionInfo(func_name, func_signature, int(func_offset, 16), func_relative_base) \ No newline at end of file diff --git a/parse_info.py b/parse_info.py new file mode 100644 index 0000000..e0364bb --- /dev/null +++ b/parse_info.py @@ -0,0 +1,27 @@ +import json +import argparse + + +from flure.parser.dwarf import DwarfParser +from flure.parser.reflutter import ReFlutterDumpParser + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="Parse and reformat snapshot information") + parser.add_argument("input", help="Input file to parse") + parser.add_argument("input_type", choices=["dwarf", "reflutter"], help="Specify which parser should be used") + parser.add_argument("-o", "--output", help="Output file") + + args = parser.parse_args() + if args.input_type == "dwarf": + parser = DwarfParser(args.input) + elif args.input_type == "reflutter": + parser = ReFlutterDumpParser(args.input) + else: + raise Exception(f"Unknown input type {args.input_type}") + + if args.output is not None: + with open(args.output, 'w') as fp: + json.dump(parser.code_info.dump(), fp, indent=4) + else: + print(json.dumps(parser.code_info.dump(), indent=4)) diff --git a/rename_flutter_functions.py b/rename_flutter_functions.py new file mode 100644 index 0000000..af20883 --- /dev/null +++ b/rename_flutter_functions.py @@ -0,0 +1,18 @@ +import sys, os, json +import idaapi, ida_kernwin +try: + import flure +except ImportError: + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +idaapi.require("flure.code_info") +idaapi.require("flure.ida.patch_names") +from flure.code_info import CodeInfo +from flure.ida.patch_names import create_ida_folders + +if __name__ == "__main__": + function_info_file = ida_kernwin.ask_file(False, f"*.json", "Flutter snapshot function name filename") + if function_info_file is not None: + with open(function_info_file, 'r') as fp: + code_info = CodeInfo.load(json.load(fp)) + create_ida_folders(code_info) diff --git a/samples/normal.apk b/samples/normal.apk new file mode 100644 index 0000000..21fa596 Binary files /dev/null and b/samples/normal.apk differ diff --git a/samples/obfu.apk b/samples/obfu.apk new file mode 100644 index 0000000..228a373 Binary files /dev/null and b/samples/obfu.apk differ diff --git a/samples/obfu_debug_info/app.android-arm.symbols b/samples/obfu_debug_info/app.android-arm.symbols new file mode 100644 index 0000000..2684a72 Binary files /dev/null and b/samples/obfu_debug_info/app.android-arm.symbols differ diff --git a/samples/obfu_debug_info/app.android-arm64.symbols b/samples/obfu_debug_info/app.android-arm64.symbols new file mode 100644 index 0000000..018ae5a Binary files /dev/null and b/samples/obfu_debug_info/app.android-arm64.symbols differ