Freestream Max Investigation

Freestream MAX

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”.

Splash Screen

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.

Main 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.

MAX App

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. Theotaupgrade` 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.

Backup Screen

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.