Challenge SSTIC 2019 - Documentation

Le challenge peut être retrouvé à l’adresse suivante: http://static.sstic.org/challenge2019/challenge_SSTIC_2019-virtual_phone.tar.gz

$ tar -tzvf challenge_SSTIC_2019-virtual_phone.tar.gz
drwxr-xr-x david/david        0 2019-03-27 12:43 virtual_phone/
-rw-r--r-- david/david      622 2019-03-28 23:24 virtual_phone/README
-rw-r--r-- david/david    30897 2019-03-28 23:24 virtual_phone/rom.bin
-rw-r--r-- david/david 14737268 2019-03-28 23:24 virtual_phone/power_consumption.npz
-rw-r--r-- david/david 35879224 2019-03-28 23:24 virtual_phone/flash.bin

L’ensemble des scripts utilisés pour résoudre les étapes sont disponibles dans le dossier scripts

Etape 1

« Nous avons consacré du temps à rendre possible le démarrage du téléphone sécurisé dans un environnement virtualisé. Malheureusement le coffre de clef du téléphone ciblé n’a pas pu être copié. Avant de devoir restituer le téléphone, nous avons été en mesure d’enregistrer une trace de consommation de courant lors du démarrage du téléphone. » – Présentation, Challenge 2019

La présentation laisse supposer une possible attaque par analyse de consommation. L’environnement virtuel suivant nous est fourni :

$ qemu-system-aarch64 -nographic -machine virt,secure=on -cpu max -smp 1 -m 1024 -bios rom.bin -semihosting-config enable,target=native -device loader,file=./flash.bin,addr=0x04000000
##########################################
#      virtual environment detected      #
#           QEMU 3.1+ is needed          #
##########################################
NOTICE:  Booting SSTIC ARM Trusted Firmware
KEYSTORE: AES Key is still encrypted, need decryption
KEYSTORE: Need RSA key to decrypt
KEYSTORE: RSA private exponent is not set, please set it in the keystore or enter hex value :

La première étape va donc consister à recupérer l’exposant privé RSA.

La trace de consommation nous est donnée en format .npz avec un seul .npy qui contient un tableau NumPy sérialisé.

_images/etape1_1.png

Le déchiffrement s’effectue de ~700,000 jusqu’à ~1,489,300. On remarquera sur cette plage que 2 motifs se répètent lors du déchiffrement; suggérant l’utilisation de l’exponentiation rapide par cette implémentation de RSA. A titre d’exemple, voici une implémentation tirée du livre Applied Cryptography: Protocols, Algorithms and Source Code in C de Bruce Schneier.

unsigned long qe2(unsigned long x, unsigned long y, unsigned long n) {
    unsigned long s,t,u;
    int i;

    s = 1; t = x; u = y;
    while(u) {
        if(u&1) s = (s*t)%n;
        u>>=1;
        t = (t*t)%n;
    }
    return(s);
}
_images/etape1_2.png

En définissant un seuil de consommation pour différencier les 2 motifs, on récupère la serie de « square+multiply » et « square » utilisée et ainsi reconstruit l’exposant privé.

# etape1/1.py
trace = numpy.load('power_consumption.npz')
data = trace['arr_0'][700000:1489300]

pattern_x = 0
peak_count = 0
peak_count_array = []
for y in data:
    if (y < -0.05): # peak
        if (pattern_x < 500):
            # same pattern
            peak_count += 1
        else:
            # next pattern
            peak_count_array.append(peak_count)
            peak_count = 1
            pattern_x = 0
    pattern_x += 1

d = 0 # private exponent
bit_sel = 0
for peak_count in peak_count_array:
    if peak_count > 26:
        # square + multiply
        d |= 1 << bit_sel
    bit_sel += 1
print("%x" % d)
# 23d87cdf97bb95abe6273c384190c765f552ab86f6de30a8db74435c95e6e3138f54af689812d8f9359cf0f4d453a0c11ec68ce470216c09e74c8947adaf23e902415d61ddf2c0ffe459cbb40f7de42bdb7cd14093100a570e8c29819765e2d8d276f86471b52ac29aa2ce2bb72cd45006279e82bec253ae9675fe45824f6001

Bravo, envoyez le flag SSTIC{a947d6980ccf7b87cb8d7c246} à l'adresse challenge2019@sstic.org pour valider votre avancée

Etape 2

Pour cette deuxième étape, un script python3 get_safe1_key.py simulant un élément sécurisé, nous est donné. Après une phase d’initialisation, il est demandé à l’utilisateur d’entrer une séquence de 8 combinaisons de 4 boutons, représentant 4 interrupteurs à bouton poussoir. Un sha256 h est calculé à partir des 8 sorties s1..s8 et ensuite comparé avec le sha256 00c8bb35d44dcbb2712a11799d8e1316045d64404f337f4ff653c27607f436ea.

[...]
key = bytearray([s1,s2,s3,s4,s5,s6,s7,s8])
h = hashlib.sha256(key).hexdigest()

if "00c8bb35d44dcbb2712a11799d8e1316045d64404f337f4ff653c27607f436ea" == h:
        ok("Hash ok")
        info("Dérivation de la clef AES safe_01")
        aes_key = hashlib.scrypt(key,salt =b"sup3r_s3cur3_k3y_d3r1v4t10n_s4lt",n=1<<0xd,r=1<<3,p=1<<1,dklen=32)
        info("aes key : %s" % aes_key.hex())
        info("Vous pouvez sauvegarder cette clef en utilisant /root/tools/add_key.py key")
else:
        error("Mauvais hash, déchiffrement impossible")
[...]

Chacune des sorties résulte d’une série d’appels à secure_device().

def step1():
        r = secure_device(0x35,0x27,3)
        r = secure_device(0x7e,r,3)
        r = secure_device(0x66,r,2)
        r = secure_device(0x8,r,1)
        r = secure_device(0x13,r,0)
        r = secure_device(0x1f,r,1)
        r = secure_device(0xa,r,2)
        r = secure_device(0xd3,r,0)
        r = secure_device(0xc6,r,3)
        return r
[...]
input()
s1 = step1()

Le secure_device() n’a pas été implémenté mais le circuit logique schematics.png n’a cependant pas été supprimé.

def secure_device(a,b,op):
        out = 0
        """
        TODO :
                - Implémentation de la communication avec le secure element
                        * Entrées A et B (A0 = bit de poids faible)
                        * Entrée OP
                        * Sortie Out (Out0 = bit de poids faible)
                        * Les boutons permettent à l'utilisateur de rentrer sa combinaison secrète pour le déchiffrement
                - Supprimer les docs de conception
        """
        return out
_images/etape2_schematics.png

Sachant que \(2^4\) (16) combinaisons sont possibles pour chacune des 8 étapes et que ces dernières sont indépendantes les unes des autres, une récupération des 16 sorties respectives permet de définir l’ensemble des clefs possibles. L’espace de recherche se réduit ici à \(2^{4^8}\) soit \(2^{32}\) clefs. Au vu de l’utilisation de SHA-256, une première approche peut consister à bruteforcer cet espace. Il serait donc question de \(2^{32}\) sha256 calculés dans le pire des cas.

Sur mon vieux core i5 de 3ème génération (i5-3320M CPU @ 2.60GHz), un test.c parallélisé en utilisant OpenMP sur le SHA256() de la bibliothèque openssl a donné un taux de \(6.59 \cdot 10^6 \text{H/s}\). Ainsi, la génération des \(2^{32}\) sha256 peut être exhaustive en \(2^{32} \text{H} \div (6.59 \cdot 10^6 \text{H/s}) \approx 651.7\text{s}\); ce qui rend le bruteforce faisable en un temps raisonnable. Cette stratégie a donc été choisi.

En suivant le schéma schematics.png, les principales fonctions logiques A, B, C et le circuit secure_device() ont été implémentés (etape2/1.py) pour déterminer les 16 octets possibles de chacune des sorties i.e

[i] s1: [25, 57, 64, 71, 98, 114, 129, 137, 143, 144, 175, 191, 194, 199, 215, 223]
[i] s2: [8, 16, 32, 41, 50, 82, 144, 164, 180, 181, 209, 217, 218, 219, 243, 245]
[i] s3: [1, 28, 79, 123, 141, 159, 176, 187, 189, 191, 217, 219, 220, 221, 223, 237]
[i] s4: [0, 26, 40, 53, 56, 64, 78, 82, 83, 85, 91, 113, 156, 169, 170, 173]
[i] s5: [0, 1, 65, 71, 96, 130, 142, 169, 175, 189, 203, 212, 222, 237, 253, 255]
[i] s6: [27, 49, 61, 78, 100, 101, 157, 178, 193, 200, 207, 221, 224, 237, 238, 246]
[i] s7: [5, 10, 77, 79, 84, 96, 134, 154, 158, 169, 187, 191, 202, 221, 223, 232]
[i] s8: [7, 72, 77, 84, 120, 131, 133, 151, 170, 176, 216, 240, 243, 251, 252, 253]

Après l’écriture du bruteforce à partir de s1..s8 (etape2/2.c), les sorties utilisées à la génération du sha256 00c8bb35d44dcbb2712a11799d8e1316045d64404f337f4ff653c27607f436ea sont retrouvées en ~2min24.

$ time ./etape2/2
key = bytearray([143, 164, 223, 169, 212, 237, 187, 240])
./etape2/2  569,02s user 0,12s system 396% cpu 2:23,67 total
>>> import hashlib
>>> key = bytearray([143, 164, 223, 169, 212, 237, 187, 240])
>>> aes_key = hashlib.scrypt(key,salt =b"sup3r_s3cur3_k3y_d3r1v4t10n_s4lt",n=1<<0xd,r=1<<3,p=1<<1,dklen=32)
>>> hashlib.scrypt(key,salt =b"sup3r_s3cur3_k3y_d3r1v4t10n_s4lt",n=1<<0xd,r=1<<3,p=1<<1,dklen=32).hex()
'5fb3a83d1fd97137076019ad6e96c6a366fb6b32618d162e00cdee9bad427a8a'

Le flag est retourné en passant la clef au script /root/tools/add_key.

Envoyez le flag SSTIC{5fb3a83d1fd97137076019ad6e96c6a366fb6b32618d162e00cdee9bad427a8a} à l'adresse challenge2019@sstic.org pour valider votre avancée

Etape 3

Suite à la validation de l’étape 2, le premier conteneur safe_01 est déchiffré.

# /root/safe_01/decrypted_file
Usage : /root/safe_01/decrypted_file <flag>

Après l’ouverture du binaire dans un désassembleur, le programme initialement écrit peut globalement se résumer comme suit :

 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
static const char* g_str;
static const char* g_nope = "Not good\n";
static const char* g_ok = "That's the correct flag :)\n";

void _throw(char* str) {
    throw(str);
}

int main(int argc, char* argv[]) {

    if (argc != 2) {
        printf("Usage : %s <flag>\n", argv[0]);
        exit(1);
    }

    try {
        _throw(argv[1]);
    } catch (char* flag) {
        register const char* str __asm__ ("x28");
        if (strcmp("SSTIC{congolexicomatisation}", flag) == 0)
            str = g_ok;
        else
            str = g_nope;
        g_str = str;
        puts(g_str);
    }
    return 0;
}

Le programme lève explicitement une exception de type char * avec l’argument saisi par l’utilisateur. Le bloc catch, supposé traiter l’exception, ne se comportera pas comme présenté.

Lors de la compilation d’un code c++ présentant des blocs try/catch, la section .gcc_except_table est créée pour permettre à la libstdc++ de trouver, entre autres, les informations concernant les blocs catch (landing pads).

La partie concernant le précédent bloc catch (resp. ligne 18-26) de la section .gcc_except_table est présentée ci-dessous; partie annotée grâce au script IDA gcc_extab.py écrit et publié par Igor Skochinsky pour sa présentation à REcon « Compiler Internals: Exceptions and RTTI » en 2012.

.gcc_except_table:00000000004032CC                 AREA .gcc_except_table, DATA, READONLY
[...]
.gcc_except_table:00000000004032D5                 DCB loc_402EB4 - 0x402E68 ; cs_start[1] = 00402EB4
.gcc_except_table:00000000004032D6                 DCB 4                     ; cs_len[1] = 4 (end = 00402EB8)
.gcc_except_table:00000000004032D7                 DCB 0xB8, 1               ; cs_lp[1] = 00402F20
.gcc_except_table:00000000004032D9                 DCB 1                     ; cs_action[1] = 1 (004032E9)
[...]

L’adresse de destination du bloc catch a ici été modifiée en post-compilation pour pointer 0x60 octets plus loin (resp. ligne 24). Initialement, l’adresse de destination du bloc catch #2 (cs_lp[1]) serait de 0x402EC0.

Lorsque une exception est levée, la section .eh_frame est parcourue pour gérer le processus de stack unwinding. Le Frame Description Entry correspondant à la fonction 0x402E34 levant l’exception (resp. _throw()) est comme suit :

$ aarch64-linux-gnu-readelf --debug-dump=frames decrypted_file
Contents of the .eh_frame section:
[...]
00000090 000000000000001c 00000094 FDE cie=00000000 pc=0000000000402e34..0000000000402e68
    DW_CFA_advance_loc: 1 to 0000000000402e35
    DW_CFA_def_cfa_offset: 32
    DW_CFA_offset: r29 (x29) at cfa-32
    DW_CFA_offset: r30 (x30) at cfa-24
    DW_CFA_val_expression: r28 (x28) (DW_OP_skip: -12222)
    DW_CFA_nop
    DW_CFA_nop
[...]

La séquence d’instructions (Call Frame Instruction) a ici été modifiée pour rediriger le flot d’exécution (i.e ligne 9) et ainsi venir interpréter un bytecode Dwarf placé dans la section .gnu.hash en 0x403215 - 12222 + 2 = 0x400259. Au retour du programme Dwarf, la valeur de retour sera ensuite dépilée de la pile (Dwarf) et mise dans x28 avant de donner le contrôle au bloc catch correspondant (i.e 0x402F20). Il s’agira donc de la chaîne de caractères à retourner à l’utilisateur.

Il a été choisi de réutiliser le code de l’interpréteur Dwarf de la bibliothèque libgcc pour décoder le bytecode et tracer le programme. Une propagation de teinte y a été ajouté afin d’écarter le code mort et de réduire le code généré e.g branchements inconditionnels, constants unfolding, … (etape3/1.c)

A partir de ce bytecode décodé et de traces d’exécution, les structures de contrôle identifiées permettent de reconstituer en partie la logique de l’algorithme.

Le corps principal de ce dernier peut se présenter comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def A(id_1, id_2, id_3, id_4):

    for rd in range(8):
        m_1, m_2 = M(id_1, id_2, K(id_3, id_4))
        id_1, id_2, id_3, id_4 = id_3, id_4, m_1, m_2

    t_1  = (id_1 ^ 0x65850b36e76aaed5) \
         + (id_2 ^ 0xd9c69b74a86ec613) \
         + (id_3 ^ 0xdc7564f1612e5347) \
         + (id_4 ^ 0x658302a68e8e1c24)

    return t_1 == 0

Note

Le details des fonctions peut être retrouvé à etape3/2.py.

Les arguments id_1..id_4 correspondent aux 4 QWORD que compose le flag saisi par l’utilisateur.

A chaque tour de boucle, 2 nouvelles valeurs m_1, m_2 sont dérivées des 4 précedents id_1, id_2, id_3, id_4 et deviendront respectivement les nouveaux id_3, id_4. Les précédents id_3, id_4 remplaceront id_1, id_2.

Le corps de la boucle possède cependant une faiblesse. Après l’exécution de la boucle (ligne 6), les valeurs connues sont id_1, id_2, id_3, id_4 qui sont respectivement id_3, id_4, m_1, m_2 au dernier tour de boucle (i.e rd = 7). Ainsi, les valeurs id_3, id_4 utilisées par K() sont connues. Les valeurs id_1, id_2 utilisées par M() restent cependant inconnues. Si il s’avère que M() est réversible alors les entrées id_1, id_2 peuvent être rétrouvées et par conséquent, les valeurs id_1, id_2, id_3, id_4 initiales à rd = 0 le seront.

def M(m_1, m_2, k):

    s_1, s_2 = m_1, m_2
    for rd in range(1, 16):
        t_3, t_4 = J(k, rd % 16)
        s_1 = t_3 ^ I(H(s_2), s_1)
        s_2 = t_4 ^ L(H(s_1), s_2)

    return s_1, s_2

Si I() et L() sont réversibles alors la fonction inverse M_inv() de M() peut s’écrire de la façon suivante :

def M_inv(s_1, s_2, k):

    m_1, m_2 = s_1, s_2
    for rd in range(15, 0, -1):
        t_3, t_4 = J(k, rd % 16)
        m_2 = L_inv(H(m_1), m_2 ^ t_4)
        m_1 = I_inv(H(m_2), m_1 ^ t_3)

    return m_1, m_2

La fonction L() est sans perte d’information i.e

def L(l_1, l_2):

    lo_l_1 = LODWORD(l_1)
    hi_l_1 = HIDWORD(l_1)
    lo_l_2 = LODWORD(l_2)
    hi_l_2 = HIDWORD(l_2)

    return (ROL((hi_l_2 ^ lo_l_1) ^ hi_l_1, 0x12) & 0xffffffff) << 0x20 | ROL(lo_l_2 ^ hi_l_1, 0x1a) & 0xffffffff

En connaissance de s = L(l_1, l_2) et l_1, l_2 peut ainsi être récupéré :

def L_inv(l_1, s):

    lo_l_1 = LODWORD(l_1)
    hi_l_1 = HIDWORD(l_1)
    lo_s = LODWORD(s)
    hi_s = HIDWORD(s)

    return ((ROR(hi_s, 0x12) & 0xffffffff) ^ lo_l_1 ^ hi_l_1) << 0x20 | ((ROR(lo_s, 0x1a) & 0xffffffff) ^ hi_l_1) & 0xffffffff

La fonction I() est également réversible i.e etape3/3.py .

Somme toute, M() est bien réversible et par conséquent les valeurs id_1, id_2, id_3, id_4 initiales utilisées à la génération des valeurs lignes 7-10 peuvent être obtenues comme suit :

def A_inv():

    id_1 = 0x65850b36e76aaed5
    id_2 = 0xd9c69b74a86ec613
    id_3 = 0xdc7564f1612e5347
    id_4 = 0x658302a68e8e1c24

    for _ in range(8):
        id_1, id_2, id_3, id_4 = id_3, id_4, id_1, id_2
        id_1, id_2 = M_inv(id_1, id_2, K(id_3, id_4))

    key = struct.pack("<Q", id_1) + struct.pack("<Q", id_2) + struct.pack("<Q", id_3) + struct.pack("<Q", id_4)
    print(key.decode())
    # SSTIC{Dw4rf_VM_1s_co0l_isn_t_It}

Etape 4

Après avoir soumis le flag de l’étape précedente au script /root/tools/add_key, le second conteneur safe_02 est déchiffré. Il est necessaire de redémarrer pour charger le Secure-OS.

[+] Container /root/safe_02/.encrypted decrypted to /root/safe_02/decrypted_file
[w] You must reboot in order to decrypt Secure OS

Le Secure-OS est ici un TEE-OS basé sur la technologie TrustZone d’ARM. Comme informé lors du reboot, il intervient en BL32. (Bootloader stage 3-2).

NOTICE:  BL1: Booting BL31
NOTICE:  BL31: Initializing BL32
NOTICE:  Booting Secure-OS

En étudiant l’initialisation du BL32 dans un désassembleur, on remarquera que la ligne de commande utilisée pour démarrer l’environnement virtuel est parsée i.e (ps -ef |egrep -q 'qemu-system.* -[s] |qemu-system.* -[g]db ') && exit 42, à la recherche des arguments -s ou -g afin de détecter si QEMU est à l’écoute d’un possible debugging via gdb.

[...]
mmap_add_region(0xE053000, 0x413000, 0x1000);
if (debug_is_present())
    addr = 0xE055000;
else
    addr = 0xE054000;
mmap_add_region(addr, 0x414000, 0x100000);
[...]

Dans le cas où debug_is_present() est vrai, le mapping [0xE054000, 0xE055000[ sera omis, créant une discontinuité sur 0x1000 + 0x100000 i.e

 0x0413000           ...           0x1414000
[0xE053000, 0xE054000[ [0xE055000, 0xE155000[.

Lorsqu’il est venu d’étudier le Secure Monitor, il a été découvert qu’un buffer (de taille 0x101010) envoyé par decrypted_file est finalement copiée en 0xE053000, couvrant le mapping précédent.

La table des vecteurs d’exceptions (EVT) du Secure OS a ensuite été étudiée. Le format de cette dernière en ARMv8 est comme suit :

_images/aarch64_exception_vector_table.png

Le handler d’exception à 0xE203000 + 0x200 (correspondant à une exception synchrone venant du même level avec l’utilisation de SP_EL1) se positionne comme un spécifique write()/load() lors d’une page fault. Pour toutes lectures comprises entre [0x0, 0x100fff], le handler commence par translater cette adresse à 0x413000; ce qui coïncide avec le mapping vu précédemment. La valeur correspondante est ensuite chiffrée via l’utilisation de quelques instructions de l’extension cryptographique de ARMv8 avant d’être retournée à l’initiateur de la lecture.

Les instructions de l’extension e.g sm4ekey, sm4e ne sont pas encore reconnues par IDA Pro. Cela a ainsi été l’occasion de remplacer IDA par Ghidra le temps de cette étape 4 (qui supporte bien ces instructions).

Le handler d’exception à 0xE203000 + 0x600 (correspondant à une exception synchrone venant d’un level inférieur en AArch32) a été implémenté. Dans le cas d’un SVC #0x1338, le handler sert de pivot au Secure Monitor. En recherchant des fonctions modifiant le SPSR_EL1, deux fonctions ont été identifiées i.e sub_E2025A4() et sub_E202610() mettant à jour le bit #4 (M) à 1 du SPSR_EL1 afin de simuler une exception venant d’un contexte AArch32 et ainsi y retourner. sub_E202610() modifie également le bit #5 (T) du SPSR_EL1 pour changer le jeu d’instruction (ARM / Thumb). Ces 2 fonctions modifient le ELR_EL1 avant de ERET. En étudiant les parties de code (Thumb/ARM) aux différentes adresses de retour, un anti-debug a été découvert utilisant l’horloge interne de QEMU pour mesurer le temps d’exécution entre deux parties.

Le binaire decrypted_file du second conteneur safe_02 a ensuite été regardé.

# /root/safe_02/decrypted_file
usage: /root/safe_02/decrypted_file [32-bytes-key-hex-encoded]

Le rôle principal du binaire est de transmettre un tableau d’octets et le flag décodé au character device /dev/sstic. Une boucle est utilisée pour faire avancer une certaine machine à état et tester le retour de celle-ci.

[...]
ioctl(fd, 0xC0105300, &array_44DBD8, 0x101010);
ioctl(fd, 0xC0105301, &flag, 0);
while (1) {
    ioctl(fd, 0xC0105302, 0, 0);
    res = ioctl(fd, 0xC0105303, 0, 0);
    if (LOWORD(res) == 1) {
        if (HIWORD(res) != 0)
            printf("Loose\n");
        else
            printf("Win\n");
        return 0;
    }
    [...]
}
_images/trustzone_model.png

Le driver SSTIC en EL1 sert principalement à relayer les informations entre decrypted_file et le Secure Monitor (qui s’exécute en EL3) via des Secure Monitor Calls (SMC). Le Trusted OS est ici le Secure-OS qui s’exécute en Secure-EL1.

En étudiant le Secure Monitor, deux principaux handlers ont été identifiés :
  • sub_E034158(): code SMC commençant par 0xF200xxxx
  • sub_E032014(): code SMC commençant par 0x8301xxxx

sub_E032014() maintient une structure de 16 lignes à 4 entiers dont le contenu est chiffré (en utilisant comme clef les 4-7ième DWORD composants le flag saisi par l’utilisateur). Quelques instructions des extensions Advanced SIMD et cryptographique de ARMv8 sont ici utilisées pour effectuer la majorité des opérations arithmétiques et logiques sur les lignes de cette structure.

Par l’utilisation des codes SMC 0xF2005001, 0xF2005002, 0xF2005003, le handler sub_E034158() (qui s’exécute en EL3) redirige l’exécution en S-EL1 en 0xE200094. La fonction sub_E200C08(), qui est ensuite appelée, gère principalement la logique de l’algorithme sous-jacent. Ce dernier se présente en deux phases:
  • 0xF2005001 permet de « charger » le prochain code (non SMC, spécifique à l’algorithme)
  • 0xF2005002 permet de décoder ce « code » à l’aide de la sous-fonction sub_E2005A4()

La logique de sub_E2005A4() et sub_E032014() a ensuite été retranscrite et simplifiée (etape4/1.c) afin d’extraire par la suite la logique de l’algorithme sous-jacent.

En pseudo-code, la logique de l’algorithme peut se traduire comme suit :

for (i = 0; i < 8; i += 2)
{
    z0 = REVWORD(LOWORD(user[i]));
    z1 = REVWORD(HIWORD(user[i]));
    z2 = REVWORD(LOWORD(user[i+1]));
    z3 = REVWORD(HIWORD(user[i+1]));

    k = 0x7f;
    for (j = 0x20; j > 0; j--)
    {
        l = L(&k, z1);

        if ((((j-1) >> 0x3) & 0x1) == 1) {
            z0, z1, z2, z3 = l, j ^ l ^ z2, z3, z0;
        } else {
            z0, z1, z2, z3 = l, z2, z3, j ^ z0 ^ z1;
        }
    }
    res[i]   = REVWORD(z1) << 0x10 | REVWORD(z0);
    res[i+1] = REVWORD(z3) << 0x10 | REVWORD(z2);
}

Note

Le code complet de ce dernier ainsi que la version reverse peuvent être retrouvés à etape4/2.c

La solution obtenue acadaa8b5b55306fb3c6dfc3b2d1c80770084644225febd71a9189aa26ec740e permet de valider l’étape :

[+] Envoyez le flag SSTIC{acadaa8b5b55306fb3c6dfc3b2d1c80770084644225febd71a9189aa26ec740e} à l'adresse challenge2019@sstic.org pour valider votre avancée

Etape 5

Suite à la validation de l’étape 4, le dernier conteneur safe_03 est déchiffré. Il s’agit d’une archive .tar.gz des données des applications du téléphone de l’individu « au comportement suspect ». Une recherche de la chaîne challenge.sstic.org sur le dossier révèle un message contenu dans la base de données MMS/SMS :

« Mission accomplie, comme d’habitude la perspective d’une tournée de shooter au cactus a suffi à corrompre le CO et à chosir la date de publication du challenge. Pour être sûr que les experts sont toujours occupés, j’ai redirigé l’adresse 9e915a63d3c4d57eb3da968570d69e95@challenge.sstic.org vers votre boîte mail. Tant que vous ne voyez passer aucun mail, la voie est libre… »