Ce document présente la méthodologie suivie pour résoudre l’édition 2015 du challenge SSTIC. La validation de celui-ci nécessite de retrouver une adresse email au sein d’un fichier téléchargé depuis le site Internet du symposium.

Six étapes doivent être séquentiellement résolues pour obtenir l’adresse email recherchée :

  • l’étude d’une image de carte SD contenant une charge utile pour Rubber Ducky ;

  • l’identification d’une clé de déchiffrement dans une carte du jeu Quake 3 ;

  • la reconstitution d’une image depuis les déplacements et les clics d’une souris extraits d’une capture USB ;

  • l’analyse d’un code JavaScript obfusqué pour identifier une routine de déchiffrement et obtenir la clé valide avec une attaque par force brute ;

  • la rétro-conception d’une routine de déchiffrement implémentée sur un processeur ST20 permettant de constater une faiblesse dans l’algorithme qui peut alors être exploitée pour obtenir la clé attendue ;

  • enfin, l’analyse de quatre images imbriquées, l’adresse de validation étant affichée sur la dernière image.

Stage 1 : carte mémoire

Le fichier challenge.zip téléchargé sur la page du challenge contient une image de carte SD, comme présenté ci-dessous :

$ wget http://static.sstic.org/challenge2015/challenge.zip
$ unzip challenge.zip
Archive:  challenge.zip
  inflating: sdcard.img
$ file sdcard.img
sdcard.img: DOS/MBR boot sector

Cette image peut être montée en loopback pour en examiner le contenu :

$ sudo mount -o loop sdcard.img /mnt/loop
$ ls -al /mnt/loop
total 33472
drwxr-xr-x 2 root root    16384 janv.  1  1970 .
drwxr-xr-x 7 root root     4096 avril  3 19:54 ..
-rwxr-xr-x 1 root root 34253730 mars  26 02:49 inject.bin

Un rapide examen du fichier inject.bin ne relève rien de particulier :

$ file /mnt/loop/inject.bin
/mnt/loop/inject.bin: data
$ strings /mnt/loop/inject.bin | wc -l
0

Par contre, la commande strings appelée directement sur l’image retourne deux chaînes intéressantes :

$ strings sdcard.img|tail -n 2
INJECT  BIN
java -jar encoder.jar -i /tmp/duckyscript.txt

Le nom du fichier duckyscript.txt fait penser au Rubber Ducky, outil bien connu des pentesteurs.

rubberducky
Figure 1. Rubber Ducky

Un “Rubber Ducky” est une clé USB permettant de lancer un code exécutable sur le poste d’une victime, selon le principe suivant :

  • la clé USB est connectée à l’ordinateur ;

  • elle émule un périphérique de type clavier ;

  • ce périphérique va simuler des frappes au clavier pour exécuter un script de décodage permettant de reconstituer la charge binaire finale (généralement un exécutable Windows) ;

  • enfin, la charge finale est déclenchée sur le poste de la victime.

La séquence de frappes clavier à simuler est décrite dans un fichier au format “Ducky Script”. Ce fichier est ensuite compilé avec l’outil duckendoder pour produire un fichier binaire input.bin : au moment de la connexion de la clé USB, le micro-contrôleur ira lire le contenu de ce fichier pour démarrer l’attaque.

L’objectif à ce stade est de pouvoir retrouver le code source du script à partir du fichier input.bin présent sur la carte SD. Heureusement, l’outil ducky-decode.pl permet justement de réaliser cette opération.

Le lancement de ce script sur notre fichier input.bin retourne le résultat suivant :

$ ./ducky-decode.pl -f /mnt/loop/inject.bin
00ff 007d
GUI R

DELAY 500

ENTER

DELAY 1000
 c m d
ENTER

DELAY 50
 p o w e r s h e l l
SPACE
 - e n c
SPACE
 Z g B 1 [...] D s A f Q A = 00a0
ENTER
 p o w e r s h e l l
SPACE
 - e n c
SPACE
 Z g B 1 [...] D s A f Q A = 00a0
 [...]

La signification du paramètre -enc de powershell est détaillée sur la page https://technet.microsoft.com/fr-fr/library/hh847736.aspx : il permet de passer des commandes à exécuter codées en base64. La valeur 00a0 correspond à un opcode Ducky Script inconnu de ducky-decode.pl et donc non décodé.

On peut donc tenter d’extraire la chaîne passée en paramètre puis la décoder avec la commande base64 :

$ ./ducky-decode.pl -f /tmp/inject.bin | grep "Z g" | head -n 1 |  sed 's/\(\s\|00a0\)//g' | base64 -d
function write_file_bytes{param([Byte[]] $file_bytes, [string] $file_path = ".\stage2.zip");$f = [io.file]::OpenWrite($file_path);$f.Seek($f.Length,0);$f.Write($file_bytes,0,$file_bytes.Length);$f.Close();}function check_correct_environment{$e=[Environment]::CurrentDirectory.split("\");$e=$e[$e.Length-1]+[Environment]::UserName;$e -eq "challenge2015sstic";}if(check_correct_environment){write_file_bytes([Convert]::FromBase64String('UEsDBAoDAAAAADaK[...]8AJFW2UwdXtOh6gUsBzWnXw=='));}else{write_file_bytes([Convert]::FromBase64String('VAByAHkASABhAHIAZABlAHIA'));}

Comme anticipé, on obtient alors une série d’instructions Powershell qui réalisent les opérations suivantes :

  • définition d’une fonction write_file_bytes qui écrit les données $file_bytes à la fin du fichier spécifié par $file_path (stage2.zip par défaut);

  • définition d’une fonction check_correct_environment qui teste si le nom de l’utilisateur courant est bien challenge2015sstic ;

  • en fonction du résultat de l’appel à check_correct_environment, appel de la fonction write_file_bytes avec :

    • une longue chaîne de caractères en base64 si le résultat est positif,

    • la chaîne "VAByAHkASABhAHIAZABlAHIA" (“Try harder”) sinon.

Pour reconstruire le fichier stage2.zip, il ne reste plus qu’à extraire les données des instructions Powershell, les décoder puis écrire le résultat binaire dans le fichier de sortie. Le script Ruby ci-dessous réalise ces opérations :

#!/usr/bin/env ruby
# encoding: UTF-8

require 'base64'

input = ARGV.shift

File.open("stage2.zip", "wb") do |fo|
  IO.popen("./ducky-decode.pl -f #{input}").each_line do |line|
    next unless line =~ /^ Z/ (1)
    s = line.gsub(/( |00a0)/, '').strip (2)
    t = Base64.decode64(s).force_encoding("UTF-16LE").encode("UTF-8") (3)
    if t =~ /FromBase64String\('([^']+)'\)/
      fo.write Base64.decode64($1) (4)
    end
  end
end
1 teste si la ligne commence par " Z"
2 suppression des espaces et opcode non décodé
3 conversion d’UTF-16LE vers UTF-8
4 extraction des données de stage2.zip et écriture dans le fichier de sortie

Le fichier obtenu peut alors être testé :

$ md5sum stage2.zip
979ff7961addd9ce982ff51fe2a0a058  stage2.zip
$ unzip -t stage2.zip
Archive:  stage2.zip
    testing: encrypted                OK
    testing: memo.txt                 OK
    testing: sstic.pk3                OK
No errors detected in compressed data of stage2.zip.

L’analyse de cette archive constitue la seconde étape de ce challenge.

Stage 2 : Quake 3

Le fichier memo.txt de l’archive stage2.zip est le suivant :

$ cat memo.txt
Cipher: AES-OFB
IV: 0x5353544943323031352d537461676532
Key: Damn... I ALWAYS forget it. Fortunately I found a way to hide it into my favorite game !

SHA256: 91d0a6f55cce427132fc638b6beecf105c2cb0c817a4b7846ddb04e3132ea945 - encrypted
SHA256: 845f8b000f70597cf55720350454f6f3af3420d8d038bb14ce74d6f4ac5b9187 - decrypted

Ce mémo nous donne des informations sur le mode de chiffrement, le vecteur d’initialisation utilisé et quelques indications sur la clé : celle-ci semble en effet cachée dans un jeu.

Le fichier sstic.pk3, également présent dans l’archive stage2.zip, est en fait une carte pour le jeu Quake 3 Arena. En chargeant la carte et en explorant les lieux, on aperçoit certaines textures contenant des données hexadécimales. On suppose à ce stade qu’il s’agit de morceaux de la clé à reconstituer.

La vidéo ci-dessous présente un parcours de la carte sous OpenArena permettant de retrouver l’ensemble de ces textures :

Ces textures, extraites depuis le fichier sstic.pk3 sont présentées ci-dessous dans leur ordre de découverte :

rk montage

Enfin, la pièce finale après le rocket jump affiche la séquence à respecter pour obtenir la clé finale :

rk seq

En combinant les textures observées avec la séquence finale, on peut déduire la clé de déchiffrement :

Drapeau Pulse Location Goutte Drapeau Maillon Wi-Fi PC

Vert

Blanc

Orange

Blanc

Orange

Vert

Vert

Blanc

9e2f31f7

8153296b

3d9b0ba6

7695dc7c

b0daf152

b54cdc34

ffe0d355

26609fac

Une fois la clé identifiée, le déchiffrement peut être réalisé par le script suivant :

#!/usr/bin/env ruby

require 'openssl'
require 'digest'

def hex_to_bin(s)
  s.scan(/../).map {|x| x.to_i(16)}.pack('C*')
end

iv = "5353544943323031352d537461676532"
key  = "9e2f31f7" # flag green
key << "8153296b" # pulse white
key << "3d9b0ba6" # loc orange
key << "7695dc7c" # drop white
key << "b0daf152" # flag orange
key << "b54cdc34" # link green
key << "ffe0d355" # wifi green
key << "26609fac" # pc white

encrypted_data = File.open("input/encrypted", "rb").read
encrypted_sha256 = Digest::SHA256.hexdigest(encrypted_data)

raise unless encrypted_sha256 == "91d0a6f55cce427132fc638b6beecf105c2cb0c817a4b7846ddb04e3132ea945"

cipher = OpenSSL::Cipher.new('aes-256-ofb')
cipher.decrypt
cipher.key = hex_to_bin(key)
cipher.iv = hex_to_bin(iv)

plain = cipher.update(encrypted_data) + cipher.final

File.open("decrypted", "wb") do |f|
   f.write plain
end

Étrangement, l’empreinte sha256 du fichier obtenu ne correspond pas à celle mentionnée dans le fichier memo.txt

$ sha256sum decrypted
f9ca4432afe87cbb1fca914e35ce69708c6bfa360b82bff21503b6723d1cfbf0  decrypted

Cependant, en observant la fin du fichier, on constate la présence de données de bourrage :

$ hexdump -C decrypted| tail -n 4
0007a4e0  00 70 61 69 6e 74 2e 63  61 70 50 4b 05 06 00 00  |.paint.capPK....|
0007a4f0  00 00 03 00 03 00 a4 00  00 00 46 a4 07 00 00 00  |..........F.....|
0007a500  10 10 10 10 10 10 10 10  10 10 10 10 10 10 10 10  |................|
0007a510

En supprimant les 16 derniers octets, on retrouve la bonne empreinte :

$ dd if=decrypted bs=1 count=$(( $(stat -c %s decrypted) - 16)) | sha256sum -
500992+0 records in
500992+0 records out
500992 bytes (501 kB) copied, 0.520727 s, 962 kB/s
845f8b000f70597cf55720350454f6f3af3420d8d038bb14ce74d6f4ac5b9187  -

L’archive obtenue peut alors être testée pour en découvrir le contenu :

$ unzip -t decrypted
Archive:  decrypted
    testing: encrypted                OK
    testing: memo.txt                 OK
    testing: paint.cap                OK
No errors detected in compressed data of decrypted.

L’étude de ces fichiers fait l’objet de l’étape suivante du challenge.

Stage 3 : Paint

Le fichier memo.txt obtenu à l’étape précédente contient les informations suivantes :

# cat memo.txt
Cipher: Serpent-1-CBC-With-CTS
IV: 0x5353544943323031352d537461676533
Key: Well, definitely can't remember it... So this time I securely stored it with Paint.

SHA256: 6b39ac2220e703a48b3de1e8365d9075297c0750e9e4302fc3492f98bdf3a0b0 - encrypted
SHA256: 7beabe40888fbbf3f8ff8f4ee826bb371c596dd0cebe0796d2dae9f9868dd2d2 - decrypted

Cette fois ci, la clé de déchiffrement semble avoir été stockée avec le logiciel Paint, ce qui est plutôt original. L’archive contient également un fichier paint.cap qu’il est possible d’ouvrir avec Wireshark, comme présenté ci-dessous :

rk paint cap

Il s’agit d’une capture d’une trace USB dans laquelle on distingue trois types de messages :

  • des requêtes de type “Request DEVICE” pour énumérer les périphériques ;

  • des réponses “Response DEVICE” aux précédentes requêtes ;

  • enfin des messages de type “URB_INTERRUPT”.

Au niveau des périphériques découverts, on retrouve en particulier une souris USB :

rk mouse cap

Le reste de la capture est une série de messages “URB_INTERRUPT” tel que celui présenté ci-dessous :

rk urb cap

Certaines données (“Leftover Capture Data”) ne sont pas décodées par Wireshark, faute de pouvoir les interpréter correctement.

La lecture du fichier usbmouse.c, responsable du support des souris USB dans le noyau Linux, permet de comprendre le format de ces données :

input_report_key(dev, BTN_LEFT,   data[0] & 0x01);
input_report_key(dev, BTN_RIGHT,  data[0] & 0x02);
input_report_key(dev, BTN_MIDDLE, data[0] & 0x04);
input_report_key(dev, BTN_SIDE,   data[0] & 0x08);
input_report_key(dev, BTN_EXTRA,  data[0] & 0x10);

input_report_rel(dev, REL_X,     data[1]);
input_report_rel(dev, REL_Y,     data[2]);
input_report_rel(dev, REL_WHEEL, data[3]);

Le premier octet contient l’état des différents boutons de la souris, les octets deux et trois représentent le déplacement de la souris sur les axes X et Y et le dernier octet correspond au déplacement de la roulette.

L’analyse de la trace va nous permettre de reconstituer tous les déplacements de la souris ainsi que les clics effectués. On peut donc ainsi espérer retrouver le dessin réalisé sous Paint, en associant à chaque clic un pixel.

Le script Ruby ci-dessous exploite les informations des interruptions pour récupérer les coordonnées de chaque clic et construire l’image correspondante :

#!/usr/bin/env ruby

require 'sdl'

clicks = []
x, y = 0, 0
min_x, max_x, min_y, max_y = 0, 0, 0, 0 (1)

IO.popen("tshark -r input/paint.cap -V").each_line do |line|
  next unless line =~ /Leftover Capture Data: (.{8})/
  data = $1.scan(/../).map {|x| x.to_i(16)}.pack('C*').unpack('c*')

  buttons, x_dep, y_dep, dev_spec = *data

  x += x_dep if x_dep != 0
  y += y_dep if y_dep != 0

  min_x = x if x < min_x
  max_x = x if x > max_x
  min_y = y if y < min_y
  max_y = y if y > max_y

  [ 0, 1, 2 ].each do |bit|
    if ((buttons >> bit) & 1) == 1 then
      clicks << [x, y]
    end
  end
end

extra_space = 128
width = (max_x - min_x) + extra_space
height = (max_y - min_y) + extra_space

SDL.init(SDL::INIT_VIDEO)
screen = SDL::Screen.open(width, height,16,SDL::HWSURFACE)

white = screen.format.map_rgb(255, 255, 255)
black = screen.format.map_rgb(0, 0, 0)
screen.fill_rect(0, 0, width, height, white)

clicks.each do |x, y|
  screen.put_pixel(x - min_x + extra_space / 2, (2)
                   y - min_y + extra_space / 2,
                   black)
end

screen.flip
sleep(2)
screen.save_bmp("screen.bmp")
1 Dimensions de la bounding-box
2 Translation des coordonnées du point vers la bounding-box

Le résultat obtenu est le suivant :

rk trim

Pour obtenir la clé de déchiffrement, il faut donc calculer l’empreinte de la chaîne “The quick brown fox jumps over the lobster dog” (référence au challenge SSTIC 2011) à l’aide de l’algorithme Blake256.

Cet algorithme étant récent, il n’est pas implémenté dans les bibliothèques classiques telles qu’OpenSSL. Il faut donc télécharger puis compiler l’implémentation de référence à l’adresse https://131002.net/blake/blake_c.tar.gz .

$ wget https://131002.net/blake/blake_c.tar.gz
$ tar xvf blake_c.tar.gz
blake/
blake/blake256.c
blake/blake384.c
blake/blake224.c
blake/README
blake/blake.h
blake/Makefile
blake/blake512.c
blake/astyle-clean.sh
$ cd blake
$ make
make
cc -Wall -O3 -fomit-frame-pointer    blake224.c   -o blake224
cc -Wall -O3 -fomit-frame-pointer    blake256.c   -o blake256
cc -Wall -O3 -fomit-frame-pointer    blake384.c   -o blake384
cc -Wall -O3 -fomit-frame-pointer    blake512.c   -o blake512
Checking test vectors
./blake224
./blake256
./blake384
./blake512
$ echo -n "The quick brown fox jumps over the lobster dog" > key
$ ./blake256 key
66c1ba5e8ca29a8ab6c105a9be9e75fe0ba07997a839ffeae9700b00b7269c8d key

Il ne reste plus qu’à déchiffrer le fichier encrypted à l’aide des informations contenues dans le mémo, à savoir l’algorithme de chiffrement et le mode (Serpent-1-CBC-With-CTS) ainsi que le vecteur d’initialisation.

Le script Ruby ci-dessous réalise l’opération de déchiffrement, en se basant sur les bindings vers la bibliothèque CryptoPP :

#!/usr/bin/env ruby

require 'cryptopp'

IV = "5353544943323031352d537461676533"
KEY = "66c1ba5e8ca29a8ab6c105a9be9e75fe0ba07997a839ffeae9700b00b7269c8d"

inputfile, outputfile = ARGV.shift, ARGV.shift

serpent = CryptoPP::Serpent.new
serpent.block_mode = :cbc_cts
serpent.iv_hex = IV
serpent.key_hex = KEY

File.open(inputfile, "rb") do |fi|
  File.open(outputfile, "wb") do |fo|
    serpent.decrypt_io fi, fo
  end
end

Les données en clair sont alors récupérées à l’aide du script :

$ ./decrypt.rb input/encrypted input/decrypted
$ sha256sum input/decrypted
7beabe40888fbbf3f8ff8f4ee826bb371c596dd0cebe0796d2dae9f9868dd2d2  input/decrypted
$ file input/decrypted
input/decrypted: Zip archive data, at least v2.0 to extract
$ unzip -t input/decrypted
Archive:  input/decrypted
    testing: stage4.html              OK
No errors detected in compressed data of input/decrypted.

L’empreinte sha256 est correcte et le fichier obtenu est une archive zip contenant un fichier HTML qui sera analysé dans l’étape suivante.

Stage 4 : JavaScript

Le chargement du fichier stage4.html obtenu précédemment dans un navigateur donne le résultat suivant :

rk stage4 1

Pour comprendre la raison de l’erreur, on est alors tenté de regarder le code source de la page. Le corps du fichier est uniquement constitué de code JavaScript qui commence par l’initialisation d’une variable data :

rk stage4 2

Ensuite, le script définit une seconde variable hash puis exécute une série d’instructions JavaScript obfusquées :

rk stage4 3

Le premier réflexe est d’essayer de réindenter le fichier pour faciliter sa lisibilité. Pour cela, on passe en mode hipster moustachu en installant Node.js et l’outil js-beautify. Le résultat obtenu est le suivant :

<html>

<head>
[...]
</head>

<body>
    <script>
        var data = "2b1f25cf8db5d243f[...]bfd3ac1646ffe2";
        var hash = "08c3be636f7dffd91971f65be4cec3c6d162cb1c";
        $ = ~[];
        $ = {
            ___: ++$,
            $$$$: (![] + "")[$],
            __$: ++$,
            $_$_: (![] + "")[$],
            _$_: ++$,
            $_$$: ({} + "")[$],
            $$_$: ($[$] + "")[$],
            _$$: ++$,
            $$$_: (!"" + "")[$],
            $__: ++$,
            $_$: ++$,
            $$__: ({} + "")[$],
            $$_: ++$,
            $$$: ++$,
            $___: ++$,
            $__$: ++$
        };
        $.$_ = ($.$_ = $ + "")[$.$_$] + ($._$ = $.$_[$.__$]) + ($.$$ = ($.$ + "")[$.__$]) + ((!$) + "")[$._$$] + ($.__ = $.$_[$.$$_]) + ($.$ = (!"" + "")[$.__$]) + ($._ = (!"" + "")[$._$_]) + $.$_[$.$_$] + $.__ + $._$ + $.$;
        $.$$ = $.$ + (!"" + "")[$._$$] + $.__ + $._ + $.$ + $.$$;
        $.$ = ($.___)[$.$_][$.$_];
        $.$($.$($.$$ + "\"" + "__=" + $.$$_$ + [...] + "\"")())();
    </script>
</body>

On peut alors lancer un interpréteur JavaScript avec Node.js et exécuter les instructions ligne par ligne pour suivre le déroulement du script :

> $ = ~[];
-1
> $ = { ___: ++$, $$$$: (![] + "")[$], __$: ++$, $_$_: (![] + "")[$], _$_: ++$, $_$$: ({} + "")[$], $$_$: ($[$] + "")[$], _$$: ++$, $$$_: (!"" + "")[$], $__: ++$, $_$: ++$, $$__: ({} + "")[$], $$_: ++$, $$$: ++$, $___: ++$, $__$: ++$ };
{ ___: 0,
  '$$$$': 'f',
  '__$': 1,
  '$_$_': 'a',
  '_$_': 2,
  '$_$$': 'b',
  '$$_$': 'd',
  '_$$': 3,
  '$$$_': 'e',
  '$__': 4,
  '$_$': 5,
  '$$__': 'c',
  '$$_': 6,
  '$$$': 7,
  '$___': 8,
  '$__$': 9 }
> $.$_ = ($.$_ = $ + "")[$.$_$] + ($._$ = $.$_[$.__$]) + ($.$$ = ($.$ + "")[$.__$]) + ((!$) + "")[$._$$] + ($.__ = $.$_[$.$$_]) + ($.$ = (!"" + "")[$.__$]) + ($._ = (!"" + "")[$._$_]) + $.$_[$.$_$] + $.__ + $._$ + $.$;
'constructor'
> $.$$ = $.$ + (!"" + "")[$._$$] + $.__ + $._ + $.$ + $.$$;
'return'
> $.$ = ($.___)[$.$_][$.$_];
[Function: Function]

Ces instructions se contentent de définir un objet $. On peut remarquer que le champ $.$ est une fonction qui retourne elle-même une fonction (prototype Function: Function).

L’exécution de la dernière ligne retourne l’erreur suivante :

>  $.$($.$($.$$ + "\"" + "__=" + $.$$_$ + [...] + "\"")())(); (1)
ReferenceError: document is not defined
    at eval (eval at <anonymous> (repl:1:3), <anonymous>:2:4)
    at repl:1:13972
    at REPLServer.self.eval (repl.js:110:21)
    at repl.js:249:20
    at REPLServer.self.eval (repl.js:122:7)
    at Interface.<anonymous> (repl.js:239:12)
    at Interface.EventEmitter.emit (events.js:95:17)
    at Interface._onLine (readline.js:202:10)
    at Interface._line (readline.js:531:8)
    at Interface._ttyWrite (readline.js:760:14)
1 double appel de la fonction $.$ sur une chaîne de caractères

Le script référence l’objet document, qui existe dans le contexte d’un navigateur mais pas dans celui d’un interpréteur en console. On constate la présence de la fonction eval en haut de la pile d’appels. On soupçonne alors que le code JavaScript final est construit par concaténation de différentes chaînes de caractères puis exécuté avec un appel à eval (ou fonction équivalente).

Comme évoqué précédemment, la fonction $.$ retourne une fonction. En lui passant des arguments de différente nature, on peut essayer de deviner son comportement :

> f = $.$('coucou');
[Function]
> f();
ReferenceError: coucou is not defined
    at eval (eval at <anonymous> (repl:1:7), <anonymous>:2:1)
    at repl:1:1
    at REPLServer.self.eval (repl.js:110:21)
    at repl.js:249:20
    at REPLServer.self.eval (repl.js:122:7)
    at Interface.<anonymous> (repl.js:239:12)
    at Interface.EventEmitter.emit (events.js:95:17)
    at Interface._onLine (readline.js:202:10)
    at Interface._line (readline.js:531:8)
    at Interface._ttyWrite (readline.js:760:14)

La fonction $.$ semble définir une nouvelle fonction qui exécute la chaîne de caractères passée en argument. Ce comportement est confirmé par le test ci-dessous :

> f = $.$('console.log(2+3)');
[Function]
> f();
5

A ce stade, on cherche à récupérer la chaîne de caractères avant les deux appels successifs à la fonction $.$ :

> s = $.$$ + "\"" + "__=" + [...] + "\\" + $.__$ + $.__$ + "\"";
'return"__=docu\\155e\\156t;$$$=[...]_,$_$);\\12\\11\\11"'

Le résultat correspond bien à du code JavaScript qui semble se contenter de retourner une seconde chaîne de caractères. Un appel à la fonction $.$ permet d’obtenir le code JavaScript qui référence bien l’objet document :

> s1 = $.$(s)();
'__=document;$$$=\'stage5\';$$_$=\'load\';[...];$_$);\n\t\t'
> console.log(s1);
__=document;$$$='stage5';$$_$='load';[...],$_$);

Le contenu de la variable s1 est sauvegardé dans un fichier puis le code est réindenté avec js-beautify pour obtenir le résultat ci-dessous :

Fichier obfu.js
__ = document;
$$$ = 'stage5';
$$_$ = 'load';
$_$$ = ' ';
_$$$$$ = 'user';
_$$$ = 'div';
$$_$$$ = 'navigator';
$$_$$ = 'preferences';
[...]
________________________ = '';
_________________________ = 'byteLength';
__________________________ = $_$$$ + 'String';
__[___]('<h' + ______________ + '>Down' + $$_$ + $_$$ + 'manager</h' + ______________ + '>');
__[___]('<' + _$$$ + $_$$ + 'id' + $$$$_ + _$$$$ + ___$ + _$$$$ + '><i>' + $$_$ + 'ing...</i></' + _$$$ + '>');
__[___]('<' + _$$$ + $_$$ + 'style' + $$$$_ + _$$$$ + 'display:none' + _$$$$ + '><a' + $_$$ + 'target' + $$$$_ + _$$$$ + 'blank' + _$$$$ + $_$$ + $$$_$ + $$$$_ + _$$$$ + $$$$$ + '://browser/content/' + $$_$$ + '/' + $$_$$ + '.xul' + _$$$$ + '>Back' + $_$$ + $_$$$ + $_$$ + $$_$$ + '</a></' + _$$$ + '>');

[...]

function ___________________________() {
    $_ = _____(__________[_____________](__________[__________________](______$_) + ______________, _________________));
    _$__ = _____(__________[_____________](__________[__________________](_______$) - _________________, _________________));
    _$ = {};
    _$[_$______] = __$_____;
    _$[___$____] = $_;
    _$[____________] = _$__[____________] * ________________;
    __$[$____](_$_, _$__, _$, false, [__$_])[__$__](function(_$___) {
        __$[__$_](_$, _$___, _____________________(____$_))[__$__](function(___$_) {
            ____$ = new ______________________(___$_);
            __$[_$____](___$__, ____$)[__$__](function(____$$) {
                if (_____$ == _______________________(new ______________________(____$$))) {
                    _____$_ = {};
                    _____$_[______$] = $_______;
                    _____$ = new _$_____([____$], _____$_);
                    __$____ = ___$___[____$__](_____$);
                    __[____](___$)[__$___] = ____$___ + __$____ + _____$__;
                } else {
                    __[____](___$)[__$___] = $;
                }
            });
        }).catch(function() {
            __[____](___$)[__$___] = $;
        });
    }).catch(function() {
        __[____](___$)[__$___] = $;
    });
}
$$[$________](___________________________, $_$);

La structure du code commence à apparaître mais sa compréhension est rendue difficile par l’utilisation de nombreuses variables nommées de façon similaire.

L’outil clean.js basé sur esprima a été développé pour simplifier le code obtenu. Cet outil travaille sur l’arbre syntaxique abstrait du code JavaScript pour remplacer les références aux constantes globales par les valeurs associées et simplifier certaines expressions binaires (concaténation de chaînes de caractères et opérations arithmétiques sur des entiers).

L’exécution de cet outil, ainsi qu’un retraitement manuel (renommage des fonctions et des variables locales), aboutit au code nettoyé suivant :

var data = "2b1f25c[...]1646ffe2";
var hash = "08c3be636f7dffd91971f65be4cec3c6d162cb1c";

ua = window.navigator.userAgent;
document.write('<h1>Download manager</h1>');
document.write('<div id="status"><i>loading...</i></div>');
document.write('<div style="display:none"><a target="blank" href="chrome://browser/content/preferences/preferences.xul">Back to preferences</a></div>');

function stringToBytes(arg) {
    res = [];
    for (i = 0; i < arg.length; ++i)
        res.push(arg.charCodeAt(i));
    return new Uint8Array(res);
}

function hexToBytes(arg) {
    res = [];
    for (i = 0; i < arg.length / 2; ++i)
        res.push(parseInt(arg.substr(i * 2, 2), 16));
    return new Uint8Array(res);
}

function bytesToHex(arg) {
    res = '';
    for (i = 0; i < arg.byteLength; ++i) {
        s = arg[i].toString(16);
        if (s.length < 2)
            res += 0;
        res += s;
    }
    return res;
}

function f3() {
    iv = stringToBytes(ua.substr(ua.indexOf('(') + 1, 16));
    key = stringToBytes(ua.substr(ua.indexOf(')') - 16, 16));
    ctx = {};
    ctx['name'] = 'AES-CBC';
    ctx['iv'] = iv;
    ctx['length'] = key['length'] * 8;
    window.crypto.subtle.importKey('raw', key, ctx, false, ['decrypt']).then(function(arg0) {
        window.crypto.subtle.decrypt(ctx, arg0, hexToBytes(data)).then(function(arg1) {
            plainText = new Uint8Array(arg1);
            window.crypto.subtle.digest({
                name: 'SHA-1'
            }, plainText).then(function(arg2) {
                if (hash == bytesToHex(new Uint8Array(arg2))) {
                    props = {};
                    props['type'] = 'application/octet-stream';
                    blob = new Blob([plainText], props);
                    url = URL.createObjectURL(blob);
                    document.getElementById('status').innerHTML = '<a href="' + url + '" download="stage5.zip">download stage5</a>';
                } else {
                    document.getElementById('status').innerHTML = '<b>Failed to load stage5</b>';
                }
            });
        }).catch(function() {
            document.getElementById('status').innerHTML = '<b>Failed to load stage5</b>';
        });
    }).catch(function() {
        document.getElementById('status').innerHTML = '<b>Failed to load stage5</b>';
    });
}
window.setTimeout(f3, 1000);

Ce code extrait un vecteur d’initialisation et une clé de déchiffrement depuis l’agent utilisateur (“User-Agent”) du navigateur. Le déchiffrement est ensuite effectué en AES-CBC puis une empreinte SHA-1 est calculée sur les données obtenues. Si cette empreinte correspond à la valeur de la variable hash, alors un lien vers les données déchiffrées est ajouté à la page. Sinon, le message <b>Failed to load stage5</b> est affiché.

Pour arriver à déchiffrer correctement, il faut donc déterminer l’agent utilisateur qui permet de retrouver le bon SHA-1. Trois indices dans le fichier HTML permettent d’en savoir plus sur cet agent utilisateur :

  • la page contient un lien vers chrome://browser/content/preferences/preferences.xul qui n’est valable que sous Firefox ;

  • le code JavaScript fait appel à la bibliothèque crypto.subtle qui n’est compatible qu’avec les versions de Firefox ultérieures à 34 (d’après la page SubleCrypto#Browser_compatibility) ;

  • enfin, le fichier HTML utilise la série de polices Lucida Grande, Lucida Sans Unicode, Lucida Sans, Geneva, Verdana, sans-serif : une recherche sur Internet retourne la page Wikipedia Lucida_Grande qui précise que cette police est utilisée sous MAC OS X.

Sur la base de ces informations, il ne reste plus qu’à générer tous les agent utilisateurs possibles, en respectant la convention décrite sur la page Gecko_user_agent_string_reference, tenter un déchiffrement après avoir extrait le vecteur d’initialisation et la clé puis vérifier l’empreinte SHA-1 des données déchiffrées jusqu’à obtenir celle attendue.

Le script Ruby suivant implémente cette méthode pour retrouver l’agent utilisateur recherché :

bf.rb
#!/usr/bin/env ruby

require 'openssl'
require 'digest'

bindata = File.open("data.bin", "rb").read
expected_sha1 = "08c3be636f7dffd91971f65be4cec3c6d162cb1c"

def extract_key_iv_from_ua(ua)
  iv = ua[ua.index('(') + 1, 16]
  key = ua[ua.index(')') - 16, 16]
  return [iv, key]
end

def generate_ua_list
  res = []
  s = "Mozilla/5.0"
  platforms = []

  osx_versions = [ "10.0", "10.1", "10.2", "10.3", "10.4", "10.5", "10.6", "10.7", "10.8", "10.9", "10.10" ]
  osx_versions.each do |ver|
    platforms << "Macintosh; Intel Mac OS X #{ver}"
    platforms << "Macintosh; PPC Mac OS X #{ver}"
  end

  gecko_versions = []
  (34..38).each { |i| gecko_versions << "#{i}.0" }

  platforms.each do |platform|
    gecko_versions.each { |version| res << "#{s} (#{platform}; rv:#{version}) Gecko" }
  end

  return res
end

generate_ua_list.each do |ua|
  iv, key = extract_key_iv_from_ua(ua)

  cipher = OpenSSL::Cipher::AES.new('128-CBC')
  cipher.decrypt
  cipher.key = key
  cipher.iv = iv

  begin
    plain = cipher.update(bindata) + cipher.final
    sha1 = Digest::SHA1.hexdigest(plain)
    if sha1 == expected_sha1 then
      puts "[*] Found, ua = #{ua}"
      File.open("stage5.zip", "wb") { |f| f.write plain }
      exit
    end
  rescue OpenSSL::Cipher::CipherError => e
    puts e
  end

end

Son exécution sur le fichier data.bin contenant les données de la variable data retourne le résultat suivant :

$ ./bf.rb
bad decrypt
bad decrypt
[...]
bad decrypt
bad decrypt
[*] Found, ua = Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:35.0) Gecko
$ file stage5.zip
stage5.zip: Zip archive data, at least v2.0 to extract
$ unzip -t stage5.zip
Archive:  stage5.zip
    testing: input.bin                OK
    testing: schematic.pdf            OK
No errors detected in compressed data of stage5.zip.

Le contenu de l’archive obtenue sera étudié dans la suite de ce document.

Stage 5 : ST20

Découverte

L’archive obtenue suite à la résolution de l’étape précédente contient deux fichiers, input.bin et schematic.pdf.

D’après le résultat de la commande file, le fichier input.bin ne correspond pas à un format connu. Cependant, il contient quelques chaînes de caractères intéressantes, comme présenté ci-dessous :

$ file input.bin
input.bin: data
$ strings input.bin
$ P#
$z$y
Boot ok
Code Ok
Decrypt
$ P#
[...]
a       qC$
 e         |
KEY:
congratulations.tar.bz2
[...]

Le fichier schematic.pdf correspond à l’image ci-dessous :

rk schematic
Figure 2. schematic.pdf

Plusieurs informations intéressantes sont sur ce schéma :

  • on y découvre la notion de “transputer” qui est une architecture matérielle particulière conçue pour effectuer des calculs en parallèle ;

  • le vecteur de test fourni permet de déduire qu’il s’agit d’une implémentation d’un algorithme de déchiffrement ;

  • enfin, toujours au niveau de ce même vecteur de test, la chaîne déchiffrée mentionne une architecture dite “ST20”.

La section Design de la page Wikipedia sur les transputers explique les grands principes de cette architecture matérielle. En particulier, un transputer est capable de communiquer avec d’autres transputers à l’aide d’un lien série. De plus, un transputer peut démarrer de façon classique à l’aide d’une ROM ou de façon plus originale en utilisant un lien série. Le schéma fourni laisse penser que le transputer 0 démarre de cette façon, en lisant les données du fichier input.bin depuis un lien.

La documentation disponible sur le processeur ST20 détaille ce mode de démarrage :

When booting from a link, the ST20-GP1 will wait for the first bootstrap message to arrive on the link. The first byte received down the link is the control byte. If the control byte is greater than 1 (i.e. 2 to 255), it is taken as the length in bytes of the boot code to be loaded down the link. The bytes following the control byte are then placed in internal memory starting at location MemStart. Following reception of the last byte the ST20-GP1 will start executing code at MemStart. The memory space immediately above the loaded code is used as work space. A byte arriving on the bootstrapping link after the last bootstrap byte, is retained and no acknowledge is sent until a process inputs from the link.
— ST20-GP1 datasheet

Pour comprendre la routine de déchiffrement implémentée au sein du fichier input.bin, il va être nécessaire de désassembler les données correspondant au programme puis de les analyser pour retrouver l’algorithme. Fort heureusement, un désassembleur pour le processeur ST20 est disponible sur Internet : st20dis.

De plus, l’identification sur Internet d’un manuel sur le jeu d’instruction du processeur aide à interpréter la sortie du désassembleur.

Présentation de l’architecture

Pour faciliter la compréhension de la suite de ce document, il est nécessaire de présenter quelques spécificités de l’architecture du processeur ST20.

Tout d’abord, il s’agit d’un processeur 32 bits qui utilise une représentation little-endian des données en mémoire. Ainsi, 0xcafebabe sera représenté en mémoire par BE BA FE CA.

Les registres disponibles sur ST20 sont :

  • Wptr : pointe vers une zone mémoire où sont stockées les données locales (peut être assimilé à un pointeur de pile) ;

  • IptrReg : pointe sur la prochaine instruction à exécuter ;

  • StatusReg : registre de statut ;

  • Areg, Breg et Creg : forme une pile d’évaluation.

Le registre Wptr contient l’adresse pour accéder aux variables locales d’une fonction. La valeur du registre est décrémentée à l’entrée d’une fonction pour réserver l’espace nécessaire. L’accès aux variables se fait alors via le registre Wptr qui se comporte comme un tableau de mots (32 bits) : l’adresse de la variable var_3 est égale à Wptr + 4 * 3.

Enfin, les registres Areg, Breg et Creg sont la source et la destination pour la plupart des opérations arithmétiques ou logiques. Lorsqu’une valeur est chargée, alors Breg est poussé dans Creg, Areg est poussé dans Breg et la valeur chargée est stockée dans Areg. A l’inverse, lorsqu’une valeur est stockée depuis Areg, Areg prend la valeur de Breg, Breg celle de Creg et Creg devient indéfini.

Rétro-conception

Pour commencer, le premier réflexe est alors de lancer st20dis sur le fichier input.bin en commençant à désassembler à l’octet 0 :

$ st20dis-linux -A input.bin
; New subroutine 0+1; References: 0, Local Vars: 0
00000000: f8                    sub_0:        prod             ; product - A = A * B (no overflow check)

; New subroutine 1+d; References: 0, Local Vars: 76
00000001: 64 b4                 sub_1:        ajw #-4c         ; adjust workspace - Move workspace pointer
00000003: 40                                  ldc #0           ; load constant - A = n, B=A, C=B
00000004: d1                                  stl #1 [var_1]   ; store local - workspace[n] = A, A=B, B=C
00000005: 40                                  ldc #0           ; load constant - A = n, B=A, C=B
00000006: d3                                  stl #3 [var_3]   ; store local - workspace[n] = A, A=B, B=C
00000007: 24 f2                               mint             ; minimum integer - A = MostNeg
00000009: 24 20 50                            ldnlp #400       ; load non-local pointer - A = &A[n]
0000000c: 23 fc                               gajw             ; general adjust workspace - Wptr <=> A

; New subroutine e+f8; References: 0, Local Vars: 76
0000000e: 64 b4                 sub_e:        ajw #-4c         ; adjust workspace - Move workspace pointer
00000010: 2c 49                               ldc #c9          ; load constant - A = n, B=A, C=B
00000012: 21 fb                               ldpi [str_dd]    ; Load pointer to instruction - A = next instruction + A
00000014: 24 f2                               mint             ; minimum integer - A = MostNeg
[...]

Le désassembleur a l’air de fonctionner correctement et la sortie semble cohérente. Cependant, la toute première instruction semble étrange. Le registre A est mis à jour avec le résultat de la multiplication entre les registres A et B sachant que, d’après la documentation, tous les registres sont dans un état indéfini au démarrage. En réalité, le premier octet du fichier (0xf8) correspond à la valeur “control byte” qui spécifie, lors d’un démarrage via un lien série, la quantité de données qui sera placée dans la mémoire du processeur pour être exécutée.

Le code du démarrage du premier transputer est alors extrait avec les commandes ci-dessous :

$ dd if=input.bin count=1 bs=1 | xxd -
0000000: f8 (1)
$ dd if=input.bin bs=1 skip=1 count=$((0xf8)) of=t0.bin
1 valeur du control byte

Le désassembleur st20dis est alors appelé sur le fichier t0.bin résultat pour enfin obtenir le fichier t0.asm (disponible en annexe).

Transputer 0

Le transputer 0 s’initialise avec les instructions ci-dessous qui ne servent qu’à positionner le pointeur de pile :

; New subroutine 0+d; References: 0, Local Vars: 76
00000000: 64 b4     sub_0:        ajw #-4c       ; réserve 76 variables locales
00000002: 40                      ldc #0
00000003: d1                      stl #1 [var_1] ; var_1 = 0
00000004: 40                      ldc #0
00000005: d3                      stl #3 [var_3] ; var_3 = 0
00000006: 24 f2                   mint           ; A = MostNeg (0x80000000)
00000008: 24 20 50                ldnlp #400     ; A = MostNeg @ 0x400
0000000b: 23 fc                   gajw           ; Wptr = MostNeg @ 0x400

L’exécution continue avec les instructions suivantes :

; New subroutine d+eb; References: 0, Local Vars: 76
0000000d: 64 b4     sub_d:        ajw #-4c           ; réserve 76 variables locales (word)
0000000f: 2c 49                   ldc #c9
00000011: 21 fb                   ldpi [str_dc]
00000013: 24 f2                   mint
00000015: 48                      ldc #8
00000016: fb                      out               ; out(8, 0x80000000, "Boot ok")
00000017: 24 19     loc_17:       ldlp #49 [&var_73]
00000019: 24 f2                   mint              ; A = 0x80000000
0000001b: 54                      ldnlp #4          ; A = 0x80000000 + 4 * 4 = 0x80000010
0000001c: 4c                      ldc #c
0000001d: f7                      in                ; in(12, 0x80000010, &var_73)
0000001e: 24 79                   ldl #49 [var_73]
00000020: 21 a5                   cj loc_37         ; saute à loc_37 si var_73 == 0
00000022: 2c 4d                   ldc #cd
00000024: 21 fb                   ldpi [loc_f3]
00000026: 24 f2                   mint
00000028: 54                      ldnlp #4
00000029: 24 79                   ldl #49 [var_73]
0000002b: f7                      in                ; in(var_73, 0x80000010, loc_f3)
0000002c: 2c 43                   ldc #c3
0000002e: 21 fb                   ldpi [loc_f3]
00000030: 24 7a                   ldl #4a [var_74]
00000032: 24 79                   ldl #49 [var_73]
00000034: fb                      out               ; out(var_73, var_74, loc_f3)
00000035: 61 00                   j loc_17

Ce code fait appel à deux instructions d’entrée/sortie in et out qui ont pour paramètres :

Instruction Arg0 Arg1 Arg2

in

Longueur

Pointeur vers un channel

Adresse destination

out

Longueur

Pointeur vers un channel

 Adresse source

On peut alors résumer ce code ainsi :

  • envoi de "Boot ok" sur le channel 0x80000000 ;

  • lecture de 12 octets (3 mots) du channel 0x80000010 vers l’adresse &var_73 (les variables var_73, var_74 et var_75 sont mises à jour) ;

  • si var_73 == 0, sortie de la boucle et poursuite de l’exécution à l’adresse loc_37 ;

  • sinon lecture de var_73 octets depuis le channel 0x80000010 vers l’adresse loc_f3 ;

  • écriture de var_73 octets depuis l’adresse loc_f3 vers le channel référencé par var_74.

La documentation précise le rôle des channels 0x80000000 et 0x80000010 :

rk st20 memmap

Les channels 0x80000000 et 0x80000010 sont donc respectivement la sortie et l’entrée du lien 0, qui n’est autre que le lien de démarrage qui permet de lire le contenu du fichier input.bin.

Dans le reste de ce document, un channel sera désigné par son index relatif à 0x80000000. Par exemple, le channel 4 correspond à l’adresse 0x80000010.

Pour comprendre la nature des données stockées à l’adresse &var_73, on peut alors simuler une lecture de 3 mots sur le channel 4 avec la commande dd :

$ dd if=input.bin bs=1 skip=$((1+0xf8)) count=12 | xxd -
0000000: 7100 0000 0400 0080 0000 0000

Le résultat de la lecture (instruction in à l’adresse 0x1d) est donc :

  • var_73 : 0x71 ;

  • var_74 : 0x80000004 (channel 1) ;

  • var_75 : 0.

Par la suite, 0x71 octets vont être lus sur le channel 4 puis envoyés sur le channel 1. L’opération peut alors être répétée plusieurs fois, jusqu’à obtenir une longueur (var_73) égale à 0 :

$ dd if=input.bin bs=1 skip=$((1+0xf8+12+0x71)) count=12 | xxd -
0000000: 7100 0000 0800 0080 0000 0000
$ dd if=input.bin bs=1 skip=$((1+0xf8+12+0x71+12+0x71)) count=12 | xxd -
0000000: 7100 0000 0c00 0080 0000 0000
[...]

Ces opérations peuvent être automatisées par le script Ruby ci-dessous :

split.rb
#!/usr/bin/env ruby

def read_byte
  b = $data[$offset]
  $offset += 1
  return b
end

def read_word
  word = $data[$offset, 4]
  word = word.pack('C*').unpack('L').first
  $offset += 4
  return word
end

def read_bytes(n)
  bytes = $data[$offset, n]
  $offset += n
  return bytes
end

def read_msg
  [ read_word, read_word, read_word ]
end

input = ARGV.shift
ext = File.extname(input)
fname = File.basename(input, ext)

$data = File.open(input, "rb").read.unpack('C*')
$offset = 0

control_byte = read_byte
puts "[+] control byte: 0x%02x" % control_byte

code = read_bytes(control_byte).pack('C*')
File.open("#{fname}_code.bin", "wb") { |f| f.write code }
puts "[+] code written to #{fname}_code.bin"

channels_data = Hash.new {|h, k| h[k] = ""}
loop do
  len, channel, var_75 = read_msg
  if len == 0
    puts "[+] len: 0x%02x, channel: 0x%02x, var_75: 0x%02x" % [ len, channel, var_75 ]
    break
  end
  channel_idx = (channel & 0xff) / 4
  puts "[+] 0x%02x bytes => channel #{channel_idx}" % len
  channels_data[channel_idx] << read_bytes(len).pack('C*')
end

channels_data.each do |idx, s|
  File.open("#{fname}_channel_#{idx}.bin", "wb") { |f| f.write s}
  puts "[+] wrote 0x%02x bytes to #{fname}_channel_#{idx}.bin" % s.size
end

remaining = $data[$offset..-1].pack('C*')
File.open("#{fname}_remaining.bin", "wb") { |f| f.write remaining }
puts "[+] wrote 0x%02x bytes to #{fname}_remaining.bin" % remaining.size

On obtient alors le résultat suivant :

$ ./split.rb input.bin
[+] control byte: 0xf8
[+] code written to input_code.bin
[+] 0x71 bytes => channel 1
[+] 0x71 bytes => channel 2
[+] 0x71 bytes => channel 3
[+] 0x31 bytes => channel 1
[+] 0x31 bytes => channel 1
[+] 0x31 bytes => channel 1
[+] 0x31 bytes => channel 2
[+] 0x31 bytes => channel 2
[+] 0x31 bytes => channel 2
[+] 0x31 bytes => channel 3
[+] 0x31 bytes => channel 3
[+] 0x31 bytes => channel 3
[+] 0x5c bytes => channel 1
[+] 0x5c bytes => channel 1
[+] 0x98 bytes => channel 1
[+] 0x70 bytes => channel 2
[+] 0xa8 bytes => channel 2
[+] 0x60 bytes => channel 2
[+] 0xa4 bytes => channel 3
[+] 0x7c bytes => channel 3
[+] 0x90 bytes => channel 3
[+] len: 0x00, channel: 0x00, var_75: 0x00
[+] wrote 0x254 bytes to input_channel_1.bin
[+] wrote 0x27c bytes to input_channel_2.bin
[+] wrote 0x2b4 bytes to input_channel_3.bin
[+] wrote 0x3d316 bytes to input_remaining.bin

Les données enregistrées dans input_channel_1.bin, input_channel_2.bin et input_channel_3.bin seront analysées dans la suite de ce document : d’après le schéma fourni, on peut supposer qu’il s’agit des données envoyées respectivement aux transputers 1, 2 et 3.

On s’intéresse à ce stade aux données contenues dans le fichier input_remaining.bin, c’est-à-dire celles présentes sur le lien 0 à la sortie de la première boucle :

$ hexdump -C input_remaining.bin|head -n 4
00000000  4b 45 59 3a ff ff ff ff  ff ff ff ff ff ff ff ff  |KEY:............|
00000010  17 63 6f 6e 67 72 61 74  75 6c 61 74 69 6f 6e 73  |.congratulations|
00000020  2e 74 61 72 2e 62 7a 32  fe f3 50 dc 81 bc 97 27  |.tar.bz2..P....'|
00000030  89 ac 72 28 cb 50 a4 09  d3 18 17 fc c3 9a 61 a0  |..r(.P........a.|
[...]

Plusieurs valeurs intéressantes peuvent être distinguées :

  • la chaîne de caractères KEY: ;

  • une série de 12 octets valant 0xff ;

  • la valeur 0x17 = 23 sur un octet ;

  • la chaîne de caractères congratulations.tar.bz2 ;

  • enfin, au décalage 4 + 12 + 1 + 23 = 40, des données semblant aléatoires, jusqu’à la fin du fichier.

On peut remarquer que la longueur de la chaîne congratulations.tar.bz2 veut 23 octets.

L’analyse du code assembleur se poursuit alors à l’adresse loc_37, après la sortie de la boucle :

00000037: 24 19     loc_37:       ldlp #49 [&var_73]
00000039: 24 f2                   mint
0000003b: 51                      ldnlp #1
0000003c: 4c                      ldc #c
0000003d: fb                      out               ; out(12, MostNeg @ 1, &var_73)
0000003e: 24 19                   ldlp #49 [&var_73]
00000040: 24 f2                   mint
00000042: 52                      ldnlp #2
00000043: 4c                      ldc #c
00000044: fb                      out                ; out(12, MostNeg @ 2, &var_73)
00000045: 24 19                   ldlp #49 [&var_73]
00000047: 24 f2                   mint
00000049: 53                      ldnlp #3
0000004a: 4c                      ldc #c
0000004b: fb                      out                ; out(12, MostNeg @ 3, &var_73)
0000004c: 29 44                   ldc #94
0000004e: 21 fb                   ldpi [str_e4]
00000050: 24 f2                   mint
00000052: 48                      ldc #8
00000053: fb                      out                ; out(8, MostNeg, "Code Ok")

Ces instructions se contentent d’écrire le contenu des variables var_73, var_74 et var_75 sur les channels 1, 2, et 3. A la sortie de la première boucle, ces trois variables valent 0 d’après la sortie du script split.rb. Les fichiers input_channel_1.bin, input_channel_2.bin et input_channel_3.bin sont mis à jour en conséquence :

$ dd if=/dev/zero bs=1 count=12 of=input_channel_1.bin seek=$((0x254))
$ dd if=/dev/zero bs=1 count=12 of=input_channel_2.bin seek=$((0x27c))
$ dd if=/dev/zero bs=1 count=12 of=input_channel_3.bin seek=$((0x2b4))

Le message Code Ok est ensuite envoyé sur le channel 0 et l’exécution continue à l’adresse loc_54 :

00000054: 12                      ldlp #2 [&var_2]
00000055: 24 f2                   mint
00000057: 54                      ldnlp #4
00000058: 44                      ldc #4
00000059: f7                      in                 ; in(4, MostNeg @ 4, &var_2)
0000005a: 15                      ldlp #5 [&var_5]
0000005b: 24 f2                   mint
0000005d: 54                      ldnlp #4
0000005e: 4c                      ldc #c
0000005f: f7                      in                 ; in(12, MostNeg @ 4, &var_5)
00000060: 28 48                   ldc #88
00000062: 21 fb                   ldpi [str_ec]
00000064: 24 f2                   mint
00000066: 48                      ldc #8
00000067: fb                      out                ; out(8, MostNeg, "Decrypt")
00000068: 13                      ldlp #3 [&var_3]
00000069: 24 f2                   mint
0000006b: 54                      ldnlp #4
0000006c: 41                      ldc #1
0000006d: f7                      in                 ; in(1, MostNeg @ 4, &var_3)
0000006e: 19                      ldlp #9 [&var_9]
0000006f: 24 f2                   mint
00000071: 54                      ldnlp #4
00000072: 13                      ldlp #3 [&var_3]
00000073: f1                      lb
00000074: f7                      in                ; in(*var3, MostNeg @ 4, &var_9)

Il est intéressant de mettre en relation ces instructions avec les données du fichier remaining.bin précédemment analysées :

  • la lecture de 4 octets sur le channel 4 permet de stocker la chaîne KEY: dans var_2 ;

  • la lecture de 12 octets sur le channel 4 stocke les 12 octets valant 0xFF dans var_5, var_6 et var_7 ;

  • un octet est lu sur le channel 4, qui vaut 23 (longueur de congratulations.tar.bz2) ;

  • 23 octets sont donc lus sur le channel, ce qui permet de stocker congratulations.tar.bz2 à l’adresse de var_9.

De plus, la chaîne Decrypt est envoyée sur le channel 0, ce qui laisse supposer que les instructions à venir implémentent une routine de déchiffrement.

Le transputer 0 poursuit son exécution avec les instructions suivantes :

00000075: 40                      ldc #0
00000076: d4                      stl #4 [var_4]    ; var_4 = 0
00000077: 11        loc_77:       ldlp #1 [&var_1]
00000078: 24 f2                   mint
0000007a: 54                      ldnlp #4
0000007b: 41                      ldc #1
0000007c: f7                      in                ; in(1, MostNeg @ 4, &var_1)
0000007d: 15                      ldlp #5 [&var_5]
0000007e: 24 f2                   mint
00000080: 51                      ldnlp #1
00000081: 4c                      ldc #c
00000082: fb                      out               ; out(12, MostNeg @ 1, &var_5)
00000083: 15                      ldlp #5 [&var_5]
00000084: 24 f2                   mint
00000086: 52                      ldnlp #2
00000087: 4c                      ldc #c
00000088: fb                      out               ; out(12, MostNeg @ 2, &var_5)
00000089: 15                      ldlp #5 [&var_5]
0000008a: 24 f2                   mint
0000008c: 53                      ldnlp #3
0000008d: 4c                      ldc #c
0000008e: fb                      out               ; out(12, MostNeg @ 3, &var_5)
0000008f: 10                      ldlp #0 [&var_0]
00000090: 81                      adc #1
00000091: 24 f2                   mint
00000093: 55                      ldnlp #5
00000094: 41                      ldc #1
00000095: f7                      in                ; in(1, MostNeg @ 5, &var_0 + 1)
00000096: 10                      ldlp #0 [&var_0]
00000097: 82                      adc #2
00000098: 24 f2                   mint
0000009a: 56                      ldnlp #6
0000009b: 41                      ldc #1
0000009c: f7                      in                ; in(1, MostNeg @ 6, &var_0 + 2)
0000009d: 10                      ldlp #0 [&var_0]
0000009e: 83                      adc #3
0000009f: 24 f2                   mint
000000a1: 57                      ldnlp #7
000000a2: 41                      ldc #1
000000a3: f7                      in                ; in(1, MostNeg @ 7, &var_0 + 3)
000000a4: 10                      ldlp #0 [&var_0]
000000a5: 81                      adc #1
000000a6: f1                      lb
000000a7: 10                      ldlp #0 [&var_0]
000000a8: 82                      adc #2
000000a9: f1                      lb
000000aa: 23 f3                   xor               ; A = var_0[2] ^ var_0[1]
000000ac: 10                      ldlp #0 [&var_0]
000000ad: 83                      adc #3
000000ae: f1                      lb
000000af: 23 f3                   xor               ; A = var_0[3] ^ ( var_0[2] ^ var_0[1] )
000000b1: 10                      ldlp #0 [&var_0]
000000b2: 81                      adc #1
000000b3: 23 fb                   sb                ; var_0[1] = var_0[3] ^ ( var_0[2] ^ var_0[1] )
000000b5: 11                      ldlp #1 [&var_1]
000000b6: f1                      lb                ; A = var_1
000000b7: 74                      ldl #4 [var_4]    ; A = var_4, B = var_1
000000b8: 15                      ldlp #5 [&var_5]  ; A = &var_5, B = var_4, C = var_1
000000b9: f2                      bsub              ; A = &var_5 + var_4, B = var_1
000000ba: f1                      lb                ; A = var_5[var_4], B = var_1
000000bb: 74                      ldl #4 [var_4]    ; A = var_4, B = var_5[var_4], C = var_1
000000bc: 2c f1                   ssub              ; A = var_4 + 2 * var_5[var_4], B = var_1
000000be: 23 f3                   xor               ; A = (var_4 + 2 * var_5[var_4]) ^ var_1
000000c0: 10                      ldlp #0 [&var_0]
000000c1: 23 fb                   sb                ; var_0[0] = (var_4 + 2 * var_5[var_4]) ^ var_1
000000c3: 10                      ldlp #0 [&var_0]
000000c4: 81                      adc #1
000000c5: f1                      lb                ; A = var_0[1]
000000c6: 74                      ldl #4 [var_4]
000000c7: 15                      ldlp #5 [&var_5]
000000c8: f2                      bsub              ; A = &var_5 + var_4, B = var_0[1]
000000c9: 23 fb                   sb                ; var_5[var_4] = var_0[1]
000000cb: 74                      ldl #4 [var_4]
000000cc: 81                      adc #1
000000cd: 25 fa                   dup               ; A = var_4 + 1, B = var_4 + 1
000000cf: d4                      stl #4 [var_4]    ; var_4 var_4 + 1, A = var_4 + 1
000000d0: cc                      eqc #c
000000d1: a3                      cj loc_d5         ; saute à loc_d5 si var_4 != 12
000000d2: 80                      adc #0
000000d3: 40                      ldc #0
000000d4: d4                      stl #4 [var_4]    ; var_4 = 0
000000d5: 10        loc_d5:       ldlp #0 [&var_0]
000000d6: 24 f2                   mint
000000d8: 41                      ldc #1
000000d9: fb                      out               ; out(1, MostNeg, &var_0)
000000da: 66 0b                   j loc_77          ; saute à loc_77
000000dc: **        str_dc:      .string "Boot ok"
000000e4: **        str_e4:      .string "Code Ok"
000000ec: **        str_ec:      .string "Decrypt"
000000f4: 24 bc                   ajw #4c
000000f6: 22 f0                   ret

Il s’agit de la boucle principale du transputer 0. La variable var_4 est mise à 0 puis la boucle effectue les opérations suivantes :

  • loc_7c : lecture d’un octet depuis le channel 4, stockage dans var_1 ;

  • loc_80 à loc_8e : envoi des 12 octets à l’adresse &var_5 vers les channels 1, 2, 3 ;

  • loc_8f à loc_a3 : lecture d’un octet sur les channels 5, 6, et 7, respectivement stocké dans var_0[1], var_0[2] et var_0[3] ;

  • loc_b3 : mise à jour de l’octet var_0[1] avec l’opération var_0[3] ^ ( var_0[2] ^ var_0[1] ) ;

  • loc_c1 : mise à jour de l’octet var_0[0] avec l’opération (var_4 + 2 * var_5[var_4]) ^ var_1 ;

  • loc_c9 : mise à jour de l’octet var_5[var_4] avec l’octet précédemment calculé var_0[1] ;

  • loc_cb à loc_d4 : incrément de 1 de la variable var_4, remise à 0 si var_4 == 12 ;

  • loc_d5 à loc_da: envoi de l’octet var_0[0] sur le channel 0 et retour au début de la boucle.

Sachant qu’il s’agit très certainement d’une routine de déchiffrement, il est raisonnable de faire les hypothèses suivantes sur le rôle de chaque variable :

  • var_4 est un compteur de boucle qui varie entre 0 et 11 ;

  • var_1 est un octet chiffré ;

  • var_5 contient la clé courante (12 fois 0xff à l’entrée de la boucle), celle-ci est envoyée aux transputers 1, 2 et 3 via leurs channels respectifs ;

  • les transputers 1, 2 et 3 retournent chacun un octet sur les channels 5, 6 et 7 qui sont stockés dans la variable temporaire var_0 ;

  • var_0[1] contient un octet qui sera utilisé pour mettre à jour la clé courante à la position var_4 ;

  • var_0[0] correspond au résultat du déchiffrement de l’octet stocké dans var_1.

Ces opérations peuvent être traduites en pseudo-code C de la façon suivante :

void transputer_0(void) {
    uint8_t c, p, t1, t2, t3, i;
    uint8_t key[12] = "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF";
    i = 0;
    for (;;) {
       in(1, 4, &c);    // lecture de l'octet chiffré

       out(12, 1, key); // envoi de la clé au transputer 1
       out(12, 2, key); // envoi de la clé au transputer 2
       out(12, 3, key); // envoi de la clé au transputer 3

       in(1, 5, &t1);   // lecture d'un octet depuis le transputer 1
       in(1, 6, &t2);   // lecture d'un octet depuis le transputer 2
       in(1, 7, &t3);   // lecture d'un octet depuis le transputer 3

       p = ( i + 2 * key[i] ) ^ c; // déchiffrement de l'octet
       key[i] = t3 ^ (t2 ^ t1);    // mise à jour de la clé
       i++;
       i = (i % 12);

       out(1, 0, &p); // envoi de l'octet déchiffré
    }
}

Maintenant que le fonctionnement du transputer 0 est compris, il reste à analyser les transputers 1, 2 et 3 pour déterminer les traitements effectués sur la clé courante à chaque itération. Pour cela, les données envoyées sur les channels 1, 2 et 3 avant le début de la boucle principale du transputer 0 doivent être examinées : il s’agit en pratique des fichiers input_channel_1.bin, input_channel_2.bin et input_channel_3.bin.

Transputers 1, 2 et 3

Pour des raisons de clarté, les fichiers input_channel_1.bin, input_channel_2.bin et input_channel_3.bin ont été renommés en t1.bin, t2.bin et t3.bin.

De façon similaire au premier transputer, ces trois transputers démarrent sur un lien série. Pour le transputer 1, la valeur du “control byte” et le bloc de code sont extraits avec les commandes ci-dessous :

$ dd if=t1.bin count=1 bs=1 | xxd -
0000000: 70
$ dd if=t1.bin count=$((0x70)) bs=1 skip=1 of=t1_code.bin

Le résultat peut alors être désassemblé avec st20dis :

$ st20dis-linux t1_code.bin > t1.asm

Le fichier résultant t1.asm est disponible en annexe.

Comme vu précédemment, le transputer 1 commence par positionner le pointeur de pile :

; New subroutine 0+9; References: 0, Local Vars: 8
00000000: 60 b8     sub_0:        ajw #-8       ; réserve 8 variables locales
00000002: 24 f2                   mint          ; A = MostNeg
00000004: 24 20 50                ldnlp #400    ; A = MostNeg @ 0x400
00000007: 23 fc                   gajw          ; Wptr = MostNeg @ 0x400

Une première boucle est ensuite exécutée :

; New subroutine 9+67; References: 0, Local Vars: 8
00000009: 60 b8     sub_9:        ajw #-8           ; réserve 8 variables locales
0000000b: 15        loc_b:        ldlp #5 [&var_5]
0000000c: 24 f2                   mint
0000000e: 54                      ldnlp #4
0000000f: 4c                      ldc #c
00000010: f7                      in                ; in(12, MostNeg @ 4, &var_5)
00000011: 75                      ldl #5 [var_5]
00000012: 21 a2                   cj loc_26         ; saute à loc_26 si var_5 == 0
00000014: 25 44                   ldc #54
00000016: 21 fb                   ldpi [loc_6c]
00000018: 24 f2                   mint
0000001a: 54                      ldnlp #4
0000001b: 75                      ldl #5 [var_5]
0000001c: f7                      in                ; in(var_5, MostNeg @ 4, loc_6c)
0000001d: 24 4b                   ldc #4b
0000001f: 21 fb                   ldpi [loc_6c]
00000021: 76                      ldl #6 [var_6]
00000022: 75                      ldl #5 [var_5]
00000023: fb                      out               ; out(var_5, var_6, loc_6c)
00000024: 61 05                   j loc_b

Cette boucle est quasiment identique à la première boucle du transputer 0. On est alors tenté d’utiliser le script split.rb développé précédemment pour extraire les données envoyées sur chaque channel :

$ ./split.rb t1.bin
[+] control byte: 0x70
[+] code written to t1_code.bin
[+] 0x25 bytes => channel 1
[+] 0x25 bytes => channel 2
[+] 0x25 bytes => channel 3
[+] 0x50 bytes => channel 1
[+] 0x50 bytes => channel 2
[+] 0x8c bytes => channel 3
[+] len: 0x00, channel: 0x00, var_75: 0x00
[+] wrote 0x75 bytes to t1_channel_1.bin
[+] wrote 0x75 bytes to t1_channel_2.bin
[+] wrote 0xb1 bytes to t1_channel_3.bin
[+] wrote 0x00 bytes to t1_remaining.bin
$ ./split.rb t2.bin
[+] control byte: 0x70
[+] code written to t2_code.bin
[+] 0x25 bytes => channel 1
[+] 0x25 bytes => channel 2
[+] 0x25 bytes => channel 3
[+] 0x64 bytes => channel 1
[+] 0x9c bytes => channel 2
[+] 0x54 bytes => channel 3
[+] len: 0x00, channel: 0x00, var_75: 0x00
[+] wrote 0x89 bytes to t2_channel_1.bin
[+] wrote 0xc1 bytes to t2_channel_2.bin
[+] wrote 0x79 bytes to t2_channel_3.bin
[+] wrote 0x00 bytes to t2_remaining.bin
$ ./split.rb t3.bin
[+] control byte: 0x70
[+] code written to t3_code.bin
[+] 0x25 bytes => channel 1
[+] 0x25 bytes => channel 2
[+] 0x25 bytes => channel 3
[+] 0x98 bytes => channel 1
[+] 0x70 bytes => channel 2
[+] 0x84 bytes => channel 3
[+] len: 0x00, channel: 0x00, var_75: 0x00
[+] wrote 0xbd bytes to t3_channel_1.bin
[+] wrote 0x95 bytes to t3_channel_2.bin
[+] wrote 0xa9 bytes to t3_channel_3.bin
[+] wrote 0x00 bytes to t3_remaining.bin

Le script fonctionne correctement sur les trois transputers et pour chacun d’eux, les données envoyées sur les channels 1, 2 et 3 sont extraites. On peut remarquer qu’après avoir effectué cette première boucle, il n’y a plus de données en attente sur le lien série (les fichiers t1_remaining.bin, t2_remaining.bin et t3_remaining.bin sont vides). On remarque également que le “control byte” est identique pour les trois transputers. En réalité, le code extrait est le même pour les trois, comme indiqué ci-dessous :

$ md5sum t*_code.bin
0cbeb2a7a4f269356aeec0fe6bfc9192  t1_code.bin
0cbeb2a7a4f269356aeec0fe6bfc9192  t2_code.bin
0cbeb2a7a4f269356aeec0fe6bfc9192  t3_code.bin

Il suffit donc d’analyser un seul transputer pour comprendre les trois.

L’analyse continue ensuite après la première boucle, à l’adresse loc_26 :

00000026: 11        loc_26:       ldlp #1 [&var_1]
00000027: 24 f2                   mint
00000029: 54                      ldnlp #4
0000002a: 4c                      ldc #c
0000002b: f7                      in                 ; in(12, MostNeg @ 4, &var_1)
0000002c: 11                      ldlp #1 [&var_1]
0000002d: 24 f2                   mint
0000002f: 51                      ldnlp #1
00000030: 4c                      ldc #c
00000031: fb                      out                ; out(12, MostNeg @ 1, &var_1)
00000032: 11                      ldlp #1 [&var_1]
00000033: 24 f2                   mint
00000035: 52                      ldnlp #2
00000036: 4c                      ldc #c
00000037: fb                      out                ; out(12, MostNeg @ 2, &var_1)
00000038: 11                      ldlp #1 [&var_1]
00000039: 24 f2                   mint
0000003b: 53                      ldnlp #3
0000003c: 4c                      ldc #c
0000003d: fb                      out                ; out(12, MostNeg @ 3, &var_1)
0000003e: 10                      ldlp #0 [&var_0]
0000003f: 81                      adc #1
00000040: 24 f2                   mint
00000042: 55                      ldnlp #5
00000043: 41                      ldc #1
00000044: f7                      in                 ; in(1, MostNeg @ 5, &var_0 + 1)
00000045: 10                      ldlp #0 [&var_0]
00000046: 82                      adc #2
00000047: 24 f2                   mint
00000049: 56                      ldnlp #6
0000004a: 41                      ldc #1
0000004b: f7                      in                 ; in(1, MostNeg @ 6, &var_0 + 2)
0000004c: 10                      ldlp #0 [&var_0]
0000004d: 83                      adc #3
0000004e: 24 f2                   mint
00000050: 57                      ldnlp #7
00000051: 41                      ldc #1
00000052: f7                      in                 ; in(1, MostNeg @ 7, &var_0 + 3)
00000053: 10                      ldlp #0 [&var_0]
00000054: 81                      adc #1
00000055: f1                      lb                 ; A = var_0[1]
00000056: 10                      ldlp #0 [&var_0]
00000057: 82                      adc #2
00000058: f1                      lb                 ; A = var_0[2], B = var_0[1]
00000059: 23 f3                   xor                ; A = var_0[2] ^ var_0[1]
0000005b: 10                      ldlp #0 [&var_0]
0000005c: 83                      adc #3
0000005d: f1                      lb                 ; A = var_0[3], B = var_0[2] ^ var_0[1]
0000005e: 23 f3                   xor                ; A = var_0[3] ^ (var_0[2] ^ var_0[1])
00000060: 25 fa                   dup                ; A = var_0[3] ^ (var_0[2] ^ var_0[1]), B = A
00000062: 10                      ldlp #0 [&var_0]
00000063: 23 fb                   sb                 ; var_0[0] = var_0[3] ^ (var_0[2] ^ var_0[1])
00000065: 10                      ldlp #0 [&var_0]
00000066: 24 f2                   mint
00000068: 41                      ldc #1
00000069: fb                      out                ; out(1, MostNeg, var_0[3] ^ (var_0[2] ^ var_0[1]))
0000006a: 64 0a                   j loc_26           ; saute à loc_26
0000006c: 00        loc_6c:       nop
0000006d: b8                      ajw #8
0000006e: 22 f0                   ret

Ce code revient à effectuer les opérations suivantes :

  • loc_26 à loc_2b : lecture de 12 octets du channel 4 vers l’adresse de var_1 ;

  • loc_2c à loc_3d : envoi de 12 octets de l’adresse de var_1 vers les channels 1, 2 et 3 ;

  • loc_3e à loc_52 : lecture d’un octet sur les channels 5, 6, et 7, respectivement stocké dans var_0[1], var_0[2] et var_0[3] ;

  • loc_53 à loc_63 : mise à jour de l’octet var_0[0] avec l’opération var_0[3] ^ ( var_0[2] ^ var_0[1] ) ;

  • loc_65 à loc_6a : envoi de l’octet var_0[0] vers le channel 0 et saut à l’adresse loc_26.

Ces opérations peuvent être traduites en pseudo-code C de la façon suivante :

Pseudo-code transputer 1
void transputer_1(void) {
    uint8_t res, t4, t5, t6;
    uint8_t key[12];

    for (;;) {
        in(12, 4, key); // lecture de la clé

        out(12, 1, key); // envoi de la clé au transputer 4
        out(12, 2, key); // envoi de la clé au transputer 5
        out(12, 3, key); // envoi de la clé au transputer 6

        in(1, 5, &t4); // lecture d'un octet depuis le transputer 4
        in(1, 6, &t5); // lecture d'un octet depuis le transputer 5
        in(1, 7, &t6); // lecture d'un octet depuis le transputer 6

        res = t6 ^ (t5 ^ t4);

        out(1, 0, &res); // envoi de res sur le channel 0
    }
}

Le fonctionnement des transputers 2 et 3 est identique. Pour terminer l’analyse, il faut donc s’intéresser aux transputers 4 à 12.

Transputers 4 à 12

Le transputer 4 est initialisé avec les données envoyées par le transputer 1 sur le channel 1, c’est-à-dire :

$ hexdump -C t1_channel_1.bin
00000000  24 60 bd 24 f2 24 20 50  23 fc 60 bd 10 24 f2 54  |$`.$.$ P#.`..$.T|
00000010  4c f7 4b 21 fb 24 f2 54  70 f7 43 21 fb 72 f2 f6  |L.K!.$.Tp.C!.r..|
00000020  00 b3 22 f0 20 44 00 00  00 00 00 00 00 0c 00 00  |..". D..........|
00000030  00 73 72 74 f7 22 f0 73  72 74 fb 22 f0 60 bb 40  |.srt.".srt.".`.@|
00000040  d1 40 11 23 fb 4c d0 12  24 f2 54 76 61 93 40 d0  |.@.#.L..$.Tva.@.|
00000050  70 12 f2 f1 11 f1 f2 2f  4f 24 f6 11 23 fb 70 81  |p....../O$..#.p.|
00000060  d0 4c 70 f9 a2 61 09 41  d0 11 24 f2 76 63 98 20  |.Lp..a.A..$.vc. |
00000070  62 03 20 20 20                                    |b.   |
00000075

Le “control byte” valant 0x24, on peut alors extraire le code d’initialisation (fichier t4_code.bin) et les données restantes sur le lien (fichier t4_remaining.bin) :

$ dd if=t1_channel_1.bin of=t4_code.bin bs=1 skip=1 count=$((0x24))
36+0 records in
36+0 records out
36 bytes (36 B) copied, 0.000228839 s, 157 kB/s
$ dd if=t1_channel_1.bin of=t4_remaining.bin bs=1 skip=$((1+0x24))
80+0 records in
80+0 records out
80 bytes (80 B) copied, 0.000308885 s, 259 kB/s
$ st20dis-linux t4_code.bin > t4.asm

Le résultat du désassemblage du code d’initialisation est présenté ci-dessous :

t4.asm
; New subroutine 0+9; References: 0, Local Vars: 3
00000000: 60 bd     sub_0:        ajw #-3            ; réserve 3 variables
00000002: 24 f2                   mint
00000004: 24 20 50                ldnlp #400
00000007: 23 fc                   gajw               ; Wptr = MostNeg @ 0x400

; New subroutine 9+1b; References: 0, Local Vars: 3
00000009: 60 bd     sub_9:        ajw #-3            ; réserve 3 variables
0000000b: 10                      ldlp #0 [&var_0]
0000000c: 24 f2                   mint
0000000e: 54                      ldnlp #4
0000000f: 4c                      ldc #c
00000010: f7                      in                 ; in(12, MostNeg @ 4, &var_0)
00000011: 4b                      ldc #b
00000012: 21 fb                   ldpi [loc_1f]
00000014: 24 f2                   mint
00000016: 54                      ldnlp #4
00000017: 70                      ldl #0 [var_0]
00000018: f7                      in                 ; in(var_0, MostNeg @ 4, loc_1f)
00000019: 43                      ldc #3
0000001a: 21 fb                   ldpi [loc_1f]
0000001c: 72                      ldl #2 [var_2]
0000001d: f2                      bsub               ; A = var_2 + loc_1f
0000001e: f6                      gcall              ; gcall(var_2 + loc_1f)
0000001f: 00        loc_1f:       nop
00000020: b3                      ajw #3
00000021: 22 f0                   ret
00000023: 20                      .db #20 ' '

Ce code d’initialisation réalise les opérations suivantes :

  • lecture de 12 octets sur le channel 4, stockage à l’adresse &var_0 (les variables var_0, var_1 et var_2 sont donc mises à jour) ;

  • lecture de var_0 octets sur le channel, stockage à l’adresse loc_1f ;

  • appel d’une fonction à l’adresse loc_1f + var_2.

On peut alors s’intéresser aux données restantes sur le lien pour comprendre l’état des trois variables :

$ dd if=t4_remaining.bin bs=1 count=12 | xxd -
0000000: 4400 0000 0000 0000 0c00 0000            D...........

Les trois variables sont alors initialisées de la façon suivante :

  • var_0 : 0x44 ;

  • var_1 : 0 ;

  • var_2 : 0xc.

Le code d’initialisation va donc lire 0x44 = 68 octets sur le lien série, stocker le résultat à l’adresse loc_1f, puis appeler une fonction à l’adresse loc_1f + 0xc. Le code exécuté est extrait puis désassemblé avec les commandes ci-dessous :

$ dd if=t4_remaining.bin bs=1 count=$((0x44)) skip=12 > t4_code_gcall.bin
$ hexdump -C t4_code_gcall.bin
00000000  73 72 74 f7 22 f0 73 72  74 fb 22 f0 60 bb 40 d1  |srt.".srt.".`.@.|
00000010  40 11 23 fb 4c d0 12 24  f2 54 76 61 93 40 d0 70  |@.#.L..$.Tva.@.p|
00000020  12 f2 f1 11 f1 f2 2f 4f  24 f6 11 23 fb 70 81 d0  |....../O$..#.p..|
00000030  4c 70 f9 a2 61 09 41 d0  11 24 f2 76 63 98 20 62  |Lp..a.A..$.vc. b|
00000040  03 20 20 20                                       |.   |
00000044
$ st20dis-linux t4_code_gcall.bin > t4_code_gcall.asm
$ ls -al t4_remaining.bin
-rw-rw-r-- 1 jpe jpe 80 avril 21 21:12 t4_remaining.bin

Au passage, on peut remarquer que le fichier t4_remaining.bin (80 octets) contient uniquement les 12 octets permettant d’initialiser les trois variables et les 0x44 = 68 octets correspondant au code exécuté par l’instruction gcall : il n’y a donc plus de données en attente sur le lien 0.

Le résultat du désassemblage est le suivant :

t4_code_gcall.asm
; New subroutine 0+6; References: 1, Local Vars: 0
;       Called/referenced from 1b
00000000: 73     sub_0:        ldl #3 [arg_3]
00000001: 72                   ldl #2 [arg_2]
00000002: 74                   ldl #4 [arg_4]
00000003: f7                   in                   ; in(arg_4, arg_2, arg_3)
00000004: 22 f0                ret

; New subroutine 6+6; References: 1, Local Vars: 0
;       Called/referenced from 3c
00000006: 73     sub_6:        ldl #3 [arg_3]
00000007: 72                   ldl #2 [arg_2]
00000008: 74                   ldl #4 [arg_4]
00000009: fb                   out                 ; out(arg_4, arg_2, arg_3)
0000000a: 22 f0                ret

; New subroutine c+38; References: 0, Local Vars: 5
0000000c: 60 bb  sub_c:        ajw #-5               ; point d'entrée
0000000e: 40                   ldc #0
0000000f: d1                   stl #1 [var_1]        ; var_1 = 0
00000010: 40                   ldc #0
00000011: 11                   ldlp #1 [&var_1]
00000012: 23 fb                sb                    ; var_1[0] = 0
00000014: 4c     loc_14:       ldc #c
00000015: d0                   stl #0 [var_0]        ; var_0 = 12
00000016: 12                   ldlp #2 [&var_2]      ; A = &var_2
00000017: 24 f2                mint                  ; A = MostNeg, B = &var_2
00000019: 54                   ldnlp #4              ; A = MostNeg @ 4, B = &var_2
0000001a: 76                   ldl #6 [arg_1]        ; A = arg_1, B = MostNeg @ 4, C = &var_2
0000001b: 61 93                call sub_0            ; sub_0(arg1, MostNeg @ 4, &var_2) <=>
                                                     ; in(var_0 = 12, MostNeg @ 4, &var_2)
0000001d: 40                   ldc #0
0000001e: d0                   stl #0 [var_0]        ; var_0 = 0
0000001f: 70     loc_1f:       ldl #0 [var_0]
00000020: 12                   ldlp #2 [&var_2]
00000021: f2                   bsub
00000022: f1                   lb                    ; A = var_2[var_0]
00000023: 11                   ldlp #1 [&var_1]
00000024: f1                   lb                    ; A = var_1[0], B = var_2[var_0]
00000025: f2                   bsub                  ; A = var_1[0] + var_2[var_0]
00000026: 2f 4f                ldc #ff
00000028: 24 f6                and                   ; A = (var_1[0] + var_2[var_0]) & 0xff
0000002a: 11                   ldlp #1 [&var_1]
0000002b: 23 fb                sb                    ; var_1[0] = (var_1[0] + var_2[var_0]) & 0xff
0000002d: 70                   ldl #0 [var_0]
0000002e: 81                   adc #1
0000002f: d0                   stl #0 [var_0]        ; var_0 += 1
00000030: 4c                   ldc #c
00000031: 70                   ldl #0 [var_0]
00000032: f9                   gt
00000033: a2                   cj loc_36             ; saute à loc_36 si var_0 >= 12
00000034: 61 09                j loc_1f              ; saute à loc_1f
00000036: 41     loc_36:       ldc #1
00000037: d0                   stl #0 [var_0]        ; var_0 = 1
00000038: 11                   ldlp #1 [&var_1]      ; A = &var_1
00000039: 24 f2                mint                  ; A = MostNeg, B = &var_1
0000003b: 76                   ldl #6 [arg_1]        ; A = arg_1, B = MostNeg, C = &var_1
0000003c: 63 98                call sub_6            ; sub_6(arg_1, MostNeg, &var_1) <=>
                                                     ; out(var_0 = 1, MostNeg, &var_1)
0000003e: 20                   .db #20 ' '
0000003f: 62 03                j loc_14              ; saute à loc_14
00000041: 20                   .db #20 ' '
00000042: 20                   .db #20 ' '
00000043: 20                   .db #20 ' '

L’exécution commence avec l’appel de la fonction sub_c (point d’entrée). On constate la présence de deux boucles imbriquées, ainsi que des appels à deux fonctions, sub_0 et sub_6. Pour comprendre comment s’effectue le passage de paramètres, il est nécessaire de se référer à la documentation. D’après celle-ci, les paramètres sont passés dans les registres A, B, C et le pointeur de pile est décalé pour réserver 4 variables :

rk st20 call

Les deux fonctions sub_0 et sub_6 font référence à un quatrième argument, arg_4. Il s’agit en fait de la valeur stockée dans var_0 au niveau de la fonction appelante.

La fonction sub_c peut être traduite dans le pseudo-code C ci-dessous :

/* Transputer 4 */
void sub_c_t4(uint32_t arg_1) {
    uint8_t i, var_1;
    uint8_t var_2[12];

    var_1 = 0;
    for (;;) {
        in(12, 4, var_2);
        for (i = 0; i < 12; i++) {
            var_1 = var_1 + var_2[i];
        }
        out(1, 0, &var_1);
    }
}

La fonction sub_c commence par initialiser un état (la variable var_1) puis entre dans une boucle infinie. Celle-ci va lire 12 octets sur le channel 4 puis entre dans une seconde boucle pour mettre à jour la variable var_1 en fonction des 12 octets lus. Enfin, le contenu de la variable var_1 est envoyé sur le channel 0 et l’exécution reprend au début de la boucle infinie.

Les 12 octets lus sur le channel 4 correspondent aux données envoyées par le transputer 1 (voir Pseudo-code transputer 1), à savoir la clé courante. Le résultat est stocké à l’adresse var_2.

Il faut maintenant analyser les transputers 5 à 12 de la même façon. Le script Ruby ci-dessous automatise les opérations d’extraction des différentes parties de code :

#!/usr/bin/env ruby

Dir["t*_channel_*.bin"].each do |p|
  p =~ /t(\d)+_channel_(\d+)+\.bin/
  t_src, c = $1.to_i, $2.to_i
  t_dst = t_src * 3 + c
  data = File.open(p, "rb").read.unpack('C*')

  control_byte = data.shift
  init_code = data.shift(control_byte).pack('C*')

  size, _, offset = *data.shift(12).pack('C*').unpack('LLL')
  code = data.shift(size).pack('C*')

  init_code_path = "t#{t_dst}_code.bin"
  File.open(init_code_path, "wb") {|f| f.write init_code}

  code_path = "t#{t_dst}_code_gcall.bin"
  File.open(code_path, "wb") {|f| f.write code}

  remaining = data.size
  puts "T#{t_dst}, init_code => #{init_code_path}, code => #{code_path}, offset: #{offset}, remaining: #{remaining}"
end

Son exécution retourne le résultat suivant :

$  ./extract.rb
T12, init_code => t12_code.bin, code => t12_code_gcall.bin, offset: 12, remaining: 0
T10, init_code => t10_code.bin, code => t10_code_gcall.bin, offset: 12, remaining: 0
T6, init_code => t6_code.bin, code => t6_code_gcall.bin, offset: 12, remaining: 0
T11, init_code => t11_code.bin, code => t11_code_gcall.bin, offset: 12, remaining: 0
T4, init_code => t4_code.bin, code => t4_code_gcall.bin, offset: 12, remaining: 0
T5, init_code => t5_code.bin, code => t5_code_gcall.bin, offset: 12, remaining: 0
T7, init_code => t7_code.bin, code => t7_code_gcall.bin, offset: 12, remaining: 0
T9, init_code => t9_code.bin, code => t9_code_gcall.bin, offset: 12, remaining: 0
T8, init_code => t8_code.bin, code => t8_code_gcall.bin, offset: 12, remaining: 0

On constate que le code d’initialisation est identique pour tous ces transputers :

$ md5sum t*_code.bin
3baf20a3228b40f92da74bc37f1655b1  t10_code.bin
3baf20a3228b40f92da74bc37f1655b1  t11_code.bin
3baf20a3228b40f92da74bc37f1655b1  t12_code.bin
3baf20a3228b40f92da74bc37f1655b1  t4_code.bin
3baf20a3228b40f92da74bc37f1655b1  t5_code.bin
3baf20a3228b40f92da74bc37f1655b1  t6_code.bin
3baf20a3228b40f92da74bc37f1655b1  t7_code.bin
3baf20a3228b40f92da74bc37f1655b1  t8_code.bin
3baf20a3228b40f92da74bc37f1655b1  t9_code.bin

Par contre, le code correspondant à la fonction appelée par l’instruction gcall est différent pour chaque transputer :

$ md5sum t*_code_gcall.bin
36301de282128261ce30c7c7ee9e7bbd  t10_code_gcall.bin
cd8aebe05554ed86a6f3ad427baa6518  t11_code_gcall.bin
33ae0ea641c046913845a24e2033cd34  t12_code_gcall.bin
51be7b81c12f08e0756fa2d91c70f199  t4_code_gcall.bin
94441e3bff1beafb2a871078875f6d8a  t5_code_gcall.bin
26f82821abe326242096929df7af0bcb  t6_code_gcall.bin
13433117ca2c08b05a2d67b43f8ff40c  t7_code_gcall.bin
e72410105cdc42151108b42000dc4c17  t8_code_gcall.bin
0107b594c3601eee98adc4bf7fae57d4  t9_code_gcall.bin

Pour poursuivre, il faut donc désassembler chacun de ces fichiers (sauf pour le transputer 4 qui a déjà été traité) puis analyser la sortie.

L’analyse du code désassemblé ne pose pas de difficultés particulières et peut être effectuée de façon similaire à celle du transputer 4. Tous les transputers de 4 à 12 suivent une même logique qui peut être décrite ainsi :

  • initialisation d’un état (exemple : mise à zéro d’une variable) ;

  • démarrage d’une boucle infinie :

    • lecture des 12 octets de la clé courante,

    • mise à jour de l’état en fonction des 12 octets lus,

    • calcul de la valeur de retour,

    • envoi d’un octet de l’état et retour au début de la boucle.

Le pseudo-code ci-dessous est obtenu pour les transputers 5 à 10 :

/* Transputer 5 */
void sub_c_t5(uint32_t arg_1) {
    uint8_t i, var_1;
    uint8_t var_2[12];

    var_1 = 0; // initialisation de l'état
    for (;;) {
        in(12, 4, var_2); // lecture de la clé
        for (i = 0; i < 12; i++) {
            var_1 = var_1 ^ var_2[i];
        }
        out(1, 0, &var_1); // envoi du résultat
    }
}

/* Transputer 6 */
void sub_c_t6(uint32_t arg1) {
    uint16_t k1, k2, k3, var_1, var_3;
    uint8_t var_4[12];
    int i;

    var_1 = 0;
    var_3 = 0;
    for (;;) {
        in(12, 4, var_4); // lecture de la clé

        if (var_3 == 0) { // initialisation de l'état
            for (i = 0; i < 12; i++) {
                var_1 = (var_1 + var_4[i]) & 0xffff;
            }
            var_3 = 1;
        }

        k1 = (var_1 & 0x8000) >> 0xf;
        k2 = (var_1 & 0x4000) >> 0xe;
        k2 = k2 ^ k1;
        k3 = var_1 << 1;
        var_1 = k3 ^ k2; // mise à jour de l'état
        out(1, 0, &var_1); // envoi du résultat
    }
}

/* Transputer 7 */
void sub_c_t7(uint32_t arg1) {
    uint8_t var_1, var_2;
    uint8_t var_3[12];
    int i;

    for (;;) {
        in(12, 4, &var_3); // lecture de la clé
        var_1 = 0;
        var_2 = 0;

        for (i = 0; i < 6; i++) {
            var_1 += var_3[i];
            var_2 += var_3[i + 6];
        }

        var_1 = var_1 ^ var_2;
        out(1, 0, &var_1); // envoi du résultat
   }
}

/* Transputer 8 */
void sub_c_t8(uint32_t arg1) {
    uint8_t var_1, var_2, var_3;
    uint8_t var_4[4][12];
    int i, j;

    /* initialisation de l'état */
    var_2 = 0;
    for (i = 0; i < 4; i++) {
       for (j = 0; j < 12; j++) {
           var_4[i][j] = 0;
       }
    }

    for (;;) {
        /* mise à jour de l'état */
        in(12, 4, var_4[var_2]); // lecture de la clé courante
        var_2 = (var_2 + 1) % 4;

        /* calcul du résultat */
        var_3 = 0;
        for (i = 0; i < 4; i++) {
            var_1 = 0;
            for (j = 0; j < 12; j++) {
                var_1 += var_4[i][j];
            }
            var_3 = var_1 ^ var_3;
        }
        out(1, 0, &var_3); // envoi du résultat
    }
}

/* Transputer 9 */
void sub_c_t9(uint32_t arg1) {
    uint8_t var1;
    uint8_t var_2[12];
    int i;

    for (;;) {
        var_1 = 0;
        in(12, 4, var_2); // lecture de la clé
        for (i = 0; i < 12; i++) {
            var_1 = var_1 ^ (var_2[i] << (i & 7));
        }
        out(1, 0, &var_1); // envoi du résultat
    }
}

/* Transputer 10 */
void sub_c_t10(uint32_t arg1) {
    uint8_t var_1, var_2;
    uint8_t var_4[4][12];
    int i, j;

    /* initialisation de l'état */
    var_2 = 0;
    for (i = 0; i < 4; i++) {
       for (j = 0; j < 12; j++) {
           var_4[i][j] = 0;
       }
    }

    for (;;) {
        /* mise à jour de l'état */
        in(12, 4, var_4[var_2]); // lecture de la clé courante
        var_2 = (var_2 + 1) % 4;

        /* calcul du résultat */
        var_1 = 0;
        for (i = 0; i < 4; i++) {
            var_1 += var_4[i][0];
        }
        i = var_1 & 3;
        j = (var_1 >> 4) % 12;
        var_1 = var_4[i][j];

        out(1, 0, &var_1); // envoi du résultat
    }
}

Les transputers 11 et 12 présentent cependant une particularité par rapport aux précédents : ils sont en effet reliés par un lien de communication. Pour comprendre l’utilité de celui-ci, il faut examiner le code assembleur des deux transputers.

Le code du transputer 11 est présenté ci-dessous :

0000000c: 60 ba  sub_c:        ajw #-6
0000000e: 40                   ldc #0
0000000f: d1                   stl #1 [var_1]      ; var_1 = 0
00000010: 40                   ldc #0
00000011: d2                   stl #2 [var_2]      ; var_2 = 0
00000012: 4c     loc_12:       ldc #c
00000013: d0                   stl #0 [var_0]      ; var_0 = 12
00000014: 13                   ldlp #3 [&var_3]
00000015: 24 f2                mint
00000017: 54                   ldnlp #4
00000018: 77                   ldl #7 [arg_1]
00000019: 61 95                call sub_0           ; in(12, MostNeg @ 4, &var_3)
0000001b: 40                   ldc #0
0000001c: 11                   ldlp #1 [&var_1]
0000001d: 23 fb                sb                   ; var_1[0] = 0
0000001f: 13                   ldlp #3 [&var_3]
00000020: f1                   lb                   ; A = var_3[0]
00000021: 13                   ldlp #3 [&var_3]
00000022: 83                   adc #3
00000023: f1                   lb                   ; A = var_3[3], B = var_3[0]
00000024: 23 f3                xor                  ; A = var_3[3] ^ var_3[0]
00000026: 13                   ldlp #3 [&var_3]
00000027: 87                   adc #7
00000028: f1                   lb                   ; A = var_3[7], B = var_3[3] ^ var_3[0]
00000029: 23 f3                xor                  ; A = var_3[7] ^ ( var_3[3] ^ var_3[0] )
0000002b: 2f 4f                ldc #ff              ;
0000002d: 24 f6                and                  ;
0000002f: 11                   ldlp #1 [&var_1]     ;
00000030: 23 fb                sb                   ; var_1[0] = ( var_3[7] ^ ( var_3[3] ^ var_3[0] ) ) & 0xff
00000032: 41                   ldc #1
00000033: d0                   stl #0 [var_0]
00000034: 11                   ldlp #1 [&var_1]
00000035: 24 f2                mint
00000037: 51                   ldnlp #1
00000038: 77                   ldl #7 [arg_1]
00000039: 63 9b                call sub_6           ; out(1, MostNeg @ 1, &var_1)
                                                    ; envoi d'un octet à T12
0000003b: 41                   ldc #1
0000003c: d0                   stl #0 [var_0]
0000003d: 11                   ldlp #1 [&var_1]
0000003e: 24 f2                mint
00000040: 55                   ldnlp #5
00000041: 77                   ldl #7 [arg_1]
00000042: 64 9c                call sub_0           ; in(1, MostNeg @ 5, &var_1)
                                                    ; lecture d'un octet depuis T12
00000044: 11                   ldlp #1 [&var_1]
00000045: f1                   lb
00000046: 4c                   ldc #c
00000047: 21 ff                rem                  ; A = var_1[0] % 12
00000049: 2f 4f                ldc #ff
0000004b: 24 f6                and
0000004d: 11                   ldlp #1 [&var_1]
0000004e: 23 fb                sb                   ; var_1[0] = (var_1[0] % 12) & 0xff
00000050: 11                   ldlp #1 [&var_1]
00000051: f1                   lb
00000052: 13                   ldlp #3 [&var_3]
00000053: f2                   bsub
00000054: f1                   lb                   ; A = var_3[var_1[0]]
00000055: 12                   ldlp #2 [&var_2]
00000056: 23 fb                sb                   ; var_2[0] = var_3[var_1[0]]
00000058: 41                   ldc #1
00000059: d0                   stl #0 [var_0]
0000005a: 12                   ldlp #2 [&var_2]
0000005b: 24 f2                mint
0000005d: 77                   ldl #7 [arg_1]
0000005e: 65 96                call sub_6           ; out(1, MostNeg, &var_2)
00000060: 20                   .db #20 ' '
00000061: 65 0f                j loc_12

Les opérations suivantes sont donc effectuées :

  • mise à zéro de var_1 et var_2 ;

  • début de la boucle infinie :

    • lecture des 12 octets de clé dans var_3,

    • mise à jour de var_1 en fonction des 12 octets lus,

    • envoi de la nouvelle valeur de var_1 au transputer 12,

    • lecture d’une nouvelle valeur de var_1 depuis le transputer 12,

    • mise à jour de var_2 selon l’opération var_2 = var_3[var_1 % 12],

    • envoi de var_2 sur le lien 0 puis retour au début de la boucle.

Le pseudo-code C correspondant est présenté ci-dessous :

/* Transputer 11 */
void sub_c_t11(uint32_t arg1) {
   uint8_t var_1, var_2;
   uint8_t var_3[12];

   for (;;) {
       in(12, 4, var_3); // lecture de la clé courante
       var_1 = var_3[7] ^ ( var_3[3] ^ var_3[0] );

       out(1, 1, &var_1); // envoi d'un octet au transputer 12
       in(1, 5, &var_1);  // lecture d'un octet depuis le transputer 12

       var_2 = var_3[var_1 % 12];
       out(1, 0, &var_2); // envoi du résultat sur le lien 0
   }
}

Une analyse similaire est alors effectuée sur le code du transputer 12 :

0000000c: 60 ba  sub_c:        ajw #-6
0000000e: 40                   ldc #0
0000000f: d2                   stl #2 [var_2]    ; var_2 = 0
00000010: 40                   ldc #0
00000011: d1                   stl #1 [var_1]    ; var_1 = 0
00000012: 40                   ldc #0
00000013: d0                   stl #0 [var_0]    ; var_0 = 0
00000014: 40     loc_14:       ldc #0
00000015: 70                   ldl #0 [var_0]
00000016: 13                   ldlp #3 [&var_3]
00000017: f2                   bsub
00000018: 23 fb                sb                ; var_3[var_0] = 0
0000001a: 70                   ldl #0 [var_0]
0000001b: 81                   adc #1
0000001c: d0                   stl #0 [var_0]    ; var_0 += 1
0000001d: 4c                   ldc #c
0000001e: 70                   ldl #0 [var_0]
0000001f: f9                   gt
00000020: a2                   cj loc_23         ; saute à loc_23 si var_0 >= 12
00000021: 60 01                j loc_14          ; saute à loc_14
00000023: 40     loc_23:       ldc #0
00000024: 11                   ldlp #1 [&var_1]
00000025: 23 fb                sb                ; var_1[0] = 0
00000027: 13                   ldlp #3 [&var_3]
00000028: 81                   adc #1
00000029: f1                   lb                ; A = var_3[1]
0000002a: 13                   ldlp #3 [&var_3]
0000002b: 85                   adc #5
0000002c: f1                   lb                ; A = var_3[5], B = var_3[1]
0000002d: 23 f3                xor               ; A = var_3[5] ^ var_3[1]
0000002f: 13                   ldlp #3 [&var_3]
00000030: 89                   adc #9
00000031: f1                   lb                ; A = var_3[9], B = var_3[5] ^ var_3[1]
00000032: 23 f3                xor               ; A = var_3[9] ^ (var_3[5] ^ var_3[1])
00000034: 2f 4f                ldc #ff
00000036: 24 f6                and
00000038: 11                   ldlp #1 [&var_1]
00000039: 23 fb                sb                ; var_1[0] = (var_3[9] ^ (var_3[5] ^ var_3[1])) & 0xff
0000003b: 4c                   ldc #c
0000003c: d0                   stl #0 [var_0]
0000003d: 13                   ldlp #3 [&var_3]
0000003e: 24 f2                mint
00000040: 54                   ldnlp #4
00000041: 77                   ldl #7 [arg_1]
00000042: 64 9c                call sub_0        ; in(12, MostNeg @ 4, &var_3)
                                                 ; lecture des 12 octets de clé courante
00000044: 41                   ldc #1
00000045: d0                   stl #0 [var_0]
00000046: 12                   ldlp #2 [&var_2]
00000047: 24 f2                mint
00000049: 55                   ldnlp #5
0000004a: 77                   ldl #7 [arg_1]
0000004b: 64 93                call sub_0       ; in(1, MostNeg @ 5, &var_2)
                                                ; lecture d'un octet depuis le transputer 11
0000004d: 41                   ldc #1
0000004e: d0                   stl #0 [var_0]
0000004f: 11                   ldlp #1 [&var_1]
00000050: 24 f2                mint
00000052: 51                   ldnlp #1
00000053: 77                   ldl #7 [arg_1]
00000054: 64 90                call sub_6       ; out(1, MostNeg @ 1, &var_1)
                                                ; envoi d'un octet vers le transputer 11
00000056: 12                   ldlp #2 [&var_2]
00000057: f1                   lb
00000058: 4c                   ldc #c
00000059: 21 ff                rem
0000005b: 2f 4f                ldc #ff
0000005d: 24 f6                and
0000005f: 12                   ldlp #2 [&var_2]
00000060: 23 fb                sb               ; var_2[0] = (var_2[0] % 12) & 0xff
00000062: 12                   ldlp #2 [&var_2]
00000063: f1                   lb
00000064: 13                   ldlp #3 [&var_3]
00000065: f2                   bsub
00000066: f1                   lb
00000067: 11                   ldlp #1 [&var_1]
00000068: 23 fb                sb               ; var_1[0] = var_3[var_2[0]]
0000006a: 41                   ldc #1
0000006b: d0                   stl #0 [var_0]
0000006c: 11                   ldlp #1 [&var_1]
0000006d: 24 f2                mint
0000006f: 77                   ldl #7 [arg_1]
00000070: 66 94                call sub_6       ; out(1, MostNeg, &var_1)
00000072: 20                   .db #20 ' '
00000073: 65 0e                j loc_23

Le transputer 12 effectue les opérations suivantes :

  • initialisation d’un état : 12 octets mis à 0 à l’adresse &var_3 ;

  • début de la boucle infinie :

    • mise à jour de la variable var_1 en fonction des données de var_3,

    • lecture et stockage dans var_3 des 12 octets de la clé courante,

    • lecture et stockage dans var_2 d’un octet lu depuis le transputer 11,

    • envoi du contenu de var_1 vers le transputer 11,

    • mise à jour de var_1 selon l’opération var_1 = var_3[var_2 % 12],

    • envoi de var_1 sur le lien 0 et retour au début de la boucle.

Le pseudo-code C correspondant est :

/* Transputer 12 */
void sub_c_t12(uint32_t arg1) {
  uint8_t var1, var2;
  uint8_t var_3[12];

  memset(var_3, 0, 12); // initialisation de l'état
  for (;;) {
     var_1 = var_3[9] ^ (var_3[5] ^ var_3[1]);
     in(12, 4, var_3); // lecture de la clé courante, mise à jour de l'état

     in(1, 5, &var_2); // lecture d'un octet depuis le transputer 11
     out(1, 1, &var_1); // envoi d'un octet vers le transputer 11

     var_1 = var_3[var_2 % 12];
     out(1, 0, &var_1); // envoi du résultat sur le lien 0
  }
}

Le lien entre les transputers 11 et 12 ne sert finalement qu’à échanger un octet. Il est donc possible de les rendre indépendants l’un de l’autre en transposant le calcul de la valeur échangée. La gestion de l’état faite par le transputer 12 doit également être transposée au niveau du transputer 11 car la valeur envoyée dépend de cet état.

Le résultat en pseudo-code est alors le suivant :

void sub_c_t11(uint32_t arg1) {
   uint8_t var_1, var_2;
   uint8_t var_3[12];

   memset(var_3, 0, 12); // initialisation de l'état
   for (;;) {
       var_1 = var_3[9] ^ (var_3[5] ^ var_3[1]);
       in(12, 4, var_3); // lecture de la clé courante, mise à jour de l'état

       var_2 = var_3[var_1 % 12];
       out(1, 0, &var_2); // envoi du résultat sur le lien 0
   }
}

void sub_c_t12(uint32_t arg1) {
  uint8_t var1, var2;
  uint8_t var_3[12];

  for (;;) {
     in(12, 4, var_3); // lecture de la clé courante
     var_1 = var_3[7] ^ ( var_3[3] ^ var_3[0] );

     var_2 = var_3[var_1 % 12];
     out(1, 0, &var_2); // envoi du résultat sur le lien 0
  }
}

Cette implémentation des transputers 11 et 12 est équivalente à celle obtenue précédemment mais les deux transputers sont cette fois indépendants.

La fonction de chaque transputer étant maintenant connue, il reste alors à implémenter la routine de déchiffrement.

Implémentation de l’algorithme

L’objectif à cet stade est de produire une implémentation de l’algorithme qui passe le vecteur de test fourni dans le schéma, à savoir :

Test vector:
key = “*SSTIC-2015*”
data = “1d87c4c4e0ee40383c59447f23798d9fefe74fb82480766e”.decode(“hex”)
decrypt(key, data) == “I love ST20 architecture”

Plusieurs difficultés se présentent lors de la conception du programme :

  • les transputers communiquent entre eux avec des instructions in et out ;

  • la plupart des transputers initialisent et maintiennent un état entre chaque itération.

Il est assez facile de résoudre le premier point. En effet, on peut remarquer que les communications entre deux transputers suivent toujours le même schéma : un envoi de 12 octets suivi d’une lecture d’un octet. Ces communications peuvent donc être remplacées par un appel de fonction qui prendrait en paramètre un chaîne de 12 octets et retournerait un octet comme résultat. Chacune de ces fonctions représenterait une itération d’un transputer.

Au niveau de la gestion des états, trois possibilités existent :

  • un état d’un transputer peut être représenté par des variables statiques déclarées dans le corps de la fonction. Une de ces variables précise si le transputer est initialisé ou non. Dans ce dernier cas, la fonction initialise l’état avant de procéder à l’itération ;

  • plutôt que des variables statiques, il est possible de déclarer des variables globales qui peuvent être modifiées en dehors de la fonction correspondant au transputer (par exemple pour définir une fonction d’initialisation globale) ;

  • enfin, les états des transputers peuvent être déclarés au sein d’une même structure qui sera passée en paramètre des fonctions. Cette structure sera initialisée par une fonction principale avant de passer la main au transputer 0.

La dernière possibilité est celle qui a été retenue : elle présente l’avantage d’être thread-safe.

La structure correspondante est définie ainsi :

typedef struct tr_ctx {
    uint8_t t4_st;
    uint8_t t5_st;
    uint16_t t6_st;
    uint8_t t8_idx;
    uint8_t t8_st[4][12];
    uint8_t t10_idx;
    uint8_t t10_st[4][12];
    uint8_t t12_st[12];
} tr_ctx_t;

Elle est initialisée simplement par la fonction ci-dessous :

void init_ctx(tr_ctx_t * ctx, const uint8_t * key) {
    int i;

    memset(ctx, 0, sizeof(struct tr_ctx));

    /* t6 init */
    for (i = 0; i < 12; i++)
        ctx->t6_st = (ctx->t6_st + key[i]) & 0xffff;
}

La fonction correspondant au transputer 0 devient alors :

void transputer_0(const char *key, const char *cipher, int cipher_len, char *plain, tr_ctx_t * ctx) {
    uint8_t current_key[12];
    int i;
    uint8_t t1_res, t2_res, t3_res;

    memcpy(current_key, key, 12);

    for (i = 0; i < cipher_len; i++) {
        t1_res = transputer_1(current_key, ctx);
        t2_res = transputer_2(current_key, ctx);
        t3_res = transputer_3(current_key, ctx);

#if DEBUG
        printf("[T0] T1 : 0x%2.2x T2 : 0x%2.2x T3 : 0x%2.2x\n", t1_res, t2_res, t3_res);
#endif

        plain[i] = cipher[i] ^ (2 * current_key[i % 12] + i % 12);

        current_key[i % 12] = (t1_res ^ t2_res) ^ t3_res;
    }
}

La fonction correspondant à un transputer de second niveau (1, 2 ou 3) est présentée ci-dessous :

uint8_t transputer_1(const uint8_t * key, tr_ctx_t * ctx) {
    uint8_t a, b, c;

    a = transputer_4(key, ctx);
    b = transputer_5(key, ctx);
    c = transputer_6(key, ctx);
#if DEBUG
    printf("[T1] T4 : 0x%2.2x T5 : 0x%2.2x T6 : 0x%2.2x\n", a, b, c);
#endif

    return (a ^ b) ^ c;
}

Enfin, un exemple de fonction correspondant à un transputer de troisième niveau est :

uint8_t transputer_4(const uint8_t * key, tr_ctx_t * ctx) {
    int i;

    for (i = 0; i < 12; i++)
        ctx->t4_st += key[i];

    return ctx->t4_st;
}

La fonction principale pour lancer un déchiffrement est la suivante :

void decrypt(const char *key, const char *cipher, char *plain, int size) {
    tr_ctx_t ctx;
    init_ctx(&ctx, (const uint8_t *)key);
    transputer_0(key, cipher, size, plain, &ctx);
}

Il ne reste plus maintenant qu’à tester cette implémentation. Pour cela, l’émulateur st20emu est utilisé pour vérifier que le résultat de chaque transputer est cohérent avec une trace d’exécution obtenue par émulation.

Par exemple, le résultat des transputers de 7 à 12 avec pour la clé *SSTIC-2015* est :

$ ./decrypt
[T1] T4 : 0xcf T5 : 0x75 T6 : 0x9e
[T2] T7 : 0xaf T8 : 0xcf T9 : 0x06
[T3] T10: 0x00 T11: 0x2a T12: 0x49
[T0] T1 : 0x24 T2 : 0x66 T3 : 0x63
[...]

On peut alors charger dans l’émulateur le code du transputer 4 (fichier t4_code_gcall.bin) et vérifier que le résultat (0xcf) est identique. L’émulateur ne supportant pas l’instruction in, il faut donc simuler le chargement de la clé en affectant les variables var_2, var_3 et var_3 à la main (voir t4_code_gcall.asm à l’adresse 0x1b). La trace de l’émulateur est présentée ci-dessous :

$ wine st20emu.exe
Cannot open INI file ST20EMU.INI
Using program defaults
MAX_UNPROMPTED_INSTR=1000000
WARN_UNPROMPTED_INSTR=100000
UNDEFINED_WORD=0xaaaaaaaa
START_ADDR=0x7ffffffe
MEM_START_VAL=0x80000140
ST20_PRODUCT_ID=0x2d4c9041
TIMER_GUESS=0x20000000
WPTR_END_ADDR=0x1fffffff

A=0xaaaaaaaa B=0xaaaaaaaa C=0xaaaaaaaa  Iptr=0x7ffffffe
Wptr  0=0xaaaaaaaa

An uninitialized memory byte was accessed
Error occurred when reading instruction at 7ffffffe, offset  0
7ffffffe   j loc_7ffffffe
> l 7ff80000 t4_code_gcall.bin (1)
Read 68 bytes from t4_code_gcall.bin
> i 7ff8000c (2)
> s i 7ff8001d (3)
> g
ERROR: Invalid Wptr word referenced
This instruction (in) has not been implemented yet
Watch condition encountered
A=0x0000000c B=0x80000010 C=0x1ffffff0  Iptr=0x7ff8001d
Wptr  0=0x0000000c  1=0x00000000  2=0xaaaaaaaa  3=0xaaaaaaaa
      4=0xaaaaaaaa  5=0xaaaaaaaa

7ff8001d  40  ldc 0
> w 2 5453532a (4)
> w 3 322d4349 (5)
> w 4 2a353130 (6)
> s i 7ff8003c (7)
> g
ERROR: Invalid Wptr word referenced
Watch condition encountered
A=0x00000000 B=0x80000000 C=0x1fffffec  Iptr=0x7ff8003c
Wptr  0=0x00000001  1=0x000000cf  2=0x5453532a  3=0x322d4349 (8)
      4=0x2a353130  5=0xaaaaaaaa

7ff8003c  63 98  call sub_7ff80006
1 charge le code du transputer
2 règle le point d’entrée
3 place un point d’arrêt après la lecture de la clé
4 *SST dans var_2
5 IC-2 dans var_3
6 015* dans var_4
7 place un point d’arrêt avant l’envoi du résultat
8 résultat dans var_1

Le résultat est bien 0xcf dans les deux cas. De la même manière, il est possible de valider individuellement l’implémentation de chaque transputer. On peut alors essayer de passer le vecteur de test.

La fonction ci-dessous implémente ce test :

int self_test(int count) {
    int ret = 0, i;
    char *key = "*SSTIC-2015*";
    char *cipher = "\x1d\x87\xc4\xc4\xe0\xee\x40\x38\x3c\x59\x44\x7f\x23\x79\x8d\x9f\xef\xe7\x4f\xb8\x24\x80\x76\x6e";
    char plain[24];
    int data_size = 24;

    for (i = 0; i < count && ret == 0; i++) {
        decrypt(key, cipher, plain, data_size);
        ret = strncmp("I love ST20 architecture", plain, data_size);
    }

    return ret;
}

int main(int argc, char **argv) {
    if (self_test(2) != 0) {
        fprintf(stderr, "self-test failed\n");
        exit(EXIT_FAILURE);
    }
    printf("[+] self-test passed\n");

    return EXIT_SUCCESS;
}

Il est recommandé d’effectuer le test plusieurs fois de suite pour s’assurer que la réinitialisation des transputers est correcte. Le résultat est le suivant :

$ make decrypt
gcc -O3 -march=native -fomit-frame-pointer -funroll-loops -Wall -D_STANDALONE_ -o decrypt decrypt.c
$ ./decrypt
[T1] T4 : 0xcf T5 : 0x75 T6 : 0x9e
[T2] T7 : 0xaf T8 : 0xcf T9 : 0x06
[T3] T10: 0x00 T11: 0x2a T12: 0x49
[T0] T1 : 0x24 T2 : 0x66 T3 : 0x63
[...]
[+] self-test passed

On dispose alors à ce stade d’une implémentation supposée correcte (fichier decrypt.c en annexe) de la routine de déchiffrement.

Attaque par force brute

Il reste maintenant à déterminer la clé valide permettant de déchiffrer les données embarquées dans le fichier input.bin. La démarche initiale est alors la suivante :

  • déterminer une liste de clés candidates et, pour chacune, :

    • effectuer un déchiffrement,

    • calculer l’empreinte SHA256 des données obtenues et comparer le résultat avec l’empreinte decrypted présente sur le schéma schematic.pdf.

Si les deux empreintes sont identiques, alors la clé candidate courante est celle recherchée.

Cette approche se heurte cependant à plusieurs difficultés :

  • sachant que la longueur de la clé est de 12 octets, soit 96 bits, l’espace de clés à tester est gigantesque (\$2^96\$ clés différentes) ;

  • l’algorithme de déchiffrement n’est pas particulièrement efficace car il agit octet par octet sur les données chiffrées : le test d’une clé est en conséquence assez long.

Pour poursuivre, ces deux difficultés doivent être résolues. Au niveau du nombre de clés à tester, il est possible de fortement réduire leur nombre. En effet, si on reprend l’algorithme de déchiffrement, on peut se rendre compte que les 12 premiers octets déchiffrés sont directement corrélés avec la clé par la formule suivante :

\$p_i = c_i \oplus ( (2 * k_i + i) % 256 )\$

où \$p_i\$ est l’octet déchiffré, \$c_i\$ l’octet chiffré et \$k_i\$ l’octet de clé à la position \$i\$, pour \$i in [0, 11\$]. L’opération modulo 256 est nécessaire car le résultat est stocké dans octet (soit un entier de 8 bits).

La formule précédente revient à dire qu’il existe un entier \$n\$ tel que :

\$256 * n + c_i \oplus p_i = 2 * k_i + i\$

Un octet de clé est donc corrélé avec les octets de clair et chiffré correspondants de la façon suivante :

\$k_i = (256 * n + c_i \oplus p_i - i) / 2 = 128 * n + (c_i \oplus p_i - i) / 2\$

Par conséquent, en identifiant un clair connu sur le début du fichier, on pourra alors réduire grandement l’espace des clés à tester. On peut alors faire l’hypothèse, à cause de la présence de la chaîne congratulations.tar.bz2 dans le fichier input.bin, que les données déchiffrées correspondant à une archive au standard bzip2.

La page Wikipedia sur bzip2 décrit le format d’un fichier bzip2. On identifie alors certaines valeurs constantes au début du fichier :

.magic:16                       = 'BZ' signature/magic number
.version:8                      = 'h' for Bzip2 ('H'uffman coding), '0' for Bzip1 (deprecated)
.hundred_k_blocksize:8          = '1'..'9' block-size 100 kB-900 kB (uncompressed)

.compressed_magic:48            = 0x314159265359 (BCD (pi))
.crc:32                         = checksum for this block

Il s’agit donc de :

  • BZh pour les trois premiers octets ;

  • une valeur comprise entre 1 et 9 pour le quatrième octet ;

  • enfin la valeur de Pi pour les 6 octets suivants.

Les 10 premiers octets sont donc prévisibles et permettent de limiter les clés candidates. Pour cela, un script Ruby a été développé qui détermine, pour les 10 premiers octets, les valeurs possibles d’un octet de clé en fonction de l’octet de clair connu et de l’octet chiffré correspondants.

find-key.rb
#!/usr/bin/env ruby

cipher = [ 0xfe, 0xf3, 0x50, 0xdc, 0x81, 0xbc, 0x97, 0x27, 0x89, 0xac ]
plains = []
(1..9).each do |i|
  plains << [ 'B'.ord, 'Z'.ord, 'h'.ord, 0x30 + i, 0x31, 0x41, 0x59, 0x26, 0x53, 0x59 ]
end

key = []

plains.each do |plain|
  (0..9).each do |i|
    (0..255).each do |c|
      t = cipher[i] ^ (( 2 * c + i ) & 0xff)
      if t == plain[i]
        a = (key[i] ||= [])
        a << c unless a.include? c
      end
    end
  end
end

keys = key[0].product(*key[1..-1])

result = "#define KEYS_COUNT #{keys.size}\n\n"
result << "char keys[KEYS_COUNT][12] = {\n"

result << keys.map do |x|
  a = x + [0, 0]
  "    { " + a.map {|y| "0x%02x" % y}.join(", ") + " }"
end.join(",\n")

result << "\n};"

puts result

Le script génère un fichier .h contenant 5120 clés qui peut alors être inclus dans un programme en C pour effectuer l’attaque par force brute.

$ ./find-key.rb
#define KEYS_COUNT 5120

char keys[KEYS_COUNT][12] = {
    { 0x5e, 0x54, 0x1b, 0x75, 0x56, 0x7c, 0x64, 0x7d, 0x69, 0x76, 0x00, 0x00 },
    { 0x5e, 0x54, 0x1b, 0x75, 0x56, 0x7c, 0x64, 0x7d, 0x69, 0xf6, 0x00, 0x00 },
    { 0x5e, 0x54, 0x1b, 0x75, 0x56, 0x7c, 0x64, 0x7d, 0xe9, 0x76, 0x00, 0x00 },
    { 0x5e, 0x54, 0x1b, 0x75, 0x56, 0x7c, 0x64, 0x7d, 0xe9, 0xf6, 0x00, 0x00 },
    { 0x5e, 0x54, 0x1b, 0x75, 0x56, 0x7c, 0x64, 0xfd, 0x69, 0x76, 0x00, 0x00 },
    { 0x5e, 0x54, 0x1b, 0x75, 0x56, 0x7c, 0x64, 0xfd, 0x69, 0xf6, 0x00, 0x00 },
    { 0x5e, 0x54, 0x1b, 0x75, 0x56, 0x7c, 0x64, 0xfd, 0xe9, 0x76, 0x00, 0x00 },
[...]

Les deux derniers octets doivent être déterminés par force brute, ce qui revient à un espace de clés correspondant à \$5120 * 2^8 * 2^8 = 5120 * 2^16\$ possibilités.

La seconde difficulté réside dans la lenteur de l’algorithme qui limite les possibilités d’attaque par force brute. Pour contourner cette difficulté, l’idéal serait de pouvoir éliminer les mauvaises clés candidates sans avoir à déchiffrer l’intégralité des données. S’il est possible de déterminer un clair connu dans le début du fichier déchiffré (autre que les 10 premiers octets), alors une vérification sur le début des données déchiffrées permettrait de ne retenir que les bons candidats. Si une clé candidate passe ce premier filtre, alors un déchiffrement complet est ensuite réalisé pour comparer les empreintes SHA256.

Un fichier au format bzip2, de taille comparable à celui recherché, est alors généré pour tenter d’identifier un clair connu :

$ dd if=/dev/urandom of=test.bin bs=256K count=1
1+0 records in
1+0 records out
262144 bytes (262 kB) copied, 0.0222051 s, 11.8 MB/s
$ bzip2 test.bin
$ hexdump -C test.bin.bz2
00000000  42 5a 68 39 31 41 59 26  53 59 48 b3 90 17 00 32  |BZh91AY&SYH....2|
00000010  78 ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |x...............|
00000020  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
[...]

On remarque que le début du fichier contient de nombreuses occurrences de la valeur 0xff. L’heuristique retenue consiste alors à tester la présence des octets ff ff ff ff dans les 32 premiers octets déchiffrés.

Une version simplifiée de l’attaque par force brute est présentée ci-dessous :

const char *plain_sha256 = "9128135129d2be652809f5a1d337211affad91ed5827474bf9bd7e285ecef321";

void bf(const char *cipher_data, int size) {
        char *key;
        char *plain_data = NULL;
        char sha256[65];
        int i, j, k, l;

        plain_data = malloc(size);

        for (i = 0; i < KEYS_COUNT; i++) {
                key = keys[i];

                for (j = 0; j < 256; j++) {
                        key[10] = j;

                        for (k = 0; k < 256; k++) {
                                key[11] = k;

                                decrypt(key, cipher_data, plain_data, 32);
                                if (!memmem(plain_data, 32, "\xFF\xFF\xFF\xFF", 4))
                                        continue;

                                decrypt(key, cipher_data, plain_data, size);
                                sha256sum(plain_data, size, sha256);

                                if (!strncmp(sha256, plain_sha256, 64)) {
                                        fprintf(stderr, "[!] key = ");
                                        for (l = 0; l < 12; l++)
                                                fprintf(stderr, "%2.2x", key[l] & 0xff);

                                        goto finish;
                                }
                        }
                }
        }

finish:
        if (plain_data)
                free(plain_data);
}

Une version améliorée, disponible en annexe (bf.c), utilise OpenMP pour paralléliser les calculs et permet de retrouver la clé valide en l’espace d’une minute :

$ make bf
./find-key.rb > keys.h
gcc -c -O3 -march=native -fomit-frame-pointer -funroll-loops -Wall -fopenmp -o bf.o bf.c
gcc -c -O3 -march=native -fomit-frame-pointer -funroll-loops -Wall -o decrypt.o decrypt.c
gcc -O3 -march=native -fomit-frame-pointer -funroll-loops -Wall -fopenmp -o bf bf.o decrypt.o -lcrypto
$ dd if=input_remaining.bin bs=1 skip=40 of=encrypted.bin (1)
$ sha256sum encrypted.bin
a5790b4427bc13e4f4e9f524c684809ce96cd2f724e29d94dc999ec25e166a81  encrypted.bin
$ time ./bf encrypted.bin
[+] self-test passed
[+] starting 4 threads
[+] testing 5120 keys
[!] key = 5ed49b7156fce47de976dac5
[+] result saved in congratulations.tar.bz2
./bf encrypted.bin  282,40s user 0,09s system 385% cpu 1:13,34 total
1 input_remaining est obtenu à l’aide du script split.rb depuis le fichier input.bin, 40 est le décalage jusqu’aux données chiffrées

La clé a finalement été identifiée (5ed49b7156fce47de976dac5) et le résultat est sauvegardé dans le fichier congratulations.tar.bz2.

Stage 6 : stéganographie

congratulations.jpg

Le fichier obtenu à l’étape précédente est une archive au format .tar.bz2 qui contient une image JPEG :

$ sha256sum congratulations.tar.bz2
9128135129d2be652809f5a1d337211affad91ed5827474bf9bd7e285ecef321  congratulations.tar.bz2
$ tar jxvf congratulations.tar.bz2
congratulations.jpg
$ file congratulations.jpg
congratulations.jpg: JPEG image data, JFIF standard 1.01
$ ls -al congratulations.jpg
-rw-r--r-- 1 jpe jpe 252569 mars  23 10:34 congratulations.jpg

Le fichier congratulations.jpg correspond à l’image ci-dessous :

rk congratulations

A première vue, selon la commande jpeginfo, le fichier semble valide :

$ jpeginfo -c congratulations.jpg
congratulations.jpg  636 x 474  24bit JFIF  P  252569  [OK]

La taille du fichier est néanmoins suspecte (252569 octets), pour une image de cette dimension. En effet, en utilisant l’outil hachoir, on identifie une autre archive .tar.bz2 à l’intérieur de l’image :

$ hachoir-subfile congratulations.jpg
[+] Start search on 252569 bytes (246.6 KB)

[+] File at 0 size=55248 (54.0 KB): JPEG picture
[+] File at 55248: bzip2 archive

[+] End of search -- offset=252569 (246.6 KB)
$ dd if=congratulations.jpg of=out.tar.bz2 bs=1 skip=55248 2>/dev/null
$ tar jxvf out.tar.bz2
congratulations.png

L’analyse du fichier congratulations.png constitue la seconde phase de cette étape.

congratulations.png

L’image obtenue est présentée ci-dessous :

rk congratulations

Elle est similaire au fichier congratulations.jpg, seul le message du bas a changé. On peut alors tester l’intégrité du fichier avec la commande pngcheck, comme présenté ci-dessous :

$ pngcheck -v congratulations.png
File: congratulations.png (197557 bytes)
  chunk IHDR at offset 0x0000c, length 13
    636 x 474 image, 32-bit RGB+alpha, non-interlaced
  chunk bKGD at offset 0x00025, length 6
    red = 0x00ff, green = 0x00ff, blue = 0x00ff
  chunk pHYs at offset 0x00037, length 9: 3543x3543 pixels/meter (90 dpi)
  chunk tIME at offset 0x0004c, length 7: 27 Feb 2015 13:40:19 UTC
  chunk sTic at offset 0x0005f, length 4919:  illegal reserved-bit-set chunk
ERRORS DETECTED in congratulations.png

Le fichier semble contenir un "chunk" de type sTic. Une rapide recherche sur Internet permet de confirmer qu’il ne s’agit pas d’un type valide.

Le script Ruby ci-dessous permet de lister tous les types de "chunk" :

list-chunks.rb
#!/usr/bin/env ruby

require 'chunky_png'

png_stream = ChunkyPNG::Datastream.from_file(ARGV.shift)
png_stream.each_chunk { |chunk| puts chunk.type }

Son exécution retourne le résultat suivant :

$ ./list-chunks.rb congratulations.png
IHDR
bKGD
pHYs
tIME
sTic
sTic
[...]
sTic
sTic
IDAT
IDAT
IDAT
IDAT
IDAT
IDAT
IDAT
IDAT
IEND

Une invite irb est alors utilisée pour examiner le fichier en mode interactif :

$ irb
irb(main):001:0> require 'chunky_png'
 => true
irb(main):002:0> png_stream = ChunkyPNG::Datastream.from_file("congratulations.png")
irb(main):003:0> chunks = png_stream.chunks.select {|c| c.type == "sTic"}
irb(main):004:0> chunks.size
 => 28
irb(main):005:0> chunks.first
 => #<ChunkyPNG::Chunk::Generic:0x0000000280a910 @type="sTic", @content="x\x9C\x84[...]

Le fichier contient donc 28 chunks de type sTic. On peut alors s’intéresser aux données contenues dans le premier chunk.

irb(main):006:0> chunks[0].content[0, 4].unpack('H*')
 => ["789c84b6"]

Une recherche de 0x78 0x9c sur Internet permet d’identifier un probable début de stream Zlib. On peut alors tenter une décompression et sauvegarder le résulat.

irb(main):007:0> data = chunks.map {|c| c.content}.join
irb(main):008:0> require 'zlib'
 => false
irb(main):009:0> File.open("out.bin", "wb") {|f| f.write Zlib::Inflate.inflate(data) }
 => 133048
irb(main):010:0> puts `file out.bin`
out.bin: bzip2 compressed data, block size = 900k

Le résultat obtenu est un fichier bzip2 qui est en fait une archive tar.bz2 contenant une nouvelle image :

$ tar jxvf out.bin
congratulations.tiff

Cette image doit alors être analysée pour poursuivre le challenge.

congratulations.tiff

L’image obtenue est la suivante :

rk congratulations tiff

L’outil tiffinfo permet d’obtenir des informations sur le fichier :

$ ls -al congratulations.tiff
-rw-r--r-- 1 jpe jpe 904520 mars  23 10:34 congratulations.tiff
$ tiffinfo -v congratulations.tiff
TIFF Directory at offset 0x8 (8)
  Image Width: 636 Image Length: 474
  Bits/Sample: 8
  Compression Scheme: None
  Photometric Interpretation: RGB color
  Samples/Pixel: 3
  Rows/Strip: 474
  Planar Configuration: single image plane

Sachant que 8 bits sont utilisés pour stocker un "sample", qu’un pixel nécessite 3 "samples" et que les dimensions de l’image sont 636 x 474, il faut donc 3 * 636 * 474 = 904392 octets pour stocker l’ensemble des pixels. Par rapport à la taille totale du fichier (904520 octets), il ne reste plus que 128 octets qui correspondent aux entêtes du fichier. Un rapide examen de ces derniers, en utlisant par exemple le script parse_tiff.rb disponible en annexe, ne permet pas d’identifier de données cachées pouvant représenter l’adresse email recherchée. Toutes les données du fichier étant alors utilisées pour représenter l’image, il faut donc aller chercher ailleurs pour poursuivre le challenge.

Une hypothèse intéressante est de supposer que la suite du challenge ne peut être stockée que dans les informations décrivant chaque pixel. Une technique stéganographique bien connue permet justement de réaliser cet objectif, en utilisant les bits les moins significatifs (dits de poids faible) de chaque pixel pour dissimuler de l’information.

L’outil StegSolve @ GitHub, bien connu des participants de CTF, est utile pour détecter l’utilisation de techniques stéganographiques au sein d’une image. Il permet notamment de visualiser séparement chaque bit des trois composantes RGB pour vérifier leur utilisation au niveau de l’image.

StegSolve ne sait pas analyser directement une image au format TIFF mais une conversion sans perte au format PNG (par exemple avec ImageMagick) conserve le codage des pixels.

L’examen de la composante rouge met en évidence une anomalie au niveau du bit 0 :

rk stegsolve red0

De la même manière, le bit 0 de la composante verte semble être utilisé pour stocker des données :

rk stegsolve green0

Ces données sont finalement extraites à l’aide de la fonctionnalité “Data Extract” de StegSolve, en sélectionnant les bits 0 des composantes vertes et rouges. On peut alors reconnaitre un entête d’une archive bzip2, comme présenté ci-dessous :

rk stegsolve data

Il ne reste plus alors qu’à sauvegarder le résultat pour poursuivre le challenge.

congratulations.gif

Le fichier obtenu à la phase précédente est une archive tar.bz2 contenant une nouvelle image à analyser, congratulations.gif.

$ tar jxvf step4.bz2

bzip2: (stdin): trailing garbage after EOF ignored
congratulations.gif

Cette image est présentée ci-dessous :

rk congratulations gif

On peut alors profiter que StegSolve soit toujours lancé pour charger l’image obtenue et l’analyser.

La fonctionnalité “Random colour map” affiche finalement l’adresse email de validation, comme présenté ci-dessous :

rk stegsolve random

En plissant les yeux et en faisant attention, on arrive enfin à recopier l’adresse de validation qui est : 1713e7c1d0b750ccd4e002bb957aa799@challenge.sstic.org

Conclusion

Le challenge de cette année s’est révélé original par rapport au nombre d’étapes à franchir. Celles-ci, de difficulté variable, touchent à des problématiques variées qui requièrent une certaine polyvalence au niveau des compétences nécessaires.

L’ensemble des outils développés sera mis à disposition à l’adresse https://github.com/nieluj/sstic2015 une fois le challenge terminé.

Annexes

Stage 4

clean.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
var fs = require('fs'); var esprima = require('esprima'); var estraverse = require('estraverse'); var escodegen = require('escodegen'); var filename = process.argv[2]; var ast = esprima.parse(fs.readFileSync(filename)); function simplify_node(node) { if ((node.type == 'BinaryExpression') && (node.left.type == 'Literal') && (node.right.type == 'Literal')) { if (node.operator == '+') { return { type: 'Literal', value: node.left.value + node.right.value, raw: node.left.raw + node.right.raw }; } else if (node.operator == '*') { return { type: 'Literal', value: node.left.value * node.right.value, raw: node.left.raw * node.right.raw }; } } return node; } function simplify_ast(ast) { var assignments = {}; var f_count = 0; var scopeChain = []; estraverse.traverse(ast, { enter: function(node, parent) { var scopeName; if (node.type == 'FunctionDeclaration') scopeName = 'function_' + node.id.name; if (node.type == 'Program') scopeName = 'program'; if (scopeName) { scopeChain.push(scopeName); assignments[scopeName] = {}; } if (node.type == 'Identifier') { var currentScope = scopeChain[scopeChain.length - 1]; k = assignments[currentScope][node.name]; if (parent.type == 'AssignmentExpression') { /* Node is on the left-hand side of the assignment */ if (parent.left == node) { right = parent.right; if (!k) { assignments[currentScope][node.name] = { node: right, ref_count: 1 }; } else { /* Not the first assignment, increasing ref count */ k['ref_count']++; } } } else if ((parent.type == 'UpdateExpression') && k) { k['ref_count']++; } } }, leave: function(node, parent) { if ((node.type == 'FunctionDeclaration') || (node.type == 'Program')) { scopeChain.pop(); } } }); scopeChain = []; result = estraverse.replace(ast, { enter: function(node, parent) { if (node.type == 'FunctionDeclaration') { scopeChain.push('function_' + node.id.name); } if (node.type == 'Program') { scopeChain.push('program'); } if (node.type == 'Identifier') { var currentScope = scopeChain[scopeChain.length - 1]; k = assignments[currentScope][node.name] || assignments['program'][node.name]; if ((parent.type == 'AssignmentExpression') && (parent.left == node) || (parent.type == 'UpdateExpression')) { return node; } if (k && k['ref_count'] == 1) { if ((k.node.type == 'Literal') || (k.node.type == 'Identifier')) { return k.node; } } } return simplify_node(node); }, leave: function(node, parent) { if ((node.type == 'FunctionDeclaration') || (node.type == 'Program')) { scopeChain.pop(); } } }); return result; } /* Looking for a fix point */ new_code = "" do { old_code = new_code; ast = simplify_ast(ast); new_code = escodegen.generate(ast); } while (new_code != old_code); console.log(new_code);

Stage 5

t0.asm
; New subroutine 0+d; References: 0, Local Vars: 76
00000000: 64 b4     sub_0:        ajw #-4c       ; réserve 76 variables locales
00000002: 40                      ldc #0
00000003: d1                      stl #1 [var_1] ; var_1 = 0
00000004: 40                      ldc #0
00000005: d3                      stl #3 [var_3] ; var_3 = 0
00000006: 24 f2                   mint           ; A = MostNeg
00000008: 24 20 50                ldnlp #400     ; A = MostNeg @ 0x400
0000000b: 23 fc                   gajw           ; Wptr = MostNeg @ 0x400

; New subroutine d+eb; References: 0, Local Vars: 76
0000000d: 64 b4     sub_d:        ajw #-4c           ; réserve 76 variables locales
0000000f: 2c 49                   ldc #c9
00000011: 21 fb                   ldpi [str_dc]
00000013: 24 f2                   mint
00000015: 48                      ldc #8
00000016: fb                      out                ; out(8, MostNeg, "Boot ok")
00000017: 24 19     loc_17:       ldlp #49 [&var_73]
00000019: 24 f2                   mint
0000001b: 54                      ldnlp #4
0000001c: 4c                      ldc #c
0000001d: f7                      in                ; in(12, MostNeg @ 4, &var_73)
0000001e: 24 79                   ldl #49 [var_73]
00000020: 21 a5                   cj loc_37         ; saute à loc_37 si var_73 == 0
00000022: 2c 4d                   ldc #cd
00000024: 21 fb                   ldpi [loc_f3]
00000026: 24 f2                   mint
00000028: 54                      ldnlp #4
00000029: 24 79                   ldl #49 [var_73]
0000002b: f7                      in                ; in(var_73, MostNeg @ 4, loc_f3)
0000002c: 2c 43                   ldc #c3
0000002e: 21 fb                   ldpi [loc_f3]
00000030: 24 7a                   ldl #4a [var_74]
00000032: 24 79                   ldl #49 [var_73]
00000034: fb                      out               ; out(var_73, var_74, loc_f3)
00000035: 61 00                   j loc_17
00000037: 24 19     loc_37:       ldlp #49 [&var_73]
00000039: 24 f2                   mint
0000003b: 51                      ldnlp #1
0000003c: 4c                      ldc #c
0000003d: fb                      out               ; out(12, MostNeg @ 1, &var_73)
0000003e: 24 19                   ldlp #49 [&var_73]
00000040: 24 f2                   mint
00000042: 52                      ldnlp #2
00000043: 4c                      ldc #c
00000044: fb                      out                ; out(12, MostNeg @ 2, &var_73)
00000045: 24 19                   ldlp #49 [&var_73]
00000047: 24 f2                   mint
00000049: 53                      ldnlp #3
0000004a: 4c                      ldc #c
0000004b: fb                      out                ; out(12, MostNeg @ 3, &var_73)
0000004c: 29 44                   ldc #94
0000004e: 21 fb                   ldpi [str_e4]
00000050: 24 f2                   mint
00000052: 48                      ldc #8
00000053: fb                      out                ; out(8, MostNeg, "Code Ok")
00000054: 12                      ldlp #2 [&var_2]
00000055: 24 f2                   mint
00000057: 54                      ldnlp #4
00000058: 44                      ldc #4
00000059: f7                      in                 ; in(4, MostNeg @ 4, &var_2)
0000005a: 15                      ldlp #5 [&var_5]
0000005b: 24 f2                   mint
0000005d: 54                      ldnlp #4
0000005e: 4c                      ldc #c
0000005f: f7                      in                 ; in(12, MostNeg @ 4, &var_5)
00000060: 28 48                   ldc #88
00000062: 21 fb                   ldpi [str_ec]
00000064: 24 f2                   mint
00000066: 48                      ldc #8
00000067: fb                      out                ; out(8, MostNeg, "Decrypt")
00000068: 13                      ldlp #3 [&var_3]
00000069: 24 f2                   mint
0000006b: 54                      ldnlp #4
0000006c: 41                      ldc #1
0000006d: f7                      in                 ; in(1, MostNeg @ 4, &var_3)
0000006e: 19                      ldlp #9 [&var_9]
0000006f: 24 f2                   mint
00000071: 54                      ldnlp #4
00000072: 13                      ldlp #3 [&var_3]
00000073: f1                      lb
00000074: f7                      in                ; in(*var3, MostNeg @ 4, &var_9)
00000075: 40                      ldc #0
00000076: d4                      stl #4 [var_4]    ; var_4 = 0
00000077: 11        loc_77:       ldlp #1 [&var_1]
00000078: 24 f2                   mint
0000007a: 54                      ldnlp #4
0000007b: 41                      ldc #1
0000007c: f7                      in                ; in(1, MostNeg @ 4, &var_1)
0000007d: 15                      ldlp #5 [&var_5]
0000007e: 24 f2                   mint
00000080: 51                      ldnlp #1
00000081: 4c                      ldc #c
00000082: fb                      out               ; out(12, MostNeg @ 1, &var_5)
00000083: 15                      ldlp #5 [&var_5]
00000084: 24 f2                   mint
00000086: 52                      ldnlp #2
00000087: 4c                      ldc #c
00000088: fb                      out               ; out(12, MostNeg @ 2, &var_5)
00000089: 15                      ldlp #5 [&var_5]
0000008a: 24 f2                   mint
0000008c: 53                      ldnlp #3
0000008d: 4c                      ldc #c
0000008e: fb                      out               ; out(12, MostNeg @ 3, &var_5)
0000008f: 10                      ldlp #0 [&var_0]
00000090: 81                      adc #1
00000091: 24 f2                   mint
00000093: 55                      ldnlp #5
00000094: 41                      ldc #1
00000095: f7                      in                ; in(1, MostNeg @ 5, &var_0 + 1)
00000096: 10                      ldlp #0 [&var_0]
00000097: 82                      adc #2
00000098: 24 f2                   mint
0000009a: 56                      ldnlp #6
0000009b: 41                      ldc #1
0000009c: f7                      in                ; in(1, MostNeg @ 6, &var_0 + 2)
0000009d: 10                      ldlp #0 [&var_0]
0000009e: 83                      adc #3
0000009f: 24 f2                   mint
000000a1: 57                      ldnlp #7
000000a2: 41                      ldc #1
000000a3: f7                      in                ; in(1, MostNeg @ 7, &var_0 + 3)
000000a4: 10                      ldlp #0 [&var_0]
000000a5: 81                      adc #1
000000a6: f1                      lb
000000a7: 10                      ldlp #0 [&var_0]
000000a8: 82                      adc #2
000000a9: f1                      lb
000000aa: 23 f3                   xor               ; A = var_0[2] ^ var_0[1]
000000ac: 10                      ldlp #0 [&var_0]
000000ad: 83                      adc #3
000000ae: f1                      lb
000000af: 23 f3                   xor               ; A = var_0[3] ^ ( var_0[2] ^ var_0[1] )
000000b1: 10                      ldlp #0 [&var_0]
000000b2: 81                      adc #1
000000b3: 23 fb                   sb                ; var_0[1] = var_0[3] ^ ( var_0[2] ^ var_0[1] )
000000b5: 11                      ldlp #1 [&var_1]
000000b6: f1                      lb                ; A = var_1
000000b7: 74                      ldl #4 [var_4]    ; A = var_4, B = var_1
000000b8: 15                      ldlp #5 [&var_5]  ; A = &var_5, B = var_4, C = var_1
000000b9: f2                      bsub              ; A = &var_5 + var_4, B = var_1
000000ba: f1                      lb                ; A = var_5[var_4], B = var_1
000000bb: 74                      ldl #4 [var_4]    ; A = var_4, B = var_5[var_4], C = var_1
000000bc: 2c f1                   ssub              ; A = var_4 + 2 * var_5[var_4], B = var_1
000000be: 23 f3                   xor               ; A = (var_4 + 2 * var_5[var_4]) ^ var_1
000000c0: 10                      ldlp #0 [&var_0]
000000c1: 23 fb                   sb                ; var_0[0] = (var_4 + 2 * var_5[var_4]) ^ var_1
000000c3: 10                      ldlp #0 [&var_0]
000000c4: 81                      adc #1
000000c5: f1                      lb                ; A = var_0[1]
000000c6: 74                      ldl #4 [var_4]
000000c7: 15                      ldlp #5 [&var_5]
000000c8: f2                      bsub              ; A = &var_5 + var_4, B = var_0[1]
000000c9: 23 fb                   sb                ; var_5[var_4] = var_0[1]
000000cb: 74                      ldl #4 [var_4]
000000cc: 81                      adc #1
000000cd: 25 fa                   dup               ; A = var_4 + 1, B = var_4 + 1
000000cf: d4                      stl #4 [var_4]    ; var_4 var_4 + 1, A = var_4 + 1
000000d0: cc                      eqc #c
000000d1: a3                      cj loc_d5         ; saute à loc_d5 si var_4 != 12
000000d2: 80                      adc #0
000000d3: 40                      ldc #0
000000d4: d4                      stl #4 [var_4]    ; var_4 = 0
000000d5: 10        loc_d5:       ldlp #0 [&var_0]
000000d6: 24 f2                   mint
000000d8: 41                      ldc #1
000000d9: fb                      out               ; out(1, MostNeg, &var_0)
000000da: 66 0b                   j loc_77          ; saute à loc_77
000000dc: **        str_dc:      .string "Boot ok"
000000e4: **        str_e4:      .string "Code Ok"
000000ec: **        str_ec:      .string "Decrypt"
000000f4: 24 bc                   ajw #4c
000000f6: 22 f0                   ret
t1.asm
; New subroutine 0+9; References: 0, Local Vars: 8
00000000: 60 b8     sub_0:        ajw #-8       ; réserve 8 variables locales
00000002: 24 f2                   mint          ; A = MostNeg
00000004: 24 20 50                ldnlp #400    ; A = MostNeg @ 0x400
00000007: 23 fc                   gajw          ; Wptr = MostNeg @ 0x400

; New subroutine 9+67; References: 0, Local Vars: 8
00000009: 60 b8     sub_9:        ajw #-8            ; réserve 8 variables locales
0000000b: 15        loc_b:        ldlp #5 [&var_5]
0000000c: 24 f2                   mint
0000000e: 54                      ldnlp #4
0000000f: 4c                      ldc #c
00000010: f7                      in                 ; in(12, MostNeg @ 4, &var_5)
00000011: 75                      ldl #5 [var_5]
00000012: 21 a2                   cj loc_26          ; saute à loc_26 si var_5 == 0
00000014: 25 44                   ldc #54
00000016: 21 fb                   ldpi [loc_6c]
00000018: 24 f2                   mint
0000001a: 54                      ldnlp #4
0000001b: 75                      ldl #5 [var_5]
0000001c: f7                      in                 ; in(var_5, MostNeg @ 4, loc_6c)
0000001d: 24 4b                   ldc #4b
0000001f: 21 fb                   ldpi [loc_6c]
00000021: 76                      ldl #6 [var_6]
00000022: 75                      ldl #5 [var_5]
00000023: fb                      out                ; out(var_5, var_6, loc_6c)
00000024: 61 05                   j loc_b
00000026: 11        loc_26:       ldlp #1 [&var_1]
00000027: 24 f2                   mint
00000029: 54                      ldnlp #4
0000002a: 4c                      ldc #c
0000002b: f7                      in                 ; in(12, MostNeg @ 4, &var_1)
0000002c: 11                      ldlp #1 [&var_1]
0000002d: 24 f2                   mint
0000002f: 51                      ldnlp #1
00000030: 4c                      ldc #c
00000031: fb                      out                ; out(12, MostNeg @ 1, &var_1)
00000032: 11                      ldlp #1 [&var_1]
00000033: 24 f2                   mint
00000035: 52                      ldnlp #2
00000036: 4c                      ldc #c
00000037: fb                      out                ; out(12, MostNeg @ 2, &var_1)
00000038: 11                      ldlp #1 [&var_1]
00000039: 24 f2                   mint
0000003b: 53                      ldnlp #3
0000003c: 4c                      ldc #c
0000003d: fb                      out                ; out(12, MostNeg @ 3, &var_1)
0000003e: 10                      ldlp #0 [&var_0]
0000003f: 81                      adc #1
00000040: 24 f2                   mint
00000042: 55                      ldnlp #5
00000043: 41                      ldc #1
00000044: f7                      in                 ; in(1, MostNeg @ 5, &var_0 + 1)
00000045: 10                      ldlp #0 [&var_0]
00000046: 82                      adc #2
00000047: 24 f2                   mint
00000049: 56                      ldnlp #6
0000004a: 41                      ldc #1
0000004b: f7                      in                 ; in(1, MostNeg @ 6, &var_0 + 2)
0000004c: 10                      ldlp #0 [&var_0]
0000004d: 83                      adc #3
0000004e: 24 f2                   mint
00000050: 57                      ldnlp #7
00000051: 41                      ldc #1
00000052: f7                      in                 ; in(1, MostNeg @ 7, &var_0 + 3)
00000053: 10                      ldlp #0 [&var_0]
00000054: 81                      adc #1
00000055: f1                      lb                 ; A = var_0[1]
00000056: 10                      ldlp #0 [&var_0]
00000057: 82                      adc #2
00000058: f1                      lb                 ; A = var_0[2], B = var_0[1]
00000059: 23 f3                   xor                ; A = var_0[2] ^ var_0[1]
0000005b: 10                      ldlp #0 [&var_0]
0000005c: 83                      adc #3
0000005d: f1                      lb                 ; A = var_0[3], B = var_0[2] ^ var_0[1]
0000005e: 23 f3                   xor                ; A = var_0[3] ^ (var_0[2] ^ var_0[1])
00000060: 25 fa                   dup                ; A = var_0[3] ^ (var_0[2] ^ var_0[1]), B = A
00000062: 10                      ldlp #0 [&var_0]
00000063: 23 fb                   sb                 ; var_0[0] = var_0[3] ^ (var_0[2] ^ var_0[1])
00000065: 10                      ldlp #0 [&var_0]
00000066: 24 f2                   mint
00000068: 41                      ldc #1
00000069: fb                      out                ; out(1, MostNeg, var_0[3] ^ (var_0[2] ^ var_0[1]))
0000006a: 64 0a                   j loc_26           ; saute à loc_26
0000006c: 00        loc_6c:       nop
0000006d: b8                      ajw #8
0000006e: 22 f0                   ret
decrypt.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#define DEBUG 0

typedef struct tr_ctx {
    uint8_t t4_st;
    uint8_t t5_st;
    uint16_t t6_st;
    uint8_t t8_idx;
    uint8_t t8_st[4][12];
    uint8_t t10_idx;
    uint8_t t10_st[4][12];
    uint8_t t12_st[12];
} tr_ctx_t;

inline uint8_t transputer_4(const uint8_t * key, tr_ctx_t * ctx) {
    int i;

    for (i = 0; i < 12; i++)
        ctx->t4_st += key[i];

    return ctx->t4_st;
}

inline uint8_t transputer_5(const uint8_t * key, tr_ctx_t * ctx) {
    int i;

    for (i = 0; i < 12; i++)
        ctx->t5_st ^= key[i];

    return ctx->t5_st;
}

inline uint8_t transputer_6(const uint8_t * key, tr_ctx_t * ctx) {
    uint16_t k1, k2, k3;

    k1 = (ctx->t6_st & 0x8000) >> 0xf;
    k2 = (ctx->t6_st & 0x4000) >> 0xe;

    k2 ^= k1;
    k3 = (ctx->t6_st << 1);

    ctx->t6_st = k3 ^ k2;

    return ctx->t6_st & 0xff;
}

inline uint8_t transputer_1(const uint8_t * key, tr_ctx_t * ctx) {
    uint8_t a, b, c;

    a = transputer_4(key, ctx);
    b = transputer_5(key, ctx);
    c = transputer_6(key, ctx);
#if DEBUG
    printf("[T1] T4 : 0x%2.2x T5 : 0x%2.2x T6 : 0x%2.2x\n", a, b, c);
#endif

    return (a ^ b) ^ c;
}

inline uint8_t transputer_7(const uint8_t * key, tr_ctx_t * ctx) {
    uint8_t var_1, var_2;
    int i;

    var_1 = 0; var_2 = 0;

    for (i = 0; i < 6; i++) {
        var_1 += key[i];
        var_2 += key[i + 6];
    }

    return var_1 ^ var_2;
}

inline uint8_t transputer_8(const uint8_t * key, tr_ctx_t * ctx) {
    int i, j;
    uint8_t var_3, var_1;

    memcpy(ctx->t8_st[ctx->t8_idx], key, 12);
    ctx->t8_idx = (ctx->t8_idx + 1) % 4;

    var_3 = 0;
    for (i = 0; i < 4; i++) {
        var_1 = 0;
        for (j = 0; j < 12; j++)
            var_1 += ctx->t8_st[i][j];

        var_3 = var_1 ^ var_3;
    }

    return var_3;
}

inline uint8_t transputer_9(const uint8_t * key, tr_ctx_t * ctx) {
    uint8_t var_1;
    int i;

    var_1 = 0;
    for (i = 0; i < 12; i++)
        var_1 ^= (key[i] << (i & 0x7));

    return var_1;
}

inline uint8_t transputer_2(const uint8_t * key, tr_ctx_t * ctx) {
    uint8_t a, b, c;

    a = transputer_7(key, ctx);
    b = transputer_8(key, ctx);
    c = transputer_9(key, ctx);
#if DEBUG
    printf("[T2] T7 : 0x%2.2x T8 : 0x%2.2x T9 : 0x%2.2x\n", a, b, c);
#endif

    return (a ^ b) ^ c;
}

inline uint8_t transputer_10(const uint8_t * key, tr_ctx_t * ctx) {
    int i, j;
    uint8_t var_1;

    memcpy(ctx->t10_st[ctx->t10_idx], key, 12);
    ctx->t10_idx = (ctx->t10_idx + 1) % 4;

    var_1 = 0;
    for (i = 0; i < 4; i++) {
        var_1 += ctx->t10_st[i][0];
    }

    i = var_1 & 3;
    j = (var_1 >> 4) % 12;

    return ctx->t10_st[i][j];
}

inline uint8_t transputer_11(const uint8_t * key, tr_ctx_t * ctx) {
    uint8_t var_1;

    var_1 = ctx->t12_st[9] ^ (ctx->t12_st[5] ^ ctx->t12_st[1]);	/* from T12 */
    memcpy(ctx->t12_st, key, 12);

    return key[var_1 % 12];
}

inline uint8_t transputer_12(const uint8_t * key, tr_ctx_t * ctx) {
    uint8_t var_2;

    var_2 = key[7] ^ (key[3] ^ key[0]);	/* from T11 */

    return key[var_2 % 12];
}

inline uint8_t transputer_3(const uint8_t * key, tr_ctx_t * ctx) {
    uint8_t a, b, c;

    a = transputer_10(key, ctx);
    b = transputer_11(key, ctx);
    c = transputer_12(key, ctx);
#if DEBUG
    printf("[T3] T10: 0x%2.2x T11: 0x%2.2x T12: 0x%2.2x\n", a, b, c);
#endif

    return (a ^ b) ^ c;
}

void
transputer_0(const char *key, const char *cipher, int cipher_len, char *plain, tr_ctx_t * ctx) {
    uint8_t current_key[12];
    int i;
    uint8_t t1_res, t2_res, t3_res;

    memcpy(current_key, key, 12);

    for (i = 0; i < cipher_len; i++) {
        t1_res = transputer_1(current_key, ctx);
        t2_res = transputer_2(current_key, ctx);
        t3_res = transputer_3(current_key, ctx);

#if DEBUG
        printf("[T0] T1 : 0x%2.2x T2 : 0x%2.2x T3 : 0x%2.2x\n", t1_res, t2_res, t3_res);
#endif

        plain[i] = cipher[i] ^ (2 * current_key[i % 12] + i % 12);

        current_key[i % 12] = (t1_res ^ t2_res) ^ t3_res;
    }
}

void init_ctx(tr_ctx_t * ctx, const uint8_t * key) {
    int i;

    memset(ctx, 0, sizeof(struct tr_ctx));

    /* t6 init */
    for (i = 0; i < 12; i++)
        ctx->t6_st = (ctx->t6_st + key[i]) & 0xffff;
}

void decrypt(const char *key, const char *cipher, char *plain, int size) {
    tr_ctx_t ctx;
    init_ctx(&ctx, (const uint8_t *)key);
    transputer_0(key, cipher, size, plain, &ctx);
}

int self_test(int count) {
    int ret = 0, i;
    char *key = "*SSTIC-2015*";
    char *cipher = "\x1d\x87\xc4\xc4\xe0\xee\x40\x38\x3c\x59\x44\x7f\x23\x79\x8d\x9f\xef\xe7\x4f\xb8\x24\x80\x76\x6e";
    char plain[24];
    int data_size = 24;

    for (i = 0; i < count && ret == 0; i++) {
        decrypt(key, cipher, plain, data_size);
        ret = strncmp("I love ST20 architecture", plain, data_size);
    }

    return ret;
}

#ifdef _STANDALONE_
int main(int argc, char **argv) {
    if (self_test(2) != 0) {
        fprintf(stderr, "self-test failed\n");
        exit(EXIT_FAILURE);
    }
    printf("[+] self-test passed\n");

    return EXIT_SUCCESS;
}
#endif
bf.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <openssl/sha.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <omp.h>

#include "decrypt.h"
#include "keys.h"

#define PLAIN_TEXT_LOOKUP_SZ 32

void sha256sum(const char *data, int len, char output[65]) {
    int i;
    uint8_t hash[SHA256_DIGEST_LENGTH];
    SHA256_CTX sha256;
    SHA256_Init(&sha256);
    SHA256_Update(&sha256, data, len);
    SHA256_Final(hash, &sha256);

    for (i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        sprintf(output + (i * 2), "%02x", hash[i]);
    }
    output[64] = 0;
}

const char *cipher_sha256 = "a5790b4427bc13e4f4e9f524c684809ce96cd2f724e29d94dc999ec25e166a81";
const char *plain_sha256 = "9128135129d2be652809f5a1d337211affad91ed5827474bf9bd7e285ecef321";

void bf(const char *path) {
    int fd, ret, hcount = 0;
    struct stat st;
    off_t size;
    char *cipher_bf = NULL;
    char sha256[65];
    int i;
    int keyfound;
    uint8_t hchars[4] = { '-', '\\', '|', '/' };

    fd = open(path, O_RDONLY);
    if (fd == -1) {
        perror("open failed");
        exit(EXIT_FAILURE);
    }
    ret = fstat(fd, &st);
    if (ret == -1) {
        perror("fstat");
        exit(EXIT_FAILURE);
    }
    size = st.st_size;

    cipher_bf = malloc(size);
    if (!cipher_bf) {
        perror("malloc cipher");
        goto finish;
    }

    ret = read(fd, cipher_bf, size);
    if (ret != size) {
        printf("error during read\n");
        goto finish;
    }
    close(fd);

    sha256sum(cipher_bf, size, sha256);
    if (strcmp(sha256, cipher_sha256) != 0) {
        printf("wrong sha256: %s\n", sha256);
        goto finish;
    }
#pragma omp parallel
    {
#pragma omp barrier
        if (omp_get_thread_num() == 0)
            fprintf(stderr, "[+] starting %d threads\n", omp_get_num_threads());
    }

    keyfound = 0;
    printf("[+] testing %d keys\n", KEYS_COUNT);
#pragma omp parallel
    for (i = 0; i < KEYS_COUNT; i++) {
        char *key;
        int j, k, l, out_fd, thread_id;
        char *plain_bf = NULL;
        char plain_text_lookup[PLAIN_TEXT_LOOKUP_SZ];
        char sha256[65];

        if (keyfound == 1) {
            i = KEYS_COUNT;
            continue;
        }
        key = keys[i];

        thread_id = omp_get_thread_num();
        if (thread_id == 0) {
            fprintf(stderr, "\r[%c] key = ", hchars[hcount++ % 4]);
            for (l = 0; l < 10; l++)
                fprintf(stderr, "%2.2x", key[l] & 0xff);
            fprintf(stderr, "????");
            fflush(stderr);
        }

        for (j = 0; j < 256; j++) {
            key[10] = j;

            for (k = 0; k < 256; k++) {
                key[11] = k;

                decrypt(key, cipher_bf, plain_text_lookup, PLAIN_TEXT_LOOKUP_SZ);

                if (!memmem(plain_text_lookup, PLAIN_TEXT_LOOKUP_SZ,
                            "\xFF\xFF\xFF\xFF", 4))
                    continue;

                if (!plain_bf)
                    plain_bf = malloc(size);

                decrypt(key, cipher_bf, plain_bf, size);
                sha256sum(plain_bf, size, sha256);

                if (!strncmp(sha256, plain_sha256, 64))
#pragma omp critical
                {
                    keyfound = 1;
                    fprintf(stderr, "\r[!] key = ");
                    for (l = 0; l < 12; l++)
                        fprintf(stderr, "%2.2x", key[l] & 0xff);

                    fprintf(stderr, "\n[+] result saved in congratulations.tar.bz2\n");
                    out_fd = open("congratulations.tar.bz2", O_WRONLY | O_CREAT,
                            S_IRUSR | S_IWUSR);
                    ret = write(out_fd, plain_bf, size);
                    if (ret != size)
                        perror("write:");
                    close(out_fd);
                }
            }
        }
    }

finish:
    if (cipher_bf)
        free(cipher_bf);
}

int main(int argc, char **argv) {
    if (self_test(2) != 0) {
        fprintf(stderr, "self-test failed\n");
        exit(EXIT_FAILURE);
    }
    printf("[+] self-test passed\n");

    if (argc != 2) {
        printf("usage: %s encrypted.bin\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    bf(argv[1]);

    return EXIT_SUCCESS;
}

Stage 6

parse_tiff.rb
#!/usr/bin/env ruby

def read_byte
  b = $data[$offset]
  $offset += 1
  return b
end

def read_word
  dword = $data[$offset, 2]
  dword = dword.unpack('S').first
  $offset += 2
  return dword
end

def read_dword
  word = $data[$offset, 4]
  word = word.unpack('L').first
  $offset += 4
  return word
end

def read_bytes(n)
  bytes = $data[$offset, n]
  $offset += n
  return bytes
end

TAG_TO_ASCII = {
  256 => "ImageWidth",
  257 => "ImageLength",
  258 => "BitsPerSample",
  259 => "Compression",
  262 => "PhotometricInterpretation",
  273 => "StripOffsets",
  277 => "SamplesPerPixel",
  278 => "RowsPerStrip",
  279 => "StripByteCounts"
}

if ARGV.size != 1
  $stderr.puts "usage: #{File.basename(__FILE__)} image.tiff"
  exit
end

$data = File.open(ARGV.shift, "rb").read
$offset = 0

magic = read_bytes(2)
raise unless magic == "II"

number = read_word
raise unless number == 42

ifd_offset = read_word

$offset = ifd_offset

num_dir_entry = read_word

num_dir_entry.times do |i|
  tag = read_word
  type = read_word
  count = read_dword
  offset = read_dword

  puts "[#{i}] tag: %s, type: %d, count: %d, offset: %d" % [ TAG_TO_ASCII[tag] || "#{tag}", type, count, offset ]
end

next_ifd = read_dword
if next_ifd == 0
  puts "No other IFD"
end

puts "offset = %d" % $offset