# Heap Exploitation **List Dynamic Dependencies :** `ldd` **Get version of libc :** `file /lib/x86_64-linux-gnu/libc.so.6` --- ## Heap construction ```c= void *a = malloc(9); ``` La taille minimum d'un chunk est de 24 bytes. ![](https://i.imgur.com/f73QfQx.png) En bleu c'est le chunk alloué par malloc(), on peut voir que malloc() alloue 24 bytes sur la heap. ![](https://i.imgur.com/0zcf3IM.png) ### Heap metadata Tout les chunks contiennent une metadonnée au début du chunk : ![](https://i.imgur.com/LPkogea.png) C'est la chunk size, dans notre cas la taille du chunk est de 8 bytes * 4 soit 32 bytes ou 0x20 en hexadécimal. Le dernier byte du chunk size est 1 ce qui signifie que le chunk précédent est utilisé par le programme. ### Top chunk Le top chunk est le gros chunk qui est segmenté à chaque appel de malloc. Il a aussi une métadonnée avec la taille du chunk. Mais dans beaucoup de libc le top chunk n'est pas sujet à une vérification d'intégrité ce qui nous permet d'introduire la technique de l'House of Force : --- ## House of Force On nous donne un binaire qui nous donne l'adresse de puts() dans la glibc ainsi que la start adresse de la heap : ![](https://i.imgur.com/TaBf0zr.png) On peut facilement retrouver la base adresse de la libc : ``` libc.address = ADRESSE_PUTS_LEAK - ADRESSE_PUTS_LIBC ``` Il faut donc bien comprendre ce schéma pour pouvoir comprendre l'exploitation : ``` -------------------------------------------------------------- | 0x0000000000000000 | <-. -------------------------------------------------------------| | | ////////////////////////////////////////////////////////// | | -------------------------------------------------------------- | | 0x0000000000400000 | | 0x602010 -------------------------------------------------------------| | | ////////////////////////////////////////////////////////// | | -------------------------------------------------------------| | | TARGET | <= 0x602010 | <-' -------------------------------------------------------------| | ////////////////////////////////////////////////////////// | -------------------------------------------------------------| | HEAP START | <= 0x603000 | 0x0000000000000000 | <-. -------------------------------------------------------------| | | 0x0000000000000021 | <= 0x603008 | CHUNK SIZE + IN_USE | | 0x20 | 0x4141414141414141 | <= 0x603010 | | | | 0x4141414141414141 | <= 0x603018 | | | | 0x4141414141414141 | <= 0x603020 | | <-' -------------------------------------------------------------| | HEAP TOP CHUNK | <= 0x603028 | 0xffffffffffffffff | TOP CHUNK = HEAP START + CHUNKS SIZE = 0x603000 + 0x20 = 0x603020 -------------------------------------------------------------| <-. | ////////////////////////////////////////////////////////// | | | ////////////////////////////////////////////////////////// | |---. | /////////////// .------------------------. /////////////// | | | ------------------| 0xffffffffffffffff |-----------------' <-' | '------------------------' | .----------------------------------------' 0x603020 => TOP CHUNK | 0xffffffffffffffff - 0x603020 = 0xffffffffff9fcfdf TARGET_OVERWRITE = 0xffffffffff9fcfdf + (TARGET - CHUNK_SIZE) TARGET_OVERWRITE = 0xffffffffff9fcfdf + (0x602010 - 0x20) TARGET_OVERWRITE = 0xffffffffff9fcfdf + 0x601ff0 = 0xffffffffffffefcf ``` La vulnérabilité réside dans le fait que la taille du top chunk n'est pas vérifiée. Cela permet donc de pouvoir avoir un top chunk très grand qui lorsque l'on allouera une grosse valeur nous permettra d'écrire à l'adresse de notre choix dans le programme. Solution : ``` 1 - malloc où l'on dépasse notre chunk pour écraser le top chunk 2 - malloc pour que le top chunk soit situé juste avant target 3 - malloc avec la data que l'on veut réécrire sur target ``` ### <span style="color:red">**Code Execution**</span> Avec cette technique on peut écraser où l'on le souhaite dans la mémoire, maintenant pour avoir une exécution de code on a plusieurs manière de overwrite : - Pointeurs présent sur la stack ou une adresse de retour - PLT - fini_array - GOT Pour avoir une explication plus poussé sur l'utilité de la PLT et de la GOT se référer à la section dédiée. Malheureusement aucunes de ces options marcheront notamment à cause de l'ASLR et du RELRO. Pour palier à ce problème on va utiliser une technique très connu lors de l'exploitation de format string : - <span style="color:blue">**__malloc_hook**</span> est un pointeur qui va être trigger lors de l'appel de la fonction malloc() mais ce seulement s'il est initialisé afin de pointer lors d'un flux d'exécution normal vers malloc(). Notre objectif va donc être de réécrire **__malloc_hook** afin de pouvoir pointer où on le souhaite comme par exemple notre shellcode si notre programme fait appel à malloc() plus tard. Mais dans notre cas nous allons préféré écrasé notre pointeur avec l'adresse de system() puisqu'on a la base adresse de la libc. La fonction system() prend en argument une string, vu que malloc prend un argument on pourra donc appeler notre malloc() qui prend notre SIZE normalement sauf que cette fois-ci ce sera l'adresse de notre `/bin/sh\x00` qui sera sur la heap à l'adresse `START_HEAP + 0x30` soit juste après notre TOP_CHUNK lors du premier malloc(). Reprenons donc notre petit schéma habituel : ``` .------------------------------------------------------------. | HEAP START | <= 0x603000 | 0x0000000000000000 | <-. |------------------------------------------------------------| | | 0x0000000000000021 | <= 0x603008 | CHUNK SIZE + IN_USE | | 0x20 | 0x4141414141414141 | <= 0x603010 | | | | 0x4141414141414141 | <= 0x603018 | | | | 0x4141414141414141 | <= 0x603020 | | <-' |------------------------------------------------------------| | HEAP TOP CHUNK | <= 0x603028 | 0xffffffffffffffff | TOP CHUNK = HEAP START + CHUNKS SIZE = 0x603000 + 0x20 = 0x603020 '------------------------------------------------------------' __malloc_hook est à 0x7fd988e9bc10 et system() à 0x7fd988b2db70. Pour écraser __malloc_hook if faut que le top chunk soit situé juste avant __malloc_hook. Pour ceux faire il nous faut faire un calcul pour connaitre la taille du prochain chunk a alloué : - (__malloc_hook - FIRST_CHUNK_SIZE) - (HEAP_START + FIRST_CHUNK_SIZE) - (__malloc_hook - 0x20) - (0x603000 + 0x20) - (0x7fd988e9bc10 - 0x20) - (0x603000 + 0x20) = 0x7fd9879e1bd0 Maintenant on va alloué notre chunk de 0x7fd9879e1bd0 ainsi qu'y mettre notre chaîne '/bin/sh' qui sera situé juste après le top chunk actuel donc à l'adresse 0x603030. La heap va donc ressembler à cela maintenant : .------------------------------------------------------------. | HEAP START | <= 0x603000 | 0x0000000000000000 | <-. |------------------------------------------------------------| | | 0x0000000000000021 | <= 0x603008 | CHUNK SIZE + IN_USE | | 0x20 | 0x4141414141414141 | <= 0x603010 | | | | 0x4141414141414141 | <= 0x603018 | | | | 0x4141414141414141 | <= 0x603020 | | <-' |------------------------------------------------------------| | PAST TOP CHUNK | <= 0x603028 | 0x00007fd9879e1be1 | SECOND CHUNK = 0x7fd9879e1bd0 + 0x10 + 0x1 (PREV_INUSE) = 0x7fd9879e1be1 '------------------------------------------------------------' | | 0x68732f2f6e69622f | <= 0x603030 | "/bin/sh" | | | ////////////////// | | | | SIZE = 0x7fd9879e1bd0 | ////////////////// | | | | |------------------------------------------------------------| | | HEAP TOP CHUNK | <= __malloc_hook - 0x10 | TOP CHUNK = 0xffff80267861e41f |------------------------------------------------------------| | 0x0000000000000000 | <= __malloc_hook (0x7fd988e9bc10) | '------------------------------------------------------------' Donc notre top chunk est placé à l'adresse juste avant __malloc_hook, on va donc pouvoir ré-écrire __malloc_hook en faisant un malloc() ce qui nous donne une heap qui ressemble à cela : .------------------------------------------------------------. | HEAP START | <= 0x603000 | 0x0000000000000000 | <-. |------------------------------------------------------------| | | 0x0000000000000021 | <= 0x603008 | CHUNK SIZE + IN_USE | | 0x20 | 0x4141414141414141 | <= 0x603010 | | | | 0x4141414141414141 | <= 0x603018 | | | | 0x4141414141414141 | <= 0x603020 | | <-' |------------------------------------------------------------| | PAST TOP CHUNK | <= 0x603028 | 0x00007fd9879e1be1 | SECOND CHUNK = 0x7fd9879e1bd0 + 0x10 + 0x1 (PREV_INUSE) = 0x7fd9879e1be1 '------------------------------------------------------------' | | 0x0068732f6e69622f | <= 0x603030 | "/bin/sh" | | | ////////////////// | | | | SIZE = 0x7fd9879e1bd0 | ////////////////// | | | | |------------------------------------------------------------| | | PAST TOP CHUNK | <= __malloc_hook - 0x10 | THIRD CHUNK = 0x0000000000000021 |------------------------------------------------------------| | 0x00007fd988b2db70 | <= __malloc_hook (0x7fd988e9bc10) | system() | 0x0000000000000000 | <= 0x7fd988e9bc18 | | 0x0000000000000000 | <= 0x7fd988e9bc20 | | 0x0000000000000000 | <= 0x7fd988e9bc28 | |------------------------------------------------------------| | HEAP TOP CHUNK | '------------------------------------------------------------' Maintenant que __malloc_hook pointe vers system() on peut refaire un malloc() en lui donnant l'adresse de notre string "/bin/sh" (0x603030). ``` Voilà maintenant nous avons un shell ! Cette technique marche pour la GLIBC 2.28 et en dessous. Une protéction a été mise en place pour éviter cela en comparant la taille du top chunk à une variable **system_mem**. --- ## Fastbins La fonction **free()** prend en argument un pointeur vers un chunk et va donc le mettre (le linker) dans une des plusieurs listes de free appelé **bins**. Un de ces bins est le **fastbin**, les chunks y sont recyclés vite et ce seront des chunks de taille spécifique qui y seront stockés. Maintenant il nous faut comprendre dans quel contexte et comment les fastbins fonctionnent. Tout d'abord on doit comprendre que lorsque l'on alloue un chunk et qu'on le free, celui-ci va être mis dans le fastbin correspondant à la taille de son chunk. C'est à dire que pour un chunk de 0x20 bytes il va être donc placé dans le fastbin des chunks de 0x20 bytes. Les fastbins fonctionnent à la manière LIFO (Last In First Out). Pour mieux comprendre voici un exemple : ![](https://i.imgur.com/K4py33h.png) Ici on va allouer trois chunks de taille minimale soit 0x20 bytes, la heap va donc ressembler à cela après ces trois malloc() : ![](https://i.imgur.com/uT9IGv2.png) Nous avons donc : - <span style="color:#048573">Chunk A</span> - 0x20 bytes - <span style="color:#5f455f">Chunk B</span> - 0x20 bytes - <span style="color:#3f8c0a">Chunk C</span> - 0x20 bytes Maintenant que se passera-t-il si l'on free un chunk, et bien regardons cela : ![](https://i.imgur.com/HxXLcQZ.png) On free donc le **chunk A** ce qui nous donne cette heap : ![](https://i.imgur.com/3OoE4d1.png) Ce qui s'est passé c'est que le chunk A a été mis dans le fastbin pour les chunks 0x20. A chaque fois qu'un chunk sera free, son adresse est écrite à la tête du fastbin correspondant. Maintenant on peut se demander comment on sait que c'est un fastbin puisqu'il n'y a rien qui nous le dit en regardant seulement la heap, c'est pour ça que l'on va regarder une structure qui est situé dans la section *.data* dans la libc qui est la **main_arena**. Chose importante à savoir c'est que chaque heap à son arena et celle du thread principal est appelé la **main_arena**. Les arenas ont été mise en place pour permettre le multi-threading et donc permettre à plusieurs régions dans la mémoire d'être utilisées en même temps sans interférer entre eux. Le nombre maximal d'arena par défaut sur un CPU 64bits est le nombre de CPU x 8, tandis que pour un CPU 32bits c'est le nombre de CPU x 2. Si l'on souhaite modifier cela on peut utiliser la fonction mallopt(). --- On va donc voir en détails ce qui compose cette structure (notez que toutes les informations que je donne n'ont pas forcément de rapport avec les fastbins mais plus pour comprendre le fonctionnement) : ```json { mutex = 0, flags = 0, have_fastchunks = 1, fastbinsY = {0x602020, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x602060, last_remainder = 0x0, bins = {0x7ffff7dd0bc0 <main_arena+96>, ...}, binmap = {0, 0, 0, 0}, next = 0x7ffff7dd0b60 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168 } ``` - **mutex** : - **fastbinsY** : Tableau de pointeur vers le dernier fastbin (max: 0x110) - **top** : Top chunk address ``` A continuer [...........] ``` --- Reprenons la suite du programme, nous allons free() une deuxième fois maintenant : ![](https://i.imgur.com/FqJe9M2.png) Le <span style="color:#5f455f">**chunk B**</span> étant free() il va contenir un **FORWARD_POINTER** (ou **FD**) qui va pointer vers le <span style="color:#048573">**chunk A**</span> qui lui même contient un FD nul ce qui signifie que c'est la fin de la liste. De ce fait seulement l'adresse du <span style="color:#5f455f">**chunk B**</span> sera contenu dans la main arena : ![](https://i.imgur.com/jRiUhJ2.png) ⚠️ Chose importante c'est que les fastbins sont la seule exception à la règle du PREV_INUSE. Petit schéma pour récapituler ce qui se passe après avoir free tous les chunks : ``` .--------------------------------. .=>| FD = NULL | CHUNK A | | | '------------| | | | | |--------------------------------| .='=>| FD = CHUNK A | CHUNK B | | | '------------| | | | | |--------------------------------| '===•| FD = CHUNK B | CHUNK C | | '------------| | | '--------------------------------' ``` Maintenant que se passera-t-il si nous faisons un malloc() : ![](https://i.imgur.com/nuyaU4U.png) Le dernier fastbin va être utilisé et maintenant le fastbin va contenir l'adresse du <span style="color:#5f455f">**chunk B**</span>. On peut donc justifier le fonctionnement en LIFO. ## Unlink ```c= static void unlink_chunk (mstate av, mchunkptr p) { if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size"); mchunkptr fd = p->fd; mchunkptr bk = p->bk; if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) malloc_printerr ("corrupted double-linked list"); fd->bk = bk; bk->fd = fd; if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL) { if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p) malloc_printerr ("corrupted double-linked list (not small)"); if (fd->fd_nextsize == NULL) { if (p->fd_nextsize == p) fd->fd_nextsize = fd->bk_nextsize = fd; else { fd->fd_nextsize = p->fd_nextsize; fd->bk_nextsize = p->bk_nextsize; p->fd_nextsize->bk_nextsize = fd; p->bk_nextsize->fd_nextsize = fd; } } else { p->fd_nextsize->bk_nextsize = p->bk_nextsize; p->bk_nextsize->fd_nextsize = p->fd_nextsize; } } } ``` ![](https://i.imgur.com/P3DOBC5.png) ```c= #define unlink(P, BK, FD) { \ FD = P->fd; \ BK = P->bk; \ FD->bk = BK; \ BK->fd = FD; \ } ``` 1. The unsortedbin list is a circular doubly-linked list. 2. Let's **unlink** the red chunk. 3. Follow the victim chunk FD so we overwrite the BK of next chunk with victim BK. 4. Follow the victim chunk BK so we overwrite the FD of previous chunk with victim FD. 5. The list is now rebuilt. --- ## Tcache C'est une feature de malloc introduite depuis la glibc 2.26. --- ## PLT La PLT ou Procedure Linkage Table est un tableau de pointeurs sur fonction. Chaques fonctions que le programme appelle et qui sont dans une librairie externe comme la GLIBC vont apparaître dans la PLT. Une entrée dans la PLT d'une fonction va contenir un JMP vers la GOT (Global Offset Table) Cette section est writable pendant l'exécution d'un programme à cause de ce qu'on appelle le Lazy Linking puisque que l'adresse d'une fonction est résolue seulement quand elle est appelé la première fois. --- ## fini_array Cette section est un tableau de pointeurs sur fonctions qui lorsque le programme va exit, vont être exécutées. --- ## <span style="color:orange">RELRO</span> Protection mise en place pour empêcher d'écrire sur les sections comme la PLT, la GOT et fini_array dès qu'elles sont initialisés. --- ## pwndbg **Restrict display to only code section :** `set context-sections code` **Visualize heap chunks :** `vis_heap_chunks | vis` **Extended information for virtual addresses :** xinfo target ### Resources https://www.segmentationfault.fr/linux/role-plt-got-ld-so/ https://sourceware.org/glibc/wiki/MallocInternals **Format strings** : - http://phrack.org/archives/issues/59/7.txt - https://blog.dragonsector.pl/2017/03/0ctf-2017-easiestprintf-pwn-150.html - https://thibaut.sautereau.fr/2016/09/09/bypassing-aslr-overwriting-the-dynamic-section/ - https://static.lwn.net/2000/1214/a/sec-dtors.php3 - https://buffer.antifork.org/security/heap_atexit.txt - http://www.madchat.fr/coding/c/c.seku/format_string/formatstring.pdf - https://www.areizen.fr/post/fmt-str-full-relro/ - https://ret2rop.blogspot.com/2018/08/format-string-defeating-stack-canary-nx-aslr-remote.html - https://kileak.github.io/ctf/2017/HackLu-heapsofprint/ - https://petircysec.com/hack-lu-2017-heaps-of-print/ - https://blog.idiot.sg/2018-09-04/tokyowesterns-ctf-2018-neighbour-c/ - https://github.com/yuawn/CTF/tree/master/2018/TokyoWesterns/Neighbor_C - https://pwn3r.tistory.com/entry/Tokyo-Western-CTF-2018-Neighbor-C - https://web.archive.org/web/20120205123148/http://althing.cs.dartmouth.edu/local/formats-teso.html - http://phrack.org/issues/67/9.html - http://web.archive.org/web/20090415224123/http://doc.bughunter.net/format-string/technique.html - https://jackgrence.github.io/phrack-67-9/ - https://www.voidsecurity.in/2012/09/exploit-exercise-format-string.html **Linux Kernel** : - https://0x434b.dev/dabbling-with-linux-kernel-exploitation-ctf-challenges-to-learn-the-ropes/ - https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/ - https://gist.github.com/elnx/bc7980ca8f47dbc0e0cef8eb66e6245d - https://github.com/MaherAzzouzi/LinuxKernelExploitation - https://github.com/martinradev/ctf-pwns - https://github.com/mephi42/ctf - https://github.com/MaherAzzouzi/LinuxKernelExploitation/ - https://github.com/pr0cf5/kernel-exploit-practice - http://www.alexlambert.com/2017/12/18/kernel-debugging-for-newbies.html - https://sjp38.github.io/post/qemu_kernel_debugging/ - https://pr0cf5.github.io/ctf/2020/03/09/the-plight-of-tty-in-the-linux-kernel.html - https://ptr-yudai.hatenablog.com/ **SLUB/SLAB** : - https://github.com/PaoloMonti42/salt/blob/master/docs/0x00_SLUB_refresher.md - https://ruffell.nz/programming/writeups/2019/02/15/looking-at-kmalloc-and-the-slub-memory-allocator.html - https://ypl.coffee/m0lecon-ctf-2020-babyk/ - https://hammertux.github.io/slab-allocator - https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628 - https://duasynt.com/blog/linux-kernel-heap-spray - https://meowmeowxw.gitlab.io/ctf/3k-2021-klibrary/ **Stack based** : - https://uaf.io/exploitation/misc/2016/04/02/Finding-Functions.html - https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-di-frederico.pdf - https://reverseengineering.stackexchange.com/questions/6525/elf-link-map-when-linked-as-relro - https://github.com/welchbj/ctf/blob/master/docs/binary-exploitation.md **Windows Kernel** : - https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/setting-up-qemu-kernel-mode-debugging-using-exdi **Linux Heap Exploitation** : #### Safe Linking : ![](https://i.imgur.com/Lavrt9y.png) - https://ctftime.org/writeup/27591 (how to retrieve heap base with mangled pointer) - https://kileak.github.io/ctf/2022/lakectf-nile/ (double free libc 2.32)