mecker. mecker. mecker.

RSDF/DLC/CCF Decryption

tl;dr decrypt! RSDF, DLC und CCF online. In bunt und in Farbe!1 Nur so lange der Vorrat die API reicht. Ent­wick­lung auf GitHub: posativ/opendlc.


Ich bin es leid. Es gibt Software dafür, eine be­hin­der­ter als die andere. Von ganz schwer bis fast: jDown­loa­der, Cryptload, Ci­nuxLoa­der, RSD/MSD und pyLoad. Alles ominöse Tools, um der Content-Mafia eins aus­zu­wi­schen und dabei selbst etwas Geld zu verdienen – mit Hehlerware. Irgendwie sehr verlogen, diese „Szene”.

Stand der Dinge: Wir haben drei Container-Formate, namentlich: RSDF, CCF und DLC, eingeführt von respektive: RapidShare Downloader (RSD), Cryptload und JDown­loa­der. Diese Container verstecken ver­schlüs­seln die URLs zu den Hostern wie RapidShare, uploaded.to, Netload und so weiter, damit die gemeinen 14-jährigen Poweruser/Kinder nicht auf die Idee kommen, Links zu verpetzen und die schöne Idylle zu stören. Wer diese sogenannte „Szene” noch nicht so genau kennt, der kann sich im Gulli:Board unter Ein-Klick-Hoster Tools einen Überblick ver­schaf­fen.

Nun ist es so, dass Container broken by design sind. Denn wer hindert mich daran, mal tcpdump anzuwerfen, wenn ich JDown­loa­der mit einem DLC-Container füttere? Aber das ist dieser „Szene” egal, man setzt weiterhin auf Security through Obscurity und hat doch zumindest einen Teilerfolg mit DLC erzielt, und eine mo­no­po­li­sier­te Client-Server-Encryption entwickelt, ebenfalls broken by design, aber dafür in den API-Calls pro IP limitiert.

RSDFrsdf.py

Mit etwas pycrypto, base64 und binascii ist das aufgrund des bekannten Schlüssels trivial (via).

import binascii
import base64
from Crypto.Cipher import AES

def decrypt(rsdf):

    key = binascii.unhexlify("8C35192D964DC3182C6F84F3252239EB4A320D2500000000")

    iv = binascii.unhexlify("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
    IV_Cipher = AES.new(key, AES.MODE_ECB)
    iv = IV_Cipher.encrypt(iv)

    aes = AES.new(key, AES.MODE_CFB, iv)
    data = binascii.unhexlify("".join(rsdf.split())).splitlines()

    urls = []
    for link in data:
        link = base64.b64decode(link)
        link = aes.decrypt(link)
        link = link.replace("CCF: ","")
        urls.append(link)

    return urls

DLCdlc.py

DLC steht für Download Link Container, sagt Wiki, und ist irgendwie sehr merkwürdig konzipiert. Hier ein DLC (stark gekürzt):

xQba6qa7UKC...zAHRs1eMdtNB/C8g==cGNSRlhycHR1bFpHdmN...lFUTmgyR2EzY2JjbVJhag==

Und zwar sind dort zwei base64-kodierte Strings drin, die dann auch im Al­go­rith­mus gesplittet werden müssen. Die Nutzdaten sind der erste Teil $$len(dlc)-88$$ und der dlckey ist lo­gi­scher­wei­se der Rest ab Position 88. Dieser wird an eine ob­fu­s­ca­ted­te Adresse geschickt:

http://service.jdownloader.org/dlcrypt/service.php?srcType=dlc&destType=$APPNAME&data=$DLCKEY

Ob da wer was wieviel loggt, ist unklar. Allgemein wie diese API funk­tio­niert: unklar. Von Do­ku­men­ta­ti­on hält man bei JDown­loa­der wohl auch nichts. Wie das mit ihrer GPL in Einklang bringen wollen, weiß ich auch nicht so recht.

Wie DLC aufgebaut ist, wurde ja schon früher publiziert. Bisher hat sich außer Sperrungen von geheimen ver­öf­fent­lich­ten Keys, nichts getan. Hier mal der grobe Al­go­rith­mus, den ich fast identisch vom Decompiler bekommen habe.

import base64
from Crypto.Cipher import AES
import urllib2

def decrypt(dlc, key, iv, name):

    api = "http://service.jdownloader.org/dlcrypt/service.php?srcType=dlc&destType=%s&data=%s"
    urlopen = urllib2.build_opener()
    urlopen.addheaders = [("User-Agent", "Mozilla/5.0 (Macintosh ... some headers"), ]

    aes = AES.new(key, AES.MODE_CBC, iv)

    dlckey = dlc[(-88):]
    dlcdata = base64.standard_b64decode(dlc[:(-88)])
    data = urlopen.open(api % (name, dlckey)).read()
    rc = data.replace("<rc>", "").replace("</rc>", "")

    dlckey = aes.decrypt(base64.standard_b64decode(rc))
    aes = AES.new(dlckey, AES.MODE_CBC, dlckey)
    return base64.standard_b64decode(aes.decrypt(dlcdata))

Damit erhalten wir ein doch etwas merk­wür­di­ges XML-File:

<dlc>
  <header>
    <generator>
      <app>TGlua0ZhaXJVc2UgRnJhbWV3b3JrIERMQyBFeHBvcnQgTW9kdWxl</app>
      <version>MC40Mg==</version>
      <url>TEZV</url>
    </generator>
    <tribute>
      <name/>
    </tribute>
    <dlcxmlversion>MjBfMDJfMjAwOA==</dlcxmlversion>
  </header>
  <content>
    <package name="" passwords="" comment="" category="">
      <file>
        <url>aHR0cDovL3JhcGlkc2hhcmUuY29tL2ZpbGVzLzEyMzQ1Njc4OS90ZXN0LnJhcg==</url>
        <filename>dGVzdC5yYXI=</filename>
        <url>aHR0cDovL3JhcGlkc2hhcmUuY29tL2ZpbGVzLzEyMzQ1Njc4OS90ZXN0LnJhcg==</url>
        <filename>dGVzdC5yYXI=</filename>
      </file>
    </package>
  </content>
</dlc>

Ja. WTF. Wie jemand auf die Idee kam, Attribute und Text base64 zu „ver­schlüs­seln”, aber dann doch die Tags plain lassen, ist mir ein Rätsel. Das macht einfach nur mehr Arbeit beim Parsen. Die URLs lassen sich dann aber simpel filtern: Element <content>, finde alle url-Elemente und base64-dekodiere die TEXT_NODE.

CCFccf.py

De­a­kro­ny­mi­siert heißt es Cryptload Container Format. Eher vom Schlage RSDF baut es auf einen möglichst gut ver­schlei­er­ten Al­go­rith­mus auf. Es wurde bereits mehrfach analysiert und deshalb schon öfters gepatcht, sprich, Al­go­rith­mus und Format ab­ge­wan­delt. superwayne.org hat eine sehr de­tail­lier­te Analyse (freundlich be­reit­ge­stellt von superwayne), die zeigt, dass dieses Format im worst-case un­ver­schlüs­selt und mit einigen Bit-Ope­ra­tio­nen schnell wieder in plaintext um­ge­wan­delt werden kann. Die Analyse bezieht sich auf nur CL 1.1.3 – mit CL 1.1.5 wurde das Format wieder über­ar­bei­tet und mir ist derzeit keine offener Al­go­rith­mus bekannt.

In­ter­es­san­ter­wei­se stellt aber service.jdown­loa­der.org eine API bereit, die ein beliebiges CCF ent­schlüs­seln kann – und liefert ein DLC zurück. Das ist nicht wirklich schön, aber somit lassen sich auch CCFs relativ problemlos decrypten, denn meine Versuche zur direkten Ent­schlüs­se­lung waren erfolglos; mangels Wissen an CRC und überhaupt Ko­die­rungs­theo­rie.

Der API-Aufruf für CCF nach DLC Conversion (crap-to-crap) sieht wie folgt aus (ja, ich habe nur fünf Zeilen selbst ge­schrie­ben):

import httplib
import mimetypes
import mimetools

def decrypt(ccf):

    ## {{{ http://code.activestate.com/recipes/146306-http-client-to-post-using-multipartform-data/#c3
    def post_multipart(host, selector, fields, files):
        content_type, body = encode_multipart_formdata(fields, files)
        h = httplib.HTTPConnection(host)
        headers = {
            "User-Agent": "Mozilla/5.0",
            "Content-Type": content_type
            }
        h.request("POST", selector, body, headers)
        res = h.getresponse()
        return res.status, res.reason, res.read()
    ## end of commented extension}}}

    ## {{{ http://code.activestate.com/recipes/146306/ (r1)
    def encode_multipart_formdata(fields, files):
        """
        fields is a sequence of (name, value) elements for regular form fields.
        files is a sequence of (name, filename, value) elements for data to be uploaded as files
        Return (content_type, body) ready for httplib.HTTP instance
        """
        BOUNDARY = mimetools.choose_boundary()
        CRLF = "\r\n"
        L = []
        for (key, value) in fields:
            L.append("--" + BOUNDARY)
            L.append("Content-Disposition: form-data; name="%s"" % key)
            L.append("")
            L.append(value)
        for (key, filename, value) in files:
            L.append("--" + BOUNDARY)
            L.append("Content-Disposition: form-data; name="%s"; filename="%s"" % (key, filename))
            L.append("Content-Type: %s" % get_content_type(filename))
            L.append("")
            L.append(value)
        L.append("--" + BOUNDARY + "--")
        L.append("")
        body = CRLF.join(L)
        content_type = "multipart/form-data; boundary=%s" % BOUNDARY
        return content_type, body

    def get_content_type(filename):
        return mimetypes.guess_type(filename)[0] or "application/octet-stream"
    ## end of http://code.activestate.com/recipes/146306/ }}}

    host, uri = "service.jdownloader.net", "/dlcrypt/getDLC.php"
    code, reason, data = post_multipart(host, uri,
            [("src", "ccf"), ("filename", "test.ccf")],
            [("upload", "../test/test.ccf", ccf)])
    x = data.find("<dlc>")
    return data[x+5:-6]
blog comments powered by Disqus