[ En mode Draft ... relecture en cours]
Introduction:
Cette année j'ai pu me pencher sur le challenge SSTIC (http://communaute.sstic.org/ChallengeSSTIC2014) et ce dernier n'a pas failli à sa réputation ...Merci à l'équipe QuarksLab pour cette souffrance :)
Etape1: usbmon
La première partie correspond à une capture USB apparemment réalisée entre un device 'Android' et une machine utilisant le protocole ADB.
Pour avoir une idée sur le contenu de ce fichier l'outil usb-analyzer a été utilisé.Un autre outil (voir "usbmon_helper.py") permet de visualiser le contenu d'une capture usbmon en HTML.
Une première analyse a vite révélé que la capture contient un transfert de fichier entre le device Android et une machine cible.
Le protocole ADB indique que pour un transfert de fichier les commandes suivantes doivent être enchaînées selon le format suivant:
Send → AdbMessage(A_OPEN, local_id, 0, "sync:");
Receive ← AdbMessage(A_OKAY, remote_id, local_id, NULL);
Query File Attributes. If file exists, then proceed.
Send → AdbMessage(A_WRTE, local_id, remote_id, "RECVnnnn");
Receive ← AdbMessage(A_OKAY, remote_id, local_id, NULL);
Send → AdbMessage(A_WRTE, local_id, remote_id, "remote file name");
Receive ← AdbMessage(A_OKAY, remote_id, local_id, NULL);
Receive ← AdbMessage(A_WRTE, remote_id, local_id, "DATAnnnn......");
Send → AdbMessage(A_OKAY, local_id, remote_id, NULL);
Receive ← AdbMessage(A_WRTE, remote_id, local_id, data);
Send → AdbMessage(A_OKAY, local_id, remote_id, NULL);
...
Receive ← AdbMessage(A_WRTE, remote_id, local_id, "DONEnnnn");
Send → AdbMessage(A_WRTE, local_id, remote_id, "QUITnnnn");
Receive ← AdbMessage(A_CLSE, remote_id, local_id, NULL);
Send → AdbMessage(A_CLSE, local_id, remote_id, NULL);
L'outil "usbmon_helper.py" a été légèrement modifié pour récupérer le contenu binaire du fichier transféré par le protocole ADB (voire stage1.bin)
Le fichier idb produit est stage1.i64
Etape 2: premier binaire ARM 64 bit
Environnement de travail
Afin d'avoir un environnement d'analyse les composants suivants ont été utilisés:
Donc en gros on dispose d'une machine virtuelle ubuntu ARM 64 bits. Cela permet de compiler les outils nécessaires à l'analyse de notre binaire.
L'idée est d'analyser le binaire récupéré de l'étape précédente en combinant une analyse dynamique avec gdb et une analyse statique avec IDA.
Pour coller les deux morceaux, l'outil Qbsync de QuarksLab a été utilisé, ce dernier permet de visualiser les étapes de débogage sur IDA d'ou la nécessité d'avoir un gdb compilé avec python (possible avec la machine virtuelle):
L’exécution du binaire permet de comprendre l'objectif de cette étape, ce dernier demande la saisie d'une clé permettant de récupérer un fichier nommé 'payload.bin'.
(Pour plus de détails sur l'analyse suivante, s’appuyer le fichier idb)
En déboguant pas à pas le binaire, on arrive à identifier le flux suivant:
Au niveau du point d'entrée, on retrouve un appel vers sub_1010C:
loc_102E0 ; Store Pair
FD 7B BF A9 STP X29, X30, [SP,#var_10]!
FD 03 00 91 MOV X29, SP ; Rd = Op2
89 FF FF 97 BL sub_1010C ; Branch with Link
01 7C 40 93 SBFM X1, X0, #0, #0x1F ; Signed Bitfield Move
E0 03 01 AA MOV X0, X1 ; Rd = Op2
C8 0B 80 D2 MOV X8, #0x5E ; Rd = Op2
01 00 00 D4 SVC 0 ; Supervisor Call
E1 03 00 AA MOV X1, X0
Après plusieurs itérations, le binaire arrive à la section du code suivante:
.text:00000000000102A8 LDRSW X24, [X29,#arg_58] ; Load from Memory
.text:00000000000102AC LDR X1, [X29,#arg_50] ; Load from Memory
.text:00000000000102B0 MOV SP, X25 ; Rd = Op2
.text:00000000000102B4 SUB SP, SP, #8 ; Rd = Op1 - Op2
.text:00000000000102B8 STR X24, [SP,#0x68+var_68] ; Store to Memory
.text:00000000000102BC MOV X2, X1 ; Rd = Op2
.text:00000000000102C0 BLR X2 ; x2 0x400514 4195604
.text:00000000000102C4 MOV X23, X0 ; Rd = Op2
.text:00000000000102C8 B loc_10198 ; Branch
.text:00000000000102C8 ; End of function sub_1010C
.text:00000000000102C8
La suite d'itérations réalisées avant d'arriver à ce stade correspondent à la décompression d'une nouvelle section.
L'instruction BLR x2 permet à la première section du binaire de sauter vers une section nouvellement créée.
Ensuite avec gdb il est possible de dumper cette section (0x4000) quand peut aussi voir avec la commande "cat /proc/mem/pid".
.text:00000000000102A8 LDRSW X24, [X29,#arg_58] ; Load from Memory
.text:00000000000102AC LDR X1, [X29,#arg_50] ; Load from Memory
.text:00000000000102B0 MOV SP, X25 ; Rd = Op2
.text:00000000000102B4 SUB SP, SP, #8 ; Rd = Op1 - Op2
.text:00000000000102B8 STR X24, [SP,#0x68+var_68] ; Store to Memory
.text:00000000000102BC MOV X2, X1 ; Rd = Op2
.text:00000000000102C0 BLR X2 ; x2 0x400514 4195604
.text:00000000000102C4 MOV X23, X0 ; Rd = Op2
.text:00000000000102C8 B loc_10198 ; Branch
.text:00000000000102C8 ; End of function sub_1010C
.text:00000000000102C8
Une fois cette section dumpée, il a été possible de l'ajouter au niveau de l'idb (IDA) courant afin de faciliter la suite de l'analyse. une attention a été portée sur l'ajout de la section au niveau de l'idb d'IDA afin que le mapping dynamique correspond au sections statique.
On continue l'analyse dynamique/statique avec la combinaison gdb/qbsync/IDA.
Cette deuxième partie du binaire est 'obfusquée' en utilisant une machine virtuelle. Par conséquence, l'objectif de cette étape consiste à comprendre le fonctionnement de la machine virtuelle. Pour cela il faut:
+ Identifier et récupérer le byte code de la machine virtuelle
+ Identifier l'ensemble des instruction de la machine virtuelle
+ Identifier la mémoire, la pile et les registres utilisés par la mémoire virtuelle.
Une fois l'ensemble de ces élements, un décodeur sera écrit afin de désassembler le Byte code de la VM. Pour cela l'outil AMOCO a été utilisé.
(une lecture du chapitre obfuscation par la virtualisation Practical Reverse Engineering a aidé à connaitre la notion de VM Hanlder/Dispatcher, fonction de récupération de registre, fonction de récupération de valeur de registres, boucle principale).
La partie suivante essayera de mettre en évidence ces élements (même si je ne me rapelle plus de l'ordre que j'ai suivi pour mettre en évidence ces élements)
Une fois décompressée, le binaire saute à la section 0x400000 (à l'adresse 0x400514), il enchaîne l’exécution de plusieurs instructions(brève description) :
1. Appel de la fonction 0x4000524:
text:0000000000400524 B Call_svc_exit_group ; Branch
2.Appel de la fonction 0x04004F8 que je qualifie de VM Main Entry:
X2, X2, #unk_510018@PAGEOFF ; x2 0x510018
STR X3, [X2] ; Store to Memory
BL Main_Entry ; Branch with Link SBFM X1, X0, #0, #0x1F
3.Appel de la fonction 0x04000C8 que je que qualifie de 'VM set context' car elle permet de mettre en place les composants de la VM (sections mémoires, context crypto):
.text:00000000004000C0 ADD X0, X0, #unk_500000@PAGEOFF ; Rd = Op1 + Op2
.text:00000000004000C4 ADD X2, X29, #0x10 ; Rd = Op1 + Op2
.text:00000000004000C8 BL VM_set_context ; x0 0x500000 5242880
.text:00000000004000C8 ; x1 0x10000 65536
.text:00000000004000C8 ; x2
En effet le syscall présents au niveau de cette fonction permettent de répérer les sections mémoires utilisé par la VM.
La section 0x500000 attire notre attention, aprés avoir dumpé le contenu de cette dernière, elle semble obfusquée (voir vm_bytecode.bin) .
Toutefois en arrivant à la section du code suivant:
.text:0000000000402A88 BL Func_Ecrypt_init ; Branch with Link
.text:0000000000402A8C ADD X22, X19, #0x10 ; Rd = Op1 + Op2
.text:0000000000402A90 ADRP X1, #unk_510000@PAGE ; x1 0x510000 5308416
.text:0000000000402A94 MOV X0, X22 ; Rd = Op2
.text:0000000000402A98 ADD X1, X1, #unk_510000@PAGEOFF ; Rd = Op1 + Op2
.text:0000000000402A9C MOV W2, #128 ; le parametere kbits
.text:0000000000402AA0 MOV W3, #0 ; Rd = Op2
.text:0000000000402AA4 BL Func_KEy_setup ; void ECRYPT_keysetup(ECRYPT_ctx *x,const u8 *k,u32 kbits,u32 ivbits)
.text:0000000000402AA8 ADRP X1, #unk_510020@PAGE ; Address of Page
.text:0000000000402AAC MOV X0, X22 ; Rd = Op2
.text:0000000000402AB0 ADD X1, X1, #unk_510020@PAGEOFF ; x1 0x510020 5308448
.text:0000000000402AB4 BL func_Ecrypt_ivsetup ; Branch with Link
.text:0000000000402AB8 LDRB W0, [X19] ; Load from Memory
En inspectant le contenu de la mémoire , on retrouve la chaine ""expand 16-byte k" qui en recherchant sur Internet permet de d'identifier en premier lieu l'algorithme de chiffrement Salsa puis dans un deuxième temps sa version Chacha.
En comparant le code C des différentes fonctions de cet algorithme, comme
void ECRYPT_ivsetup(ECRYPT_ctx *x,const u8 *iv)
{
int i = 0 ;
x->input[12] = 0;
x->input[13] = 0;
x->input[14] = U8TO32_LITTLE(iv + 0);
x->input[15] = U8TO32_LITTLE(iv + 4);
//Printing the context
printf("[ECRYPT_ivsetup] The Chacha context:\n");
for (i=0; i<16 i="" p=""> printf("0x%x\n", x->input[i]);
}
et le code assembleur à l'offset 0x402AB4 on retrouve rapidement des similitudes.
La comparaison a permis de s'orienter plutôt vers la variante chacha que salsa. Ce qui est intéressant à ce satade est d'identifier la clé utilisée par cet algorithme. Cette information peut être obtenue en mettant un breakpoint à l'appel de la fonction 'ECRYPT_keysetup' qui reçoit en second paramètre la clé de chiffrement.
- Chiffrement Salsa/Chacha
-------------------------------------16>
Il se trouve que la section 0x500000 est chiffré avec l'algorithme de chiffrement chacha. le code chacha_test.c a permis de récupérer le contenu de cette section en clair
(voir VM_bytecode_dec.bin).
A ce stade on dispose du Byte code de la VM:
00000030 00 00 00 00 00 20 00 00 00 00 00 00 40 00 00 00 ..... ......@...
00000040 00 01 00 00 01 21 00 00 00 02 00 00 01 12 00 00 .....!..........
00000050 00 03 00 00 01 E3 32 00 00 04 00 00 01 44 02 00 ......2......D..
00000060 1D 00 00 01 00 00 01 11 00 00 0A 22 00 03 00 00 .
la fin du byte code contient les chaines de carctères affichés lors de l'exécution de notre binaire
20 50 6C 65 61 73 65 20 65 6E 74 65 72 20 74 68 Please enter th
00000340 65 20 64 65 63 72 79 70 74 69 6F 6E 20 6B 65 79 e decryption key
00000350 3A 20 00 00 3A 3A 20 54 72 79 69 6E 67 20 74 6F : ..:: Trying to
00000360 20 64 65 63 72 79 70 74 20 70 61 79 6C 6F 61 64 decrypt payload
00000370 2E 2E 2E 0A 20 20 20 57 72 6F 6E 67 20 6B 65 79 .... Wrong key
00000380 20 66 6F 72 6D 61 74 2E 0A 00 20 20 20 49 6E 76 format... Inv
00000390 61 6C 69 64 20 70 61 64 64 69 6E 67 2E 0A 00 00 alid padding....
000003A0 20 20 20 43 61 6E 6E 6F 74 20 6F 70 65 6E 20 66 Cannot open f
000003B0 69 6C 65 20 70 61 79 6C 6F 61 64 2E 62 69 6E 2E ile payload.bin.
000003C0 0A 00 3A 3A 20 44 65 63 72 79 70 74 65 64 20 70 ..:: Decrypted p
000003D0 61 79 6C 6F 61 64 20 77 72 69 74 74 65 6E 20 74 ayload written t
000003E0 6F 20 70 61 79 6C 6F 61 64 2E 62 69 6E 2E 0A 00 o payload.bin...
000003F0 70 61 79 6C 6F 61 64 2E 62 69 6E 00 58 58 58 58 payload.bin.XXXX
Ce qui permet de valider notre hypothèse sur le chiffrement utilisé.
- Le VM Dispatcher
----------------------------
L'analyse dynamique du code a révelé la présence du code suivant:
(La copie d'IDA n'est pas propre):
MOV X0, X19 ; param1
.text:0000000000402844 BL VM_Dispatcher ; x0 0x7fb7ffe000 --> la table des handlers + context (encryption key)
.text:0000000000402844 ; x1 0x3c 60
.text:0000000000402844 ; x2 0x7ffffff6f0
.text:0000000000402844 ; x/40xw $x2
.text:0000000000402844 ; 0x7ffffff6f0:0x00000048 0x00000000 0x00002101 0x00000001
.text:0000000000402844 ;
.text:0000000000402844 ; x3 0x4 4
.text:0000000000402848 LDRB W0, [X29,#0x5C] ; Load from Memory
.text:000000000040284C LDR W1, [X29,#0x58] ; Load from Memory
.text:0000000000402850 ADD X0, X0, #0x6C ; x0 0x6d 109
.text:0000000000402854 LDR X2, [X19,X0,LSL#3] ; Load from Memory
.text:0000000000402858 MOV X0, X19 ; Rd = Op2
.text:000000000040285C BLR X2 ; à la sortie du 9ème appel
.text:000000000040285C ; x2 0x401490
.text:000000000040285C ;
.text:000000000040285C ; 0x400d9c
.text:000000000040285C ; 0x401580
.text:000000000040285C ;
.text:000000000040285C ; après le message Wrong Key
.text:000000000040285C ; 0x401794
.text:000000000040285C ; 0x4005fc
Ce qu'il faut reteir ici à ce stade, est que cette fonction est appelée à l'exécution de chaque instruction de la VM. Cette dernière reçoit l'instruction de la VM àéxecuter et retourne l'adresse du 'Handler" qui permet de traduire le code de la VM vers le code de l'architecture arm 64 bits.
Les VM Handlers
----------------------------
(Par manque de temps je n'arriverai pas à détailler tout les handler voir l'idb IDA pour l'ensemble, je présente ici un seul mais la méthode reste la même)
exemple pour le Handler ADD
text:0000000000400934 ORR X19, X0, X0 ; Rd = Op1 | Op2
.text:0000000000400938 BL Fetch_Register_value ; Branch with Link
.text:000000000040093C MOV W22, W0 ; Rd = Op2
.text:0000000000400940 MVN X0, X19 ; Rd = ~Op2
.text:0000000000400944 UBFM X1, X21, #0xC, #0xF ; Unsigned Bitfield Move
.text:0000000000400948 MVN X0, X0 ; param1
.text:000000000040094C BL Fetch_Register_value ; Branch with Link
.text:0000000000400950 ADD W2, W0, W22 ; Rd = Op1 + Op2
.text:0000000000400954 EOR X0, X0, X19 ; Rd = Op1 ^ Op2
.text:0000000000400958 MOV W1, W20 ; Rd = Op2
.text:000000000040095C EOR X19, X19, X0 ; Rd = Op1 ^ Op2
.text:0000000000400960 EOR X0, X19, X0 ; Rd = Op1 ^ Op2
.text:0000000000400964 LDP X21, X22, [SP,#0x20+var_s0] ; Load Pair
.text:0000000000400968 ORR X19, X0, X0 ; Rd = Op1 | Op2
.text:000000000040096C LDP X19, X20, [SP,#0x20+var_10] ; Load Pair
.text:0000000000400970 LDP X29, X30, [SP+0x20+var_20],#0x30 ; Load Pair
.text:0000000000400974 B VM_update_register ; Branch
Pour l'opération ADD, la fonction nommée Fetch_Resgister récupère la valeur du premier registre dans W0 puis le deuxième appel de cette même fonction permet de récupérer la deuxième valeur. à l'offset .0000000000400950 indique la nature de l'opéreation (ADD). Enfin l'appel à la fonction VM_update_register permet de mettre à jour le registre résultat. On sait alors que l'instruction ADD a besoin d'un registre résultat et deux registre opérandes.
Il est à noter que le début de chaque handler des opération de shift (UBFM à l'offset 400944) permettent à chaque fois de récupérer l'opcode de l'instruction puis les opérandes.
Les registres
------------------
L'analyse dynamique et l'étude la fonction précédente met en évidence l'utilisation de l'adresse mémoire 0x7fb7fed000. En effet cette dernière correpond au premier registre de la VM (nommé dans le decodeur reg0).
Pour déboguer la VM, un petit script gdb aidant à afficher le contenu des registres de la VM à chaque exécution:
define vv
cont
printf "Instruction --> %0.8x\n", *(0x7ffffff6f8)
printf "----------\n"
printf "Registers:\n"
printf "----------\n"
printf "reg1 : 0x%x , \t" , *(0x7fb7fed000)
printf "reg2 : 0x%x , \t" , *(0x7fb7fed004)
printf "reg3 : 0x%x , \t" , *(0x7fb7fed008)
printf "reg4 : 0x%x , \n" , *(0x7fb7fed00c)
printf "reg5 : 0x%x , \t" , *(0x7fb7fed010)
printf "reg6 : 0x%x , \t" , *(0x7fb7fed014)
printf "reg7 : 0x%x , \t" , *(0x7fb7fed018)
Ce qui donne comme sortie à chaque passage par le VM Dispatcher:
Registers:
----------
reg1 : 0x14 , reg2 : 0x2 , reg3 : 0x38a , reg4 : 0x14 ,
reg5 : 0x46 , reg6 : 0x0 , reg7 : 0x9fff , reg8 : 0x0 ,
reg9 : 0x8 , regA : 0x0 , regB : 0x80 , regC : 0x1fff ,
regD : 0x8000 , regE : 0x40c , regF : 0x0 , regF/PC : 2b4 ,
(gdb)
Breakpoint 5, 0x0000000000402860 in ?? ()
Instruction --> 0000001c
----------
Registers:
----------
reg1 : 0x14 , reg2 : 0x2 , reg3 : 0x38a , reg4 : 0x14 ,
reg5 : 0x46 , reg6 : 0x0 , reg7 : 0x9fff , reg8 : 0x0 ,
reg9 : 0x8 , regA : 0x0 , regB : 0x80 , regC : 0x1fff ,
regD : 0x8000 , regE : 0x40c , regF : 0x0 , regF/PC : 2b4 ,
[Inferior 1 (process 478) exited normally]
[sync] exit, sync finished
Le désassemblage du code de la VM
------------------------------------------------
Disposant des handlers(et implicitement les instructions) , le nombre des registres il est possible de s'attaquer au décodeur.
En utilisant l'outil AMOCO il a été possible de décoder le Byte code de la VM. Cet outil permet en autre de définir une nouvelle architecture. Dans le répertoire arch de l'outil amoco, on peut décrire l'architecture de la VM (voir vm.zip):
Le fichier env.py permet de décrire les registre de notre VM:
reg0 = reg('reg0',32)
reg1 = reg('reg1',32)
reg2 = reg('reg2',32)
reg3 = reg('reg3',32)
reg4 = reg('reg4',32)
reg5 = reg('reg5',32)
reg6 = reg('reg6',32)
reg7 = reg('reg7',32)
reg8 = reg('reg8',32)
reg9 = reg('reg9',32)
...
Le fichier spec_vm.py permet de décrire le décodage des instructions, ci-dessous le décodage des instructions nommées MOVh et MOVl:
@ispec("32[ IMM(20) REG(4) {00} ]", mnemonic="MOVh")
@ispec("32[ IMM(20) REG(4) {01} ]", mnemonic="MOVl")
def VM_MOV(obj,IMM,REG):
src = env.cst(IMM,20)
dst = env.R[REG]
obj.operands = [dst,src]
obj.type = type_data_processing
Et enfin le fichier "asm.py" permet de décrire le comportement de l'instruction, exemple pour 'MOVh':
def i_MOVh(i,fmap):
fmap[pc] = fmap[pc]+i.length
dst,src = i.operands
fmap[dst[16:32]] = fmap(src)
Il est à noter que la taille des instructions des VMs est de 16 et 32 bits.
le fichier (test_vmbytecode.py) permet de décoder le byte code de la VM. le fichier (VM_disassembly.txt) correspond au fichier sortie.
- Analyse du code de la VM
--------------------------------------
On retrouve des informations cohérentes comme le code qui demande d'entrer la clé:
[0x40] 00 01 00 00 : MOVh reg1,0x0
[0x44] 01 21 00 00 : MOVl reg1,0x2 //syscall number 2
[0x48] 00 02 00 00 : MOVh reg2,0x0
[0x4c] 01 12 00 00 : MOVl reg2,0x1 //file descriptor
[0x50] 00 03 00 00 : MOVh reg3,0x0
[0x54] 01 e3 32 00 : MOVl reg3,0x32e //(32e offset of str: : Please enter the decryption key: ) (len 0x24)
La vérification de la taille:
[0x7e] 02 05 00 00 : LDR reg5,0x0,0x0 // get value at @adr+disp (len of buffer read)
[0x82] 00 03 00 00 : MOVh reg3,0x0
[0x86] 01 03 01 00 : MOVl reg3,0x10
[0x88] 13 35 : SUB reg5,reg3 // compare len of buffer read with 0x10
[0x8c] 08 6a b4 02 : JMPNE reg5,reg3,0x2b4 // Jump at offset 0x2B4 if reg5 != reg3 @dispaly Wrong key format
la clé doit faire 16 caractères.
Le code suivant permet de verifier que la clé ne contient que les caractères hexadécimlmaux:
[0x90] 00 0f 00 00 : MOVh regF,0x0
[0x94] 01 0f 01 00 : MOVl regF,0x10 //16 len of the key
[0x98] 00 0e 00 00 : MOVh regE,0x0
[0x9c] 01 ce 3f 00 : MOVl regE,0x3fc //offset of str: XXXXXXXXXXXXXXXX)len :0x10
[0xa0] 00 0d 00 00 : MOVh regD,0x0
[0xa4] 01 6d 32 00 : MOVl regD,0x326
[0xa6] 17 0d : DEC regD
[0xaa] 00 02 00 00 : MOVh reg2,0x0
[0xae] 01 02 03 00 : MOVl reg2,0x30 '0'
[0xb2] 00 03 00 00 : MOVh reg3,0x0
[0xb6] 01 93 03 00 : MOVl reg3,0x39 '9'
[0xba] 00 04 00 00 : MOVh reg4,0x0
[0xbe] 01 14 04 00 : MOVl reg4,0x41 'A'
[0xc2] 00 05 00 00 : MOVh reg5,0x0
[0xc6] 01 65 04 00 : MOVl reg5,0x46 'F'
[0xca] 04 ec 00 00 : LOADB regC, [regE] //RegC vaut 0x41,0x31,0x32 //c'est plutôt un load
[0xce] 02 01 2c 00 : LDR reg1,0x2c,0x0 //Load character by charcter
[0xd2] 13 21 : SUB reg1,reg2 '0'
[0xd4] 08 82 b4 02 : JMPL reg1,reg4,0x2b4 //Display Wrong key format
......
et la partie suivante est la partie la plus intéressante car elle permet de déchiffrer le payload.bin avec la clé fourni (juste après le syswrite:
[0x15e] 1d 00 : INT 0x0 (sys_write)
:: Trying to decrypt payload..
-------------------------------------
[0x160] 00 01 00 00 : MOVh reg1,0x0
[0x164] 01 61 32 00 : MOVl reg1,0x326 //806
[0x168] 02 1a 00 00 : LDR regA,0x0,
.........
L'approche adoptée à ce stade pour comprendre ce code de prime à bord illisible et de produire un code équivalent mais dans un langage plus lisible (python c'est bon!):
(lfsr_crypto.py)
while (payload_len != 0):
#print"key_part1 : 0x%x - key_part2 0x%x"%(key_part1,key_part2)
#LFSR PRNG
tmp1 = key_part1 & 0xb0000000
tmp2 = key_part2 & 0x1
tmp1 = tmp1 ^ tmp2
print "tmp1 is %x"%tmp1
parity = Parity(tmp1)
print"Parity is %x"%parity
#Right Rotate
tmp1 = (key_part1 & 0x1) << 0x1f
key_part2 = (key_part2 >> 0x1) | tmp1
tmp1 = key_part1 >> 0x1
parity = parity << 0x1f
key_part1 = tmp1 | parity
print"Keypart %x%x"%(key_part2,key_part1)
compteur = compteur -1
#print"Compteur is %x"%compteur
tmp = (key_part2 & 0x1) << compteur
#print"key_part2 : %x "%tmp
x = x | tmp
puis une optimisation pour un registre 64 bits:
(voir lfsr_crypto_64.py)
while (payload_len != 0):
#print"key_part1 : 0x%x - key_part2 0x%x"%(key_part1,key_part2)
#LFSR PRNG
tmp1 = key_part & 0xb000000000000001
#tmp1 = (tmp1 | (tmp1 >> 32)) & 0xffffffff
print"tmp1 is %x"%tmp1
parity = Parity(tmp1)
print"Parity is %x"%parity
#Left Rotate
#tmp1 = (key_part & 0x1) << 0x3f
#key_part = (key_part >> 0x1) | tmp1
key_part = key_part >> 0x1
parity = parity << 0x3f
key_part = key_part | parity
print "Keypart %x"%key_part
compteur = compteur -1
#print"Compteur is %x"%compteur
Cette vue macro nous indique l’utilisation d'un algo de type LFSR à un seul état. Pour récupérer la clé de chiffrement il suffit de partir de l'état finale (récupéré depuis la section mémoire, retrouvé à la fin, déchiffré précedement à l'aide de l'algo chahcha).
le fichier lfsr_crypto_64_inverse.py permet de réaliser cette opération et récupérer la clé de chiffrement 0BADB10515DEAD11
Etape 3
A ce niveau on dispose d'un fichier upload.py qui permet d'envoyer un firmware (fw.hex) vers un microcontrôleur à distance.
Le format du microcontrôleur fournit correspond au format Intel Hex.
Sachant que le code du microcontrôleur n'a pas été fourni avec une documentation, il est souhaitable à ce stade de produire le décodeur qui permet de désassembler le code du firmware.
Cette analyse s'est déroulée en boite noire en utilisant la démarche suivante.
Modifier un octet du firmware et l'envoyer au serveur. Ce dernier retourne à chaque fois le message d'erreur suivant:
-- Exception occurred at 07D4: Invalid instruction.\n r0:07D4 r1:0000 r2:0100 r3:00D4\n r4:0700 r5:0000 r6:0000 r7:0000\n r8:0000 r9:0000 r10:0000 r11:0000\n r12:0000 r13:EFFE r14:0000 r15:FD1C\n pc:07D4 fault_addr:0000 [S:0 Z:0] Mode:kernel\nCLOSING: Invalid instruction.\n'
Le message est très clair permet d’identifier le nombre de registre (15 + le program counter).
En utilisant AMOCO, il a été possible de produire un décodeur (voir decodeur_fw.py).
Ci-dessous, un extrait du code désassemble (voir fw.code pour la totalité du code):
: [0x0000] 21 00 : movh r1,0x0
2: [0x0002] 11 1b : movl r1,0x1b //
4: [0x0004] 20 01 : movh r0,0x1
6: [0x0006] 10 8c : movl r0,0x8c //0x18C Firmware v1.33.7 starting.
8: [0x0008] c0 d2 : jmpl [ 0x8 + 0x0d2 + 2 : 0x dc] // jmp to syscall print
une attention à l’instruction jmpl (pour laquelle j'mis du temps à comprendre) comme certain contrôleur c'st un jmp link qui permet de stocker l’instruction de retour dans le regsitre r15.
Le code ci-dessus fait un apppel à la section du code 0xdc:
220: [0x00 dc ] c8 02 : jmpl 0x2,0x8 //syscall print
222: [0x00de] d0 0f : Ret (jmp r15)
Les différents tests ont démontré que l'appel c8 02 correspond au syscall print (hypothèse) qui sera confirmée avec l'analyse du code kernel.
D'autre syscall vont être identifiées (comme le syscall exit et syscall write).
Pour le moment intéressons nous au syscall sys read. Des tests ont été effectués en remplaçant le code firmware correspondant au code désassemblé (ci-dessus) par les adresses et la taille des différents sections mémoires (Firmware, ummapped, RAM, secret memory HW registers et le kernel.
Ce test a permis de déterminer que toutes les sections sont en lecture sauf la section ummaped (ce qui est normal) et la secret memory.
Cependant, on dispose du code kernel (kernel.hex). En réutilisant le décodeur (écrit avec AMOCO) on arrive à désassembler le code du kernel (kernel.code).
La première vulnérabilité permet d'accéder en lecture à toutes les sections mémoires sauf la mémoire secrète:
Lorsqu'on demande la lecture de la mémoire secrète, on reçoit le message suivant:
x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[ERROR] Printing at unallowed address. CPU halted.\n'
Traceback (most recent call last):
File "upload.py", line 56, in
print(resp.decode("utf-8"))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xaa in position 40: invalid start byte
Le message ci-dessus nous permet de localiser le code kernel produisant ce comportement:
65050: [0xfe1a] 21 00 : movh r1,0x0
65052: [0xfe1c] 11 33 : movl r1,0x33 //len of str
65054: [0xfe1e] 20 fe : movh r0,0xfe
65056: [0xfe20] 10 26 : movl r0,0x26 // adr of str : [ERROR] Printing at unallowed address. CPU halted.\n
65058: [0xfe22] c3 c2 : jmpl 0xc2,0x3 [0xfde6] //fonction de printing
65060: [0xfe24] b3 02 : None
Cette fonction est appelée par une fonction un peu plus haut:
65018: [0xfdfa] 51 11 : and r1,r1,r1
65020: [0xfdfc] a0 1a : None
65022: [0xfdfe] 69 e8 : add r9,r14,r8
65024: [0xfe00] 79 9c : sub r9,r9,r12
65026: [0xfe02] a8 08 : None
65028: [0xfe04] 69 e8 : add r9,r14,r8
65030: [0xfe06] 79 9d : sub r9,r9,r13
65032: [0xfe08] ac 02 : None
65034: [0xfe0a] b0 0e : branch [0xfe1a] if
65036: [0xfe0c] 39 99 : xor r9,r9,r9
65038: [0xfe0e] e9 e8 : loadb r9,r14,r8
65040: [0xfe10] f9 db : storeb r9,r13,r11 //store dans HW registers
65042: [0xfe12] 68 8a : add r8,r8,r10
65044: [0xfe14] 71 1a : sub r1,r1,r10
65046: [0xfe16] b3 e2 : branch 0xfdfa if not zero
65048: [0xfe18] d0 0f : ret r0
(note: il me reste quelque opcode que je n'ai pas écrit :)).
Le code ci-dessus (correspond au code du syswrite) vérifie à chaque l'adresse source avant de copier son contenu vers un HW registre (FC00), ce qui permet l'affichage.
Si l'adresse vaut F000 un saut vers le message d'erreur est alors enclenché.
Le deuxième syscall est celui du sys write
8: [0x0026] 20 11 : movh r0,0x11
40: [0x0028] 10 00 : movl r0,0x00
42: [0x002a] c0 b4 : jmpl [ 0x2a + 0x0b4 + 2 : 0x e0] //syscall 03 syswrite
//syswrite écrite 2093 (8339 cpu cycles) à l'adresse donnée
44: [0x002c] c0 b6 : jmpl [ 0x2c + 0x0b6 + 2 : 0x e4]
Des tests visant à modifier l'adresse passée en paramètre à cette fonction permet d'avoir des erreurs indiquant des valeurs du pc différentes à chaque fois.
En analysant le code kernel de cette fonction elle s'est avéré qu'elle se charge d'écrire la valeur 0x2093 (8339) qui est le nombre de cycles d’exécution
Donc, la deuxième vulnérabilité quant à elle permet de rediriger le flux d’exécution du code kernel en contrôlant le paramètre en entrée du syscall 'write'
En manipulant le code du firmware on arrive à réduire le nombre de cycles
0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-- Exception occurred at 07D4: Invalid instruction.\n r0:07D4 r1:0000 r2:0100 r3:00D4\n r4:0700 r5:0000 r6:0000 r7:0000\n r8:0000 r9:0000 r10:0000 r11:0000\n r12:0000 r13:EFFE r14:0000 r15:FD1C\n pc:07D4 fault_addr:0000 [S:0 Z:0] Mode:kernel\nCLOSING: Invalid instruction.\n'
Traceback (most recent call last):
File "change_fw.py", line 75, in
print(resp.decode("utf-8"))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfc in position 46: invalid start byte
Le code suivant permet ansi de récupérer le contenu de la mémoire secrète
(voir fw_vuln.hex)
:1007C000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA89
:1007D000AAAAAAAA210B11FF20F010002DFC1D00CF
:1007E0002CF02A001A013BBB388859885111E908BE
:1007F000F9DB688A711AB3F4000000000000000001
VERY CHALLENGING\n\xe2\x94\x80\xe2\x96\x8c\xe2\x96\x80\xe2\x96\x90\xe2\x96\x84\xe2\x96\x88\xe2\x96\x84\xe2\x96\x88\xe2\x96\x8c\xe2\x96\x84\xe2\x96\x92\xe2\x96\x80\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x90\n\xe2\x96\x90\xe2\x96\x92\xe2\x96\x80\xe2\x96\x90\xe2\x96\x80\xe2\x96\x90\xe2\x96\x80\xe2\x96\x92\xe2\x96\x92\xe2\x96\x84\xe2\x96\x84\xe2\x96\x92\xe2\x96\x84\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x8c SO OPERATIONAL\n\xe2\x96\x90\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x80\xe2\x96\x80\xe2\x96\x84\xe2\x96\x84\xe2\x96\x92
\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x84\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x8c <66a65dc050ec0c84cf1dd5b3bbb75c8c challenge.sstic.org="">\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x96\x80\xe2\x96\x84\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe266a65dc050ec0c84cf1dd5b3bbb75c8c>
Game over
Introduction:
Cette année j'ai pu me pencher sur le challenge SSTIC (http://communaute.sstic.org/ChallengeSSTIC2014) et ce dernier n'a pas failli à sa réputation ...Merci à l'équipe QuarksLab pour cette souffrance :)
Etape1: usbmon
La première partie correspond à une capture USB apparemment réalisée entre un device 'Android' et une machine utilisant le protocole ADB.
Pour avoir une idée sur le contenu de ce fichier l'outil usb-analyzer a été utilisé.Un autre outil (voir "usbmon_helper.py") permet de visualiser le contenu d'une capture usbmon en HTML.
Une première analyse a vite révélé que la capture contient un transfert de fichier entre le device Android et une machine cible.
Le protocole ADB indique que pour un transfert de fichier les commandes suivantes doivent être enchaînées selon le format suivant:
Send → AdbMessage(A_OPEN, local_id, 0, "sync:");
Receive ← AdbMessage(A_OKAY, remote_id, local_id, NULL);
Query File Attributes. If file exists, then proceed.
Send → AdbMessage(A_WRTE, local_id, remote_id, "RECVnnnn");
Receive ← AdbMessage(A_OKAY, remote_id, local_id, NULL);
Send → AdbMessage(A_WRTE, local_id, remote_id, "remote file name");
Receive ← AdbMessage(A_OKAY, remote_id, local_id, NULL);
Receive ← AdbMessage(A_WRTE, remote_id, local_id, "DATAnnnn......");
Send → AdbMessage(A_OKAY, local_id, remote_id, NULL);
Receive ← AdbMessage(A_WRTE, remote_id, local_id, data);
Send → AdbMessage(A_OKAY, local_id, remote_id, NULL);
...
Receive ← AdbMessage(A_WRTE, remote_id, local_id, "DONEnnnn");
Send → AdbMessage(A_WRTE, local_id, remote_id, "QUITnnnn");
Receive ← AdbMessage(A_CLSE, remote_id, local_id, NULL);
Send → AdbMessage(A_CLSE, local_id, remote_id, NULL);
L'outil "usbmon_helper.py" a été légèrement modifié pour récupérer le contenu binaire du fichier transféré par le protocole ADB (voire stage1.bin)
Le fichier idb produit est stage1.i64
Etape 2: premier binaire ARM 64 bit
Environnement de travail
Afin d'avoir un environnement d'analyse les composants suivants ont été utilisés:
- Une machine virtuelle basée sur l'outil "Foundation Model v8" qui offre un environnement de virtualisation d'un cpu ARMv8 et qui pemet de démarrer un système Linux.
- Un système Linux (ubuntu) pour ARM 64 bits.
Donc en gros on dispose d'une machine virtuelle ubuntu ARM 64 bits. Cela permet de compiler les outils nécessaires à l'analyse de notre binaire.
L'idée est d'analyser le binaire récupéré de l'étape précédente en combinant une analyse dynamique avec gdb et une analyse statique avec IDA.
Pour coller les deux morceaux, l'outil Qbsync de QuarksLab a été utilisé, ce dernier permet de visualiser les étapes de débogage sur IDA d'ou la nécessité d'avoir un gdb compilé avec python (possible avec la machine virtuelle):
L’exécution du binaire permet de comprendre l'objectif de cette étape, ce dernier demande la saisie d'une clé permettant de récupérer un fichier nommé 'payload.bin'.
(Pour plus de détails sur l'analyse suivante, s’appuyer le fichier idb)
En déboguant pas à pas le binaire, on arrive à identifier le flux suivant:
Au niveau du point d'entrée, on retrouve un appel vers sub_1010C:
loc_102E0 ; Store Pair
FD 7B BF A9 STP X29, X30, [SP,#var_10]!
FD 03 00 91 MOV X29, SP ; Rd = Op2
89 FF FF 97 BL sub_1010C ; Branch with Link
01 7C 40 93 SBFM X1, X0, #0, #0x1F ; Signed Bitfield Move
E0 03 01 AA MOV X0, X1 ; Rd = Op2
C8 0B 80 D2 MOV X8, #0x5E ; Rd = Op2
01 00 00 D4 SVC 0 ; Supervisor Call
E1 03 00 AA MOV X1, X0
Après plusieurs itérations, le binaire arrive à la section du code suivante:
.text:00000000000102A8 LDRSW X24, [X29,#arg_58] ; Load from Memory
.text:00000000000102AC LDR X1, [X29,#arg_50] ; Load from Memory
.text:00000000000102B0 MOV SP, X25 ; Rd = Op2
.text:00000000000102B4 SUB SP, SP, #8 ; Rd = Op1 - Op2
.text:00000000000102B8 STR X24, [SP,#0x68+var_68] ; Store to Memory
.text:00000000000102BC MOV X2, X1 ; Rd = Op2
.text:00000000000102C0 BLR X2 ; x2 0x400514 4195604
.text:00000000000102C4 MOV X23, X0 ; Rd = Op2
.text:00000000000102C8 B loc_10198 ; Branch
.text:00000000000102C8 ; End of function sub_1010C
.text:00000000000102C8
La suite d'itérations réalisées avant d'arriver à ce stade correspondent à la décompression d'une nouvelle section.
L'instruction BLR x2 permet à la première section du binaire de sauter vers une section nouvellement créée.
Ensuite avec gdb il est possible de dumper cette section (0x4000) quand peut aussi voir avec la commande "cat /proc/mem/pid".
.text:00000000000102A8 LDRSW X24, [X29,#arg_58] ; Load from Memory
.text:00000000000102AC LDR X1, [X29,#arg_50] ; Load from Memory
.text:00000000000102B0 MOV SP, X25 ; Rd = Op2
.text:00000000000102B4 SUB SP, SP, #8 ; Rd = Op1 - Op2
.text:00000000000102B8 STR X24, [SP,#0x68+var_68] ; Store to Memory
.text:00000000000102BC MOV X2, X1 ; Rd = Op2
.text:00000000000102C0 BLR X2 ; x2 0x400514 4195604
.text:00000000000102C4 MOV X23, X0 ; Rd = Op2
.text:00000000000102C8 B loc_10198 ; Branch
.text:00000000000102C8 ; End of function sub_1010C
.text:00000000000102C8
Une fois cette section dumpée, il a été possible de l'ajouter au niveau de l'idb (IDA) courant afin de faciliter la suite de l'analyse. une attention a été portée sur l'ajout de la section au niveau de l'idb d'IDA afin que le mapping dynamique correspond au sections statique.
On continue l'analyse dynamique/statique avec la combinaison gdb/qbsync/IDA.
Cette deuxième partie du binaire est 'obfusquée' en utilisant une machine virtuelle. Par conséquence, l'objectif de cette étape consiste à comprendre le fonctionnement de la machine virtuelle. Pour cela il faut:
+ Identifier et récupérer le byte code de la machine virtuelle
+ Identifier l'ensemble des instruction de la machine virtuelle
+ Identifier la mémoire, la pile et les registres utilisés par la mémoire virtuelle.
Une fois l'ensemble de ces élements, un décodeur sera écrit afin de désassembler le Byte code de la VM. Pour cela l'outil AMOCO a été utilisé.
(une lecture du chapitre obfuscation par la virtualisation Practical Reverse Engineering a aidé à connaitre la notion de VM Hanlder/Dispatcher, fonction de récupération de registre, fonction de récupération de valeur de registres, boucle principale).
La partie suivante essayera de mettre en évidence ces élements (même si je ne me rapelle plus de l'ordre que j'ai suivi pour mettre en évidence ces élements)
Une fois décompressée, le binaire saute à la section 0x400000 (à l'adresse 0x400514), il enchaîne l’exécution de plusieurs instructions(brève description) :
1. Appel de la fonction 0x4000524:
text:0000000000400524 B Call_svc_exit_group ; Branch
2.Appel de la fonction 0x04004F8 que je qualifie de VM Main Entry:
X2, X2, #unk_510018@PAGEOFF ; x2 0x510018
STR X3, [X2] ; Store to Memory
BL Main_Entry ; Branch with Link SBFM X1, X0, #0, #0x1F
3.Appel de la fonction 0x04000C8 que je que qualifie de 'VM set context' car elle permet de mettre en place les composants de la VM (sections mémoires, context crypto):
.text:00000000004000C0 ADD X0, X0, #unk_500000@PAGEOFF ; Rd = Op1 + Op2
.text:00000000004000C4 ADD X2, X29, #0x10 ; Rd = Op1 + Op2
.text:00000000004000C8 BL VM_set_context ; x0 0x500000 5242880
.text:00000000004000C8 ; x1 0x10000 65536
.text:00000000004000C8 ; x2
En effet le syscall présents au niveau de cette fonction permettent de répérer les sections mémoires utilisé par la VM.
La section 0x500000 attire notre attention, aprés avoir dumpé le contenu de cette dernière, elle semble obfusquée (voir vm_bytecode.bin) .
Toutefois en arrivant à la section du code suivant:
.text:0000000000402A88 BL Func_Ecrypt_init ; Branch with Link
.text:0000000000402A8C ADD X22, X19, #0x10 ; Rd = Op1 + Op2
.text:0000000000402A90 ADRP X1, #unk_510000@PAGE ; x1 0x510000 5308416
.text:0000000000402A94 MOV X0, X22 ; Rd = Op2
.text:0000000000402A98 ADD X1, X1, #unk_510000@PAGEOFF ; Rd = Op1 + Op2
.text:0000000000402A9C MOV W2, #128 ; le parametere kbits
.text:0000000000402AA0 MOV W3, #0 ; Rd = Op2
.text:0000000000402AA4 BL Func_KEy_setup ; void ECRYPT_keysetup(ECRYPT_ctx *x,const u8 *k,u32 kbits,u32 ivbits)
.text:0000000000402AA8 ADRP X1, #unk_510020@PAGE ; Address of Page
.text:0000000000402AAC MOV X0, X22 ; Rd = Op2
.text:0000000000402AB0 ADD X1, X1, #unk_510020@PAGEOFF ; x1 0x510020 5308448
.text:0000000000402AB4 BL func_Ecrypt_ivsetup ; Branch with Link
.text:0000000000402AB8 LDRB W0, [X19] ; Load from Memory
En inspectant le contenu de la mémoire , on retrouve la chaine ""expand 16-byte k" qui en recherchant sur Internet permet de d'identifier en premier lieu l'algorithme de chiffrement Salsa puis dans un deuxième temps sa version Chacha.
En comparant le code C des différentes fonctions de cet algorithme, comme
void ECRYPT_ivsetup(ECRYPT_ctx *x,const u8 *iv)
{
int i = 0 ;
x->input[12] = 0;
x->input[13] = 0;
x->input[14] = U8TO32_LITTLE(iv + 0);
x->input[15] = U8TO32_LITTLE(iv + 4);
//Printing the context
printf("[ECRYPT_ivsetup] The Chacha context:\n");
for (i=0; i<16 i="" p=""> printf("0x%x\n", x->input[i]);
}
et le code assembleur à l'offset 0x402AB4 on retrouve rapidement des similitudes.
La comparaison a permis de s'orienter plutôt vers la variante chacha que salsa. Ce qui est intéressant à ce satade est d'identifier la clé utilisée par cet algorithme. Cette information peut être obtenue en mettant un breakpoint à l'appel de la fonction 'ECRYPT_keysetup' qui reçoit en second paramètre la clé de chiffrement.
- Chiffrement Salsa/Chacha
-------------------------------------16>
Il se trouve que la section 0x500000 est chiffré avec l'algorithme de chiffrement chacha. le code chacha_test.c a permis de récupérer le contenu de cette section en clair
(voir VM_bytecode_dec.bin).
A ce stade on dispose du Byte code de la VM:
00000030 00 00 00 00 00 20 00 00 00 00 00 00 40 00 00 00 ..... ......@...
00000040 00 01 00 00 01 21 00 00 00 02 00 00 01 12 00 00 .....!..........
00000050 00 03 00 00 01 E3 32 00 00 04 00 00 01 44 02 00 ......2......D..
00000060 1D 00 00 01 00 00 01 11 00 00 0A 22 00 03 00 00 .
la fin du byte code contient les chaines de carctères affichés lors de l'exécution de notre binaire
20 50 6C 65 61 73 65 20 65 6E 74 65 72 20 74 68 Please enter th
00000340 65 20 64 65 63 72 79 70 74 69 6F 6E 20 6B 65 79 e decryption key
00000350 3A 20 00 00 3A 3A 20 54 72 79 69 6E 67 20 74 6F : ..:: Trying to
00000360 20 64 65 63 72 79 70 74 20 70 61 79 6C 6F 61 64 decrypt payload
00000370 2E 2E 2E 0A 20 20 20 57 72 6F 6E 67 20 6B 65 79 .... Wrong key
00000380 20 66 6F 72 6D 61 74 2E 0A 00 20 20 20 49 6E 76 format... Inv
00000390 61 6C 69 64 20 70 61 64 64 69 6E 67 2E 0A 00 00 alid padding....
000003A0 20 20 20 43 61 6E 6E 6F 74 20 6F 70 65 6E 20 66 Cannot open f
000003B0 69 6C 65 20 70 61 79 6C 6F 61 64 2E 62 69 6E 2E ile payload.bin.
000003C0 0A 00 3A 3A 20 44 65 63 72 79 70 74 65 64 20 70 ..:: Decrypted p
000003D0 61 79 6C 6F 61 64 20 77 72 69 74 74 65 6E 20 74 ayload written t
000003E0 6F 20 70 61 79 6C 6F 61 64 2E 62 69 6E 2E 0A 00 o payload.bin...
000003F0 70 61 79 6C 6F 61 64 2E 62 69 6E 00 58 58 58 58 payload.bin.XXXX
Ce qui permet de valider notre hypothèse sur le chiffrement utilisé.
- Le VM Dispatcher
----------------------------
L'analyse dynamique du code a révelé la présence du code suivant:
(La copie d'IDA n'est pas propre):
MOV X0, X19 ; param1
.text:0000000000402844 BL VM_Dispatcher ; x0 0x7fb7ffe000 --> la table des handlers + context (encryption key)
.text:0000000000402844 ; x1 0x3c 60
.text:0000000000402844 ; x2 0x7ffffff6f0
.text:0000000000402844 ; x/40xw $x2
.text:0000000000402844 ; 0x7ffffff6f0:0x00000048 0x00000000 0x00002101 0x00000001
.text:0000000000402844 ;
.text:0000000000402844 ; x3 0x4 4
.text:0000000000402848 LDRB W0, [X29,#0x5C] ; Load from Memory
.text:000000000040284C LDR W1, [X29,#0x58] ; Load from Memory
.text:0000000000402850 ADD X0, X0, #0x6C ; x0 0x6d 109
.text:0000000000402854 LDR X2, [X19,X0,LSL#3] ; Load from Memory
.text:0000000000402858 MOV X0, X19 ; Rd = Op2
.text:000000000040285C BLR X2 ; à la sortie du 9ème appel
.text:000000000040285C ; x2 0x401490
.text:000000000040285C ;
.text:000000000040285C ; 0x400d9c
.text:000000000040285C ; 0x401580
.text:000000000040285C ;
.text:000000000040285C ; après le message Wrong Key
.text:000000000040285C ; 0x401794
.text:000000000040285C ; 0x4005fc
Ce qu'il faut reteir ici à ce stade, est que cette fonction est appelée à l'exécution de chaque instruction de la VM. Cette dernière reçoit l'instruction de la VM àéxecuter et retourne l'adresse du 'Handler" qui permet de traduire le code de la VM vers le code de l'architecture arm 64 bits.
Les VM Handlers
----------------------------
(Par manque de temps je n'arriverai pas à détailler tout les handler voir l'idb IDA pour l'ensemble, je présente ici un seul mais la méthode reste la même)
exemple pour le Handler ADD
text:0000000000400934 ORR X19, X0, X0 ; Rd = Op1 | Op2
.text:0000000000400938 BL Fetch_Register_value ; Branch with Link
.text:000000000040093C MOV W22, W0 ; Rd = Op2
.text:0000000000400940 MVN X0, X19 ; Rd = ~Op2
.text:0000000000400944 UBFM X1, X21, #0xC, #0xF ; Unsigned Bitfield Move
.text:0000000000400948 MVN X0, X0 ; param1
.text:000000000040094C BL Fetch_Register_value ; Branch with Link
.text:0000000000400950 ADD W2, W0, W22 ; Rd = Op1 + Op2
.text:0000000000400954 EOR X0, X0, X19 ; Rd = Op1 ^ Op2
.text:0000000000400958 MOV W1, W20 ; Rd = Op2
.text:000000000040095C EOR X19, X19, X0 ; Rd = Op1 ^ Op2
.text:0000000000400960 EOR X0, X19, X0 ; Rd = Op1 ^ Op2
.text:0000000000400964 LDP X21, X22, [SP,#0x20+var_s0] ; Load Pair
.text:0000000000400968 ORR X19, X0, X0 ; Rd = Op1 | Op2
.text:000000000040096C LDP X19, X20, [SP,#0x20+var_10] ; Load Pair
.text:0000000000400970 LDP X29, X30, [SP+0x20+var_20],#0x30 ; Load Pair
.text:0000000000400974 B VM_update_register ; Branch
Pour l'opération ADD, la fonction nommée Fetch_Resgister récupère la valeur du premier registre dans W0 puis le deuxième appel de cette même fonction permet de récupérer la deuxième valeur. à l'offset .0000000000400950 indique la nature de l'opéreation (ADD). Enfin l'appel à la fonction VM_update_register permet de mettre à jour le registre résultat. On sait alors que l'instruction ADD a besoin d'un registre résultat et deux registre opérandes.
Il est à noter que le début de chaque handler des opération de shift (UBFM à l'offset 400944) permettent à chaque fois de récupérer l'opcode de l'instruction puis les opérandes.
Les registres
------------------
L'analyse dynamique et l'étude la fonction précédente met en évidence l'utilisation de l'adresse mémoire 0x7fb7fed000. En effet cette dernière correpond au premier registre de la VM (nommé dans le decodeur reg0).
Pour déboguer la VM, un petit script gdb aidant à afficher le contenu des registres de la VM à chaque exécution:
define vv
cont
printf "Instruction --> %0.8x\n", *(0x7ffffff6f8)
printf "----------\n"
printf "Registers:\n"
printf "----------\n"
printf "reg1 : 0x%x , \t" , *(0x7fb7fed000)
printf "reg2 : 0x%x , \t" , *(0x7fb7fed004)
printf "reg3 : 0x%x , \t" , *(0x7fb7fed008)
printf "reg4 : 0x%x , \n" , *(0x7fb7fed00c)
printf "reg5 : 0x%x , \t" , *(0x7fb7fed010)
printf "reg6 : 0x%x , \t" , *(0x7fb7fed014)
printf "reg7 : 0x%x , \t" , *(0x7fb7fed018)
Ce qui donne comme sortie à chaque passage par le VM Dispatcher:
Registers:
----------
reg1 : 0x14 , reg2 : 0x2 , reg3 : 0x38a , reg4 : 0x14 ,
reg5 : 0x46 , reg6 : 0x0 , reg7 : 0x9fff , reg8 : 0x0 ,
reg9 : 0x8 , regA : 0x0 , regB : 0x80 , regC : 0x1fff ,
regD : 0x8000 , regE : 0x40c , regF : 0x0 , regF/PC : 2b4 ,
(gdb)
Breakpoint 5, 0x0000000000402860 in ?? ()
Instruction --> 0000001c
----------
Registers:
----------
reg1 : 0x14 , reg2 : 0x2 , reg3 : 0x38a , reg4 : 0x14 ,
reg5 : 0x46 , reg6 : 0x0 , reg7 : 0x9fff , reg8 : 0x0 ,
reg9 : 0x8 , regA : 0x0 , regB : 0x80 , regC : 0x1fff ,
regD : 0x8000 , regE : 0x40c , regF : 0x0 , regF/PC : 2b4 ,
[Inferior 1 (process 478) exited normally]
[sync] exit, sync finished
Le désassemblage du code de la VM
------------------------------------------------
Disposant des handlers(et implicitement les instructions) , le nombre des registres il est possible de s'attaquer au décodeur.
En utilisant l'outil AMOCO il a été possible de décoder le Byte code de la VM. Cet outil permet en autre de définir une nouvelle architecture. Dans le répertoire arch de l'outil amoco, on peut décrire l'architecture de la VM (voir vm.zip):
Le fichier env.py permet de décrire les registre de notre VM:
reg0 = reg('reg0',32)
reg1 = reg('reg1',32)
reg2 = reg('reg2',32)
reg3 = reg('reg3',32)
reg4 = reg('reg4',32)
reg5 = reg('reg5',32)
reg6 = reg('reg6',32)
reg7 = reg('reg7',32)
reg8 = reg('reg8',32)
reg9 = reg('reg9',32)
...
Le fichier spec_vm.py permet de décrire le décodage des instructions, ci-dessous le décodage des instructions nommées MOVh et MOVl:
@ispec("32[ IMM(20) REG(4) {00} ]", mnemonic="MOVh")
@ispec("32[ IMM(20) REG(4) {01} ]", mnemonic="MOVl")
def VM_MOV(obj,IMM,REG):
src = env.cst(IMM,20)
dst = env.R[REG]
obj.operands = [dst,src]
obj.type = type_data_processing
Et enfin le fichier "asm.py" permet de décrire le comportement de l'instruction, exemple pour 'MOVh':
def i_MOVh(i,fmap):
fmap[pc] = fmap[pc]+i.length
dst,src = i.operands
fmap[dst[16:32]] = fmap(src)
Il est à noter que la taille des instructions des VMs est de 16 et 32 bits.
le fichier (test_vmbytecode.py) permet de décoder le byte code de la VM. le fichier (VM_disassembly.txt) correspond au fichier sortie.
- Analyse du code de la VM
--------------------------------------
On retrouve des informations cohérentes comme le code qui demande d'entrer la clé:
[0x40] 00 01 00 00 : MOVh reg1,0x0
[0x44] 01 21 00 00 : MOVl reg1,0x2 //syscall number 2
[0x48] 00 02 00 00 : MOVh reg2,0x0
[0x4c] 01 12 00 00 : MOVl reg2,0x1 //file descriptor
[0x50] 00 03 00 00 : MOVh reg3,0x0
[0x54] 01 e3 32 00 : MOVl reg3,0x32e //(32e offset of str: : Please enter the decryption key: ) (len 0x24)
La vérification de la taille:
[0x7e] 02 05 00 00 : LDR reg5,0x0,0x0 // get value at @adr+disp (len of buffer read)
[0x82] 00 03 00 00 : MOVh reg3,0x0
[0x86] 01 03 01 00 : MOVl reg3,0x10
[0x88] 13 35 : SUB reg5,reg3 // compare len of buffer read with 0x10
[0x8c] 08 6a b4 02 : JMPNE reg5,reg3,0x2b4 // Jump at offset 0x2B4 if reg5 != reg3 @dispaly Wrong key format
la clé doit faire 16 caractères.
Le code suivant permet de verifier que la clé ne contient que les caractères hexadécimlmaux:
[0x90] 00 0f 00 00 : MOVh regF,0x0
[0x94] 01 0f 01 00 : MOVl regF,0x10 //16 len of the key
[0x98] 00 0e 00 00 : MOVh regE,0x0
[0x9c] 01 ce 3f 00 : MOVl regE,0x3fc //offset of str: XXXXXXXXXXXXXXXX)len :0x10
[0xa0] 00 0d 00 00 : MOVh regD,0x0
[0xa4] 01 6d 32 00 : MOVl regD,0x326
[0xa6] 17 0d : DEC regD
[0xaa] 00 02 00 00 : MOVh reg2,0x0
[0xae] 01 02 03 00 : MOVl reg2,0x30 '0'
[0xb2] 00 03 00 00 : MOVh reg3,0x0
[0xb6] 01 93 03 00 : MOVl reg3,0x39 '9'
[0xba] 00 04 00 00 : MOVh reg4,0x0
[0xbe] 01 14 04 00 : MOVl reg4,0x41 'A'
[0xc2] 00 05 00 00 : MOVh reg5,0x0
[0xc6] 01 65 04 00 : MOVl reg5,0x46 'F'
[0xca] 04 ec 00 00 : LOADB regC, [regE] //RegC vaut 0x41,0x31,0x32 //c'est plutôt un load
[0xce] 02 01 2c 00 : LDR reg1,0x2c,0x0 //Load character by charcter
[0xd2] 13 21 : SUB reg1,reg2 '0'
[0xd4] 08 82 b4 02 : JMPL reg1,reg4,0x2b4 //Display Wrong key format
......
et la partie suivante est la partie la plus intéressante car elle permet de déchiffrer le payload.bin avec la clé fourni (juste après le syswrite:
[0x15e] 1d 00 : INT 0x0 (sys_write)
:: Trying to decrypt payload..
-------------------------------------
[0x160] 00 01 00 00 : MOVh reg1,0x0
[0x164] 01 61 32 00 : MOVl reg1,0x326 //806
[0x168] 02 1a 00 00 : LDR regA,0x0,
.........
L'approche adoptée à ce stade pour comprendre ce code de prime à bord illisible et de produire un code équivalent mais dans un langage plus lisible (python c'est bon!):
(lfsr_crypto.py)
while (payload_len != 0):
#print"key_part1 : 0x%x - key_part2 0x%x"%(key_part1,key_part2)
#LFSR PRNG
tmp1 = key_part1 & 0xb0000000
tmp2 = key_part2 & 0x1
tmp1 = tmp1 ^ tmp2
print "tmp1 is %x"%tmp1
parity = Parity(tmp1)
print"Parity is %x"%parity
#Right Rotate
tmp1 = (key_part1 & 0x1) << 0x1f
key_part2 = (key_part2 >> 0x1) | tmp1
tmp1 = key_part1 >> 0x1
parity = parity << 0x1f
key_part1 = tmp1 | parity
print"Keypart %x%x"%(key_part2,key_part1)
compteur = compteur -1
#print"Compteur is %x"%compteur
tmp = (key_part2 & 0x1) << compteur
#print"key_part2 : %x "%tmp
x = x | tmp
puis une optimisation pour un registre 64 bits:
(voir lfsr_crypto_64.py)
while (payload_len != 0):
#print"key_part1 : 0x%x - key_part2 0x%x"%(key_part1,key_part2)
#LFSR PRNG
tmp1 = key_part & 0xb000000000000001
#tmp1 = (tmp1 | (tmp1 >> 32)) & 0xffffffff
print"tmp1 is %x"%tmp1
parity = Parity(tmp1)
print"Parity is %x"%parity
#Left Rotate
#tmp1 = (key_part & 0x1) << 0x3f
#key_part = (key_part >> 0x1) | tmp1
key_part = key_part >> 0x1
parity = parity << 0x3f
key_part = key_part | parity
print "Keypart %x"%key_part
compteur = compteur -1
#print"Compteur is %x"%compteur
Cette vue macro nous indique l’utilisation d'un algo de type LFSR à un seul état. Pour récupérer la clé de chiffrement il suffit de partir de l'état finale (récupéré depuis la section mémoire, retrouvé à la fin, déchiffré précedement à l'aide de l'algo chahcha).
le fichier lfsr_crypto_64_inverse.py permet de réaliser cette opération et récupérer la clé de chiffrement 0BADB10515DEAD11
Etape 3
A ce niveau on dispose d'un fichier upload.py qui permet d'envoyer un firmware (fw.hex) vers un microcontrôleur à distance.
Le format du microcontrôleur fournit correspond au format Intel Hex.
Sachant que le code du microcontrôleur n'a pas été fourni avec une documentation, il est souhaitable à ce stade de produire le décodeur qui permet de désassembler le code du firmware.
Cette analyse s'est déroulée en boite noire en utilisant la démarche suivante.
Modifier un octet du firmware et l'envoyer au serveur. Ce dernier retourne à chaque fois le message d'erreur suivant:
-- Exception occurred at 07D4: Invalid instruction.\n r0:07D4 r1:0000 r2:0100 r3:00D4\n r4:0700 r5:0000 r6:0000 r7:0000\n r8:0000 r9:0000 r10:0000 r11:0000\n r12:0000 r13:EFFE r14:0000 r15:FD1C\n pc:07D4 fault_addr:0000 [S:0 Z:0] Mode:kernel\nCLOSING: Invalid instruction.\n'
Le message est très clair permet d’identifier le nombre de registre (15 + le program counter).
En utilisant AMOCO, il a été possible de produire un décodeur (voir decodeur_fw.py).
Ci-dessous, un extrait du code désassemble (voir fw.code pour la totalité du code):
: [0x0000] 21 00 : movh r1,0x0
2: [0x0002] 11 1b : movl r1,0x1b //
4: [0x0004] 20 01 : movh r0,0x1
6: [0x0006] 10 8c : movl r0,0x8c //0x18C Firmware v1.33.7 starting.
8: [0x0008] c0 d2 : jmpl [ 0x8 + 0x0d2 + 2 : 0x dc] // jmp to syscall print
une attention à l’instruction jmpl (pour laquelle j'mis du temps à comprendre) comme certain contrôleur c'st un jmp link qui permet de stocker l’instruction de retour dans le regsitre r15.
Le code ci-dessus fait un apppel à la section du code 0xdc:
220: [0x00 dc ] c8 02 : jmpl 0x2,0x8 //syscall print
222: [0x00de] d0 0f : Ret (jmp r15)
Les différents tests ont démontré que l'appel c8 02 correspond au syscall print (hypothèse) qui sera confirmée avec l'analyse du code kernel.
D'autre syscall vont être identifiées (comme le syscall exit et syscall write).
Pour le moment intéressons nous au syscall sys read. Des tests ont été effectués en remplaçant le code firmware correspondant au code désassemblé (ci-dessus) par les adresses et la taille des différents sections mémoires (Firmware, ummapped, RAM, secret memory HW registers et le kernel.
Ce test a permis de déterminer que toutes les sections sont en lecture sauf la section ummaped (ce qui est normal) et la secret memory.
Cependant, on dispose du code kernel (kernel.hex). En réutilisant le décodeur (écrit avec AMOCO) on arrive à désassembler le code du kernel (kernel.code).
La première vulnérabilité permet d'accéder en lecture à toutes les sections mémoires sauf la mémoire secrète:
Lorsqu'on demande la lecture de la mémoire secrète, on reçoit le message suivant:
x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[ERROR] Printing at unallowed address. CPU halted.\n'
Traceback (most recent call last):
File "upload.py", line 56, in
print(resp.decode("utf-8"))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xaa in position 40: invalid start byte
Le message ci-dessus nous permet de localiser le code kernel produisant ce comportement:
65050: [0xfe1a] 21 00 : movh r1,0x0
65052: [0xfe1c] 11 33 : movl r1,0x33 //len of str
65054: [0xfe1e] 20 fe : movh r0,0xfe
65056: [0xfe20] 10 26 : movl r0,0x26 // adr of str : [ERROR] Printing at unallowed address. CPU halted.\n
65058: [0xfe22] c3 c2 : jmpl 0xc2,0x3 [0xfde6] //fonction de printing
65060: [0xfe24] b3 02 : None
Cette fonction est appelée par une fonction un peu plus haut:
65018: [0xfdfa] 51 11 : and r1,r1,r1
65020: [0xfdfc] a0 1a : None
65022: [0xfdfe] 69 e8 : add r9,r14,r8
65024: [0xfe00] 79 9c : sub r9,r9,r12
65026: [0xfe02] a8 08 : None
65028: [0xfe04] 69 e8 : add r9,r14,r8
65030: [0xfe06] 79 9d : sub r9,r9,r13
65032: [0xfe08] ac 02 : None
65034: [0xfe0a] b0 0e : branch [0xfe1a] if
65036: [0xfe0c] 39 99 : xor r9,r9,r9
65038: [0xfe0e] e9 e8 : loadb r9,r14,r8
65040: [0xfe10] f9 db : storeb r9,r13,r11 //store dans HW registers
65042: [0xfe12] 68 8a : add r8,r8,r10
65044: [0xfe14] 71 1a : sub r1,r1,r10
65046: [0xfe16] b3 e2 : branch 0xfdfa if not zero
65048: [0xfe18] d0 0f : ret r0
(note: il me reste quelque opcode que je n'ai pas écrit :)).
Le code ci-dessus (correspond au code du syswrite) vérifie à chaque l'adresse source avant de copier son contenu vers un HW registre (FC00), ce qui permet l'affichage.
Si l'adresse vaut F000 un saut vers le message d'erreur est alors enclenché.
Le deuxième syscall est celui du sys write
8: [0x0026] 20 11 : movh r0,0x11
40: [0x0028] 10 00 : movl r0,0x00
42: [0x002a] c0 b4 : jmpl [ 0x2a + 0x0b4 + 2 : 0x e0] //syscall 03 syswrite
//syswrite écrite 2093 (8339 cpu cycles) à l'adresse donnée
44: [0x002c] c0 b6 : jmpl [ 0x2c + 0x0b6 + 2 : 0x e4]
Des tests visant à modifier l'adresse passée en paramètre à cette fonction permet d'avoir des erreurs indiquant des valeurs du pc différentes à chaque fois.
En analysant le code kernel de cette fonction elle s'est avéré qu'elle se charge d'écrire la valeur 0x2093 (8339) qui est le nombre de cycles d’exécution
Donc, la deuxième vulnérabilité quant à elle permet de rediriger le flux d’exécution du code kernel en contrôlant le paramètre en entrée du syscall 'write'
En manipulant le code du firmware on arrive à réduire le nombre de cycles
0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-- Exception occurred at 07D4: Invalid instruction.\n r0:07D4 r1:0000 r2:0100 r3:00D4\n r4:0700 r5:0000 r6:0000 r7:0000\n r8:0000 r9:0000 r10:0000 r11:0000\n r12:0000 r13:EFFE r14:0000 r15:FD1C\n pc:07D4 fault_addr:0000 [S:0 Z:0] Mode:kernel\nCLOSING: Invalid instruction.\n'
Traceback (most recent call last):
File "change_fw.py", line 75, in
print(resp.decode("utf-8"))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfc in position 46: invalid start byte
Le code suivant permet ansi de récupérer le contenu de la mémoire secrète
(voir fw_vuln.hex)
:1007C000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA89
:1007D000AAAAAAAA210B11FF20F010002DFC1D00CF
:1007E0002CF02A001A013BBB388859885111E908BE
:1007F000F9DB688A711AB3F4000000000000000001
VERY CHALLENGING\n\xe2\x94\x80\xe2\x96\x8c\xe2\x96\x80\xe2\x96\x90\xe2\x96\x84\xe2\x96\x88\xe2\x96\x84\xe2\x96\x88\xe2\x96\x8c\xe2\x96\x84\xe2\x96\x92\xe2\x96\x80\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x90\n\xe2\x96\x90\xe2\x96\x92\xe2\x96\x80\xe2\x96\x90\xe2\x96\x80\xe2\x96\x90\xe2\x96\x80\xe2\x96\x92\xe2\x96\x92\xe2\x96\x84\xe2\x96\x84\xe2\x96\x92\xe2\x96\x84\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x8c SO OPERATIONAL\n\xe2\x96\x90\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x80\xe2\x96\x80\xe2\x96\x84\xe2\x96\x84\xe2\x96\x92
\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x84\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x8c <66a65dc050ec0c84cf1dd5b3bbb75c8c challenge.sstic.org="">\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x96\x80\xe2\x96\x84\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe2\x96\x92\xe266a65dc050ec0c84cf1dd5b3bbb75c8c>
Game over
No comments:
Post a Comment