JEB 4.17 ships with a Dart AOT (ahead-of-time) binary snapshot helper plugin to help with the analysis of pre-compiled Dart programs. A common use case for it may be to offer directions when reverse engineering Flutter apps compiled for Android x86/x64 or arm/aarch64 platforms.
Snapshots in ELF
Release-mode Flutter-based Android apps will generate AOT snapshots instead of shipping with bytecode or Dart code, like Debug-mode apps may choose to. The AOT snapshot contains a state of the Dart VM required to run the pre-compiled code.
The plugin supports AOT snapshots compiled with Dart version 2.10 to 2.17.
A snapshot is generally located in the
lib/<arch>/libapp.so files of an APK. Since Dart may be used outside of Flutter, or since the file name or location may change, a reliable way to locate such files is to look for an ELF
so exporting the following 4 symbols:
XxxSnapshotInstructions symbols point to pre-compiled machine code. However, getting a starting point when dealing with stripped or obfuscated binaries may prove difficult. The
XxxSnapshotData symbols point to Dart VM structures and objects that will be accessed by the executing code. That includes data elements such as pooled strings or arrays of immediate values. Snapshot data also include important metadata that will help restructure the hundreds or thousands of routines compiled in an AOT snapshot.
Using the Plugin
First, make sure that you are dealing Dart AOT snapshots or with a Flutter app containing precompiled AOT snapshots. Indeed other types of snapshots exist, such as JIT snapshots. The plugin does not provide help for those. In practice, non-AOT snapshots may be relatively easy to analyze, but you are unlikely to encounter them in the wild. Most Dart code or Flutter apps will be compiled and distributed in release mode. At best, some symbols and optional metadata may be left over. At worst, most will have been obfuscated (refer to Flutter’s
The plugin will automatically kick in and analyze AOT snapshots generated by Dart 2.10 (~Fall 2010) to Dart 2.17 (current at the time of writing). The analysis results will be placed in text sub-units located under the elf container unit. The code unit will be annotated (methods will be renamed, etc.), as explained in the next sections.
AOT snapshots contain lots of information. Deserializing them is relatively complicated, not to mention the fact that each revision of Dart changes the format — meaning that support will have to be added for Dart 2.18+ when that version ships… The plugin does not extract every potentially available bit of information. What is made available at this time is:
1- Basic information about the snapshots, such as version and features
2- The list of libraries, classes, and methods
3- A view of the primary pool strings
Aside from static information, the plugin also attempts to:
1- Rename methods. Release builds will strip the method names from the ELF file. However, the AOT snapshot information references all AOT methods as well as their names, classes, library, etc. The names provided in the snapshot information will be applied to unnamed native routines.
You will be able to locate the
main method, the entry-point of all Dart applications.
2- Annotate access to pooled strings. Native code accesses pooled items through a fixed register (containing an address into a pointer array to pooled elements). Below is a list of registers for the most common architectures:
arm : register r5 aarch64 : register x27 x64 : register r15
Pooled strings accessed on x64 binaries are marked as a meta-comment in the code unit, as follows:
Unfortunately, due to how the assembly code for arm64 binaries is generated, those comments cannot be generated on such binaries. However, decompilation will yield slightly more digestible code, e.g.:
Caveats & Conclusion
We recommend analyzing x64 or arm64 binaries, instead of their 32-bit x86 or arm counterparts, since the plugin may not parse everything properly in the latter cases. In particular, the functions are not mapped properly for arm 32-bit snapshots generated by recent versions of Dart (2.16’ish and above).
More could be done, in particular related to calling conventions (for proper decompilation), pseudo-code refactoring and restructuring (via
gendec IR plugins for instance), library code flagging (e.g. classes and their methods belonging to
dart::<well_known_namespace> could be visually standing out). Such additional features will be added depending on the feedback and the needs of the users. Please let us know your feedback via the usual means (Twitter, email, Slack).
Finally, thanks to Axelle Apvrille (@cryptax) for flagging Dart as something that JEB may be able to help with!
Discussion of the internal formats and binary details of AOT snapshots was out-of-scope in this blog. Readers interested in digging further should check the following resources:
- First, read through https://rloura.wordpress.com/2020/12/04/reversing-flutter-for-android-wip/ for an introduction to format generated by Dart version 2.10.
- You can proceed with https://blog.tst.sh/reverse-engineering-flutter-apps-part-1/ (as well as part 2) for a deeper dive into the snapshot structures as well as generated native code.
- At this point, you should be comfortable to dig through the source code at https://github.com/dart-lang/sdk. It is the ultimate source of truth, look no further 🙂 In particular, the
runtime/vm/*_snapshot.[h,cc]files (and related files, such as
raw_object.h) contain most information about the serialized snapshot formats.
- Bonus reading material: https://mrale.ph/dartvm/ to get a high-level understanding of the Dart VM.
Thank you for reading, until next time! – Nicolas