AUCTF 2020

devoops-E

Premier CTF au sein de la team Fake_News, un grand bravo à eux pour cette 23ème place du haut de nos 27429 points !

Sommaire

  • Cracker Barrel (Reverse - 50 points)
  • Plain Jane (Reverse - 730 points)
  • Don’t Break Me! (Reverse - 763 points)
  • Purple Socks (Reverse - 960 points)
  • TI-83 Beta (Reverse - 940 points)
  • ChestBuster (Reverse - 994 points)

Cracker Barrel (Reverse - 50 points)

devoops-E

Description: devoops-E

Premier challenge de la catégorie, qui s’annonce donc comme un simple échauffement.

Le binaire est un exécutable ELF 64 bits non stripé.

Celui-ci est composé de trois fonctions de vérification: check1, check2 et check3.

La première clé, correspondant à la fonction check1, doit être égale à la chaine de caractères ‘starwars’:

devoops-E

La deuxième clé est obtenue grâce à la fonction check2, qui inverse l’ordre des lettres d’une chaîne passée en entrée: devoops-E

L’algorithme inverse (oui c’est overkill pour pas grand-chose, et alors ? :sunglasses: ) est donc le suivant, et permet de valider cette deuxième vérification:

passwd = ""
string = "si siht egassem terces"
v4 = len(string)
i = 0
print(string)
while(i < v4):
    char = string[v4 - 1 - i]
    passwd += char
    i += 1
print(passwd)
[root@Arch cracker_barrel]# python2 check_2.py 
si siht egassem terces
secret message this is

Enfin, la dernière vérification va effectuer une petite opération sur la chaîne entrée, avant de la comparer à une chaîne hardcodé.

Le but est donc ici de comprendre comment fonctionne cette manipulation de donnée pour être capable d’effectuer l’opération inverse.

La manipulation en question est la suivante:

devoops-E

Notre chaîne de caractère est donc prise bytes par bytes. Chaque byte se voit additionné à 0x02, avant d’être xoré avec 0x14.

L’algorithme inverse est donc le suivant:

string = ["0x7A", "0x21", "0x21", "0x62", "0x36", "0x7E", "0x77", "0x6E", "0x26", "0x60"]
passwd = ""

for char in string:
    a = hex(int(char, 16) ^ 0x14 )
    b = chr(int(a, 16) - 0x02)
    passwd += b

print(passwd)

Et permet d’obtenir le dernier mot de passe attendu par le binaire:

[root@Arch cracker_barrel]# python2 check_3.py 
l33t hax0r

En voilà, il ne reste qu’à récupérer le flag:

[root@Arch cracker_barrel]# nc challenges.auctf.com 30000
Give me a key!
starwars
You have passed the first test! Now I need another key!
secret message this is
Nice work! You've passes the second test, we aren't done yet!
l33t hax0r
Congrats you finished! Here is your flag!
auctf{w3lc0m3_to_R3_1021}

Plain Jane (Reverse - 730 points)

Description: devoops-E

Pour ce challenge, un fichier source assembleur est fournis: plain_jane.s

Plutôt que de comprendre le fonctionnement de ce petit programme, il est simplement possible de le compiler pour le débugger.

Phase de compilation:

[root@Arch plain_jane]# gcc -c plain_jane.s

[root@Arch plain_jane]# ld -o jane plain_jane.o 

[root@Arch plain_jane]# file jane
jane: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

Attention, le but est ici de récupérer la valeur de retour du programme (qui est donc située dans le registre RAX), mais ce registre est remis à zéro au cours des quelques dernières instruction:

mov    eax,0x0
leave  
ret 

Il suffira donc de placer un breakpoint avant cette remise à zéro pour récupérer le code retours de l’application:

RAX: 0x6fcf 
RBX: 0x0 
[...]
[-------------------------------------code-------------------------------------]
   0x40102a <main+42>:	mov    edi,eax
   0x40102c <main+44>:	call   0x40108d <func_3>
   0x401031 <main+49>:	mov    DWORD PTR [rbp-0xc],eax
=> 0x401034 <main+52>:	mov    eax,0x0
   0x401039 <main+57>:	leave  
   0x40103a <main+58>:	ret    

La solution est donc 0x6fcf.

Cette valeur permet de valider l’épreuve !

Don’t Break Me! (Reverse - 763 points)

devoops-E

Description: devoops-E

Bien que ce challenge puisse être flag assez rapidement, j’aimerais m’attarder sur l’implémentation de la technique anti-re employée par l’auteur de ce challenge.

Le binaire est un exécutable ELF 32 bits non stripé, visiblement capable de détecter l’insertion de breakpoints:

devoops-E

Le programme affiche une chaîne de plusieurs bytes:

devoops-E

Avant de récupérer une entrée utilisateur, puis de la passer dans une fonction de manipulation de donnée (ici appelée encrypt). une comparaison est faite avec notre chaine, de façon à valider le mot de passe fournis:

devoops-E

En se penchant sur la fonction encrypt, nous tombons nez-à-nez avec la fonction responsable de la détection de nos breakpoints:

devoops-E

La structure générale de cette fonction est la suivante:

devoops-E

Petite explication du fonctionnement de cette fonction:

devoops-E L’instruction call $+5 va call la prochaine instruction, donc ici, celle à l’adresse 0x1235.

Dans tous les cas, cette instruction sera appelée à la suite, alors pourquoi l’appeler implicitement ?

Tout simplement puisque que l’instruction call va permettre de pousser sur la stak l’adresse de cette prochaine instruction.

L’instruction pop edi va ensuite prendre la première adresse (les 4 premiers bytes) sur la stack, et la placer dans le registre EDI.

Ces deux instructions ont donc comme objectif de mettre l’adresse de l’instruction ‘pop edi’ dans le registre EDI, à savoir 0x1235.

devoops-E L’instruction suivante va soustraire 0x05 à EDi, de façon à obtenir EDI = 0x1230.

devoops-E Un comparaison est ensuite faite entre le contenu de l’adresse sauvegardée dans EDI, et l’opode 0xCC.

Cet opcode correspond à un breakpoint software.

Lorsqu’un debugger est en action, celui-ci détecte un breakpoint puisque l’instruction sur laquelle s’arrêter à été remplacée par l’opcode 0xCC.

Cette comparaison vérifie donc que l’instruction à l’adresse 0x1230 n’a pas été remplacée par un breakpoint !

Vient ensuite une boucle, qui va incrémenter de 1 byte EDI, de façon à parcourir une plage d’adresses à la recherche de breakpoint, pendant 0x400 instructions:

devoops-E

Et voici comment ces quelques lignes assembleur sont capable de nous empêcher d’utiliser un débugger. A noter que cette fonction est appelée à chaque étape importante du binaire, de façon à couvrir l’entièreté du binaire avant toute opération ‘sensible’.

Heureusement pour nous, il existe une deuxième manière de placer des breakpoint : les ‘hardware-breakpoints’.

Ceux-ci fonctionnent au niveau matériel, et ne remplacent donc pas les instructions sur lesquelles s’arrêter par des 0xCC. De quoi bypass facilement cette fonction anti-debug.

Ce problème étant réglé, occupons-nous maintenant de trouver le mot de passe attendu par le binaire.

La fonction encrypt semble prendre notre input, la modifier, avant de la comparer avec une version modifiée du mot de passe:

devoops-E

Puisqu’il est désormais possible de placer des breakpoint sans être détecté par le programme, allons directement voir comment fonctionne cette fameuse comparaison:

gdb-peda$ b* 0x56556272
Breakpoint 1 at 0x56556272

gdb-peda$ r

gdb-peda$ info breakpoints 
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x56556272 <main>
	breakpoint already hit 1 time

gdb-peda$ del 1

gdb-peda$ hbreak *0x56556356
Hardware assisted breakpoint 2 at 0x56556356

gdb-peda$ r

Starting program: /home/homardboy/Desktop/Chall/AUCTF_2020/Dont_break_me/dont_break_me 
54 68 65 20 6d 61 6e 20 69 6e 20 62 6c 61 63 6b 20 66 6c 65 64 20 61 63 72 6f 73 73 20 74 68 65 20 64 65 73 65 72 74 2c 20 61 6e 64 20 74 68 65 20 67 75 6e 73 6c 69 6e 67 65 72 20 66 6f 6c 6c 6f 77 65 64 2e
Input: ABCDEFG

devoops-E

Le premier argument correspond à la version “chiffrée” du mot de passe recherché, et le deuxième argument correspond à la chaîne “ABCDEFG” chiffrée par le même algorithme.

Après quelques tests, il semblerait que le même caractère chiffré deux fois retourne toujours le même caractère en sortie.

L’algorithme de chiffrement souffre ici d’une grosse faiblesse, ce qui permet de passer en entrée la table ASCII, pour récupérer un tableau de correspondance entre les caractères chiffrés / déchiffrés:

Starting program: /home/homardboy/Desktop/Chall/AUCTF_2020/Dont_break_me/dont_break_me 
54 68 65 20 6d 61 6e 20 69 6e 20 62 6c 61 63 6b 20 66 6c 65 64 20 61 63 72 6f 73 73 20 74 68 65 20 64 65 73 65 72 74 2c 20 61 6e 64 20 74 68 65 20 67 75 6e 73 6c 69 6e 67 65 72 20 66 6f 6c 6c 6f 77 65 64 2e
Input: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789_!

devoops-E

En reconstituant le puzzle (‘A’ = ‘M’, ‘B’ = ‘D’, etc …), le mot de passe est découvert sans trop d’efforts: La chaîne chiffrée “SASRRWSXBIEBCMPX” correspond à “IKILLWInHMYHEAln”.

De quoi valider ce challenge et récupérer le flag:

[root@Arch AUCTF_2020]# nc challenges.auctf.com 30005
54 68 65 20 6d 61 6e 20 69 6e 20 62 6c 61 63 6b 20 66 6c 65 64 20 61 63 72 6f 73 73 20 74 68 65 20 64 65 73 65 72 74 2c 20 61 6e 64 20 74 68 65 20 67 75 6e 73 6c 69 6e 67 65 72 20 66 6f 6c 6c 6f 77 65 64 2e
Input: IKILLWInHMYHEAln
auctf{static_or_dyn@mIc?_12923}

Purple Socks (Reverse - 960 points)

devoops-E

Description: devoops-E

Pour ce challenge, le fichier fourni semble effectivement inutilisable à première vu: devoops-E

Il est cependant possible, avec un peu d’expérience, de reconnaître la structure d’un fichier binaire dans ce fichier, notamment entre les différentes sections entrecoupées de ‘0x0’ (header, section .text, .data, etc …).

Ceci me fait penser à une implémentation bien connu de l’obfuscation via un Xor, qui consiste à xorer les bytes différents de 0, et à laisser le null-bytes en l’état, de façon à ne pas leaker la clé utilisée.

En essayant bêtement de retrouver le magic byte ‘ELF’ de ce fichier, la théorie est confirmée:

0x31 ^ 0x4e = '0x7f'
0x0b ^ 0x4e = 'E'
0x02 ^ 0x4e = 'L'
0x08 ^ 0x4e = 'F'

La clé est donc ici ‘0x4e’.

L’algorithme suivant permet de reconstituer le fichier binaire d’origine:

import string

plaintext = []
d = open("purple_socks","rb").read()
l = map(lambda x: '%02x' % ord(x), d)

for xored in l:
    if xored == "00":
        unxored = hex(0)
    else:
        unxored = hex(int("0x" + str(xored), 16) ^ 0x4e)
    plaintext.append(unxored)
    

with open('purple_socks.decipher', 'wb') as f:
    for i in plaintext:
        f.write(chr(int(i, 16)))

devoops-E

Passons maintenant au reverse de ce binaire.

Première étape: retrouver le nom d’utilisateur et le mot de passe associé.

Ceux-ci sont hardcodés dans le programme, avant d’être comparée à notre input:

devoops-E

devoops-E

Avec ces credentials, une sorte de jail s’ouvre à nous:

devoops-E

Celle-ci permet de lister les fichiers du répertoire, et de les lire:

devoops-E

Un dernier mot de passe nous bloque l’accès au flag !

Une fonction (encore nommé “encrypt”) nous bloque l’accès dans le cas ou nous chercherions à lire le flag:

devoops-E

L’implémentation de cette fonction est la suivante:

devoops-E

Une ‘seed’ est initialisée, et à chaque modification d’un caractère de la chaîne entrée, cette seed est modifiée avec une nouvelle valeur.

A la fin de cette manipulation, la chaine entrée doit correspondre au contenu de la variable secret :

devoops-E

L’algorithme inverse est le suivant:

seed = ["0x61", "0x75", "0x63", "0x74", "0x66"]
secret = ["0x0E", "0x05", "0x6", "0x1A", "0x39", "0x7D", "0x60", "0x75", "0x7B", "0x54", "0x18", "0x6A"]
flag = ""

for x in range(0, len(secret)):
    seed_index = x % 5
    item = hex(int(secret[x], 16) ^ int(seed[x % 5], 16))
    flag += chr(int(item, 16))
    seed[seed_index] = secret[x]

print(flag)

Ce qui permet d’obtenir le dernier mot de passe requis:

[root@Arch Purple_Socks]# python2 pass.py
open_sesame

Il ne reste plus qu’a valider cette épreuve très sympatique:

[root@Arch Purple_Socks]# nc challenges.auctf.com 30049
Please Login: 
username: bucky
password: longing, rusted, seventeen, daybreak, furnace, nine, benign, homecoming, one, freight car
Welcome bucky

> [? for menu]: read flag.txt
This file is password protected ... 
You will have to enter the password to gain access
Password: open_sesame
auctf{encrypti0n_1s_gr8t_12921}

TI-83 Beta (Reverse - 940 points)

devoops-E

Description: devoops-E

L’exécutable fourni est ici un binaire au format PE32, histoire de changer de plateforme.

Celui-ci est une simple calculatrice, avec des fonctionnalités très basiques:

devoops-E

En observant linéairement les instructions exécutées depuis le main, il est possible de remarquer que le programme commence sa routine par en créant un ‘exception handler’:

devoops-E

Cette structure sera appelée si une exception est levée pendant l’exécution du programme (i.e: une opération impossible).

Décortiquons comment modifier la chaîne d’exception d’un programme à chaud:

Le premier ‘push’ indique le nouveau ‘handler’ en cas d’erreur.

Le second ‘push’ indique l’ancien ‘handler’ : fs[0].

Le ‘mov’ indique que le premier élément de la chaîne d’instruction est désormais la fonction référencée par le premier push.

Ces trois instructions permettent d’intercaler un nouveau handler d’exception at runtime.

Chose étrange, ce nouveau handler devrait pointer vers une fonction, chose que IDA n’est pas capable de reconnaître en l’état:

devoops-E

En essayant de reconstruire la fonction, le flag s’offre tout simplement à nous: devoops-E

auctf{oops_did_i_do_tht}

ChestBuster (Reverse - 994 points)

Description: devoops-E

Encore un binaire Windows pour ce dernier challenge !

Celui-ci semple être un crack-me classique, à l’exception du fait qu’il soit indiqué que ce que l’on cherche ne se trouve pas ici:

devoops-E

Une rapide vérification des ressources embarquées par le binaire nous permet d’y trouver un second exécutable:

devoops-E

Une fois extrait, ce nouveau fichier s’avère être un exécutable go 64 bits … Pas de chance …

Ce programme prend un argument au lancement:

devoops-E

Cet argument est parsé au format ‘< IP > : < PORT >’, et une requête HTTP GET est faite dessus, à l’URL ‘http://< IP > : < PORT > /question.php’:

devoops-E

Le binaire cherche ensuite à discuter avec le serveur en question:

devoops-E

Avant de lui demander gentiment le flag:

devoops-E

Le problème principal de cette histoire est que nous ne connaissons ni l’IP, ni le port du serveur à contacter.

En essayant avec les paramètres de connexion donnée avec l’énoncé du challenge, rien ne se passe:

devoops-E

Le FQDN de la plateforme hébergeant les challenges étant la même depuis le début du CTF, pour toutes les épreuves, je me mets alors à la recherche d’un port au hasard, dans la range des challenges effectués jusqu’à maintenant. Au bout de quelques essais, j’obtiens une réponse sur le port 30009:

devoops-E

Mais pas de flag …

La suite de la fonction semble effectuer une deuxième requête sur ce même couple IP:PORT, en réutilisant le cookie reçu par le serveur lors de la précédente requête:

devoops-E

Le problème vient simplement du fait que le programme envoi la seconde requête sensée récupérer le flag, mais ne fait rien avec la réponse.

Une écoute Wireshark permet de récupérer le flag, et de valider ce dernier challenge:

devoops-E

auctf{r3s0urc3_h4cK1Ng_1S_n3at0_1021}