During the assessment of one of the financial applications built upon the flutter framework, we came across that the application was using PGP encryption for encrypting the API requests. It is pretty common for financial applications to be implementing traffic encryption, with AES seen to be the preferred algorithm for encrypting traffic. There is plenty of research already available on decrypting AES encrypted traffic.
In this blog post, I would like to explain the methodology we followed for decrypting the PGP encrypted traffic in this case along with the challenges that flutter possess from the point of view of reverse engineering and dynamic instrumentation.
- Burp Suite
Examining the application traffic in burp, one can see that the request and response are encrypted. To conduct a thorough security assessment, it is necessary to figure out a way to get plain text traffic of an application.
The first step usually is to figure out what kind of encryption mechanism the application is using for encrypting the traffic. In this case, this was pretty straightforward. Just by base64 decoding the request/response data we could see that the application is using the OpenPGP library.
The next step usually is to decompile the application and go through the source code to see if there are any hard-coded encryption keys. This can easily be done by just opening the application in jadx for java/kotlin-based applications. But since this application is written using the flutter framework, the core application logic will be in libapp.so file which we cannot just throw into jadx.
Before moving forward, a few details about the libapp.so format.
The libapp.so file of a Flutter release build contains two snapshots: one for the VM isolate, and another for the isolate with actual substance. Each of these is split into a data section and an instructions section. The data section contains the isolate’s heap, whereas the instructions section contains the natively compiled code. Since the compiled code needs to be loaded into executable memory, it is placed in the .text section of the ELF file. The rest of the isolate’s heap, containing the VM object descriptions, is placed in the non-executable .rodata section.
You can refer to the following blog post for more details regarding the flutter architecture.
One good tool for reverse engineering flutter application is reflutter. ReFlutter modifies dart.cc to print classes, functions and rebuilds the flutter engine (libflutter.so). Once the apk is rebuilt with ReFlutter we can launch the application on the device and run the following command:
➜ ~ adb logcat -e reflutter | sed 's/.*DartVM//' >> reflutter.txt
Examining the reflutter.txt file, I could get the encryption key, decryption key, and passcode used for encrypting the traffic.
Here, the decryption key is basically a base64 encoded RSA private key. Using this private key along with the keyPassword, it was possible to decrypt the response data.
But, the request body couldn't be decrypted using this private key. Comparing the public key (EncryptionKey) with the private key, it could be concluded that the application was using different pairs of public-private keys for encryption and decryption.
One approach that can be used to get a plain text request body is to hook into the method responsible for encryption and then print out the plain text request passed to the encryption function passed as a parameter. Again going back to ReFlutter output, one function that looks interesting for our use case is “getEncryptedPGPRequest”.
For hooking into getEncryptedPGPRequest function using frida, we would need to know the offset of this function. For this, I will be using Doldrums.
Doldrums is a reverse engineering tool for Flutter apps targeting Android. Concretely, it is a parser and information extractor for the Flutter/Dart Android binary. It outputs a full dump of all classes and functions present in the isolate snapshot, along with its offsets.
So now we launch the application with frida and search for the base address of libapp.so library. We would be adding the offset of getEncryptedPGPRequest function to the base address of libapp.so library.
var address = Module.findBaseAddress('libapp.so').add(0x3e8eb4)
Now, we can use Interceptor to print out the parameter passed to this function:
Running the application with the above script gives us the plain text request body.
The output is not perfect, but does the job :)
As of the writing of this blog, Doldrums only supports parsing of VM snapshots that are produced using Dart SDK 2.12 and below. This is mainly because the format of Dart snapshot changes constantly. The developers of any Dart RE tool like doldrums will have a huge task of constantly monitoring for changes in the snapshot format and maintaining the tool.