In this third article, written by Jordi Ventayol, one of our Security Engineers, we will continue exploring the security landscape of the Flutter framework. In this opportunity we will look into code obfuscation once compiled, but from the execution flow perspective.
Building upon our previous discussion on data obfuscation, this article aims to resolve how Flutter conceals the operations it performs within the execution flow, thereby reinforcing the security measures in place. This perspective is indispensable in comprehending how the framework safeguards against potential threats, ensuring the resilience of applications developed using Flutter.
To grasp the full context of our analysis, we strongly recommend revisiting our second article, where we carefully examine obfuscation from a data-centric viewpoint. The insights gained from that exploration will serve as a foundation for understanding the execution flow obfuscation discussed below.
Analyzing the obfuscation from the execution flow perspective
When we talk about obfuscation from the execution flow perspective, we refer to how the program hides the operations it’s performing. A basic example would be:
A = x + y
A = (x ⊕ y) + 2 × (x ∧ y)
Both of the above formulas calculate the value of variable ‘A’ with an addition of the values ‘x’ and ‘y’. However, in the second representation, it’s much more complicated for an attacker to realize this, as the operation is obfuscated using a technique called Mixed Boolean-Arithmetic.
The execution flow can be obfuscated using these techniques to make it difficult for an attacker to determine which function is being called, with which parameters, to which part of the code the program is jumping, or how many times a piece of the code is being executed.
We will analyze how many of these features the Flutter framework provides to determine the level of obfuscation.
As we mentioned in the previous article, when compiling the application, the Flutter framework generates two libraries. In the case of Android, these are libapp.so and libflutter.so.
If we analyze libapp.so, where our compiled code in “*.dart” classes resides, we see that the file isn’t the typical ELF executable. Instead, complex structures are observed, and it seems that the code is serialized.
We must remember that Dart code uses a specific Stack and Heap. This causes decompilers like IDA Pro (we’ve tested version 8.3) to be unable to reconstruct the code correctly.
At this point, two strategies could be executed. The first is to develop a tool that deserializes the libapp.so code, which would require significant effort. A better strategy is to use the Flutter library itself, libflutter.so, to do this work for us since it’s embedded in our application for this same purpose.
In summary, among other things, the purpose of libflutter.so is to deserialize our Dart code located in the ‘_kDartIsolateSnapshotInstructions’ segment (in the ‘.text’ executable code section) and, together with other components, execute the logic of the Dart virtual machine which will, in turn, run our application code.
Publicly, there are projects like “reFlutter” where, semi-automatically, the modification of libflutter.so is facilitated, allowing, in addition to deserializing the code, easier code injection for the attacker.
Among the various modifications applied to the library, you can find:
- Displaying function parameters in the terminal.
- Adding a proxy to network classes and methods.
- Identifying a function’s address with a symbol. (Very useful when you want to instrument specific functions with hooking frameworks, for example).
Once you run the application with the modified libflutter.so library, the reverse engineering process would be the same as when performed on a standard application without any protection.
This means that the attacker could now obtain the offsets of the functions and use instrumentation frameworks to analyze these functions, parameters, return values, and the function’s internal instructions.
Therefore, although the reverse engineering process seems more cumbersome than for a standard Android Application, instrumentation attacks would apply in the same way.
It’s essential to note that although the process has been detailed for Android, it would be practically the same for iOS.Remember that the libflutter.so library on Android appears as a dynamic library in the Frameworks / Flutter.framework folder of our IPA.
Conclusions of our analysis
Upon our meticulous examination of Flutter’s execution flow obfuscation, it becomes apparent that Flutter’s offered obfuscation is too basic to add difficulty against an attacker trying to reverse our application. The mere serialization of the code and the removal of method names could be considered a very basic and insufficient protection layer.
Recognizing these limitations, at Build38, we advocate a proactive approach to security. We have implemented a security library that protects against static modification attacks and runtime instrumentation.
Firstly, the modification of the libflutter.so library would be detected and aborted, preventing the attacker from successfully developing this initial deserialization stage. Secondly, once the libflutter library is modified to deserialize libapp.so, the attacks are performed as a conventionally developed application since the methods are attempted to be instrumented through hooking, debugging, and emulation techniques.
This means the application should detect and prevent attacks performed by instrumentation tools. This multifaceted defense strategy ensures that Flutter applications remain resilient against external threats, reinforcing the security posture beyond the framework’s inherent capabilities.
In our upcoming posts, we will continue to explore Flutter’s security features in detail to learn more about its capabilities. For more information on how to protect your Mobile Applications, don’t hesitate to contact us here.