From fd8a6748678e1acc83f8061b9fa41cfb6ebc6970 Mon Sep 17 00:00:00 2001 From: Abhi <85984486+AbhiTheModder@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:38:55 +0530 Subject: [PATCH 1/7] fix ida script contains bad characters '#' and '|' Fixes #93 --- blutter/src/DartDumper.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/blutter/src/DartDumper.cpp b/blutter/src/DartDumper.cpp index dd06afa..699fa83 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) { @@ -842,4 +852,4 @@ void DartDumper::DumpObjects(const char* filename) of << dumpInstance(obj, simpleForm, nestedObj, 0); of << "\n\n"; } -} \ No newline at end of file +} From b822e38affcf16ed5f28e8d607951facdcb533ac Mon Sep 17 00:00:00 2001 From: Abhi <85984486+AbhiTheModder@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:55:39 +0530 Subject: [PATCH 2/7] fnNames: #0#4internal, #0#1internal gives invalid name in IDA due to '#' --- blutter/src/DartDumper.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/blutter/src/DartDumper.cpp b/blutter/src/DartDumper.cpp index 699fa83..209830a 100644 --- a/blutter/src/DartDumper.cpp +++ b/blutter/src/DartDumper.cpp @@ -54,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]; } From f3f69459e1dd04df1e83635b315b22ad07428123 Mon Sep 17 00:00:00 2001 From: Abhi <85984486+AbhiTheModder@users.noreply.github.com> Date: Mon, 12 Aug 2024 18:55:52 +0530 Subject: [PATCH 3/7] update readme with new usages --- README.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) 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 From 4400e4d6ac6b173b3084f38153c5147ce72c9b4b Mon Sep 17 00:00:00 2001 From: Abhi <85984486+AbhiTheModder@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:20:28 +0530 Subject: [PATCH 4/7] add flag to only generate IDA function names --- blutter.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/blutter.py b/blutter.py index 6e2ea35..13b2a82 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",) 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) From d1df1ff24a4755f6b439cd2da39ff1c437f1f058 Mon Sep 17 00:00:00 2001 From: Abhi <85984486+AbhiTheModder@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:21:35 +0530 Subject: [PATCH 5/7] Update CMakeLists.txt --- blutter/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) 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}) From 669842930d47aff966db81ebc229077d1b174e4f Mon Sep 17 00:00:00 2001 From: Abhi <85984486+AbhiTheModder@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:24:02 +0530 Subject: [PATCH 6/7] check `IDA_FCN` and generate comments --- blutter/src/DartDumper.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/blutter/src/DartDumper.cpp b/blutter/src/DartDumper.cpp index 209830a..ed4ec64 100644 --- a/blutter/src/DartDumper.cpp +++ b/blutter/src/DartDumper.cpp @@ -143,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()); @@ -162,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"; From 34f60057bd7d7b3fca406113118633c4da0e125d Mon Sep 17 00:00:00 2001 From: Abhi <85984486+AbhiTheModder@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:44:06 +0530 Subject: [PATCH 7/7] Update desc ida_fcn flag --- blutter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blutter.py b/blutter.py index 13b2a82..ae4b335 100644 --- a/blutter.py +++ b/blutter.py @@ -229,7 +229,7 @@ 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",) + 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: