In this blog post, we analyze 2 borderline Flutter apps. As I’ll explain in conclusion, they are not malicious but expose borderline/risky behavior such as the installation of side applications.
The goal of this blog post is to show how to analyze Android Flutter applications, use Blutter and understand the disassembly it produces. Also, we deal with Bangcle packing and explain how to workaround.
It’s packed, but we don’t care
The APKs are packed by Bangcle, a well known packer which has been around for years.
But as we notice the sample uses Flutter (presence of libapp.so
and libflutter.so
in libraries), we don’t really care because the payload of the samples is going to be in Flutter.
inflating: ./sinchatpro/lib/arm64-v8a/libapp.so
inflating: ./sinchatpro/lib/arm64-v8a/libflutter.so
inflating: ./sinchatpro/lib/armeabi-v7a/libapp.so
inflating: ./sinchatpro/lib/armeabi-v7a/libflutter.so
inflating: ./sinchatpro/lib/x86_64/libapp.so
inflating: ./sinchatpro/lib/x86_64/libflutter.so
...
Actually, we just care a little, because Bangcle detects Frida and exits. So, if we want to hook, we’re going to need to be a bit smart 😉.
Blutter
I find reversing Flutter an interesting challenge (shameless ad: my talk at BlackAlps 2023, Nullcon 2024 and Flutter Reference Notes). As the APK have an ARM64 version, I use Blutter to reverse the Dart AOT snapshot.
Sidenote: Depending on your operating system, Blutter may be a bit difficult to install, so I use a Docker container a bit like this one.
Blutter recognizes the version of Dart (Dart version: 2.19.2, Snapshot: adb4292f3ec25074ca70abcd2d5c7251, Target: android arm64
) and produces:
- objs.txt: an interesting file, but we won’t care too much here
- pp.txt: the Object Pool
- blutter_frida.js: a script to hook specific functions of Flutter payload with Frida.
- ida_script/ directory for those using IDA Pro.
- asm/ directory containing more or less reversed Dart source code. Like always with disassembly, it’s not clean Dart code, but if we pay attention to the files, there’s lots of things to learn in there.
Making sense out of the disassembly of the main
After some little investigation, we find the main()
in ./asm/sinchat_flutter/main_online.dart
. This is typically the sort of disassembly we get. Please read my prior talks at BlackAlps and Nullcon if you do not know what is the Object Pool (PP) and THR.
static void main() async {
// ** addr: 0xc17508, size: 0x26c
// 0xc17508: EnterFrame
// 0xc17508: stp fp, lr, [SP, #-0x10]!
// 0xc1750c: mov fp, SP
// 0xc17510: AllocStack(0x20)
// 0xc17510: sub SP, SP, #0x20
// 0xc17514: SetupParameters()
// 0xc17514: stur NULL, [fp, #-8]
// 0xc17518: CheckStackOverflow
// 0xc17518: ldr x16, [THR, #0x38] ; THR::stack_limit
// 0xc1751c: cmp SP, x16
// 0xc17520: b.ls #0xc17768
// 0xc17524: InitAsync() -> Future
// 0xc17524: mov x0, NULL
// 0xc17528: bl #0x451e98
// 0xc1752c: r0 = InitLateStaticField(0x157c) // [package:sinchat_flutter/common/sinchatdata.dart] SinchatData::instance
// 0xc1752c: ldr x0, [THR, #0x88] ; THR::field_table_values
// 0xc17530: ldr x0, [x0, #0x2af8]
// 0xc17534: ldr x16, [PP, #0x28] ; [pp+0x28] Sentinel
// 0xc17538: cmp w0, w16
// 0xc1753c: b.ne #0xc17548
// 0xc17540: ldr x2, [PP, #0x70d8] ; [pp+0x70d8] Field <SinchatData.instance>: static late f
// 0xc17544: bl #0xc106dc
// 0xc17548: mov x3, x0
// 0xc1754c: r0 = "https://m.caoliuli.life" (this is the explanation: we are loading the URL in x0)
// 0xc1754c: ldr x0, [PP, #0x70e0] ; [pp+0x70e0] "https://m.caoliuli.life"
So, basically, we understand we get an instance of an object of SinchatData, and sets several of its fields to hxxps://m.caoliuli.life, hxxps://api.siwasnz.life, hxxps://ws.xbluntan56.bar, hxxps://images.xishuans.xyz. Those are web hosts, file hosts, API hosts etc.
Then, we allocate an array of 4 other URLs. Read my previous talks to understand why we see integer 8 and not 4: it’s a Small Integer (SMI), and the representation of 4 as SMI is shifted once to the left, i.e. 8.
// 0xc17580: ldr x17, [PP, #0x7100] ; [pp+0x7100] "https://jjyuanj.xyz"
// 0xc17584: StoreField: r0->field_f = r17
// 0xc17584: stur w17, [x0, #0xf]
// 0xc17588: r17 = "https://api.quxiaoyuan.bar"
// 0xc17588: ldr x17, [PP, #0x7108] ; [pp+0x7108] "https://api.quxiaoyuan.bar"
// 0xc1758c: StoreField: r0->field_13 = r17
// 0xc1758c: stur w17, [x0, #0x13]
// 0xc17590: r17 = "https://api.qjixlt.info"
// 0xc17590: ldr x17, [PP, #0x7110] ; [pp+0x7110] "https://api.qjixlt.info"
// 0xc17594: StoreField: r0->field_17 = r17
// 0xc17594: stur w17, [x0, #0x17]
// 0xc17598: r17 = "https://api.qubabl.info"
// 0xc17598: ldr x17, [PP, #0x7118] ; [pp+0x7118] "https://api.qubabl.info"
// 0xc1759c: StoreField: r0->field_1b = r17
// 0xc1759c: stur w17, [x0, #0x1b]
// 0xc175a0: r1 = <String>
// 0xc175a0: ldr x1, [PP, #0x4c0] ; [pp+0x4c0] TypeArguments: <String>
// 0xc175a4: r0 = AllocateGrowableArray()
// 0xc175a4: bl #0xc11468 ; AllocateGrowableArrayStub
// 0xc175a8: mov x1, x0
// 0xc175ac: ldur x0, [fp, #-0x18]
// 0xc175b0: stur x1, [fp, #-0x20]
// 0xc175b4: StoreField: r1->field_f = r0
// 0xc175b4: stur w0, [x1, #0xf]
// 0xc175b8: r0 = 8
// 0xc175b8: movz x0, #0x8
Then, we randomly access one of these URLs, and are going to call initDownloadService
. This will be used to download the latest version of the APK (that’s how we got our second sample), and also some other APKs. The download and install is not stealthy: the app requests permission and the app is downloaded to /sdcard/download
.
// 0xc176a0: r0 = initDownloadService()
// 0xc176a0: bl #0xc17d04 ; [package:sinchat_flutter/common/utils/download_util.dart] DownloadUtil::initDownloadService
Side APKs
The side APKs which are downloaded are found by searching in the Object Pool:
[pp+0x2c3a8] String: "/BOOK/XingShuBaoDian.apk"
[pp+0x2ca28] String: "/RADIO/YinXingFM.apk"
XingShuBaoDian.apk
is downloaded from novel_view.dart
, I have not checked what it is exactly yet, and YinXingFM.apk
is apparently a chinese radio app.
Searching on the web, we find those APKs are distributed by a web server whose name is related to those seen in the Object Pool. This practice is dangerous, because the links can be poisoned by malicious applications.
curl -k "https://www.nvshe[CENSORED].info/content.php?id=1491&type=i"
{"type":"text","text":"https:\/\/dala[CENSORED].info\/BOOK\/XingShuBaoDian.apk","link":"https:\/\/dala[CENSORED].info\/BOOK\/XingShuBaoDian.apk","customkey":""}
What else?
- Communication with remote server is encrypted with AES+Base64, and the secret key is hard-coded in the Flutter source code.
- It’s interesting to see there are now many helpful Dart packages. For example, there are packages to access device info (
device_info_plus
), launch URLs (url_launch_uri
), handle web sockets (web_socket_channel
) etc.
Frida
With Blutter, I have the disassembly and all strings (URLs, keys, messages etc), but it’s a bit tedious to read it all and understand how it works. I’d like to see the app work and hook all access to URLs and encryption/decryption. Medusa is the perfect Frida-based tool for that, using the http_communications/uri_logger
and encryption/cipher_1
modules.
Except it won’t work and you’ll get a “Process terminated” message because Bangcle detects Frida and exits as an anti-Frida measure. So, we’ll tweak a little our Frida server so that Bangcle doesn’t detect it any longer, and get passed this protection 😄.
We can happily enjoy lots of Medusa logs, and see the APK goes to numerous websites.
It’s not malware, but it’s risky
The samples we analyzed are a chinese sex community app, known as “sex bar”. The app is quite complete with paid subscription for VIP access, possibility to upload or download images and videos, chat, create profile etc.
The issue with this app comes from the high amount of advertisement which is loaded (sometimes containing unsecure links which might be poisoned) and the download of side applications. The samples are detected as Riskware/Nischat!Android
.
Thanks to @apkunpacker for his help on Frida.
— Cryptax