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.
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 bienchallenge2015sstic
; -
en fonction du résultat de l’appel à
check_correct_environment
, appel de la fonctionwrite_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 :
Enfin, la pièce finale après le rocket jump affiche la séquence à respecter pour obtenir la clé finale :
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 |
|
|
|
|
|
|
|
|
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 :
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 :
Le reste de la capture est une série de messages “URB_INTERRUPT” tel que celui présenté ci-dessous :
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 :
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 :
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
:
Ensuite, le script définit une seconde variable hash
puis exécute une série
d’instructions JavaScript obfusquées :
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 :
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é :
#!/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 :
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.
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
etCreg
: 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 |
---|---|---|---|
|
Longueur |
Pointeur vers un channel |
Adresse destination |
|
Longueur |
Pointeur vers un channel |
Adresse source |
On peut alors résumer ce code ainsi :
-
envoi de
"Boot ok"
sur le channel0x80000000
; -
lecture de 12 octets (3 mots) du channel
0x80000010
vers l’adresse&var_73
(les variablesvar_73
,var_74
etvar_75
sont mises à jour) ; -
si
var_73 == 0
, sortie de la boucle et poursuite de l’exécution à l’adresseloc_37
; -
sinon lecture de
var_73
octets depuis le channel0x80000010
vers l’adresseloc_f3
; -
écriture de
var_73
octets depuis l’adresseloc_f3
vers le channel référencé parvar_74
.
La documentation précise le rôle des channels 0x80000000
et 0x80000010
:
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 :
#!/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:
dansvar_2
; -
la lecture de 12 octets sur le channel 4 stocke les 12 octets valant
0xFF
dansvar_5
,var_6
etvar_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 devar_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 dansvar_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é dansvar_0[1]
,var_0[2]
etvar_0[3]
; -
loc_b3
: mise à jour de l’octetvar_0[1]
avec l’opérationvar_0[3] ^ ( var_0[2] ^ var_0[1] )
; -
loc_c1
: mise à jour de l’octetvar_0[0]
avec l’opération(var_4 + 2 * var_5[var_4]) ^ var_1
; -
loc_c9
: mise à jour de l’octetvar_5[var_4]
avec l’octet précédemment calculévar_0[1]
; -
loc_cb
àloc_d4
: incrément de 1 de la variablevar_4
, remise à 0 sivar_4 == 12
; -
loc_d5
àloc_da
: envoi de l’octetvar_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 fois0xff
à 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 positionvar_4
; -
var_0[0]
correspond au résultat du déchiffrement de l’octet stocké dansvar_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 devar_1
; -
loc_2c
àloc_3d
: envoi de 12 octets de l’adresse devar_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é dansvar_0[1]
,var_0[2]
etvar_0[3]
; -
loc_53
àloc_63
: mise à jour de l’octetvar_0[0]
avec l’opérationvar_0[3] ^ ( var_0[2] ^ var_0[1] )
; -
loc_65
àloc_6a
: envoi de l’octetvar_0[0]
vers le channel 0 et saut à l’adresseloc_26
.
Ces opérations peuvent être traduites en pseudo-code C de la façon suivante :
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 :
; 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 variablesvar_0
,var_1
etvar_2
sont donc mises à jour) ; -
lecture de
var_0
octets sur le channel, stockage à l’adresseloc_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 :
; 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 :
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
etvar_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érationvar_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 devar_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érationvar_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
etout
; -
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émaschematic.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 :
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 :
Un octet de clé est donc corrélé avec les octets de clair et chiffré correspondants de la façon suivante :
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.
#!/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 :
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 :
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" :
#!/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 :
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 :
De la même manière, le bit 0 de la composante verte semble être utilisé pour stocker des données :
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 :
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 :
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 :
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
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
; 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
; 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
#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
#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
#!/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