Sthack 2022 — Pwn, Reverse Write-Ups

Les Pires Hat
7 min readMay 27, 2022

Dans la nuit du 20 au 21 mai 2022, s’est déroulé le CTF de la Sthack. Notre équipe a fini 9ème au classement général sur 18 équipes.

Pour ce write-up, je vous expliquerai comment j’ai pu résoudre l’unique challenge pwn ainsi que deux challenges de reverse proposé durant le CTF.

Reverse — JeanLouis

En ouvrant le binaire dans Ghidra et après avoir renommé les variables, on tombe sur une sorte de boucle qui réalise un memcpy like. Cette boucle copie un shellcode dans une variable qui sera ensuite XORé avec une clé statique plus loin dans le programme.

Figure 1: Copie du shellcode et XOR de celui-ci.
SHELLCODE = "4F274DEB5442071D4F075746544F530743424F42074A484F425D074B4F624953559F232727279C26272727AEC69D3F272727EAA79F242727279C27272727AA6B03C79D37272727EAA7AEE8AD30A7D56AA7DD3928A2CC27272760AD30A7D548A7DD1C28A2FB27272760AD30A7D553A7DD1B28A2EA27272760AD30A7D543A7DD0228A29927272760AD30A7D542A7DD0128A28827272760AD30A7D557A7DD1C28A28727272760AD30A7D546A7DD3D28A2B627272760AD30A7D554A7DD0D28A2A527272760AD30A7D554A7DD31525060AD30A7D542A7DD3B524B60AD30A7D514A7DD71524660AD30A7D514A7DD6D527160AD30A7D545A7DD20526C60AD30A7D546A7DD75526760AD30A7D543A7DD70521260AD30A7D543A7DD3E520D604F0E0E2D274F0042071D4F074D48524F654E42499F232727279C26272727AEC69D28272727EAA7CC1D4D2D4F090909094F544209094F075746544F530743424F54074A484F5251464E4F6A4627279F232727279C26272727AEC69D07272727EAA7CC279F262727279C27272727EAA700"SHELLCODE = bytes.fromhex(SHELLCODE)final_shellcode = bytearray(len(SHELLCODE))for k in range(0, 0x18b):
final_shellcode[k] = SHELLCODE[k] ^ 0x27
print(bytes(final_shellcode))

Pour comprendre les instructions exécutées, il suffit de prendre le shellcode hardcodé, le XORer et récupérer la valeur hexa (comme présenté dans le code ci-dessus). La valeur hexa peut ensuite être envoyé sur un site comme defuse.ca pour retrouver les instructions.

Une fois les instructions en notre possession, nous pouvons les placer dans un fichier afin de les compiler :

BITS 32section .text
global _start
_start:
push 0x73cc6a00
and [edx],bh
push 0x73617020
push 0x65642074
push 0x6f6d2065
push 0x6c207a65
push 0x72746e45
mov eax,0x4
mov ebx,0x1
mov ecx,esp
mov edx,0x18
int 0x80
mov eax,0x3
mov ebx,0x0
lea ecx,[esp-0x20]
mov edx,0x10
int 0x80
mov edi,ecx
mov dl,[edi]
xor dl,0x4d
cmp dl,0x1e
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x6f
cmp dl,0x3b
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x74
cmp dl,0x3c
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x64
cmp dl,0x25
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x65
cmp dl,0x26
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x70
cmp dl,0x3b
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x61
cmp dl,0x1a
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x73
cmp dl,0x2a
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x73
cmp dl,0x16
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x65
cmp dl,0x1c
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x33
cmp dl,0x56
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x33
cmp dl,0x4a
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x62
cmp dl,0x7
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x61
cmp dl,0x52
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x64
cmp dl,0x57
jne 0x144
inc edi
mov dl,[edi]
xor dl,0x64
cmp dl,0x19
jne 0x144
inc edi
push 0xa2929
push 0x3a206527
push 0x756f6a20
push 0x6e656942
mov eax,0x4
mov ebx,0x1
mov ecx,esp
mov edx,0xf
int 0x80
jmp 0x17e
push 0xa
push 0x2e2e2e2e
push 0x2e2e6573
push 0x73617020
push 0x65642074
push 0x6f6d2073
push 0x69617675
push 0x614d
mov eax,0x4
mov ebx,0x1
mov ecx,esp
mov edx,0x20
int 0x80
jmp 0x17e
mov eax,0x1
mov ebx,0x0
int 0x80
daa

Une fois compilé à l’aide de la commande nasm -f elf32 shellcode.asm -o shellcode.o && ld -i elf_i386 shellcode.o -o shellcode. Le binaire généré peut être envoyé dans Ghidra pour comprendre ce qu’il fait.

Bien évidemment la condition saute aux yeux et permet de récupérer le flag.

Figure 2: Condition de vérification du flag.

flag: STHACK{Yeyeye33}

Reverse — ghozt

Ce challenge propose un binaire ARM. L’ARM est un langage que je ne connais pas très bien. Donc, après avoir découvert la fonction qui permet de faire la vérification du flag, j’ai décidé de le bruteforcer. Je ne voulais pas reverse la fonction permettant de faire le calcul sur la chaîne passée en argument, j’aurais sans aucun doute passé trop de temps à essayer de la comprendre.

Pour son fonctionnement, dans un premier temps, le binaire récupère notre entrée, affiche quelques messages et encode notre input.

Figure 1: Read, Puts, Encode.

Une fois ces actions effectuées, le flag encodé est placé sur la stack et comparé avec notre input caractère par caractère.

Figure 2: Set encoded flag et check du flag avec notre input.

Si on regarde le code assembleur de cette fonction on remarque l’instruction cmp r2,r3 qui compare deux registres entre eux. Je vais pouvoir lancer un gdb-multiarch et placer un breakpoint sur cette instruction pour checker à chaque fois les deux registres qui correspondent à un caractère de mon input et du flag.

Figure 3: Code assembleur de la boucle de vérification.

Bruteforce goes brrrrrrrrrrrrrrru

Pour le bruteforce, je vais lancer dans un terminal le binaire avec qemu et je vais lui passer le flag -g qui permet de debug le binaire en remote.

Maintenant, il ne reste plus qu’à boucler caractère par caractère et vérifier la sortie de gdb pour s’assurer que les registres sont égaux.

Voici le code python :

import subprocessFLAG_LENGTH = 32
FLAG = ""
OLD_R3 = ""char = -1while char != FLAG_LENGTH:
guess = 32
char += 1
while guess != 126:
with open("/tmp/armflag", "w") as f:
f.write(FLAG + chr(guess))
CMD = [
"gdb-multiarch",
"--ex", "target remote:8090",
"--ex", "b *0x000107cc",
]
for k in range(len(FLAG) + 1):
CMD.append("--ex")
CMD.append("c")
CMD.extend(["--ex", "i r", "--ex", "q"]) process = subprocess.Popen(CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = process.stdout.read() for line in output.splitlines():
if line.startswith(b"r2"):
r2 = line
if line.startswith(b"r3"):
r3 = line
if OLD_R3 == r3:
continue
elif r2[2:] == r3[2:]:
print(r2)
print(r3)
FLAG += (chr(guess) if len(FLAG) == 0 else chr(guess-1))
print(f"Flag: {FLAG}")
OLD_R3 = ""
break
else:
OLD_R3 = r3
guess += 1# while true; do cat /tmp/armflag | qemu-arm-static -g 8090 ./armfull; done

Pour chaque caractère le programme, va lancer la commande gdb-multiarch, se connecter sur le binaire taget remote:8090 placer un breakpoint sur 0x000107cc continuer l’exécution du programme tant qu’on connait le caractère et lorsque qu’on ne le connait pas on regarde la valeur des deux registres i r pour enfin faire notre comparaison et quitter q.

Petit à petit le flag se dessine sur notre terminal.

Pour obtenir :

flag: STHARMK{th1s_1s_t00_3asy_F0R_M3}

Pwn

Ce challenge propose un binaire dans un docker qui contient quelques fichiers .pw . Les fichiers .pw ont l’air de contenir les mots de passe des utilisateurs. Le nom du fichier lui a l’air d’être le nom de l’utilisateur à qui appartient le mot de passe.

Une fois le binaire récupéré sur notre poste, on peut l’ouvrir dans Ghidra pour comprendre son fonctionnement.

Dans un premier temps, le binaire va récupérer notre choix puis va exécuter une fonction dynamique en fonction de cela.

Figure 1: Choix de l’utilisateur.

La liste des fonctions est la suivante :

Figure 2: Liste des fonctions possibles en fonction du choix.

Path to flag

En regardant de plus près la fonction choice_auth, on remarque qu’il va regarder si des utilisateurs ont été enregistrés dans le programme, si c’est le cas, alors le programme demande un nom d’utilisateur et un mot de passe. Il vérifie ensuite les identifiants avec ceux du compte admin. Si les identifiants sont bons alors le flag est affiché sinon un message d’erreur est affiché.

Figure 3: Code de la fonction choice_auth.

Le fichier contenant le mot de passe du compte admin et guest étant lisible il n’y a rien de plus simple pour arriver jusqu’au flag.

Il suffit de se laisser guider par le programme.

pleutre@dog:~$ cat admin.pw
flag{mA1l0c_1s_e4syY}
pleutre@dog:~$ ./dog
[1] — Add user
[2] — List available users
[3] — Delete user
[4] — Log in
[5] — Exit
.····[?] Choice
ˇ·-> 1
.····[+] Username
ˇ·-> admin
Successfully added user ‘admin’.
[1] — Add user
[2] — List available users
[3] — Delete user
[4] — Log in
[5] — Exit
.····[?] Choice
ˇ·-> 1
.····[+] Username
ˇ·-> guest
Successfully added user ‘guest’.
[1] — Add user
[2] — List available users
[3] — Delete user
[4] — Log in
[5] — Exit
.····[?] Choice
ˇ·-> 2
[ADDED] admin: Unbreakable password.
[ADDED] guest: Weak password.
[1] — Add user
[2] — List available users
[3] — Delete user
[4] — Log in
[5] — Exit
.····[?] Choice
ˇ·-> 4
.····[!] Username
ˇ·-> admin
.····[!] Password
ˇ·-> flag{mA1l0c_1s_e4syY}
Successfully logged in as ‘admin’.ri3n_n3_Se_P4s5e_C0mm3_pRevU

Je ne pense pas que ce soit comme ça qu’il fallût résoudre ce challenge. Typiquement au vu du mot de passe admin cela aurait du ressembler à un Use After Free.

flag: ri3n_n3_Se_P4s5e_C0mm3_pRevU

Contactez-moi :
Site Web personnel
Twitter de l’équipe : LesPiresHat

--

--