Dart AOT snapshot helper plugin to better analyze Flutter-based apps

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:

_kDartVmSnapshotInstructions
_kDartIsolateSnapshotInstructions
_kDartVmSnapshotData
_kDartIsolateSnapshotData

The 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 --obfuscate option).

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.

An aarch64 ELF file containing Dart AOT snapshots. The plugin generated reports in the dart_aot_snapshots sub-unit folder. Other information would be embedded into the native code unit itself (e.g. renamed routines, re-packaged routines, extra comments, etc.)is directly placed onto .

Textual Information

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

Basic information about AOT snapshots

2- The list of libraries, classes, and methods

Classes, methods, libraries present a snapshot. Here, we can see that most names were obfuscated.

3- A view of the primary pool strings

Pooled items (including strings), some of them may be used by the natively executed code.

Code Annotations

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:

0x1BFF / 8 (pointer size on 64-bit arch.) = 0x37F = 895

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.:

Pooled string access on an arm64 binary

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!

Further Reading

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:

Thank you for reading, until next time! – Nicolas