Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# B(l)utter
Flutter Mobile Application Reverse Engineering Tool by Compiling Dart AOT Runtime

Currently the application supports only Android libapp.so (arm64 only).
Currently the application supports Android libapp.so (arm64 only) and experimental iOS App.framework/App snapshot extraction (arm64).
Also the application is currently work only against recent Dart versions.

For high priority missing features, see [TODO](#todo)
Expand Down Expand Up @@ -53,7 +53,12 @@ Extract "lib" directory from apk file
```
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 call executable of blutter to get the information from libapp.so.
Or pass an IPA file / extracted iOS app bundle
```
python3 blutter.py path/to/app.ipa out_dir
python3 blutter.py path/to/Payload/Runner.app out_dir
```
The blutter.py will automatically detect the Dart version from the Flutter engine and call the blutter executable to dump the Dart AOT image.

If the blutter executable for required Dart version does not exists, the script will automatically checkout Dart source code and compiling it.

Expand Down Expand Up @@ -94,5 +99,4 @@ python blutter.py path\to\lib\arm64-v8a build\vs --vs-sln
- More internal classes
- Object modification
- Obfuscated app (still missing many functions)
- Reading iOS binary
- Input as apk or ipa
- iOS code analysis parity
61 changes: 49 additions & 12 deletions blutter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import mmap
import os
import platform
import re
import shutil
import subprocess
import sys
Expand Down Expand Up @@ -51,23 +52,34 @@ def find_lib_files(indir: str):
app_file = os.path.join(indir, 'libapp.so')
if not os.path.isfile(app_file):
app_file = os.path.join(indir, 'App')
if not os.path.isfile(app_file):
sys.exit("Cannot find libapp file")

flutter_file = os.path.join(indir, 'libflutter.so')
if not os.path.isfile(flutter_file):
flutter_file = os.path.join(indir, 'Flutter')
if not os.path.isfile(flutter_file):
sys.exit("Cannot find libflutter file")

if os.path.isfile(app_file) and os.path.isfile(flutter_file):
return os.path.abspath(app_file), os.path.abspath(flutter_file)

app_bundle_dirs = []
if os.path.isdir(os.path.join(indir, 'Frameworks')):
app_bundle_dirs.append(indir)
app_bundle_dirs.extend(glob.glob(os.path.join(indir, 'Payload', '*.app')))
app_bundle_dirs.extend(glob.glob(os.path.join(indir, '*.app')))

for app_dir in app_bundle_dirs:
app_file = os.path.join(app_dir, 'Frameworks', 'App.framework', 'App')
flutter_file = os.path.join(app_dir, 'Frameworks', 'Flutter.framework', 'Flutter')
if os.path.isfile(app_file) and os.path.isfile(flutter_file):
return os.path.abspath(app_file), os.path.abspath(flutter_file)

return os.path.abspath(app_file), os.path.abspath(flutter_file)
sys.exit("Cannot find libapp/App and libflutter/Flutter files")

def extract_libs_from_apk(apk_file: str, out_dir: str):
with zipfile.ZipFile(apk_file, "r") as zf:
try:
app_info = zf.getinfo('lib/arm64-v8a/libapp.so')
flutter_info = zf.getinfo('lib/arm64-v8a/libflutter.so')
except:
infos = {info.filename: info for info in zf.infolist()}
app_info = infos.get('lib/arm64-v8a/libapp.so')
flutter_info = infos.get('lib/arm64-v8a/libflutter.so')
if app_info is None or flutter_info is None:
sys.exit("Cannot find libapp.so or libflutter.so in the APK")

zf.extract(app_info, out_dir)
Expand All @@ -77,6 +89,27 @@ 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 extract_libs_from_ipa(ipa_file: str, out_dir: str):
app_re = re.compile(r'^Payload/[^/]+\.app/Frameworks/App\.framework/App$')
flutter_re = re.compile(r'^Payload/[^/]+\.app/Frameworks/Flutter\.framework/Flutter$')

with zipfile.ZipFile(ipa_file, "r") as zf:
app_infos = [info for info in zf.infolist() if app_re.match(info.filename)]
flutter_infos = [info for info in zf.infolist() if flutter_re.match(info.filename)]
if len(app_infos) == 0 or len(flutter_infos) == 0:
sys.exit("Cannot find App.framework/App or Flutter.framework/Flutter in the IPA")

app_info = app_infos[0]
app_prefix = app_info.filename.split('/Frameworks/', 1)[0]
flutter_info = next((info for info in flutter_infos if info.filename.startswith(app_prefix + '/')), flutter_infos[0])

zf.extract(app_info, out_dir)
zf.extract(flutter_info, out_dir)

app_file = os.path.join(out_dir, app_info.filename)
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):
macros = []
include_path = os.path.join(PKG_INC_DIR, f'dartvm{dart_version}')
Expand Down Expand Up @@ -221,10 +254,15 @@ def main2(libapp_path: str, libflutter_path: str, outdir: str, rebuild_blutter:
build_and_run(input)

def main(indir: str, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no_analysis: bool):
if indir.endswith(".apk"):
in_lower = indir.lower()
if in_lower.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)
elif in_lower.endswith(".ipa"):
with tempfile.TemporaryDirectory() as tmp_dir:
libapp_file, libflutter_file = extract_libs_from_ipa(indir, tmp_dir)
main2(libapp_file, libflutter_file, outdir, rebuild_blutter, create_vs_sln, no_analysis)
else:
libapp_file, libflutter_file = find_lib_files(indir)
main2(libapp_file, libflutter_file, outdir, rebuild_blutter, create_vs_sln, no_analysis)
Expand All @@ -234,8 +272,7 @@ def main(indir: str, outdir: str, rebuild_blutter: bool, create_vs_sln: bool, no
parser = argparse.ArgumentParser(
prog='B(l)utter',
description='Reversing a flutter application tool')
# TODO: accept ipa
parser.add_argument('indir', help='An apk or a directory that contains both libapp.so and libflutter.so')
parser.add_argument('indir', help='An apk, ipa, or a directory that contains Flutter app binaries')
parser.add_argument('outdir', help='An output directory')
parser.add_argument('--rebuild', action='store_true', default=False, help='Force rebuild the Blutter executable')
parser.add_argument('--vs-sln', action='store_true', default=False, help='Generate Visual Studio solution at <outdir>')
Expand Down
30 changes: 25 additions & 5 deletions blutter/src/CodeAnalyzer_arm64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ static VarValue* getPoolObject(DartApp& app, intptr_t offset, A64::Register dstR
}
}

#if defined(DART_COMPRESSED_POINTERS)
static inline void handleDecompressPointer(AsmIterator& insn, arm64_reg reg) {
INSN_ASSERT(insn.id() == ARM64_INS_ADD);
INSN_ASSERT(insn.ops(0).reg == insn.ops(1).reg && insn.ops(0).reg == reg);
Expand All @@ -196,6 +197,19 @@ static inline void handleExtraDecompressPointer(AsmIterator& insn, arm64_reg reg
if (!(insn.ops(2).reg == CSREG_DART_HEAP && insn.ops(2).shift.value == 32)) return;
++insn;
}
#else
static inline void handleDecompressPointer(AsmIterator&, arm64_reg) {}
static inline void handleExtraDecompressPointer(AsmIterator&, arm64_reg) {}
#endif

static bool isScaledRegOperand(const cs_arm64_op& op, arm64_reg reg, uint8_t scale)
{
if (ToCapstoneReg(op.reg) != ToCapstoneReg(reg))
return false;
if ((op.ext == ARM64_EXT_SXTW || op.ext == ARM64_EXT_UXTW) && op.shift.value == scale)
return true;
return op.ext == ARM64_EXT_INVALID && op.shift.type == ARM64_SFT_LSL && op.shift.value == scale;
}

// Handle leave-frame restore patterns for newer Dart ARM64 codegen.
// Accepted patterns:
Expand Down Expand Up @@ -923,7 +937,7 @@ void FunctionAnalyzer::handleFixedParameters(AsmIterator& insn, arm64_reg paramC
break;
INSN_ASSERT(insn.ops(1).reg == CSREG_DART_FP);
// shift only 2 because the number of parameter is Smi (tagged)
INSN_ASSERT(ToCapstoneReg(insn.ops(2).reg) == paramCntReg && insn.ops(2).ext == ARM64_EXT_SXTW && insn.ops(2).shift.value == 2);
INSN_ASSERT(isScaledRegOperand(insn.ops(2), paramCntReg, 2));
const auto tmpReg = insn.ops(0).reg;
++insn;

Expand Down Expand Up @@ -995,7 +1009,7 @@ void FunctionAnalyzer::handleOptionalPositionalParameters(AsmIterator& insn, arm
// parameter might not be used and no loading value
if (insn.id() == ARM64_INS_ADD && insn.ops(1).reg == CSREG_DART_FP) {
// shift only 2 because the number of parameter is Smi (tagged)
INSN_ASSERT(ToCapstoneReg(insn.ops(2).reg) == optionalParamCntReg && insn.ops(2).ext == ARM64_EXT_SXTW && insn.ops(2).shift.value == 2);
INSN_ASSERT(isScaledRegOperand(insn.ops(2), optionalParamCntReg, 2));
const auto tmpReg = insn.ops(0).reg;
++insn;

Expand Down Expand Up @@ -1137,7 +1151,7 @@ void FunctionAnalyzer::handleOptionalNamedParameters(AsmIterator& insn, arm64_re

INSN_ASSERT(insn.id() == ARM64_INS_ADD);
INSN_ASSERT(fnInfo->State()->GetValue(insn.ops(1).reg) == fnInfo->Vars()->ValArgsDesc());
INSN_ASSERT(insn.ops(2).reg == tmpReg && insn.ops(2).ext == ARM64_EXT_SXTW && insn.ops(2).shift.value == 1);
INSN_ASSERT(isScaledRegOperand(insn.ops(2), tmpReg, 1));
const auto tmpReg2 = insn.ops(0).reg;
++insn;

Expand Down Expand Up @@ -1340,7 +1354,7 @@ void FunctionAnalyzer::handleOptionalNamedParameters(AsmIterator& insn, arm64_re

INSN_ASSERT(insn.id() == ARM64_INS_ADD);
INSN_ASSERT(insn.ops(1).reg == CSREG_DART_FP);
INSN_ASSERT(insn.ops(2).reg == tmpReg && insn.ops(2).ext == ARM64_EXT_SXTW && insn.ops(2).shift.value == 2);
INSN_ASSERT(isScaledRegOperand(insn.ops(2), tmpReg, 2));
const auto tmpReg2 = insn.ops(0).reg;
++insn;

Expand Down Expand Up @@ -1584,7 +1598,7 @@ void FunctionAnalyzer::handleArgumentsDescriptorTypeArguments(AsmIterator& insn)

INSN_ASSERT(insn.id() == ARM64_INS_ADD);
INSN_ASSERT(insn.ops(1).reg == CSREG_DART_FP);
INSN_ASSERT(ToCapstoneReg(insn.ops(2).reg) == sizeReg && insn.ops(2).ext == ARM64_EXT_SXTW && insn.ops(2).shift.value == 2);
INSN_ASSERT(isScaledRegOperand(insn.ops(2), sizeReg, 2));
const auto tmpReg = insn.ops(0).reg;
fnInfo->State()->ClearRegister(tmpReg);
++insn;
Expand Down Expand Up @@ -2483,13 +2497,15 @@ std::unique_ptr<MoveRegInstr> FunctionAnalyzer::processMoveRegInstr(AsmIterator&

std::unique_ptr<DecompressPointerInstr> FunctionAnalyzer::processDecompressPointerInstr(AsmIterator& insn)
{
#if defined(DART_COMPRESSED_POINTERS)
if (insn.id() == ARM64_INS_ADD && insn.ops(2).reg == CSREG_DART_HEAP && insn.ops(2).shift.value == 32) {
INSN_ASSERT(insn.ops(0).reg == insn.ops(1).reg);
const auto reg = A64::Register{ insn.ops(0).reg };
const auto ins0_addr = insn.address();
++insn;
return std::make_unique<DecompressPointerInstr>(insn.Wrap(ins0_addr), reg);
}
#endif
return nullptr;
}

Expand Down Expand Up @@ -3022,7 +3038,11 @@ std::unique_ptr<ILInstr> FunctionAnalyzer::processLoadFieldTableInstr(AsmIterato

INSN_ASSERT(insn.ops(1).mem.base == tmp_reg);
load_offset |= insn.ops(1).mem.disp;
#if defined(DART_COMPRESSED_POINTERS)
const auto field_offset = load_offset >> 1;
#else
const auto field_offset = load_offset;
#endif

if (insn.id() == ARM64_INS_STR) {
const auto reg = A64::Register{ insn.ops(0).reg };
Expand Down
5 changes: 4 additions & 1 deletion blutter/src/DartApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PRAGMA_WARNING(pop)

DartApp::DartApp(const char* path) : ppool(NULL), nativeLib(0xdeadead), throwStubAddr(0)
{
auto libInfo = ElfHelper::MapLibAppSo(path);
auto libInfo = ElfHelper::MapLibApp(path);
lib_base = libInfo.lib;
vm_snapshot_data = libInfo.vm_snapshot_data;
vm_snapshot_instructions = libInfo.vm_snapshot_instructions;
Expand Down Expand Up @@ -580,6 +580,9 @@ void DartApp::finalizeFunctionsInfo()
// Note: Signature is dropped in most function
auto& func = dart::Function::Handle();
for (auto& [_, dartFn] : functions) {
if ((intptr_t)dartFn->ptr == (intptr_t)dart::Function::null())
continue;

func = dartFn->ptr;
const auto sigPtr = func.signature();
if (!sigPtr.IsHeapObject())
Expand Down
7 changes: 3 additions & 4 deletions blutter/src/DartFunction.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ struct DartFunctionSignature
std::vector<FnParam>& Params() { return params; }
FnParam& Param(int i) { return params[i]; }

DartAbstractType* returnType;
DartAbstractType* returnType{ nullptr };
//typeParams;
std::vector<FnParam> params;
int numOptionalParam;
bool hasNamedParam;
int numOptionalParam{ 0 };
bool hasNamedParam{ false };
};

class DartFunction : public DartFnBase
Expand Down Expand Up @@ -111,4 +111,3 @@ class DartFunction : public DartFnBase

friend class DartApp;
};

2 changes: 0 additions & 2 deletions blutter/src/Disassembler_arm64.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ constexpr arm64_reg CSREG_DART_WB_VALUE = ToCapstoneReg(dart::kWriteBarrierValue
constexpr arm64_reg CSREG_DART_WB_SLOT = ToCapstoneReg(dart::kWriteBarrierSlotReg);
constexpr arm64_reg CSREG_DART_THR = ToCapstoneReg(dart::THR);
constexpr arm64_reg CSREG_DART_PP = ToCapstoneReg(dart::PP);
#if defined(DART_COMPRESSED_POINTERS)
constexpr arm64_reg CSREG_DART_HEAP = ToCapstoneReg(dart::HEAP_BITS);
#endif
constexpr arm64_reg CSREG_DART_TMP = ToCapstoneReg(dart::TMP);
constexpr arm64_reg CSREG_DART_TMP2 = ToCapstoneReg(dart::TMP2);
// Note: kTagReg is normally in wrapper function. can ignore it.
Expand Down
Loading
Loading