Déchiffrer les sous-titres KZPlay + capture vidéo

KZPlay était une ancienne plate-forme de VOD d’animés dont les vidéos possédait des sous-titres « softsubs » non intégrés à la vidéo.
C’est maintenant ADN qui a pris le relais de cette ancienne plate-forme. ADN utilise toujours des « softsubs », mais la technique de chiffrement n’est pas la même. Je me permets donc de diffuser publiquement cette article comme « étude de cas » qui − à priori − ne nuira pas à KZPlay/ADN.

/!\ Attention, technique /!\

Regardons le code source de l’applet flash.

<object id="mediaspaceba" width="100%" height="100%" type="application/x-shockwave-flash" data="http://kzplay.fr/components/com_vodvideo/mediaplayer/player.swf" bgcolor="#000000" name="mediaspaceba" tabindex="0">
<param name="allowfullscreen" value="true">
<param name="allowscriptaccess" value="always">
<param name="seamlesstabbing" value="true">
<param name="wmode" value="opaque">
<param name="flashvars" value="netstreambasepath=http%3A%2F%2Fkzplay.fr%2Fvideo%2Fto_love_darkness&id=mediaspaceba&image=http%3A%2F%2Fimage.kzplay.fr%2Flicense%2Ftoloveru%2Ftv3%2Fweb%2Feps1_729x410.jpg&skin=http%3A%2F%2Fkzplay.fr%2Fcomponents%2Fcom_vodvideo%2Fmediaplayer%2Fblueratio%2Fblueratio.zip&bufferlength=16000&repeat=list&title=La%20suite&autostart=false&plugins=http%3A%2F%2Fkzplay.fr%2Fcomponents%2Fcom_vodvideo%2Fmediaplayer%2Fplugins%2Fova%2Fova-jw.swf%2Chttp%3A%2F%2Fkzplay.fr%2Fcomponents%2Fcom_vodvideo%2Fmediaplayer%2Fplugin...va.canFireAPICalls=false&ova.ads=%5B%5BJSON%5D%5D%7B%22pauseOnClickThrough%22%3Afalse%2C%22enablePause%22%3Afalse%2C%22schedule%22%3A%5B%7B%22position%22%3A%22pre-roll%22%2C%22tag%22%3A%22http%3A%2F%2Fkzplay.fr%2Fpreroll.php%22%2C%22applyToParts%22%3A%5B0%5D%2C%22mulutibu.hidden%22%3Atrue%7D%5D%7D&ova.debug=%5B%5BJSON%5D%5D%7B%22levels%22%3A%22none%22%7D&ova.pluginmode=FLASH&control.pluginmode=FLASH&mulutibu.back=false&mulutibu.cc=false&mulutibu.pluginmode=FLASH&controlbar.position=over&dock.position=true">
</object>

La partie flashvars contient tout un tas de paramètres, décodons-les avec un urldecode.

netstreambasepath=http://kzplay.fr/video/to_love_darkness
&id=mediaspaceba
&image=http://image.kzplay.fr/license/toloveru/tv3/web/eps1_729x410.jpg
&skin=http://kzplay.fr/components/com_vodvideo/mediaplayer/blueratio/blueratio.zip
&bufferlength=16000
&repeat=list
&title=La suite
&autostart=false
&plugins=
    http://kzplay.fr/components/com_vodvideo/mediaplayer/plugins/ova/ova-jw.swf,
    http://kzplay.fr/components/com_vodvideo/mediaplayer/plugins/control/control.swf,
    http://kzplay.fr/components/com_vodvideo/mediaplayer/plugins/mulutibu/mulutibu.swf
&playlist=[[JSON]]
    [{
        "file": "http://dmeizi6er88hs.cloudfront.net/aod/LogoKAZE.mp4",
        "sd.vf": null,
        "sd.vo": "http://dmeizi6er88hs.cloudfront.net/aod/LogoKAZE.mp4",
        "default": "http://dmeizi6er88hs.cloudfront.net/aod/LogoKAZE.mp4",
        "mulutibu.hidden": true,
        "image": "http://image.kzplay.fr/license/toloveru/tv3/web/eps1_729x410.jpg"
    }, {
        "start": 0,
        "mulutibu.hidden": false,
        "image": "http://image.kzplay.fr/license/toloveru/tv3/web/eps1_729x410.jpg",
        "ccid": "2701",
        "streamer": "rtmpe://ec2-46-137-138-250.eu-west-1.compute.amazonaws.com/redirect?bufferlenght=16",
        "provider": "rtmp",
        "rtmp.tunneling": false,
        "sd.vo": "default",
        "file": "a3a2dc8d99300191a0175bf3482cad75.mp4?audioIndex=0",
        "default": "a3a2dc8d99300191a0175bf3482cad75.mp4?audioIndex=0"
    }]
&ova.clearPlaylist=false
&ova.assessControlBarState=false
&ova.canFireAPICalls=false
&ova.ads=[[JSON]]{
    "pauseOnClickThrough":false,
    "enablePause":false,
    "schedule":[{
        "position":"pre-roll",
        "tag":"http://kzplay.fr/preroll.php",
        "applyToParts":[0],
        "mulutibu.hidden":true
    }]
}
&ova.debug=[[JSON]]{"levels":"none"}
&ova.pluginmode=FLASH
&control.pluginmode=FLASH
&mulutibu.back=false
&mulutibu.cc=false
&mulutibu.pluginmode=FLASH
&controlbar.position=over
&dock.position=true

3 plugins JWPlayer sont chargés : ova-jw.swf, control.swf et mulutibu.swf.

ova-jw est à priori un truc permettant d’afficher une publicité avant de lancer une vidéo… passons. control est un module pour interagir avec la barre de contrôle (play/pause, etc.).
Enfin, ce qui nous intéresse mulutibu.swf, en cherchant sur internet, rien, le néant, qu’est-ce donc ?

Gestion des sous-titres (mulutibu.swf)

Et oui ! C’est lui qui va afficher les sous-titres sur la vidéo. Décompressons-le et regardons les chaînes de caractères qu’il contient en dur :

flasm -x mulutibu.swf
strings mulutibu.swf
[... Tronqué ...]
<dc:title>Adobe Flex 4 Application</dc:title>
void!com.longtailvideo.jwplayer.player
IPlayer"com.longtailvideo.jwplayer.plugins
Mulutibu+com.longtailvideo.plugins.mulutibu:Mulutibu
SRT&com.longtailvideo.plugins.mulutibu:SRT+com.longtailvideo.plugins.mulutibu:Renderer  _captions       _playResX       _playResY
decryptSubtitle
base64charsAABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
encode
decode
No captions entries found in file. Probably not a valid SRT or DFXP file?
Japonais        Fran
backcolour
textFormat
Dialogue!^Dialogue\:\s?((?:(?:[^,]*)\,?)+)

Pas de doute possible, c’est bien ce qui permet l’affichage des sous-titres, qui seraient d’ailleurs protégés via un système d’encodage (en base64 ?!).

Où sont les sous-titres ?
Analysons les requêtes HTTP lors de la lecture de la vidéo. Hop on récupère (http://kzplay.fr/picture_2701.png) qui de toute évidence n’est pas un fichier png… Ce sont nos sous-titres ! On trouve ceci dans le swf de mulutibu :

strings mulutibu.swf| grep pict
ccid    /picture_

Les sous-titres sont donc stockés ici : kzplay.fr/picture_ccid.png. ccid correspondant à l’ID de la vidéo.

Ce fichier est illisible, il est donc encodé via un algorithme particulier, voyons comment on pourrait le décoder. Pour cela, dé-compilons cette application flex avec le flex-sdk d’Adobe (OpenSource). On obtiens du bytecode AS, pas très lisible.

swfdump -acb mulutibu.swf > mulutibu.acb

On y trouve plusieurs fonctions intéressantes, qui indique clairement que c’est bien le module qui va, décrypter, parser, et afficher les sous-titres :

function com.longtailvideo.plugins.mulutibu:Mulutibu:::decryptSubtitle(:String, :String)::String
function com.longtailvideo.plugins.mulutibu:SRT:::parseCaptions(:Array)::Array
function com.longtailvideo.plugins.mulutibu:ASS:::parseCaptions(:Array)::Array
function com.longtailvideo.plugins.mulutibu:Renderer::private:_renderCaption(:Array)::void

On va dé-compiler ce petit swf avec une application gratuit de HP (oui oui HP) : SWFscan (Outil Windows :().
On trouvera facilement la fameuse fonction decryptSubtitle, elle est assez simple. Bref, plus qu’à la convertir bêtement/salement dans le langage de son choix (ici PHP) et on obtient un décodeur.

<?php
// Decompiled flash/flex/AS code.
// public static function decryptSubtitle(arg0:String, arg1:String = "5463201897") : String
// {
//     var loc4:* = null;
//     var loc5:* = 0;
//     var loc6:* = 0;
//     var loc7:* = 0;
//     var loc0:* = new ByteArray();
//     var loc1:* = "";
//     var loc2:* = 0;
//     while(loc2 < arg0.length)
//     {
//         loc4 = arg0.substr(loc2, 1);
//         loc5 = parseInt(arg1.substr(loc2 % arg1.length - 1, 1));
//         loc6 = loc4.charCodeAt(0);
//         loc7 = loc4.charCodeAt(0) - loc5;
//         loc4 = String.fromCharCode(loc7);
//         loc1 = loc1 + loc4;
//         loc2++;
//     }
//     var loc3:* = replace(new RegExp(""), "");
//     loc0.writeMultiByte(loc3, "iso-8859-1");
//     return loc0.toString();
//     
// }
function decryptSubtitle($encryptedSub, $key = "5463201897") {

    $loc4 = null;
    $loc5 = 0;
    $loc6 = 0;
    $loc7 = 0;
    $loc0 = array();
    $loc1 = "";
    $loc2 = 0;
    while ($loc2 < strlen($encryptedSub)) {
        $loc4 = substr($encryptedSub, $loc2, 1); // Progress byte by byte.        
        /* Convert the value to integer ($loc2 % 10)-1. */
        $loc5 = intval(substr($key, (($loc2 % (strlen($key)) - 1)), 1));
        $loc6 = ord($loc4); //Get the ASCII code of the char.
        $loc7 = ord($loc4) - $loc5; // ASCII char. - calculated value.
        $loc4 = chr($loc7); // Get the final character from  ASCII code.
        $loc1 = $loc1 . $loc4; // Store the final decrypted char.
        $loc2++; // Go to next byte.
    }
    echo base64_decode($loc1);
}

$filename = $argv[1];
$handle = fopen($filename, "r");
$contents = fread($handle, filesize($filename));
decryptSubtitle($contents);
?>
php decryptSubtitle.php picture_2701.png > 2701.ass

La technique de chiffrement est relativement faible. Elle consiste à encoder en base64 le fichier de sous-titres, puis changer chaque caractère avec un calcul basé sur un des chiffre de la clé 5463201897 selon la position -1.
Par exemple pour le premier caractère W (87 en ASCII décimale), on calcule ainsi :
La position de ce premier caractère dans la chaîne base64 est 0.
(0 modulo 10) -1 = -1.
On additionne le caractère (87) avec la position -1 de la clé (7).
Soit 87 + 7 = 94, ainsi, W (87), devient ^ (94).

Autre exemple avec le caractère j (106), en position 3.
(3 modulo 10) -1 = 2.
On additionne le caractère (106) avec la position 2 de la clé (6).
Soit 106 + 6 = 112, ainsi, j (106), devient p (112).

C’est donc, un dérivé du chiffre de Vigenère.

Capture de la vidéo (RTMP FMS)

Impossible de capturer la vidéo avec rtmpdump, il faut que je cherche encore pourquoi ce n’est pas possible, dans tout les cas il y a une histoire de loadbalancer. Pour info, voici la liste de tout les serveurs FMS (Wowza Media Server 2) de Kazé.

On va donc utiliser rtmpsuck qui permet de faire un proxy RTMP et de capturer tout ce qui transite.

  • Créer ou utiliser un utilisateur « proxy » (ou autre) ;
  • Ajouter (en root/sudo) les règles iptables suivantes :
# iptables -t nat -A OUTPUT -p tcp --dport 1935 -m owner --uid-owner proxy -j ACCEPT
# iptables -t nat -A OUTPUT -p tcp --dport 1935 -j REDIRECT
  • Démarrer le proxy rtmpsuck

$ rtmpsuck

  • Lancer la vidéo sur le navigateur. Attendre le temps qu’elle dure 🙁 (C’est l’inconvénient de la méthode du proxy).

Lire la vidéo avec les sous-titres avec mplayer 🙂 !

mplayer mp4_944fb8867af5ff97fab8afabbdd739dd.mp4 -ass -sub 2701.ass

3 replies on “Déchiffrer les sous-titres KZPlay + capture vidéo”

  1. Salut

    Merci pour ce tuto, mais je n’ai pas très bien compris la partie avec les subs

    php decryptSubtitle.php picture_2701.png > 2701.ass

    Comment faut-il la lancer ? Avec quel logiciel ?

    Amicalement

  2. C’est dans un terminal Linux. Ça appelle l’interpréteur PHP et redirige la sortie dans le fichier 2701.ass.

    (C’est bien sûr aussi possible sous Windows, mais c’est un blog orienté Linux ;-))

Comments are closed.