diff --git a/README.md b/README.md index ce61747..f41c01b 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,35 @@ pip3 install pyelftools requests ``` ## Usage -Extract "lib" directory from apk file -``` -python3 blutter.py path/to/app/lib/arm64-v8a out_dir +Blutter can analyze Flutter applications in several ways. + +### APK File +If you have an `.apk` file. Simply provide the path to the APK file and the output directory as arguments: +```shell +python3 blutter.py path/to/app.apk out_dir ``` -The blutter.py will automatically detect the Dart version from the flutter engine and call executable of blutter to get the information from libapp.so. -If the blutter executable for required Dart version does not exists, the script will automatically checkout Dart source code and compiling it. +### `.so` File(s) +Blutter can also analyze `.so` files directly. This can be done in two ways: + +1. **Analyzing `.so` files extracted from an APK:** + + If you have extracted the lib directory from an APK file, you can analyze it using Blutter. Provide the path to the lib directory and the output directory as arguments: + ```shell + python3 blutter.py path/to/app/lib/arm64-v8a out_dir + ``` + > The `blutter.py` will automatically detect the Dart version from the Flutter engine and use the appropriate executable to extract information from `libapp.so`. + +2. **Analyzing `libapp.so` with a known Dart version:** + + If you only have `libapp.so` and know its Dart version, you can specify it to Blutter. Provide the Dart version with `--dart-version` option, the path to `libapp.so`, and the output directory as arguments: + ```shell + python3 blutter.py --dart-version X.X.X_android_arm64 libapp.so out_dir + ``` + > Replace `X.X.X` with your lib dart version such as "3.4.2_android_arm64". + + +If the Blutter executable for the required Dart version does not exist, the script will automatically checkout the Dart source code and compile it. ## Update You can use ```git pull``` to update and run blutter.py with ```--rebuild``` option to force rebuild the executable @@ -83,4 +105,4 @@ python blutter.py path\to\lib\arm64-v8a build\vs --vs-sln - Object modification - Obfuscated app (still missing many functions) - Reading iOS binary -- Input as apk or ipa +- Input as ipa diff --git a/blutter.py b/blutter.py index 6e2ea35..ae4b335 100644 --- a/blutter.py +++ b/blutter.py @@ -22,12 +22,13 @@ class BlutterInput: - def __init__(self, libapp_path: str, dart_info: DartLibInfo, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no_analysis: bool): + def __init__(self, libapp_path: str, dart_info: DartLibInfo, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no_analysis: bool, ida_fcn: bool): self.libapp_path = libapp_path self.dart_info = dart_info self.outdir = outdir self.rebuild_blutter = rebuild_blutter self.create_vs_sln = create_vs_sln + self.ida_fcn = ida_fcn vers = dart_info.version.split('.', 2) if int(vers[0]) == 2 and int(vers[1]) < 15: @@ -42,6 +43,8 @@ def __init__(self, libapp_path: str, dart_info: DartLibInfo, outdir: str, rebuil self.name_suffix += '_no-compressed-ptrs' if no_analysis: self.name_suffix += '_no-analysis' + if ida_fcn: + self.name_suffix += "_ida-fcn" # derive blutter executable filename self.blutter_name = f'blutter_{dart_info.lib_name}{self.name_suffix}' self.blutter_file = os.path.join(BIN_DIR, self.blutter_name) + ('.exe' if os.name == 'nt' else '') @@ -77,7 +80,7 @@ def extract_libs_from_apk(apk_file: str, out_dir: str): flutter_file = os.path.join(out_dir, flutter_info.filename) return app_file, flutter_file -def find_compat_macro(dart_version: str, no_analysis: bool): +def find_compat_macro(dart_version: str, no_analysis: bool, ida_fcn: bool): macros = [] include_path = os.path.join(PKG_INC_DIR, f'dartvm{dart_version}') vm_path = os.path.join(include_path, 'vm') @@ -125,14 +128,17 @@ def find_compat_macro(dart_version: str, no_analysis: bool): if no_analysis: macros.append('-DNO_CODE_ANALYSIS=1') - + + if ida_fcn: + macros.append("-DIDA_FCN=1") + return macros def cmake_blutter(input: BlutterInput): blutter_dir = os.path.join(SCRIPT_DIR, 'blutter') builddir = os.path.join(BUILD_DIR, input.blutter_name) - macros = find_compat_macro(input.dart_info.version, input.no_analysis) + macros = find_compat_macro(input.dart_info.version, input.no_analysis, input.ida_fcn) my_env = None if platform.system() == 'Darwin': llvm_path = subprocess.run(['brew', '--prefix', 'llvm@16'], capture_output=True, check=True).stdout.decode().strip() @@ -171,7 +177,7 @@ def build_and_run(input: BlutterInput): # creating Visual Studio solution overrides building if input.create_vs_sln: - macros = find_compat_macro(input.dart_info.version, input.no_analysis) + macros = find_compat_macro(input.dart_info.version, input.no_analysis, input.ida_fcn) blutter_dir = os.path.join(SCRIPT_DIR, 'blutter') dbg_output_path = os.path.abspath(os.path.join(input.outdir, 'out')) dbg_cmd_args = f'-i {input.libapp_path} -o {dbg_output_path}' @@ -190,25 +196,25 @@ def build_and_run(input: BlutterInput): # execute blutter subprocess.run([input.blutter_file, '-i', input.libapp_path, '-o', input.outdir], check=True) -def main_no_flutter(libapp_path: str, dart_version: str, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no_analysis: bool): +def main_no_flutter(libapp_path: str, dart_version: str, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no_analysis: bool, ida_fcn: bool): version, os_name, arch = dart_version.split('_') dart_info = DartLibInfo(version, os_name, arch) - input = BlutterInput(libapp_path, dart_info, outdir, rebuild_blutter, create_vs_sln, no_analysis) + input = BlutterInput(libapp_path, dart_info, outdir, rebuild_blutter, create_vs_sln, no_analysis, ida_fcn) build_and_run(input) -def main2(libapp_path: str, libflutter_path: str, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no_analysis: bool): +def main2(libapp_path: str, libflutter_path: str, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no_analysis: bool, ida_fcn: bool): dart_info = get_dart_lib_info(libapp_path, libflutter_path) - input = BlutterInput(libapp_path, dart_info, outdir, rebuild_blutter, create_vs_sln, no_analysis) + input = BlutterInput(libapp_path, dart_info, outdir, rebuild_blutter, create_vs_sln, no_analysis, ida_fcn) build_and_run(input) -def main(indir: str, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no_analysis: bool): +def main(indir: str, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no_analysis: bool, ida_fcn: bool): if indir.endswith(".apk"): with tempfile.TemporaryDirectory() as tmp_dir: libapp_file, libflutter_file = extract_libs_from_apk(indir, tmp_dir) - main2(libapp_file, libflutter_file, outdir, rebuild_blutter, create_vs_sln, no_analysis) + main2(libapp_file, libflutter_file, outdir, rebuild_blutter, create_vs_sln, no_analysis, ida_fcn) else: libapp_file, libflutter_file = find_lib_files(indir) - main2(libapp_file, libflutter_file, outdir, rebuild_blutter, create_vs_sln, no_analysis) + main2(libapp_file, libflutter_file, outdir, rebuild_blutter, create_vs_sln, no_analysis, ida_fcn) if __name__ == "__main__": @@ -223,9 +229,10 @@ def main(indir: str, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no parser.add_argument('--no-analysis', action='store_true', default=False, help='Do not build with code analysis') # rare usage scenario parser.add_argument('--dart-version', help='Run without libflutter (indir become libapp.so) by specify dart version such as "3.4.2_android_arm64"') + parser.add_argument("--ida-fcn", action="store_true", default=False, help="Generate IDA function names script, Doesn't Generates Thread and Object Pool structs comments",) args = parser.parse_args() if args.dart_version is None: - main(args.indir, args.outdir, args.rebuild, args.vs_sln, args.no_analysis) + main(args.indir, args.outdir, args.rebuild, args.vs_sln, args.no_analysis, args.ida_fcn) else: - main_no_flutter(args.indir, args.dart_version, args.outdir, args.rebuild, args.vs_sln, args.no_analysis) + main_no_flutter(args.indir, args.dart_version, args.outdir, args.rebuild, args.vs_sln, args.no_analysis, args.ida_fcn) diff --git a/blutter/CMakeLists.txt b/blutter/CMakeLists.txt index 88d2b73..4770fd6 100644 --- a/blutter/CMakeLists.txt +++ b/blutter/CMakeLists.txt @@ -104,6 +104,9 @@ endif() if (NO_METHOD_EXTRACTOR_STUB) set(defines ${defines} NO_METHOD_EXTRACTOR_STUB) endif() +if (IDA_FCN) + set(defines ${defines} IDA_FCN) +endif() target_compile_definitions(${BINNAME} PRIVATE ${defines}) target_compile_options(${BINNAME} PRIVATE ${cc_opts}) diff --git a/blutter/src/DartDumper.cpp b/blutter/src/DartDumper.cpp index dd06afa..ed4ec64 100644 --- a/blutter/src/DartDumper.cpp +++ b/blutter/src/DartDumper.cpp @@ -32,6 +32,16 @@ static std::string getFunctionName4Ida(const DartFunction& dartFn, const std::st return "_anon_closure"; } + if (fnName.starts_with("#")) { + fnName.replace(0, 1, "@"); + } + + for (size_t pos = 0; ; pos += 1) { + pos = fnName.find("|_", pos); + if (pos == std::string::npos) break; + fnName.replace(pos, 2, "_"); + } + auto periodPos = fnName.find('.'); std::string prefix; if (dartFn.IsStatic() && dartFn.Kind() == DartFunction::NORMAL && periodPos != std::string::npos) { @@ -44,6 +54,18 @@ static std::string getFunctionName4Ida(const DartFunction& dartFn, const std::st fnName = fnName.substr(periodPos + 1); } + // fnNames: #0#4internal, #0#1internal gives invalid name in IDA due to '#' + // lib file: https://github.com/worawit/blutter/issues/93#issuecomment-2283490634 + for (size_t pos = 0; pos < fnName.size(); ++pos) { + if (fnName[pos] == '@' && pos + 1 < fnName.size() && fnName[pos + 1] == '#') { + fnName.replace(pos, 2, "_"); + } else if (fnName[pos] == '0' && pos + 1 < fnName.size() && fnName[pos + 1] == '#') { + fnName.replace(pos, 2, "0"); + } else if (fnName[pos] == '#') { + fnName[pos] = '_'; + } + } + if (OP_MAP.contains(fnName)) { return prefix + "op_" + OP_MAP[fnName]; } @@ -121,6 +143,7 @@ void DartDumper::Dump4Ida(std::filesystem::path outDir) } +#ifndef IDA_FCN // Note: create struct with a lot of member by ida script is very slow // use header file then adding comment is much faster auto comments = DumpStructHeaderFile((outDir / "ida_dart_struct.h").string()); @@ -140,6 +163,20 @@ def create_Dart_structs(): for (const auto& [offset, comment] : comments) { of << "\tida_struct.set_member_cmt(ida_struct.get_member(struc, " << offset << "), '''" << comment << "''', True)\n"; } +#else + auto comments = DumpStructHeaderFile((outDir / "ida_dart_struct.h").string()); + of << R"CBLOCK( +import os +def create_Dart_structs(): + sid1 = idc.get_struc_id("DartThread") + if sid1 != idc.BADADDR: + return sid1, idc.get_struc_id("DartObjectPool") + hdr_file = os.path.join(os.path.dirname(__file__), 'ida_dart_struct.h') + idaapi.idc_parse_types(hdr_file, idc.PT_FILE) + sid1 = idc.import_type(-1, "DartThread") + sid2 = idc.import_type(-1, "DartObjectPool") +)CBLOCK"; +#endif of << "\treturn sid1, sid2\n"; of << "thrs, pps = create_Dart_structs()\n"; @@ -842,4 +879,4 @@ void DartDumper::DumpObjects(const char* filename) of << dumpInstance(obj, simpleForm, nestedObj, 0); of << "\n\n"; } -} \ No newline at end of file +}