├── .gitignore ├── README.md ├── add_dart_objects_in_decompiled_code.py ├── add_xref_to_dart_objects.py ├── create_dart_objects.py ├── flure ├── __init__.py ├── code_info.py ├── ida │ ├── __init__.py │ ├── create_segment.py │ ├── dart_obj_create.py │ ├── dart_obj_xref.py │ ├── dart_stack.py │ ├── object_pool_microcode.py │ ├── patch_names.py │ └── utils.py └── parser │ ├── __init__.py │ ├── dwarf.py │ └── reflutter.py ├── hooking └── dump_flutter_memory.js ├── map_dart_vm_memory.py ├── parse_info.py ├── patch_dart_stack_pointer.py ├── rename_flutter_functions.py └── samples ├── memory_dump ├── 0x7200000000 ├── 0x7200080000 └── ranges.json ├── normal.apk ├── normal_dump.dart ├── obfu.apk └── obfu_debug_info ├── app.android-arm.symbols └── app.android-arm64.symbols /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | *.py[cod] 3 | **/__pycache__/ 4 | **/.idea/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter-re-demo 2 | 3 | ## Overview 4 | 5 | This repository contains the files you need to reproduce our experiments on the feasibility of Flutter app reverse engineering. 6 | 7 | ### Renaming of Dart functions 8 | 9 | All details can be found in this [post](https://www.guardsquare.com/blog/current-state-and-future-of-reversing-flutter-apps). 10 | 11 | You can find the non-obfuscated and obfuscated version of the [NyaNya Rocket!](https://github.com/CaramelDunes/nyanya_rocket) Flutter game that we used in our experiments. 12 | 13 | You can use the [provided script](https://github.com/Guardsquare/flutter-re-demo/blob/main/parse_info.py) to format the output of [reFlutter](https://github.com/Impact-I/reFlutter) or DWARF file. 14 | Later on, you can use the output of this script to rename/sort functions in IDA Pro using [this IDA Python script](https://github.com/Guardsquare/flutter-re-demo/blob/main/rename_flutter_functions.py). 15 | 16 | ### Dealing with Dart decompilation 17 | 18 | All details can be found in this [post](https://www.guardsquare.com/blog/obstacles-in-dart-decompilation-and-the-impact-on-flutter-app-security). 19 | 20 | The application used in the post is [the obfuscated APK](https://github.com/Guardsquare/flutter-re-demo/blob/main/samples/obfu.apk). 21 | 22 | You can find the output of the Flutter memory dump [here](https://github.com/Guardsquare/flutter-re-demo/tree/main/samples/memory_dump). 23 | You can also generate it yourself using the [Frida script](https://github.com/Guardsquare/flutter-re-demo/blob/main/hooking/dump_flutter_memory.js). 24 | 25 | You can import the memory dump into IDA Pro with [this script](https://github.com/Guardsquare/flutter-re-demo/blob/main/map_dart_vm_memory.py). 26 | Don't forget to rebase your database before running this script. 27 | 28 | Once this is done, you can [create Dart object](https://github.com/Guardsquare/flutter-re-demo/blob/main/create_dart_objects.py) 29 | and [add cross-references](https://github.com/Guardsquare/flutter-re-demo/blob/main/add_xref_to_dart_objects.py) between Dart code and Dart objects. 30 | 31 | If you want to see Dart object in decompiled code, you can register this [decompilation plugin](https://github.com/Guardsquare/flutter-re-demo/blob/main/add_dart_objects_in_decompiled_code.py). 32 | 33 | You can also [patch Dart stack pointer register](https://github.com/Guardsquare/flutter-re-demo/blob/main/patch_dart_stack_pointer.py) to allow IDA Pro to identify function parameters and local variable. 34 | Please note that it can generate incorrect code in function which are using both ``X15`` and ``SP``, thus use it only if you now what you are doing. 35 | 36 | ## Disclaimer 37 | 38 | These scripts are only provided for education purposes and are not meant to be stable reverse engineering tools. 39 | -------------------------------------------------------------------------------- /add_dart_objects_in_decompiled_code.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import idaapi, ida_kernwin 3 | 4 | try: 5 | import flure 6 | except ImportError: 7 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 8 | 9 | idaapi.require("flure.ida.object_pool_microcode") 10 | from flure.ida.object_pool_microcode import X27ReplaceHook 11 | 12 | if __name__ == "__main__": 13 | try: 14 | x27_replace_hook.unhook() 15 | del x27_replace_hook 16 | except NameError as e: 17 | pass 18 | finally: 19 | object_pool_address = ida_kernwin.ask_addr(0x7200600040, "Please enter Object Pool address") 20 | if object_pool_address is not None: 21 | flure.ida.object_pool_microcode.OBJECT_POOL_PTR = object_pool_address 22 | x27_replace_hook = X27ReplaceHook() 23 | x27_replace_hook.hook() 24 | print("Microcode X27 hook registered") -------------------------------------------------------------------------------- /add_xref_to_dart_objects.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import idaapi, ida_kernwin 3 | try: 4 | import flure 5 | except ImportError: 6 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 7 | 8 | idaapi.require("flure.ida.dart_obj_xref") 9 | from flure.ida.dart_obj_xref import parse_code_and_add_dart_object_xref 10 | 11 | 12 | if __name__ == "__main__": 13 | object_pool_address = ida_kernwin.ask_addr(0x7200600040, "Please enter Object Pool address") 14 | if object_pool_address is not None: 15 | parse_code_and_add_dart_object_xref(object_pool_address) 16 | -------------------------------------------------------------------------------- /create_dart_objects.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import idaapi, ida_kernwin 3 | try: 4 | import flure 5 | except ImportError: 6 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 7 | 8 | idaapi.require("flure.ida.dart_obj_create") 9 | from flure.ida.dart_obj_create import DartObjectsCreator 10 | 11 | 12 | if __name__ == "__main__": 13 | object_pool_address = ida_kernwin.ask_addr(0x7200600040, "Please enter Object Pool address") 14 | if object_pool_address is not None: 15 | dart_object_creator = DartObjectsCreator(object_pool_address) 16 | dart_object_creator.create_all_objects() 17 | -------------------------------------------------------------------------------- /flure/__init__.py: -------------------------------------------------------------------------------- 1 | from flure.code_info import CodeInfo, ClassInfo, FunctionInfo 2 | 3 | LIBRARY_TOKEN = b"Library:'" 4 | CLASS_TOKEN = b" Class: " 5 | FUNCTION_TOKEN = b" Function " 6 | KNOWN_PREFIXES_IGNORED = [b"Random ", b"Map dynamic 64 | func_info = func_lines[0].strip()[:-1] 65 | func_name = func_info.split(b"'")[1].decode("ascii") 66 | func_signature = func_info.split(b"'")[2][1:].decode("ascii") 67 | # Code Offset: _kDartIsolateSnapshotInstructions + 0x00000000002ebfb0 68 | func_offset = func_lines[2].strip().split(b"+")[1].strip() 69 | func_relative_base = func_lines[2].strip().split(b"+")[0].split(b":")[1].strip().decode("ascii") 70 | return FunctionInfo(func_name, func_signature, int(func_offset, 16), func_relative_base) -------------------------------------------------------------------------------- /flure/code_info.py: -------------------------------------------------------------------------------- 1 | 2 | class FunctionInfo(object): 3 | def __init__(self, name, signature, offset, relative_base): 4 | self.name = name 5 | self.signature = signature 6 | self.offset = offset 7 | self.relative_base = relative_base 8 | 9 | @staticmethod 10 | def load(func_info_dict): 11 | return FunctionInfo(func_info_dict["name"], func_info_dict["signature"], 12 | func_info_dict["offset"], func_info_dict["relative_base"]) 13 | 14 | def dump(self): 15 | return { 16 | "name": self.name, 17 | "signature": self.signature, 18 | "offset": self.offset, 19 | "relative_base": self.relative_base, 20 | } 21 | 22 | 23 | class ClassInfo(object): 24 | def __init__(self, module_path, name, full_declaration): 25 | self.module_path = module_path 26 | self.name = name 27 | self.full_declaration = full_declaration 28 | self.functions = [] 29 | 30 | def add_function(self, func_info: FunctionInfo): 31 | self.functions.append(func_info) 32 | 33 | @staticmethod 34 | def load(class_info_dict): 35 | class_info = ClassInfo(class_info_dict["module"], class_info_dict["name"], class_info_dict["full_declaration"]) 36 | for func_info_dict in class_info_dict["functions"]: 37 | class_info.add_function(FunctionInfo.load(func_info_dict)) 38 | return class_info 39 | 40 | def dump(self): 41 | return { 42 | "module": self.module_path, 43 | "name": self.name, 44 | "full_declaration": self.full_declaration, 45 | "functions": [func_info.dump() for func_info in self.functions] 46 | } 47 | 48 | 49 | class CodeInfo(object): 50 | def __init__(self): 51 | self.classes = [] 52 | 53 | def add_classes(self, func_info: ClassInfo): 54 | self.classes.append(func_info) 55 | 56 | @staticmethod 57 | def load(code_info_dict): 58 | code_info = CodeInfo() 59 | for class_info_dict in code_info_dict["classes"]: 60 | code_info.add_classes(ClassInfo.load(class_info_dict)) 61 | return code_info 62 | 63 | def dump(self): 64 | return { 65 | "classes": [class_info.dump() for class_info in self.classes] 66 | } 67 | -------------------------------------------------------------------------------- /flure/ida/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/flutter-re-demo/578942ce6504489c48dea2045e9571c8b582566c/flure/ida/__init__.py -------------------------------------------------------------------------------- /flure/ida/create_segment.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | import ida_segment 3 | import ida_bytes 4 | 5 | 6 | def add_dartvm_segment(start_ea, name, perm, input_file): 7 | with open(input_file, "rb") as fp: 8 | seg_data = fp.read() 9 | end_ea = start_ea + len(seg_data) 10 | s = ida_segment.segment_t() 11 | s.start_ea = start_ea 12 | s.end_ea = end_ea 13 | s.perm = perm 14 | s.bitness = 2 15 | s.align = ida_segment.saRelByte 16 | idaapi.add_segm_ex(s, name, None, ida_segment.ADDSEG_OR_DIE) 17 | with open(input_file, "rb") as fp: 18 | ida_bytes.patch_bytes(start_ea, seg_data) 19 | 20 | -------------------------------------------------------------------------------- /flure/ida/dart_obj_create.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | import idc 4 | import ida_bytes, ida_name, ida_offset 5 | 6 | MAX_OBJ_NAME_LEN = 30 7 | 8 | def read_smi(smi_ptr): 9 | smi_data = idc.get_qword(smi_ptr) 10 | if smi_data & 1 == 0: 11 | return smi_data >> 1 12 | raise Exception(f"Invalid Smi at 0x{smi_ptr}: {smi_data}") 13 | 14 | 15 | def get_info_from_tag(tag): 16 | cid = (tag >> 16) & 0xffff 17 | size_tag = (tag >> 8) & 0xff 18 | is_canonical = (tag >> 4) & 0x1 19 | gc_data = (tag) & 0x0f 20 | return cid, size_tag, is_canonical, gc_data 21 | 22 | 23 | class DartObject(object): 24 | def __init__(self, dart_obj_ptr, create_object=False): 25 | self.dart_obj_ptr = dart_obj_ptr 26 | self.dart_obj_name = None 27 | self.tag, _ = struct.unpack(", X27, #0x,LSL# 14 | ADD_X27_PATTERN = "ADD (\S*), X27, #0x(\S*),LSL#(\S*)" 15 | RE_ADD_X27_PATTERN = re.compile(ADD_X27_PATTERN) 16 | # LDR , [,#0x] 17 | LDR_AFTER_ADD_PATTERN = "LDR (\S*), \[(\S*),#0x(\S*)\]" 18 | RE_LDR_AFTER_ADD_PATTERN = re.compile(LDR_AFTER_ADD_PATTERN) 19 | 20 | 21 | def get_dart_object_index_pattern_1(addr): 22 | # LDR , [X27,#0x] 23 | if idc.print_insn_mnem(addr) != "LDR": 24 | return None 25 | match_info = RE_LOAD_X27_PATTERN.match(idc.print_operand(addr, 1)) 26 | if not match_info: 27 | return None 28 | obj_index = int(match_info.group(1), 16) 29 | return obj_index 30 | 31 | 32 | def get_dart_object_index_pattern_2(addr): 33 | # To speed up a bit 34 | # if (idc.print_insn_mnem(addr) != "ADD") or (idc.print_insn_mnem(idc.next_head(addr)) != "LDR"): 35 | # return False 36 | # ADD , X27, #0x,LSL# 37 | disasm_line = idc.generate_disasm_line(addr, 0) 38 | add_match_info = RE_ADD_X27_PATTERN.match(disasm_line) 39 | if not add_match_info: 40 | return None 41 | disasm_line = idc.generate_disasm_line(idc.next_head(addr), 0) 42 | ldr_match_info = RE_LDR_AFTER_ADD_PATTERN.match(disasm_line) 43 | if not ldr_match_info: 44 | return None 45 | tmp_reg, index_high, index_shift = add_match_info.group(1), add_match_info.group(2), add_match_info.group(3) 46 | dst_reg, tmp_reg_2, index_low = ldr_match_info.group(1), ldr_match_info.group(2), ldr_match_info.group(3) 47 | if tmp_reg != tmp_reg_2: 48 | return None 49 | index_high = int(index_high, 16) 50 | index_low = int(index_low, 16) 51 | index_shift = int(index_shift, 10) 52 | obj_index = (index_high << index_shift) + index_low 53 | return obj_index 54 | 55 | 56 | def add_dart_object_xref(addr, object_pool_ptr, dart_object_index): 57 | dart_obj_ptr_ptr = object_pool_ptr + dart_object_index 58 | dart_obj_ptr = idc.get_qword(dart_obj_ptr_ptr) 59 | idc.add_dref(addr, dart_obj_ptr_ptr, idc.dr_R) 60 | if dart_obj_ptr & 1 == 1: 61 | dart_obj_ptr = dart_obj_ptr - 1 62 | idc.add_dref(addr, dart_obj_ptr, idc.dr_R) 63 | 64 | 65 | def check_and_add_dart_object_xref(addr, object_pool_ptr): 66 | dart_object_index = get_dart_object_index_pattern_1(addr) 67 | if dart_object_index is None: 68 | dart_object_index = get_dart_object_index_pattern_2(addr) 69 | addr = idc.next_head(addr) 70 | if dart_object_index is None: 71 | return False 72 | add_dart_object_xref(addr, object_pool_ptr, dart_object_index) 73 | return True 74 | 75 | 76 | def parse_code_and_add_dart_object_xref(object_pool_ptr): 77 | start_ea = 0 78 | ea = start_ea 79 | nb_xref_added = 0 80 | while ea < idc.BADADDR: 81 | xref_added = check_and_add_dart_object_xref(ea, object_pool_ptr) 82 | if xref_added: 83 | nb_xref_added += 1 84 | ea = idc.next_head(ea) 85 | print(f"Number of xref added to Dart Object {nb_xref_added}") 86 | -------------------------------------------------------------------------------- /flure/ida/dart_stack.py: -------------------------------------------------------------------------------- 1 | from keystone import * 2 | from capstone import * 3 | 4 | import idc 5 | import ida_bytes 6 | 7 | 8 | def replace_x15_by_sp(start_ea, end_ea, redo_analysis=True): 9 | ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) 10 | cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN) 11 | ea = start_ea 12 | while ea < end_ea: 13 | orig_ins = idc.generate_disasm_line(ea, 0) 14 | orig_ins_cs_info_list = [x for x in cs.disasm_lite(idc.get_bytes(ea, 4), ea, 1)] 15 | if len(orig_ins_cs_info_list) == 0: 16 | ea = idc.next_head(ea) 17 | continue 18 | orig_ins_cs_info = orig_ins_cs_info_list[0] 19 | orig_ins_cs = " ".join(orig_ins_cs_info[2:]) 20 | if "X15" in orig_ins: 21 | try: 22 | new_ins = orig_ins_cs.replace("x15", "SP") 23 | new_ins_asm = ks.asm(new_ins, ea, as_bytes=True)[0] 24 | ida_bytes.patch_bytes(ea, new_ins_asm) 25 | print(f"At 0x{ea:x}: Patching {orig_ins} -> {new_ins}") 26 | except KsError as e: 27 | print(f"At 0x{ea:x}: Can't patch {orig_ins} -> {new_ins}: {e}") 28 | except TypeError as e: 29 | print(f"At 0x{ea:x}: Can't patch {orig_ins} -> {new_ins}: {e}") 30 | ea = idc.next_head(ea) 31 | if redo_analysis: 32 | idc.plan_and_wait(start_ea, end_ea) -------------------------------------------------------------------------------- /flure/ida/object_pool_microcode.py: -------------------------------------------------------------------------------- 1 | import ida_hexrays 2 | 3 | OBJECT_POOL_PTR = 0 4 | 5 | def format_mop_t(mop_in: ida_hexrays.mop_t) -> str: 6 | if mop_in is None: 7 | return "mop_t is None" 8 | if mop_in.t > 15: 9 | return "Unknown mop type {0}".format(mop_in.t) 10 | return mop_in.dstr() 11 | 12 | 13 | class MyMopVisitor(ida_hexrays.mop_visitor_t): 14 | def visit_mop(self, op: ida_hexrays.mop_t, op_type, is_target: bool) -> "int": 15 | mop_info = format_mop_t(op) 16 | if is_target: 17 | return 0 18 | if op.t != ida_hexrays.mop_r: 19 | return 0 20 | if mop_info != "x27.8": 21 | return 0 22 | op.make_number(OBJECT_POOL_PTR, 8) 23 | return 0 24 | 25 | 26 | class X27ReplaceHook(ida_hexrays.Hexrays_Hooks): 27 | def microcode(self, mba: ida_hexrays.mba_t) -> "int": 28 | x = MyMopVisitor() 29 | return mba.for_all_ops(x) 30 | 31 | -------------------------------------------------------------------------------- /flure/ida/patch_names.py: -------------------------------------------------------------------------------- 1 | import idautils 2 | import ida_dirtree 3 | import idc 4 | import ida_name, ida_funcs 5 | 6 | from flure.code_info import CodeInfo 7 | from flure.ida.utils import safe_rename 8 | 9 | func_dir: ida_dirtree.dirtree_t = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS) 10 | 11 | SMALL_FUNC_MAPPING = { 12 | "==": "__equals__", 13 | ">>": "__rshift__", 14 | "<<": "__lshift__", 15 | "~/": "__truncdiv__", 16 | "[]=": "__list_set__", 17 | "unary-": "__neg__", 18 | "<=": "__inf_eq__", 19 | ">=": "__sup_eq__", 20 | "!=": "__neq__", 21 | "|": "__or__", 22 | "&": "__and__", 23 | "^": "__xor__", 24 | "+": "__add__", 25 | "*": "__mul__", 26 | "-": "__sub__", 27 | "<": "__inf__", 28 | ">": "__sup__", 29 | "%": "__mod__", 30 | "/": "__fiv__", 31 | "~": "__bnot__", 32 | } 33 | 34 | 35 | def create_ida_folders(code_info: CodeInfo): 36 | exported_entries = {} 37 | for entry in idautils.Entries(): 38 | exported_entries[entry[3]] = entry[2] 39 | for class_info in code_info.classes: 40 | dir_path = class_info.module_path.replace(":", "/") 41 | func_dir.mkdir(dir_path) 42 | class_name = class_info.name 43 | for func_info in class_info.functions: 44 | func_name = func_info.name 45 | if func_info.relative_base != 0: 46 | func_offset = func_info.offset + exported_entries[func_info.relative_base] 47 | else: 48 | func_offset = func_info.offset 49 | for k, v in SMALL_FUNC_MAPPING.items(): 50 | if func_name == k: 51 | func_name = v 52 | func_name = func_name.replace(":", "::") 53 | full_func_name = f"{class_name}::{func_name}" if class_info.name is not None else func_name 54 | 55 | ida_funcs.add_func(func_offset, idc.BADADDR) 56 | given_name = safe_rename(func_offset, full_func_name) 57 | func_dir.rename(given_name, f"{dir_path}/{given_name}") 58 | -------------------------------------------------------------------------------- /flure/ida/utils.py: -------------------------------------------------------------------------------- 1 | import idc 2 | import ida_name 3 | 4 | 5 | def safe_rename(offset, wanted_name, option=ida_name.SN_FORCE | ida_name.SN_NOCHECK): 6 | idc.set_name(offset, wanted_name, option) 7 | # Because ida_name.SN_FORCE and ida_name.SN_NOCHECK, the actual name can be different 8 | # from wanted_name, thus we read it from DB after it has been set 9 | return idc.get_name(offset) 10 | -------------------------------------------------------------------------------- /flure/parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/flutter-re-demo/578942ce6504489c48dea2045e9571c8b582566c/flure/parser/__init__.py -------------------------------------------------------------------------------- /flure/parser/dwarf.py: -------------------------------------------------------------------------------- 1 | from elftools.elf.elffile import ELFFile, SymbolTableSection 2 | 3 | from flure.code_info import CodeInfo, ClassInfo, FunctionInfo 4 | 5 | 6 | class DwarfParser(object): 7 | def __init__(self, filename): 8 | self.filename = filename 9 | self.code_info = CodeInfo() 10 | self.parse() 11 | 12 | @staticmethod 13 | def get_file_entries(dwarf_info, cu): 14 | line_program = dwarf_info.line_program_for_CU(cu) 15 | if line_program is None: 16 | print('Warning: DWARF info is missing a line program for this CU') 17 | return [] 18 | return line_program.header["file_entry"] 19 | 20 | def parse(self): 21 | known_symbol_address = self.parse_dwarf() 22 | self.parse_symbols_table(known_symbol_address) 23 | 24 | def parse_dwarf(self): 25 | known_symbol_address = [] 26 | with open(self.filename, 'rb') as f: 27 | elffile = ELFFile(f) 28 | if not elffile.has_dwarf_info(): 29 | raise Exception(f"File {self.filename} has no DWARF info") 30 | dwarf_info = elffile.get_dwarf_info() 31 | classes_by_name = {} 32 | for CU in dwarf_info.iter_CUs(): 33 | file_entries = self.get_file_entries(dwarf_info, CU) 34 | for DIE in CU.iter_DIEs(): 35 | if DIE.tag == 'DW_TAG_subprogram': 36 | if 'DW_AT_low_pc' not in DIE.attributes: 37 | continue 38 | low_pc = DIE.attributes['DW_AT_low_pc'].value 39 | if 'DW_AT_abstract_origin' not in DIE.attributes: 40 | raise Exception(f"Unknown DIE: {DIE}") 41 | abstract_origin = DIE.get_DIE_from_attribute('DW_AT_abstract_origin') 42 | full_func_name = abstract_origin.attributes['DW_AT_name'].value.decode("ascii") 43 | decl_file = file_entries[abstract_origin.attributes['DW_AT_decl_file'].value - 1].name.decode("ascii") 44 | if "." in full_func_name: 45 | class_name = full_func_name.split(".")[0] 46 | func_name = ".".join(full_func_name.split(".")[1:]) 47 | else: 48 | class_name = "" 49 | func_name = full_func_name 50 | if " " in class_name: 51 | if (class_name.split(" ")[0] != "new") or (len(class_name.split(" ")) != 2): 52 | raise Exception(f"Not-handled space in '{full_func_name}'") 53 | class_name = class_name.split(" ")[1] 54 | if class_name == "": 55 | class_name = "__null__" 56 | full_class_name = ".".join([decl_file, class_name]) 57 | if full_class_name not in classes_by_name: 58 | classes_by_name[full_class_name] = ClassInfo(decl_file, class_name, "") 59 | classes_by_name[full_class_name].add_function(FunctionInfo(func_name, "", low_pc, 0)) 60 | known_symbol_address.append(low_pc) 61 | for full_class_name, class_info in classes_by_name.items(): 62 | self.code_info.add_classes(class_info) 63 | return known_symbol_address 64 | 65 | def parse_symbols_table(self, known_symbol_address): 66 | known_symbol_address = known_symbol_address if known_symbol_address is not None else [] 67 | with open(self.filename, 'rb') as f: 68 | elffile = ELFFile(f) 69 | precompiled_code = ClassInfo("precompiled", None, None) 70 | for section in elffile.iter_sections(): 71 | if isinstance(section, SymbolTableSection): 72 | for symbol in section.iter_symbols(): 73 | symbol_address = symbol.entry['st_value'] 74 | if symbol_address not in known_symbol_address: 75 | precompiled_code.add_function(FunctionInfo(symbol.name, "", symbol.entry['st_value'], 0)) 76 | self.code_info.add_classes(precompiled_code) 77 | -------------------------------------------------------------------------------- /flure/parser/reflutter.py: -------------------------------------------------------------------------------- 1 | from flure.code_info import CodeInfo, ClassInfo, FunctionInfo 2 | 3 | LIBRARY_TOKEN = b"Library:'" 4 | CLASS_TOKEN = b"Class: " 5 | FUNCTION_TOKEN = b" Function " 6 | KNOWN_PREFIXES_IGNORED = [b"Random ", b"Map dynamic 64 | func_info = func_lines[0].strip()[:-1] 65 | func_name = func_info.split(b"'")[1].decode("ascii") 66 | func_signature = func_info.split(b"'")[2][1:].decode("ascii") 67 | # Code Offset: _kDartIsolateSnapshotInstructions + 0x00000000002ebfb0 68 | func_offset = func_lines[2].strip().split(b"+")[1].strip() 69 | func_relative_base = func_lines[2].strip().split(b"+")[0].split(b":")[1].strip().decode("ascii") 70 | return FunctionInfo(func_name, func_signature, int(func_offset, 16), func_relative_base) -------------------------------------------------------------------------------- /hooking/dump_flutter_memory.js: -------------------------------------------------------------------------------- 1 | var FLUTTER_MEM_START = 0x7200000000 2 | var FLUTTER_MEM_END = 0x7300000000 3 | var FLUTTER_MEM_MASK = 0xff00000000 4 | var SHARED_PREF_GET_INSTANCE_OFFSET = 0x6D4F88 5 | var APP_DATA_DIR = "/data/data/fr.carameldunes.nyanyarocket/" 6 | 7 | 8 | function dump_memory(start_address, end_address, dump_directory){ 9 | let modules = Process.enumerateRanges("r--"); 10 | let i, module; 11 | let module_file; 12 | 13 | module_file = new File(dump_directory + "ranges.json", "wb"); 14 | module_file.write(JSON.stringify(modules, null, 2)); 15 | module_file.close(); 16 | for (i = 0; i < modules.length; i++) { 17 | try { 18 | module = modules[i]; 19 | if ((module.base.compare(start_address) >= 0) && (module.base.compare(end_address) <= 0)) { 20 | console.log(`Dumping memory into ${dump_directory + module.base}`); 21 | module_file = new File(dump_directory + module.base, "wb"); 22 | module_file.write(module.base.readByteArray(module.size)); 23 | module_file.close(); 24 | } 25 | } catch(ex) { 26 | console.log(ex); 27 | console.log(JSON.stringify(module, null, 2)); 28 | } 29 | } 30 | } 31 | 32 | function hook_libapp() { 33 | var base_address = Module.findBaseAddress("libapp.so"); 34 | console.log(`Hooking libapp: ${base_address} `); 35 | Interceptor.attach(base_address.add(SHARED_PREF_GET_INSTANCE_OFFSET), { 36 | onEnter: function (args) { 37 | console.log(`SharedPreferences::getInstance() `); 38 | console.log(` X27: ${this.context.x27}`) 39 | if (this.context.x27.and(FLUTTER_MEM_MASK) == FLUTTER_MEM_START){ 40 | dump_memory(FLUTTER_MEM_START, FLUTTER_MEM_END, APP_DATA_DIR) 41 | }else{ 42 | console.error(`Default flutter memory ${ptr(FLUTTER_MEM_START)} seems incoherent with X27 ${this.context.x27}`) 43 | console.error(`Please modify FLUTTER_MEM_START, FLUTTER_MEM_END`) 44 | } 45 | } 46 | }); 47 | } 48 | 49 | 50 | var already_hooked = false; 51 | function hook_dlopen(target_lib_name, lib_hook_callbacks) { 52 | Interceptor.attach(Module.findExportByName(null, "dlopen"), { 53 | onEnter: function (args) { 54 | let lib_name = args[0].readCString(); 55 | this.do_hook = false; 56 | if (lib_name == target_lib_name) { 57 | if (!already_hooked) { 58 | this.do_hook = true; 59 | already_hooked = true; 60 | } 61 | } 62 | }, 63 | onLeave: function (retval) { 64 | if (this.do_hook) { 65 | lib_hook_callbacks() 66 | } 67 | } 68 | }); 69 | } 70 | hook_dlopen("libapp.so", hook_libapp) 71 | // frida -U -f fr.carameldunes.nyanyarocket -l dump_flutter_memory.js --no-pause 72 | -------------------------------------------------------------------------------- /map_dart_vm_memory.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import idaapi, ida_kernwin, ida_segment 3 | 4 | try: 5 | import flure 6 | except ImportError: 7 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 8 | 9 | idaapi.require("flure.ida.create_segment") 10 | from flure.ida.create_segment import add_dartvm_segment 11 | 12 | if __name__ == "__main__": 13 | ro_memory_file_name = ida_kernwin.ask_file(False, 14 | f"/Users/boris/Desktop/demo_flutter/obfu/dump_mem/0x7200000000", 15 | "Flutter RO memory filename") 16 | if ro_memory_file_name is not None: 17 | try: 18 | guessed_ro_memory_address = int(os.path.basename(ro_memory_file_name), 16) 19 | except ValueError: 20 | guessed_ro_memory_address = 0 21 | ro_memory_address = ida_kernwin.ask_addr(guessed_ro_memory_address, "Please enter Flutter RO memory address") 22 | if ro_memory_address is not None: 23 | print(f"Mapping {ro_memory_file_name} at 0x{ro_memory_address:x}") 24 | add_dartvm_segment(ro_memory_address, "flutter_ro", ida_segment.SEGPERM_READ, 25 | ro_memory_file_name) 26 | 27 | rw_memory_file_name = ida_kernwin.ask_file(False, 28 | f"/Users/boris/Desktop/demo_flutter/obfu/dump_mem/0x7200080000", 29 | "Flutter RW memory filename") 30 | if rw_memory_file_name is not None: 31 | try: 32 | guessed_rw_memory_address = int(os.path.basename(rw_memory_file_name), 16) 33 | except ValueError: 34 | guessed_rw_memory_address = 0 35 | rw_memory_address = ida_kernwin.ask_addr(guessed_rw_memory_address, "Please enter Flutter RW memory address") 36 | if rw_memory_address is not None: 37 | print(f"Mapping {rw_memory_file_name} at 0x{rw_memory_address:x}") 38 | add_dartvm_segment(rw_memory_address, "flutter_rw", ida_segment.SEGPERM_WRITE | ida_segment.SEGPERM_READ, 39 | rw_memory_file_name) 40 | 41 | 42 | -------------------------------------------------------------------------------- /parse_info.py: -------------------------------------------------------------------------------- 1 | import json 2 | import argparse 3 | 4 | 5 | from flure.parser.dwarf import DwarfParser 6 | from flure.parser.reflutter import ReFlutterDumpParser 7 | 8 | 9 | if __name__ == '__main__': 10 | parser = argparse.ArgumentParser(description="Parse and reformat snapshot information") 11 | parser.add_argument("input", help="Input file to parse") 12 | parser.add_argument("input_type", choices=["dwarf", "reflutter"], help="Specify which parser should be used") 13 | parser.add_argument("-o", "--output", help="Output file") 14 | 15 | args = parser.parse_args() 16 | if args.input_type == "dwarf": 17 | parser = DwarfParser(args.input) 18 | elif args.input_type == "reflutter": 19 | parser = ReFlutterDumpParser(args.input) 20 | else: 21 | raise Exception(f"Unknown input type {args.input_type}") 22 | 23 | if args.output is not None: 24 | with open(args.output, 'w') as fp: 25 | json.dump(parser.code_info.dump(), fp, indent=4) 26 | else: 27 | print(json.dumps(parser.code_info.dump(), indent=4)) 28 | -------------------------------------------------------------------------------- /patch_dart_stack_pointer.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import idaapi, idautils, ida_kernwin 3 | import idc 4 | 5 | try: 6 | import flure 7 | except ImportError: 8 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 9 | 10 | idaapi.require("flure.ida.dart_stack") 11 | from flure.ida.dart_stack import replace_x15_by_sp 12 | 13 | 14 | def get_text_segment_bound(): 15 | for seg_ea in idautils.Segments(): 16 | seg_name = idc.get_segm_name(seg_ea) 17 | if seg_name == ".text": 18 | return idc.get_segm_start(seg_ea), idc.get_segm_end(seg_ea) 19 | return 0, idc.BADADDR 20 | 21 | 22 | if __name__ == "__main__": 23 | default_start_ea, default_end_ea = get_text_segment_bound() 24 | start_ea = ida_kernwin.ask_addr(default_start_ea, "Please enter start of code") 25 | if start_ea is not None: 26 | end_ea = ida_kernwin.ask_addr(default_end_ea, "Please enter end of code") 27 | if end_ea is not None: 28 | replace_x15_by_sp(start_ea, end_ea, redo_analysis=True) 29 | -------------------------------------------------------------------------------- /rename_flutter_functions.py: -------------------------------------------------------------------------------- 1 | import sys, os, json 2 | import idaapi, ida_kernwin 3 | try: 4 | import flure 5 | except ImportError: 6 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 7 | 8 | idaapi.require("flure.code_info") 9 | idaapi.require("flure.ida.patch_names") 10 | from flure.code_info import CodeInfo 11 | from flure.ida.patch_names import create_ida_folders 12 | 13 | if __name__ == "__main__": 14 | function_info_file = ida_kernwin.ask_file(False, f"*.json", "Flutter snapshot function name filename") 15 | if function_info_file is not None: 16 | with open(function_info_file, 'r') as fp: 17 | code_info = CodeInfo.load(json.load(fp)) 18 | create_ida_folders(code_info) 19 | -------------------------------------------------------------------------------- /samples/memory_dump/0x7200000000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/flutter-re-demo/578942ce6504489c48dea2045e9571c8b582566c/samples/memory_dump/0x7200000000 -------------------------------------------------------------------------------- /samples/memory_dump/0x7200080000: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/flutter-re-demo/578942ce6504489c48dea2045e9571c8b582566c/samples/memory_dump/0x7200080000 -------------------------------------------------------------------------------- /samples/normal.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/flutter-re-demo/578942ce6504489c48dea2045e9571c8b582566c/samples/normal.apk -------------------------------------------------------------------------------- /samples/obfu.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/flutter-re-demo/578942ce6504489c48dea2045e9571c8b582566c/samples/obfu.apk -------------------------------------------------------------------------------- /samples/obfu_debug_info/app.android-arm.symbols: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/flutter-re-demo/578942ce6504489c48dea2045e9571c8b582566c/samples/obfu_debug_info/app.android-arm.symbols -------------------------------------------------------------------------------- /samples/obfu_debug_info/app.android-arm64.symbols: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/flutter-re-demo/578942ce6504489c48dea2045e9571c8b582566c/samples/obfu_debug_info/app.android-arm64.symbols --------------------------------------------------------------------------------