· 12 min read

Integrating A New Subscriber Management System

Writing the glue to tie two different systems under one managment software.

Background

$BUSINESS provides subscription digital TV services over the air called DVB to households and larger systems for hotels and resorts in $TOWN. From the source at the top of the town’s hill the TV channels are received, transcoded, muxed, encrypted and broadcast. Each customer has a decoder to decrypt the signal which is either a simple box connected via HDMI to the TV or a more advanced IRD for commerical installations. At the heart of all this is the CAS which encodes authorization messages in the broadcast signal and allows decoders to know which signals they can decrypt and for how long. You can read the patent for further details.

Problem

A new CAS was purchased but it came from a new supplier and did not mesh with the existing systems. At the time two CAS were already in use, one for hotels and one for individual customer use, and both were from the same supplier. Adding a new CAS was not going to be straightforward as this new system had a fundementally different approach towards accepting authorization information and did not provide an API for anything less than a pricey amount.

The two existing CAS are tied together by an admin interface which most importantly ties invoicing to authorizations of the decoders. Finding a way to coerce the existing system into authorizing decoders on the new CAS would be the challenge.

Also, $BUSINESS ran out of the old model of decoder and already started selling the new decoder models. So for a couple weeks juggling the invoicing system and the new CAS subscriber management system was necessary until a complete migration to just the invoicing system was possible.

Technical Background and Attack Strategies

The existing CAS at $BUSINESS each have a database and some of the tables therein are periodically checked by the CAS itself for tasks to be performed such as updating authorizations, OSD messages or mail messages to the customer decoder.

Microsoft SQL Server is the RDBS for both the invoicing system and for the all of the CAS. On the invoicing database a SQL Server Agent ran a periodic task to run stored procedures which inserts the authorization information into the CAS database. Below is an example of one of those authorizations as received by the CAS.

Note: decoders have a removable card inside them which behaves exactly the same as a SIM card for a phone. So I will continue use the word decoder but you may see card in some of the examples but just know that card is referencing a decoder. And on the new CAS you will see STB but that also just means a decoder.

| CardID     | ProductNumber | ProductStartTime        | ProductEndTime          | SendTime                |
|------------|---------------|-------------------------|-------------------------|-------------------------|
| 4291000278 | 1             | 2022-07-05 00:00:00.000 | 2022-07-31 23:59:59.000 | 2022-07-05 08:39:03.347 |

The best outcome would be to pass the same sort of instruction to the new CAS so that it would broadcast authorizations to decoders. A simple update to the SQL Server stored proceedure and we would be done.

Attacking the Database

The CAS had two databases one for the EMMG and the EMCG which work together to encode the authorizations into the broadcast signal and a second for the subscriber management system.

I first played around with the SMS to see how the database responded to particular actions. I was concerned when I saw some cryptic values in the tables that appeared when granting authorization to my test decoder.

| Auth_Buffer                                                                                                                                        | AuthVersion | STBNumber            |
|----------------------------------------------------------------------------------------------------------------------------------------------------|-------------|----------------------|
| 0x040000000200E607051F000000E607081F000000010500E607051F000000E607081F000000010800E607051F000000E607081F000000010A00E607051F000000E607081F00000001 | 44          | -8974767510011026891 |

I couldn’t quite figure them out except I knew the Auth_Buffer was describing the products to decrypt. I did know this because it looked just like the authorization strings used in the existing CAS which could be generated with a helper tool provided by the other supplier and were statically set. But this CAS was hiding the generation of this string for each and every authorization change per decoder. I figured this was hidden somewhere in their backend when it was communicating with a supplied dongle that must be inserted for the software to work. And I do not know where these STBNumber values were coming from as they did not match any actual number of the decoders that I was aware of and changed with each authorization even for the same decoder. By messing around with the database and trying to force the system to accept known good values via a replay-attack and failing I knew this database was also not the source of truth for authorizations but instead was just a running log of authorization jobs that have already been performed.

I then tried the same attacks to the tables in the SMS database to mimick the rows that would be inserted for a given user action on the web interface but nothing would happen. So the key was in the backend code and probably tied to the “softdog” dongle inserted to the server that without it the software would not work. I think it is also a backdoor for the supplier to provide support because when I asked general questions to the supplier they would respond with specific responses they couldn’t have known without looking directly at the data I had modified. Kinda scary.

Direct manipulation of the database was ruled out and as much as I would enjoy learning to reverse engineer the software further it wasn’t the fastest way to a working solution when more options were still available.

Attacking the Web User Interface

Now that direct modification of the database is ruled out, the next step would be to mimick a user on the web UI. Looking at the browser’s DevTools it was easy to record the URLs and form data required for each action I needed to perform. But before I could submit forms using requests the UI presented a challenge at the login screen.

Logging in with admin and the secure password 123456 in the form would submit the following to the backend.

{
  LoginID: "admin",
  LoginPassword: "d3Sc32H1RQ0zJcAUc1mWCb/lEGJTHes8b+uykb3Xr+3djFmJAQsZBgEOLyz9Bwt7csBH2LjUr7XjoZnOCfJOUnfGB4U9TS5f+263ZQGwQcaqzvgmQhl6h/B+/krH4vBE+TMuAckEtpnT9c1Lk4iMuHS9qh2x46x5DRMpAoo4c9A=",
}

The login form was not submitting my creditionals from the login form in cleartext but was encrypting it.

To mimick a user logging in I had to port the encryption code from their site’s Javascript files and pull out an obfuscated public key to RSA encrypt the username, password and a timestamp to create the LoginPassword field the backend expected. Then I would be rewarded with authorization to access the SMS UI further and be allowed to submit other forms.

I then found performing a single POST request to mimick a user submiting a form would invalidate the current user credentials. Each form submitted would reload the page and I found new authentication tokens were snuck into the bottom of the returned HTML body. Using Beautiful Soup it was easy to grab the <object> tag at the end of the HTML file and grab updated SessionID and AspxAuth values to use in the next request headers.

The SMS didn’t stop complicating matters there, a single form could require up to 30 fields. Most of those fields were redundant since a CustomerID field would be required along with CustomerName, CustomerRegion, etc which all could be derived on the backend from the CustomerID so I have no idea why these forms were made this way. Also, every action performed on the web UI returned plain HTML so there was no raw JSON responses or anything easy to parse. I hooked into the SMS database with SQLAlchemy to have direct access the values needed for each form.

Soon I had mapped out the actions I would need to perform on the web UI and the form data needed for each. This would include helper functions that would handle each step in the login -> subscribe to product -> make payment process.

def subscribe_product(start: date, months: int, card_id: int, product: Product):
    """Creates an authorization for a card/STB for the given product."""
    end = get_end_date(start, months)
    subs = Subscription.get_overlaps(start, end, card_id, product.id)
    logger.debug(f"Found {len(subs)} overlapping subscriptions")
    for sub in subs:
        if sub.is_active():
            unsubscribe_product(sub.card_id, sub.id)
        else:
            sub.delete()
    # perform steps to create and pay for a product
    dibsys = Dibsys()
    dibsys.update_auth_headers()
    dibsys.make_product_order(start, months, card_id, product)
    dibsys.update_auth_headers()
    dibsys.make_charge()

Although a bit cumbersome it was a working method to initiate an authorization to a decoder from a program.

Mitigating Defined Limitations

The last major issue I ran into was the web UI enforced the duration of subscriptions to be purchased in months. You can see in the last section the subscribe_product function takes a start date and a months integer for the number of months to subscribe. At first the solution is obvious, start subscriptions in the past so that the end date occurs when you want the subscription to end. But the logic hidden in the backend also enforced that no subscription may overlap a previous subscription. So when you set the start date 25 days in the past to give a 5 days trial to a customer then allow them to purchase the remainder of the month by setting a new start date 5 days in the past an error is thrown that the decoder already has a subscription in that timeframe.

So I had to dig into the SMS database to find what is enforcing this rule. I found in the SMS database the subscriptions table kept record of… subscriptions. It had an obtuse way of separating cancelled subscriptions from expired subscriptions but eventually it was decoded.

# simplified version
| CustomerID | DecoderID | ProductID | StartDate               | EndDate                 | OperateType | BackOrderID | Remark                                                   | AuthStatus |
| 1          | 134       | 5         | 2022-07-01 00:00:00.000 | 2022-07-15 00:00:00.000 | 2           | 128         | Unsubscribed IC Number [128] IC [13466-8373388000134669] | 0          |

I also realized the DecoderID had no constraint to reference an ID that actually exists in the decoder table. It appeared the backend was searching the subscription table for any subscription that matched the DecoderID and that naturally expired between the given dates. So I ended up deleting the subscriptions by changing the DecoderID value to 0 and the web UI no longer blocked me from creating overlapping subscriptions.

Lastly, to handle subscriptions of any duration I added some helper functions to select a start date based on an end date that would match the logic of the SMS backend. Sometimes this meant a subscription for one day would be two months long because I would have to go back two months for a month that had 31 days so the subsciption would end the following day on the 31st.

def get_months_between(start: date, end: date) -> int:
    """Return number of months between two dates. Good enough."""
    return (end.year - start.year) * 12 + end.month - start.month
 
 
def get_start_date(end: date) -> date:
    """Return a start date that ensures a subscription will end on given date.
 
    This is a trick I'm using to get around the one month subscription
    requirement by creating subscriptions with start dates in the past
    to trick the subscription to end on the date given.
    """
    months_with_31_days = [1, 3, 5, 7, 8, 10, 12]
    today = datetime.now().date()
    month = None
    for n, m in enumerate(months_with_31_days):
        if m >= end.month or m >= today.month:
            month = months_with_31_days[n - 1]
            break
    start = date(today.year, month, end.day)
    months_to_end = get_months_between(start, end)
    return start, months_to_end

Handling Multiple Databases

Since I was working with both the new tasks database and the SMS database I created a SQLAlchemy session class that would route to the correct engine based on the ORM model’s parent class.

from sqlalchemy import create_engine
from sqlalchemy.orm import Session as SQLA_Session, sessionmaker
 
from dibsys_cli.config import TASK_DATABASE_URI, WEB_DATABASE_URI
 
 
task_engine = create_engine(TASK_DATABASE_URI)
web_engine = create_engine(WEB_DATABASE_URI)
 
 
class RoutingSession(SQLA_Session):
    """Decides which engine to use for database queries."""
 
    def get_bind(self, mapper=None, clause=None):
        from dibsys_cli.database.models.base import WebBase
 
        if mapper and issubclass(mapper.class_, WebBase):
            return web_engine
        return task_engine
 
 
Session = sessionmaker(
    autocommit=False,
    autoflush=False,
    class_=RoutingSession,
)

This gave the advantage that I didn’t have to juggle which database I would connect to for the object I was dealing with. With an explicit base class I could also define basic operations that were allowed so it was difficult to modify a row in a table that I didn’t want modified.

from sqlalchemy import MetaData
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.declarative import as_declarative
 
from dibsys_cli.database.session import Session
 
 
@as_declarative()
class Base:
    id: Any
    __name__: str
 
    def save(self):
        raise NotImplementedError()
 
    def delete(self):
        raise NotImplementedError()
 
    def update(self, **kwargs):
        raise NotImplementedError()
 
 
class TaskBase(Base):
    __abstract__ = True
    metadata = MetaData()
 
    def save(self):
        ...
 
    def delete(self):
        ...
 
    def update(self, **kwargs):
        ...
 
 
class WebBase(Base):
    __abstract__ = True
    metadata = MetaData()

Solution

To continue use of the stable, legacy invoicing and subscriber management system I had to connect it to a new CAS target that looked like a CAS from its own supplier. First, I added a new database with tables that mirrored the tables found in the existing CAS that I called the “tasks” database. I updated the legacy invoice software so that changes to invoices that included a decoder from the new supplier would result in authorization messages arriving in the new tasks database. Then, a script I wrote would periodically view the entries to the tasks database and peform the necessary steps on the new SMS web UI to complete the authorization tasks. Essentially, making the new SMS believe someone was sitting at a desk going through the motions required to authorize a decoder.

I wrote this solution with Python and used click to make a CLI for interfacing with the new CAS. This CLI could mimick a user on the web UI or browse and modify the tables of the SMS directly where possible. Adding the CLI command to cron runs the tasks periodically and keeps customers of the new decoders subscribed.

  • python
  • sql
Share:
Back to Projects