Zygisk-based reFlutter

I developed a Zygisk module for rooted Android phones with Magisk (and Zygisk enabled). This module allows you to “reFlutter” your Flutter App at runtime, simplifying the testing and reverse engineering processes.

If you don’t want to read the detail, the release is available at:

https://github.com/yohanes/zygisk-reflutter

reFlutter

Before discussing zygisk-reflutter and how it works, I want to discuss what is reFlutter, how it works, and why you need it. and what is the problem that a zygisk-based solution solves?

I have discussed the problems of reversing a Flutter-based app in this 2021 post: Reverse Engineering a Flutter app by recompiling Flutter Engine. My proposed solution is to recompile the flutter engine (libflutter.so) because the binary format of flutter is not stable and not documented.

In 2022, a similar concept was introduced by someone from PT Swarm (detailed in their write-up). They created reFlutter, which includes a GitHub action to recompile all released Flutter engines, and a Python script that:

  1. Extracts the current hash of libflutter used in an APK or IPA.
  2. Downloads a prebuilt library matching the hash (created by the GitHub action).
  3. Patches libflutter.so with a user-provided proxy IP address.
  4. Replaces the original libflutter.so in the APK with the patched version.

To use this tool, what we need to do is:

  1. Acquire the APK or IPA.
  2. Run reflutter <APK/IPA>.
  3. Re-sign the APK/IPA.
  4. Install the APK/IPA, removing the original app if necessary.

This tool is great, and I have been using it since it was released. But there are two problems that I encountered when using this tool:

  1. Some app checks their signature, and repacking APK will change the signature (usually, I will resort to in-memory patching with Frida)
  2. It takes quite some time to extract APKs from a device, reflutter, re-sign, remove the app, and reinstall the new one
  3. If we need to compare the unpatched and patched binary, we need to reinstallt he app

What I want to have is a tool that can replace this library at runtime, solving the above problems.

Magisk and Zygisk

Magisk is a suite of open source software for customizing Android. You will need a phone with unlockable bootloader to use this. There are several features of Magisk, but the one that is useful for this is: Zygisk.

Zygisk can inject a code that can run in every Android applications’ processes. To do this: we can create a shared library making use of Zygisk API, package it in a zip file and install it using Magisk Manager.

The only documentation for Zygisk is the sample code in this repository:

https://github.com/topjohnwu/zygisk-module-sample

My understanding of Zygisk comes from studying various open-source Zygisk module repositories. The Zygisk API is straightforward, but diagnosing issues can be challenging.

For example, creating a JNI project in Android Studio links to libandroid by default, which is fine unless you use a companion process. The companion process will stop working when you connect to it (solved by removing -landroid).

As I am not a Zygisk expert, my approach might not be optimal. I am open to suggestions for improvement. The GUI part of the app is also not very good. I am not a front end Android programmer and half of the GUI the code was written with the help of Copilot.

Library Replacement

Assuming that we have a replacement flutter library available (downloaded from the release page of reFlutter), we can hook android_dlopen_ext using pltHookRegister (a Zygisk API) and pass in the new library.

How do I know to hook android_dlopen_ext? The easy method is just by guessing. But i found by tracing the calls from System.loadlibrary:

  • System.loadlibrary,will call: Runtime.nativeLoad (implemented in libcore/ojluni/src/main/native/Runtime.c).
  • Runtime.nativeLoad will call JVM_NativeLoad (in art/openjdkjvm/OpenjdkJvm.cc).
  • JVM_NativeLoad will call LoadNativeLibrary in art/runtime/jni/java_vm_ext.cc
  • LoadNativeLibrary will call OpenNativeLibrary (in art/libnativeloader/native_loader.cpp)
  • OpenNativeLibrary will call android_dlopen_ext (in bionic/libdl/libdl.cpp)

However, due to Android security, I was unable to load .so from /data/local/tmp/ or its subdirectories, even when I verify that the .so file is readable and executable. But if the library.so is in the app’s data directory, then android_dlopen_ext will work.

There are two kind of libraries provided by the reFlutter project: only for proxying and for class dumping. To make the explanation simpler, I will only discuss the proxying case.

So to make this work, I made an app that:

  1. Lists all installed Android apps.
  2. Extracts the Flutter hash from Flutter apps upon selection.
  3. Allows for downloading libflutter.so from reFlutter for chosen apps.
  4. Creates PACKAGENAME.txt containing the hash .so, if “enable proxy” is selected.
  5. Sets up a Proxy IP.

Please note that the app is not clean: it does not

When the Zygisk module is loaded, it:

  • Checks for PACKAGENAME.txt in the ZygiskReflutter app files directory, reading the content if available.
  • If the target app lacks the library, copies the .so and patches the IP with the desired proxy IP (done once unless the IP changes).

Since accessing another app directory requires root access, I am using the companion feature of Zygisk to perform this action. Another method that can also work is this:

  • Store libraries and configurations inside /data/local/tmp
  • When the app is started, copy the data from /data/local/tmp to the app files directory

I didn’t realize that we can easily get an app data directory during preSpecialize step, so I might use this approach in the future.

Installation and usage

To install this, you will need to install both the zip file (as zygisk module) and the app as ordinary APK. You will find a list of app package, scroll (or filter) to find the app. Click it. It will show “Finding hash”.

If the app is supported, it will enable the download button. This will download the library (each library is around 10 megabytes, so try using a fast internet connection). Currently I didn’t handle the case when download is corrupted.

In case that happen, delete the file from: /data/data/com.tinyhack.zygiskreflutter/files), and download manually from github.

Download proxy lib

Once it is downloaded, you can enable proxy

Proxy can be enabled now

Happy hacking

This was a weekend project, so this is not a very clean implementation. I am going for a holiday tomorrow to escape from Chiang Mai’s pollution and might continue this project later (but so far its good enough for me).

Leave a Reply

Your email address will not be published. Required fields are marked *