Freestream Max Investigation
Background
Someone I know once told me about a device they purchased at a county fair. It’s a small box that you plug into your TV and hook up to your Internet connection. It’s basically a digital content streaming box (like a Roku), except the seller claimed that you could get any movie or TV show you wanted for free. This supposedly included live television and films that were still in theaters. My acquaintance had owned one of these for a year or more and said it worked as claimed. They paid several hundred dollars for this device.
The whole thing immediately set my Spidey senses tingling. To me, this was obviously some sort of piracy device, illegally downloading all of this content. But it seemed so strange to me that some guy was showing up in person at a huge county fair selling these things out in the open. How was he getting away with this? How did the thing work? I set those thoughts aside because I had no intention of ever purchasing one.
Then last Christmas one of these devices was gifted to me randomly. I wasn’t interested in using it for its intended purpose, but I was really curious to take a look under the hood and see how it actually worked.
Unboxing
The device came in a nice black box with shrink wrapping. The box just said “Max” on it with a picture of the device. On the outside was also a MAC address and a “serial number” of MPQSG@MAX.COM
. The max.com
domain belongs to HBO as part of their HBO Max streaming service. It seemed unlikely this device would be associated with them since it obviously must be used to pirate content, so this was an odd thing to see. Also, try searching the net for the keyword max
. It’s not exactly an easy thing to search for…
The box contained a small black box with two detachable WiFi antennas along with a power cable and HDMI cable. There was also a sheet with printed instructions for how to setup the device, connect it to your WiFi, and update it. The instructions pointed to two different Facebook groups for support, but no official website or any other contact channels. The instructions also recommended setting up an account with real-debrid.com
. I had never heard of this before, but had no intention of signing up for any accounts anywhere, so I didn’t look into this right away.
Testing the Device
I plugged the device into my TV and booted it up. I was greeted with a splash screen that said “Freestream”.
I tried Googling for “freestream” and found sling.com/freestream which appears to be a legitimate service, but as far as I can tell has no affiliation with this device whatsoever.
After the splash screen, I was shown a menu.
The device seemed to obviously be running Android. After hooking it up to an isolated network segment, I ran the update process as described in the instructions after clicking the “MAX” button.
Then I tried using some of the built-in streaming features. Most of them seemed broken. When trying to choose a video to watch on-demand the device seemed to be searching for torrent files and then displayed options to me. If I chose one, it would do… something. Then it failed. None of them seemed to work. The Live TV function did work, surprisingly. Even paid channels were streaming and in decent quality. That surprised me a bit.
After searching around through all the menus I came to find that the device is basically running kodi, an open source home theater software. It also included other apps which are apparently used in the piracy scene. It seemed the reason most of the on-demand functions were not working was because the device was not configured with an active debrid
account.
I had never heard of debrid
before, so I had to look into it. It turns out there are “debrid” services you can pay a monthly subscription to. You can use a special client software which will search for torrent files for whatever content you are looking for. Your client then sends the torrent to the debrid server. The server then downloads the torrent to their server for you. These debrid servers have very fast Internet connections, so they can download videos very quickly. They also cache popular content, so if another customer already downloaded the file they can serve it to other customers instantly. They then provide your client with a URL to access the file.
This seems to be some kind of legal loophole where you pay someone else to illegally pirate the content for you. This way you don’t have to connect to the torrent tracker yourself and risk getting caught that way. You can just download the movie straight from the debrid server via an encrypted connection instead. The whole idea seemed very sketchy to me. There was no way I was going to give my payment information to some piracy service, but it was interesting to learn that these things existed.
After playing with the built-in functions for a bit I wanted to dig in deeper and see what else was on this device. It sure seemed to me like someone purchased a $30 Android box and then pre-loaded it with all of this piracy software and probably some debrid account that they share with all of their customers (no honor among thieves, I guess). Then they repackage the device and shrink wrap it and sell it at the fair.
My next thought was, “who knows what else this person put on here?!”. This thing could be rooted or backdoored or who knows what else. I decided to investigate a bit and see what was on there.
nmap
First, I ran an nmap
scan against the device to see what services were exposed.
┌──(rick㉿archlap)-[~]
└─$ nmap 192.168.55.11 -p0-65535
Starting Nmap 7.94 ( https://nmap.org ) at 2024-02-28 21:59 PST
Nmap scan report for 192.168.55.11
Host is up (0.017s latency).
Not shown: 65535 closed tcp ports (conn-refused)
PORT STATE SERVICE
5555/tcp open freeciv
Nmap done: 1 IP address (1 host up) scanned in 3.97 seconds
Only TCP port 5555
was found to be open. There was no banner so it was unclear what this port was for.
Shell
Next I wanted to try and get a shell on the device. The device came with the Google Play store, but after de-Googling my life a few years back I try to never use my old Google account for anything if I can avoid it. You can’t use the play store officially without signing in to a Google account.
I found that there was a built-in mechanism to side load applications from a USB port. I used this mechanism to first install the F-Droid package manager. I used F-Droid to install the Aurora Store app. The Aurora Store is an unofficial Google Play client application that can allow you to download and install applications from the Play store directly with no Google account. I believe it uses a number of accounts that are shared between all Aurora Store users.
I used Aurora Store to install an ssh server. Using this app, I was able to spawn an ssh server with known credentials.
┌──(rick㉿archlap)-[~]
└─$ ssh user@192.168.55.11 -p 8022
Password authentication
(user@192.168.55.11) Password:
/bin/sh: can't find: tty fd No such device or address
/bin/sh: warning: won't have full job control
:/ $
Root
For kicks, I tried just using su
to get root. I figured this device could very well be rooted since it had all of these weird piracy apps installed.
:/ $ su
id
uid=0(root) gid=0(root) groups=0(root) context=u:r:toolbox:s0
Yep, it was rooted. Next I wanted to see what that mysterious port 5555 was.
netstat -lnp | grep 5555
tcp6 0 0 :::5555 :::* LISTEN 3386/adbd
It claimed to be adbd
. adbd
is the Android Debug Bridge (ADB) daemon. If this were true, it would mean anyone on this network segment could connect to port 5555
using the adb
client and take full control of this device. Let’s try it.
┌──(rick㉿archlap)-[~]
└─$ adb connect 192.168.55.11:5555
* daemon not running; starting now at tcp:5037
* daemon started successfully
connected to 192.168.55.11:5555
┌──(rick㉿archlap)-[~]
└─$ adb devices
List of devices attached
192.168.55.11:5555 device
┌──(rick㉿archlap)-[~]
└─$ adb shell
FREMIX:/ $ su
FREMIX:/ # id
uid=0(root) gid=0(root) groups=0(root) context=u:r:toolbox:s0
FREMIX:/ #
Yep I was right. This thing basically has a root shell just sitting there on the network. Not off to a great start as far as the security posture is concerned.
APKs
Checking the network connections and running processes didn’t reveal anything obviously malicious. I decided to see what packages were installed on this device other than Android and Google packages.
FREMIX:/ # pm list packages | grep -v "com\.android" |grep -v "com\.google"
package:com.droidlogic.inputmethod.remote
package:com.kgurgul.cpuinfo
package:org.aerialview.deb
package:com.droidlogic.mediacenter
package:com.daemon.ssh
package:com.droidlogic
package:com.droidlogic.tv.settings
package:cm.aptoidetv.pt
package:android
package:com.droidlogic.FileBrower
package:com.aurora.store
package:com.droidlogic.BluetoothRemote
package:org.fdroid.fdroid
package:com.droidlogic.videoplayer
package:org.xbmc.kodi
package:com.droidlogic.miracast
package:com.ultraaiptv.ultraiptviptvbox
package:com.factorytools.factorystability
package:com.droidlogic.imageplayer
package:org.ssp.point
package:com.droidlogic.appinstall
package:com.appy.max
package:com.droidlogic.overlay
package:com.dyl.settingshow
package:com.droidlogic.otaupgrade
There were a fair number of packages. Some more obvious than others. The packages I was most interested in looking at were:
- package:com.appy.max
- package:com.ultraaiptv.ultraiptviptvbox
These two seemed custom made for this “brand” of device and were obviously related to the main menu system which linked back to other open source apps like kodi
. I pulled these apps off of the device.
First I obtained the path to the APK files on the device.
FREMIX:/ # pm path com.appy.max
package:/data/app/com.appy.max-Sam6wayRNnY_U0PSqq6IPg==/base.apk
FREMIX:/ # pm path com.ultraaiptv.ultraiptviptvbox
package:/data/app/com.ultraaiptv.ultraiptviptvbox-W8v_JzGO1HrSDg7Se0UdqA==/base.apk
Then I used adb
to download them.
┌──(rick㉿archlap)-[~/Projects/FreeStream/tmp]
└─$ adb pull /data/app/com.appy.max-Sam6wayRNnY_U0PSqq6IPg==/base.apk ./com.appy.max.apk
/data/app/com.appy.max-Sam6wayRNnY_U0PSqq6IPg==...0 skipped. 45.4 MB/s (13506771 bytes in 0.284s)
┌──(rick㉿archlap)-[~/Projects/FreeStream/tmp]
└─$ adb pull /data/app/com.ultraaiptv.ultraiptviptvbox-W8v_JzGO1HrSDg7Se0UdqA==/base.apk ./com.ultraiptv.ultraiptvbox.apk
/data/app/com.ultraaiptv.ultraiptviptvbox-W8v_J...0 skipped. 53.9 MB/s (95639304 bytes in 1.692s)
Jadx-GUI
Next I used jadx-gui to decompile the APKs and snoop around.
I first looked at the appy.max application. There were some interesting-looking classes in there related to “registration” and payment processing. It seemed strange to me that a piracy app would have payment screens, but maybe they were related to signing up for a debrid service.
I did find a fun securiy bug in this onPostExecute()
method within the preScreenActivity
activity class.
public void onPostExecute(String result) {
if (!this.success) {
PreScreenActivity.this.tvStatus.setText("Failed to check version");
PreScreenActivity.this.bRetry.setVisibility(0);
PreScreenActivity.this.bRetry.setText("Retry Check");
PreScreenActivity.this.bRetry.setOnClickListener(new View.OnClickListener() { // from class: com.appy.max.PreScreenActivity.CheckVersion.1
@Override // android.view.View.OnClickListener
public void onClick(View v) {
new CheckVersion(CheckVersion.this.mActivity).execute(PreScreenActivity.this.versionURL);
}
});
PreScreenActivity.this.bSettings.setVisibility(0);
} else if (PreScreenActivity.this.websiteVersion <= PreScreenActivity.this.version) {
new GetDeviceId().execute(new String[0]);
} else {
Log.d(PreScreenActivity.LOG_TAG, "Does not need update");
PreScreenActivity.this.tvStatus.setText("max is updating");
PreScreenActivity.this.tvStatus.setTextColor(Color.parseColor("#FFFFFF"));
PreScreenActivity.this.tvStatus.setTextSize(2, 20.0f);
PreScreenActivity.this.progressBar.setVisibility(0);
PreScreenActivity.this.bRetry.setVisibility(8);
PreScreenActivity.this.bRetry.setText("Yes");
new DownloadUpdate(PreScreenActivity.this.context).execute(PreScreenActivity.this.updateURL);
}
}
This function seems to be related to updating the max
app on the device. First, it performs a version check by issuing a request to PreScreenActivity.this.versionURL
. This URL is set up higher in the class:
private String versionURL = Constant.VERSION_CODE_TXT_FILE;
Constant.VERSION_CODE_TEXT_FILE
is configured in another location:
public static final String VERSION_CODE_TXT_FILE = "http://<redacted>.zone/freestreammax/VersionCode.txt";
If we hit that URL with curl
we get back a version number:
┌──(rick㉿archlap)-[~/Documents/]
└─$ curl http://<redacted>.zone/freestreammax/VersionCode.txt
105
If this version is newer than the currently installed version, the device downloads a new version:
new DownloadUpdate(PreScreenActivity.this.context).execute(PreScreenActivity.this.updateURL)
PreScreenActivity.this.updateURL
is defined similarly:
private String updateURL = Constant.VERSION_LOCATION_TXT_FILE;
Then in the constants:
public static final String VERSION_LOCATION_TXT_FILE = "http://<redacted>.zone/freestreammax/VersionLocation.txt";
If we use curl
again to hit this new URL, we get:
┌──(rick㉿archlap)-[~/Documents]
└─$ curl http://<redacted>.zone/freestreammax/VersionLocation.txt
https://www.<redacted>.com/<redacted>.com/jd/files/max_VC=105.apk
Notice how the final URL to download the APK is an HTTPS
URL. The version check and the version location URLs are both HTTP
. This means that an attacker with a machine-in-the-middle position on the network can intercept the request to VersionLocation.txt
and modify the response to include a URL that points to the attacker’s server. This would result in the Max app downloading and installing a malicious APK instead of the correct one. There do not appear to be any checks in place to ensure the URL points to the correct location, though I haven’t tried to perform this attack myself.
Firmware Image and Emulation?
A few weeks ago I brought this device to my local Defcon chapter meetup. We hooked it up and I showed some of what I had found and then we dug through the source code a bit more looking to see what else we could find. We didn’t have much time at the meeting, so nothing too interesting came from it, but a few folks have poked around the APKs since then, and it was nice to have inspired some curiosity in other folks.
I had a thought that it’d be cool if I could somehow image the device in such a way that it could be run in an emulator like qemu
. That way if any of these people wanted to dig in more, they could do so with a virtual replica of the same device instead of only having APK files to work with. The problem is I’m not sure how to do it or if it’s even possible.
I wasn’t provided any firmware files and haven’t found any URLs in the code yet that point to downloadable images. So I think the only way I’d be able to accomplish this would be to somehow rip the image from the physical device.
The first thing I tried was using adb pull
to copy the block device directly.
adb pull /dev/block/mmcblk0 > ~/Documents/Projects.local/FreeStream/dv_block_mmcblk0.bin
This sort of worked in that I ended up with a 15GB bin file that had at least some of the file system. But it didn’t seem to be the whole thing. I could use binwalk
to extract some of the partitions and files but not everything was there. I think maybe binwalk
is failing to extract everything, or perhaps there’s more stored on some other device. Here’s a bit of what binwalk
shows when you try to walk the bin file.
┌──(rick㉿archlap)-[/run/media/rick/2a061d0a-348e-473c-8565-8eaf8aab7839]
└─$ binwalk dev_block_mmcblk0.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
726045 0xB141D Android bootimg, kernel size: 1280131328 bytes, kernel addr: 0x55434553, ramdisk size: 1094713377 bytes, ramdisk addr: 0x7E004C4D, product name: " : fail to load internal RSA key!"
730720 0xB2660 SHA256 hash constants, little endian
1359583 0x14BEDF SHA256 hash constants, little endian
41943040 0x2800000 Flattened device tree, size: 91718 bytes, version: 17
41991128 0x280BBD8 Unix path: /dev/block/vendor
42003827 0x280ED73 eCos RTOS string reference: "ecos"
42003878 0x280EDA6 eCos RTOS string reference: "ecos_memory"
42024056 0x2813C78 eCos RTOS string reference: "ecos"
42034240 0x2816440 eCos RTOS string reference: "ecos_reserved"
42205184 0x2840000 Flattened device tree, size: 91718 bytes, version: 17
42253272 0x284BBD8 Unix path: /dev/block/vendor
42265971 0x284ED73 eCos RTOS string reference: "ecos"
42266022 0x284EDA6 eCos RTOS string reference: "ecos_memory"
42286200 0x2853C78 eCos RTOS string reference: "ecos"
42296384 0x2856440 eCos RTOS string reference: "ecos_reserved"
113246208 0x6C00000 Linux EXT filesystem, blocks count: 286720, image size: 293601280, rev 1.0, ext4 filesystem data, UUID=9df0c886-b9c1-49a3-8bf0-d0e2faf2faf2
515898368 0x1EBFFC00 Linux EXT filesystem, blocks count: 286720, image size: 293601280, rev 1.0, ext4 filesystem data, UUID=9df0c886-b9c1-49a3-8bf0-d0e2faf2faf2
1052769280 0x3EBFFC00 Linux EXT filesystem, blocks count: 286720, image size: 293601280, rev 1.0, ext4 filesystem data, UUID=9df0c886-b9c1-49a3-8bf0-d0e2faf2faf2
1379926080 0x52400040 Flattened device tree, size: 374 bytes, version: 17
1413480448 0x54400000 Linux EXT filesystem, blocks count: 4096, image size: 4194304, rev 1.0, ext4 filesystem data, UUID=16d7b493-c10d-4994-a5b2-22ab9c779c77
1418268672 0x54891000 SQLite 3.x database,
1438646272 0x55C00000 Android bootimg, kernel size: 9929392 bytes, kernel addr: 0x1080000, ramdisk size: 0 bytes, ramdisk addr: 0x1000000, product name: ""
1438648320 0x55C00800 uImage header, header size: 64 bytes, header CRC: 0x8E1E6AE6, created: 2022-09-23 15:30:25, image size: 9929328 bytes, Data Address: 0x108000, Entry Point: 0x108000, data CRC: 0xC89014C2, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "Linux-4.9.113"
1438648384 0x55C00840 Linux kernel ARM boot executable zImage (little-endian)
1438676924 0x55C077BC gzip compressed data, maximum compression, from Unix, last modified: 1970-01-01 00:00:00 (null date)
1439459400 0x55CC6848 Uncompressed Adobe Flash SWF file, Version 68, File size (header included) 3920909757
1448579072 0x56579000 Flattened device tree, size: 91718 bytes, version: 17
1448627160 0x56584BD8 Unix path: /dev/block/vendor
1448639859 0x56587D73 eCos RTOS string reference: "ecos"
1448639910 0x56587DA6 eCos RTOS string reference: "ecos_memory"
1448660088 0x5658CC78 eCos RTOS string reference: "ecos"
1448670272 0x5658F440 eCos RTOS string reference: "ecos_reserved"
1488977920 0x58C00000 Linux EXT filesystem, blocks count: 4096, image size: 4194304, rev 1.0, ext4 filesystem data, UUID=b7f4146f-56c5-473f-9819-4ab3252a252a
1524629504 0x5AE00000 Linux EXT filesystem, blocks count: 8192, image size: 8388608, rev 1.0, ext4 filesystem data, UUID=71377ccd-b56a-4f19-96b6-48507c5e7c5e
1566572544 0x5D600000 Linux EXT filesystem, blocks count: 81920, image size: 83886080, rev 1.0, ext2 filesystem data (mounted or unclean), UUID=5222b6ad-a831-5d09-bb1b-9c0b6aaa6aaa, volume name "vendor"
1650767986 0x6264B872 Unix path: /home/lwr/ssd01/905x3_cruze_magic_8822cs/common/include/uapi/asm-generic
1652322304 0x627C7000 ELF, 32-bit LSB relocatable, ARM, version 1 (SYSV)
1652409280 0x627DC3C0 Unix path: /home/lwr/ssd01/905x3_cruze_magic_8822cs/common/include/linux/dma-mapping.h
1652417004 0x627DE1EC Unix path: /home/lwr/ssd01/905x3_cruze_magic_8822cs/common/include/linux/dma-mapping.h
1652626341 0x628113A5 Unix path: /home/lwr/ssd01/905x3_cruze_magic_8822cs/out/target/product/FREMIX/obj/media_modules/frame_provider/decoder/h265
...
<snip>
I’m still working on this to see if I can get something usable.
Hidden App
In the meantime I went looking through the installed apps again and found an interesting one called ``com.droidlogic.otaupgrade. I didn't see any obvious icon or way to launch the app, though. I searched the net for this and found a few references to it. I think it _may_ be related to
droidlogic.tvbut I'm not syre. The
otaupgrade` in the app name sounded promising. I thought maybe there could be clues in there related to firmware updates and maybe I’d be able to grab a real firware image from somewhere.
I grabbed this APK and decompiled it with jadx-gui
. There were two obvious activity classes:
- MainActivity
- UpdateActivity
- BackupActivity
I was curious about the BackupActivity
, hoping that maybe there would be an option to backup the entire device to a single file? I found an onclick()
listener in MainActivity
that seemed to be setting a callback for a backup
button in the interface.
case R.id.backup /* 2131427356 */:
Intent intent = new Intent(LoaderReceiver.BACKUPDATA);
intent.setClass(this, BackupActivity.class);
startActivity(intent);
finish();
return;
...
When the button is clicked, it should launche BackupActivity
with an intent of LoaderReceiver.BACKUPDATA
. Looking at the LoaderReceiver
class, it has a few properties. One of these is BACKUPDATA
:
public class LoaderReceiver extends BroadcastReceiver {
public static final String BACKUPDATA = "com.android.amlogic.backupdata";
public static String BACKUP_FILE = "/data/data/com.droidlogic.otaupgrade/BACKUP";
public static String BACKUP_OLDFILE = "/storage/external_storage/sdcard1/BACKUP";
public static final String CHECKING_TASK_COMPLETED = "com.android.update.CHECKING_TASK_COMPLETED";
public static final String RESTOREDATA = "com.android.amlogic.restoredata";
private static final String TAG = "OTA";
public static final String UPDATE_GET_NEW_VERSION = "com.android.update.UPDATE_GET_NEW_VERSION";
private Context mContext;
private PrefUtils mPref;
...
The important bit:
BACKUPDATA = "com.android.amlogic.backupdata"
To manually launch the activity with adb
I needed this intent information. From an adb
shell, I was then able to enter the following command to launch the activity:
am start -n com.droidlogic.otaupgrade/com.droidlogic.otaupgrade.BackupActivity -a com.android.amlogic.backupdata
This launched the activity and apparently also initiated the backup.
After a few minutes there was a toast notification that the backup was complete. The backup file supposedly gets stored in /data/data/com.droidlogic.otaupgrade/BACKUP
, so I checked it.
FREMIX:/ # ls -lh /data/data/com.droidlogic.otaupgrade/BACKUP
-rwx------ 1 system system 837M 2024-02-21 23:02 /data/data/com.droidlogic.otaupgrade/BACKUP
This 837MB was a lot smaller than the 15GB I got from copying the block device. I pulled the image to my workstation and hit it with binwalk. Here’s an excerpt:
┌──(rick㉿archlap)-[/run/media/rick/2a061d0a-348e-473c-8565-8eaf8aab7839]
└─$ binwalk BACKUP
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Android Backup, compressed, unencrypted
24 0x18 Zlib compressed data, best compression
18137719 0x114C277 Zip archive data, at least v2.0 to extract, compressed size: 1077, uncompressed size: 1334, name: META-INF/DA477480.RSA
18138847 0x114C6DF Zip archive data, at least v2.0 to extract, compressed size: 49660, uncompressed size: 125784, name: META-INF/MANIFEST.MF
21171116 0x1430BAC Certificate in DER format (x509 v3), header length: 4, sequence length: 1417
22694099 0x15A48D3 Zip archive data, v0.0 compressed size: 348178, uncompressed size: 350101, name: org/bouncycastle/pqc/crypto/picnic/lowmcL3.bin.properties
23042469 0x15F99A5 Zip archive data, v0.0 compressed size: 749257, uncompressed size: 752652, name: org/bouncycastle/pqc/crypto/picnic/lowmcL5.bin.properties
24250299 0x17207BB Zip archive data, at least v2.0 to extract, compressed size: 1085, uncompressed size: 1344, name: META-INF/0C33D76B.RSA
...
<snip>
After another web search, I think this is a .ab
Android Backup file, which apparently can be created with adb
. Though, I guess there are some limitations about what can be backed up this way. So it shouldn’t necessarily be considered a full backup.
Next I looked at the UpdateActivity
and its associated classes. I found references to an updateurl
variable. It was set to http://10.28.11.53:8080/otaupdate/update
. So… not helpful. I guess this app doesn’t have any built-in way to grab firmware from the Internet.
Now What?
This is as far as I’ve gotten with this project. I haven’t had a whole lot of time to play with it, but it’s ben fun picking it apart here and there as time permits. I likely won’t have time to work on this again for a while so I wanted to document most of the steps I’ve taken up to this point, just in case I never get back around to it.
Although it seems whoever wrote these custom apps doesn’t take security very seriously, I haven’t seen any signs of malicious behavior on the device yet. I can’t say for sure, but even watching network logs with Wireshark
, I haven’t seen anything suspicious.
I’d like to poke at the APKs some more and see if I can find any other interesting vulnerabilities. Maybe something more easily exploitable than a MITM attack. We’ll see if I can find the time.