Powered By Blogger

Monday, July 21, 2014

Challenge SSTIC 2014

[ 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:


  • 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
-------------------------------------

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\xe2



Game over

No comments: